@jingyi0605/codingns 0.4.0 → 0.5.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 (267) hide show
  1. package/bin/codingns.mjs +425 -1
  2. package/dist/public/assets/AdaptiveButlerPage-B153lk5H.css +1 -0
  3. package/dist/public/assets/AdaptiveButlerPage-R-XZw7pd.js +3 -0
  4. package/dist/public/assets/App-DUAg5urj.css +1 -0
  5. package/dist/public/assets/App-DkvE5EyM.js +30 -0
  6. package/dist/public/assets/BootstrapPage-Vu5oEJ8z.js +1 -0
  7. package/dist/public/assets/ConversationPage-Cjpg6g0J.js +2 -0
  8. package/dist/public/assets/DesktopDetachPreviewPage-BgeEqbc5.js +1 -0
  9. package/dist/public/assets/DesktopWindowPage-1WelvxdH.js +2 -0
  10. package/dist/public/assets/FileContextPanel-D_ghXJuW.js +1 -0
  11. package/dist/public/assets/GitSidebar-D9f9Jxwr.js +6 -0
  12. package/dist/public/assets/MobileCreateSessionSheet-DLq5qPkx.js +1 -0
  13. package/dist/public/assets/MobileSheet-DLg-gX1t.js +1 -0
  14. package/dist/public/assets/MobileTopHeaderFrame-DArgZI7L.js +1 -0
  15. package/dist/public/assets/MobileWorkspaceSwitcherHeader-0ywJKfBQ.js +1 -0
  16. package/dist/public/assets/ServerSettingsModal-izoYMx9U.js +1 -0
  17. package/dist/public/assets/SessionIndexPage-C5aG8FIv.js +1 -0
  18. package/dist/public/assets/SettingsPage-HJIC-P-4.js +1 -0
  19. package/dist/public/assets/TerminalManagerPanel-DpyUTo9k.js +1 -0
  20. package/dist/public/assets/{TerminalPage-6jHZV9Mh.js → TerminalPage-CtKXIU0h.js} +19 -19
  21. package/dist/public/assets/TerminalRuntimeFallbackModal-CRhOQOsT.js +1 -0
  22. package/dist/public/assets/ToolFilesPage-DcYPsS-e.js +1 -0
  23. package/dist/public/assets/ToolGitPage-CsPl89ty.js +1 -0
  24. package/dist/public/assets/ToolProcessesPage-D0dvR8xK.js +1 -0
  25. package/dist/public/assets/ToolsHomePage-4fP-KRiv.js +1 -0
  26. package/dist/public/assets/WorkbenchLandingPage-kvlfyxRo.js +1 -0
  27. package/dist/public/assets/WorkbenchLayout-ByFw4eeu.js +3 -0
  28. package/dist/public/assets/WorkbenchModal-Ctob14VR.js +1 -0
  29. package/dist/public/assets/WorkbenchShellRoute-BUITtdAg.css +1 -0
  30. package/dist/public/assets/WorkbenchShellRoute-Kw7JEZI3.js +1 -0
  31. package/dist/public/assets/WorkspaceDebugDetailPage-Com5kEXJ.js +1 -0
  32. package/dist/public/assets/WorkspaceDetailPage-D0Lrx4Uz.js +1 -0
  33. package/dist/public/assets/WorkspaceHomePage-wR8d3aP9.js +1 -0
  34. package/dist/public/assets/butler-records-events-DgWCG364.js +1 -0
  35. package/dist/public/assets/default-session-permission-mode-CcGwR4Kk.js +1 -0
  36. package/dist/public/assets/event-DvH9tcej.js +1 -0
  37. package/dist/public/assets/file-tree-icon-UFVoVzhM.js +31 -0
  38. package/dist/public/assets/index-Byp9hJ0c.js +42 -0
  39. package/dist/public/assets/index-_52jxu4a.css +1 -0
  40. package/dist/public/assets/preferences-service-KIYeE2gk.js +1 -0
  41. package/dist/public/assets/session-runtime-machine-0KNSSPp5.js +17 -0
  42. package/dist/public/assets/styles-BWPBZvze.css +1 -0
  43. package/dist/public/assets/styles-CSUx5LGe.js +1 -0
  44. package/dist/public/assets/terminal-runtime-meta-AWXJpN4r.js +1 -0
  45. package/dist/public/assets/useRegisteredDebugTemplates-DBDRdptr.js +1 -0
  46. package/dist/public/assets/window-BWqRixxq.js +1 -0
  47. package/dist/public/index.html +2 -2
  48. package/dist/server/middlewares/auth-guard.d.ts +4 -0
  49. package/dist/server/middlewares/auth-guard.js +42 -4
  50. package/dist/server/middlewares/auth-guard.js.map +1 -1
  51. package/dist/server/modules/assistant-capability/assistant-capability-controller.d.ts +62 -1
  52. package/dist/server/modules/assistant-capability/assistant-capability-controller.js +58 -0
  53. package/dist/server/modules/assistant-capability/assistant-capability-controller.js.map +1 -1
  54. package/dist/server/modules/assistant-capability/assistant-capability-service.d.ts +66 -3
  55. package/dist/server/modules/assistant-capability/assistant-capability-service.js +173 -1
  56. package/dist/server/modules/assistant-capability/assistant-capability-service.js.map +1 -1
  57. package/dist/server/modules/auth/auth-controller.d.ts +11 -1
  58. package/dist/server/modules/auth/auth-controller.js +61 -2
  59. package/dist/server/modules/auth/auth-controller.js.map +1 -1
  60. package/dist/server/modules/auth/auth-device-display-name.d.ts +10 -0
  61. package/dist/server/modules/auth/auth-device-display-name.js +190 -0
  62. package/dist/server/modules/auth/auth-device-display-name.js.map +1 -0
  63. package/dist/server/modules/auth/auth-service.d.ts +80 -5
  64. package/dist/server/modules/auth/auth-service.js +333 -23
  65. package/dist/server/modules/auth/auth-service.js.map +1 -1
  66. package/dist/server/modules/butler/assistant-automation-service.d.ts +2 -0
  67. package/dist/server/modules/butler/assistant-automation-service.js +46 -0
  68. package/dist/server/modules/butler/assistant-automation-service.js.map +1 -1
  69. package/dist/server/modules/butler/assistant-sandbox-cleanup-scheduler.d.ts +32 -0
  70. package/dist/server/modules/butler/assistant-sandbox-cleanup-scheduler.js +93 -0
  71. package/dist/server/modules/butler/assistant-sandbox-cleanup-scheduler.js.map +1 -0
  72. package/dist/server/modules/butler/assistant-sandbox-service.d.ts +16 -2
  73. package/dist/server/modules/butler/assistant-sandbox-service.js +137 -4
  74. package/dist/server/modules/butler/assistant-sandbox-service.js.map +1 -1
  75. package/dist/server/modules/butler/butler-auth-service.js +7 -2
  76. package/dist/server/modules/butler/butler-auth-service.js.map +1 -1
  77. package/dist/server/modules/butler/butler-control-session-service.d.ts +4 -1
  78. package/dist/server/modules/butler/butler-control-session-service.js +20 -1
  79. package/dist/server/modules/butler/butler-control-session-service.js.map +1 -1
  80. package/dist/server/modules/butler/butler-follow-up-evaluation-instruction-adapter.d.ts +2 -1
  81. package/dist/server/modules/butler/butler-follow-up-evaluation-instruction-adapter.js +27 -25
  82. package/dist/server/modules/butler/butler-follow-up-evaluation-instruction-adapter.js.map +1 -1
  83. package/dist/server/modules/butler/butler-follow-up-service.d.ts +32 -4
  84. package/dist/server/modules/butler/butler-follow-up-service.js +436 -331
  85. package/dist/server/modules/butler/butler-follow-up-service.js.map +1 -1
  86. package/dist/server/modules/butler/butler-inbox-analysis-service.d.ts +1 -1
  87. package/dist/server/modules/butler/butler-inbox-analysis-service.js.map +1 -1
  88. package/dist/server/modules/butler/butler-inbox-service.js +1 -0
  89. package/dist/server/modules/butler/butler-inbox-service.js.map +1 -1
  90. package/dist/server/modules/butler/butler-session-service.d.ts +3 -1
  91. package/dist/server/modules/butler/butler-session-service.js +15 -1
  92. package/dist/server/modules/butler/butler-session-service.js.map +1 -1
  93. package/dist/server/modules/butler/butler-workspace-context.d.ts +1 -1
  94. package/dist/server/modules/butler/butler-workspace-context.js +54 -28
  95. package/dist/server/modules/butler/butler-workspace-context.js.map +1 -1
  96. package/dist/server/modules/provider/provider-controller.d.ts +1 -1
  97. package/dist/server/modules/provider/provider-controller.js.map +1 -1
  98. package/dist/server/modules/relay-tunnel/crypto/relay-tunnel-identity-service.d.ts +10 -0
  99. package/dist/server/modules/relay-tunnel/crypto/relay-tunnel-identity-service.js +48 -0
  100. package/dist/server/modules/relay-tunnel/crypto/relay-tunnel-identity-service.js.map +1 -0
  101. package/dist/server/modules/relay-tunnel/crypto/relay-tunnel-packets.d.ts +48 -0
  102. package/dist/server/modules/relay-tunnel/crypto/relay-tunnel-packets.js +11 -0
  103. package/dist/server/modules/relay-tunnel/crypto/relay-tunnel-packets.js.map +1 -0
  104. package/dist/server/modules/relay-tunnel/crypto/relay-tunnel-protocol.d.ts +74 -0
  105. package/dist/server/modules/relay-tunnel/crypto/relay-tunnel-protocol.js +302 -0
  106. package/dist/server/modules/relay-tunnel/crypto/relay-tunnel-protocol.js.map +1 -0
  107. package/dist/server/modules/relay-tunnel/relay-tunnel-controller.d.ts +33 -0
  108. package/dist/server/modules/relay-tunnel/relay-tunnel-controller.js +57 -0
  109. package/dist/server/modules/relay-tunnel/relay-tunnel-controller.js.map +1 -0
  110. package/dist/server/modules/relay-tunnel/relay-tunnel-edge-proof.d.ts +9 -0
  111. package/dist/server/modules/relay-tunnel/relay-tunnel-edge-proof.js +25 -0
  112. package/dist/server/modules/relay-tunnel/relay-tunnel-edge-proof.js.map +1 -0
  113. package/dist/server/modules/relay-tunnel/relay-tunnel-gateway-service.d.ts +18 -0
  114. package/dist/server/modules/relay-tunnel/relay-tunnel-gateway-service.js +230 -0
  115. package/dist/server/modules/relay-tunnel/relay-tunnel-gateway-service.js.map +1 -0
  116. package/dist/server/modules/relay-tunnel/relay-tunnel-runtime-adapter.d.ts +41 -0
  117. package/dist/server/modules/relay-tunnel/relay-tunnel-runtime-adapter.js +443 -0
  118. package/dist/server/modules/relay-tunnel/relay-tunnel-runtime-adapter.js.map +1 -0
  119. package/dist/server/modules/relay-tunnel/relay-tunnel-service.d.ts +111 -0
  120. package/dist/server/modules/relay-tunnel/relay-tunnel-service.js +771 -0
  121. package/dist/server/modules/relay-tunnel/relay-tunnel-service.js.map +1 -0
  122. package/dist/server/modules/sessions/codex-app-server-helper-client.d.ts +2 -1
  123. package/dist/server/modules/sessions/codex-app-server-helper-client.js +78 -0
  124. package/dist/server/modules/sessions/codex-app-server-helper-client.js.map +1 -1
  125. package/dist/server/modules/sessions/codex-app-server-helper-process.js +84 -2
  126. package/dist/server/modules/sessions/codex-app-server-helper-process.js.map +1 -1
  127. package/dist/server/modules/sessions/provider-session-delete-cli.d.ts +15 -0
  128. package/dist/server/modules/sessions/provider-session-delete-cli.js +148 -0
  129. package/dist/server/modules/sessions/provider-session-delete-cli.js.map +1 -0
  130. package/dist/server/modules/sessions/session-controller.d.ts +4 -1
  131. package/dist/server/modules/sessions/session-controller.js +4 -0
  132. package/dist/server/modules/sessions/session-controller.js.map +1 -1
  133. package/dist/server/modules/sessions/session-history-service.d.ts +17 -0
  134. package/dist/server/modules/sessions/session-history-service.js +150 -1
  135. package/dist/server/modules/sessions/session-history-service.js.map +1 -1
  136. package/dist/server/modules/sessions/session-live-runtime-router-service.d.ts +25 -0
  137. package/dist/server/modules/sessions/session-live-runtime-router-service.js +42 -0
  138. package/dist/server/modules/sessions/session-live-runtime-router-service.js.map +1 -0
  139. package/dist/server/modules/sessions/session-live-runtime-service.js +34 -18
  140. package/dist/server/modules/sessions/session-live-runtime-service.js.map +1 -1
  141. package/dist/server/modules/sessions/session-message-attachment-service.d.ts +1 -0
  142. package/dist/server/modules/sessions/session-message-attachment-service.js +22 -0
  143. package/dist/server/modules/sessions/session-message-attachment-service.js.map +1 -1
  144. package/dist/server/modules/sessions/session-permission-request-service.d.ts +1 -0
  145. package/dist/server/modules/sessions/session-permission-request-service.js +200 -5
  146. package/dist/server/modules/sessions/session-permission-request-service.js.map +1 -1
  147. package/dist/server/modules/sessions/session-provider-error-mapper.js +32 -0
  148. package/dist/server/modules/sessions/session-provider-error-mapper.js.map +1 -1
  149. package/dist/server/modules/sessions/session-provider-usage-guard-service.d.ts +37 -0
  150. package/dist/server/modules/sessions/session-provider-usage-guard-service.js +179 -0
  151. package/dist/server/modules/sessions/session-provider-usage-guard-service.js.map +1 -0
  152. package/dist/server/modules/sessions/session-provider-usage-limit.d.ts +17 -0
  153. package/dist/server/modules/sessions/session-provider-usage-limit.js +465 -0
  154. package/dist/server/modules/sessions/session-provider-usage-limit.js.map +1 -0
  155. package/dist/server/modules/skills/assistant-runtime-skill-catalog.d.ts +8 -0
  156. package/dist/server/modules/skills/assistant-runtime-skill-catalog.js +26 -0
  157. package/dist/server/modules/skills/assistant-runtime-skill-catalog.js.map +1 -0
  158. package/dist/server/modules/skills/assistant-runtime-skill-cleanup.d.ts +9 -0
  159. package/dist/server/modules/skills/assistant-runtime-skill-cleanup.js +55 -0
  160. package/dist/server/modules/skills/assistant-runtime-skill-cleanup.js.map +1 -0
  161. package/dist/server/modules/skills/builtin-skill-service.js +1 -6
  162. package/dist/server/modules/skills/builtin-skill-service.js.map +1 -1
  163. package/dist/server/modules/skills/skill-controller.d.ts +2 -2
  164. package/dist/server/modules/skills/skill-controller.js +9 -1
  165. package/dist/server/modules/skills/skill-controller.js.map +1 -1
  166. package/dist/server/modules/skills/skill-manager-service.d.ts +26 -1
  167. package/dist/server/modules/skills/skill-manager-service.js +346 -90
  168. package/dist/server/modules/skills/skill-manager-service.js.map +1 -1
  169. package/dist/server/modules/skills/skill-name-policy.d.ts +2 -0
  170. package/dist/server/modules/skills/skill-name-policy.js +10 -0
  171. package/dist/server/modules/skills/skill-name-policy.js.map +1 -0
  172. package/dist/server/modules/tailscale/tailscale-service.d.ts +2 -0
  173. package/dist/server/modules/tailscale/tailscale-service.js +21 -8
  174. package/dist/server/modules/tailscale/tailscale-service.js.map +1 -1
  175. package/dist/server/modules/tasks/task-types.d.ts +3 -0
  176. package/dist/server/modules/tasks/task-types.js +3 -0
  177. package/dist/server/modules/tasks/task-types.js.map +1 -1
  178. package/dist/server/modules/terminal/template-reverse-proxy-service.js +71 -3
  179. package/dist/server/modules/terminal/template-reverse-proxy-service.js.map +1 -1
  180. package/dist/server/routes/assistant.js +30 -0
  181. package/dist/server/routes/assistant.js.map +1 -1
  182. package/dist/server/routes/auth.js +4 -0
  183. package/dist/server/routes/auth.js.map +1 -1
  184. package/dist/server/routes/sessions.js +1 -0
  185. package/dist/server/routes/sessions.js.map +1 -1
  186. package/dist/server/routes/system.d.ts +2 -1
  187. package/dist/server/routes/system.js +13 -1
  188. package/dist/server/routes/system.js.map +1 -1
  189. package/dist/server/server/create-server.d.ts +10 -0
  190. package/dist/server/server/create-server.js +82 -12
  191. package/dist/server/server/create-server.js.map +1 -1
  192. package/dist/server/shared/utils/tokens.d.ts +3 -1
  193. package/dist/server/shared/utils/tokens.js +9 -2
  194. package/dist/server/shared/utils/tokens.js.map +1 -1
  195. package/dist/server/storage/repositories/assistant-automation-task-repository.d.ts +2 -0
  196. package/dist/server/storage/repositories/assistant-automation-task-repository.js +8 -2
  197. package/dist/server/storage/repositories/assistant-automation-task-repository.js.map +1 -1
  198. package/dist/server/storage/repositories/assistant-sandbox-workspace-repository.d.ts +1 -0
  199. package/dist/server/storage/repositories/assistant-sandbox-workspace-repository.js +27 -0
  200. package/dist/server/storage/repositories/assistant-sandbox-workspace-repository.js.map +1 -1
  201. package/dist/server/storage/repositories/auth-device-repository.d.ts +22 -0
  202. package/dist/server/storage/repositories/auth-device-repository.js +97 -0
  203. package/dist/server/storage/repositories/auth-device-repository.js.map +1 -0
  204. package/dist/server/storage/repositories/auth-device-session-repository.d.ts +17 -0
  205. package/dist/server/storage/repositories/auth-device-session-repository.js +82 -0
  206. package/dist/server/storage/repositories/auth-device-session-repository.js.map +1 -0
  207. package/dist/server/storage/repositories/auth-login-event-repository.d.ts +9 -0
  208. package/dist/server/storage/repositories/auth-login-event-repository.js +53 -0
  209. package/dist/server/storage/repositories/auth-login-event-repository.js.map +1 -0
  210. package/dist/server/storage/repositories/auth-token-repository.d.ts +4 -0
  211. package/dist/server/storage/repositories/auth-token-repository.js +58 -5
  212. package/dist/server/storage/repositories/auth-token-repository.js.map +1 -1
  213. package/dist/server/storage/repositories/butler-follow-up-task-repository.js +21 -3
  214. package/dist/server/storage/repositories/butler-follow-up-task-repository.js.map +1 -1
  215. package/dist/server/storage/repositories/instance-relay-tunnel-identity-repository.d.ts +8 -0
  216. package/dist/server/storage/repositories/instance-relay-tunnel-identity-repository.js +52 -0
  217. package/dist/server/storage/repositories/instance-relay-tunnel-identity-repository.js.map +1 -0
  218. package/dist/server/storage/repositories/instance-relay-tunnel-repository.d.ts +10 -0
  219. package/dist/server/storage/repositories/instance-relay-tunnel-repository.js +153 -0
  220. package/dist/server/storage/repositories/instance-relay-tunnel-repository.js.map +1 -0
  221. package/dist/server/storage/repositories/instance-tailscale-repository.js +6 -3
  222. package/dist/server/storage/repositories/instance-tailscale-repository.js.map +1 -1
  223. package/dist/server/storage/repositories/managed-skill-repository.d.ts +2 -1
  224. package/dist/server/storage/repositories/managed-skill-repository.js +14 -4
  225. package/dist/server/storage/repositories/managed-skill-repository.js.map +1 -1
  226. package/dist/server/storage/repositories/session-message-attachment-repository.d.ts +2 -0
  227. package/dist/server/storage/repositories/session-message-attachment-repository.js +24 -0
  228. package/dist/server/storage/repositories/session-message-attachment-repository.js.map +1 -1
  229. package/dist/server/storage/sqlite/client.js +297 -2
  230. package/dist/server/storage/sqlite/client.js.map +1 -1
  231. package/dist/server/storage/sqlite/schema.sql +122 -4
  232. package/dist/server/types/domain.d.ts +82 -1
  233. package/dist/server/ws/workbench-ws-hub.js +99 -75
  234. package/dist/server/ws/workbench-ws-hub.js.map +1 -1
  235. package/dist/server/ws/ws-auth-guard.js +1 -4
  236. package/dist/server/ws/ws-auth-guard.js.map +1 -1
  237. package/dist/server/ws/ws-server.d.ts +1 -1
  238. package/dist/server/ws/ws-server.js.map +1 -1
  239. package/node_modules/@codingns/session-sync-core/dist/codex-resume-history.d.ts +1 -0
  240. package/node_modules/@codingns/session-sync-core/dist/codex-resume-history.js +80 -0
  241. package/node_modules/@codingns/session-sync-core/dist/codex-resume-history.js.map +1 -0
  242. package/node_modules/@codingns/session-sync-core/dist/providers/claude-code.d.ts +1 -0
  243. package/node_modules/@codingns/session-sync-core/dist/providers/claude-code.js +11 -1
  244. package/node_modules/@codingns/session-sync-core/dist/providers/claude-code.js.map +1 -1
  245. package/node_modules/@codingns/session-sync-core/dist/providers/codex.d.ts +11 -0
  246. package/node_modules/@codingns/session-sync-core/dist/providers/codex.js +132 -21
  247. package/node_modules/@codingns/session-sync-core/dist/providers/codex.js.map +1 -1
  248. package/node_modules/@codingns/session-sync-core/dist/providers/gemini.d.ts +2 -0
  249. package/node_modules/@codingns/session-sync-core/dist/providers/gemini.js +53 -1
  250. package/node_modules/@codingns/session-sync-core/dist/providers/gemini.js.map +1 -1
  251. package/node_modules/@codingns/session-sync-core/dist/providers/kimi.d.ts +1 -0
  252. package/node_modules/@codingns/session-sync-core/dist/providers/kimi.js +10 -1
  253. package/node_modules/@codingns/session-sync-core/dist/providers/kimi.js.map +1 -1
  254. package/node_modules/@codingns/session-sync-core/dist/providers/opencode.d.ts +1 -0
  255. package/node_modules/@codingns/session-sync-core/dist/providers/opencode.js +30 -0
  256. package/node_modules/@codingns/session-sync-core/dist/providers/opencode.js.map +1 -1
  257. package/node_modules/@codingns/session-sync-core/dist/runtime/codex-runtime.d.ts +5 -1
  258. package/node_modules/@codingns/session-sync-core/dist/runtime/codex-runtime.js +145 -58
  259. package/node_modules/@codingns/session-sync-core/dist/runtime/codex-runtime.js.map +1 -1
  260. package/node_modules/@codingns/session-sync-core/dist/services.d.ts +1 -0
  261. package/node_modules/@codingns/session-sync-core/dist/services.js +7 -0
  262. package/node_modules/@codingns/session-sync-core/dist/services.js.map +1 -1
  263. package/node_modules/@codingns/session-sync-core/dist/types.d.ts +2 -0
  264. package/package.json +1 -1
  265. package/scripts/postinstall.mjs +0 -33
  266. package/dist/public/assets/index-CSVhg7I8.js +0 -123
  267. package/dist/public/assets/index-Ce1VX19m.css +0 -1
@@ -1,11 +1,9 @@
1
- import fs from "node:fs";
2
- import os from "node:os";
3
- import path from "node:path";
4
1
  import { AppError } from "../../shared/errors/app-error.js";
5
2
  import { createId } from "../../shared/utils/id.js";
6
3
  import { nowIso } from "../../shared/utils/time.js";
7
- import { ensureButlerWorkspaceIsolation } from "./butler-profile-service.js";
8
4
  import { resolveButlerCodexBackgroundModel } from "./butler-codex-model-policy.js";
5
+ import { normalizeProviderUsageLimit } from "../sessions/session-provider-usage-limit.js";
6
+ import { resolveProviderUsageLimitBlockedUntil, resolveProviderUsageLimitFromError, SessionProviderUsageLimitGuardService } from "../sessions/session-provider-usage-guard-service.js";
9
7
  const DEFAULT_CHECK_INTERVAL_SECONDS = 300;
10
8
  const MIN_CHECK_INTERVAL_SECONDS = 60;
11
9
  const MAX_CHECK_INTERVAL_SECONDS = 3600;
@@ -15,6 +13,8 @@ const MAX_MAX_AUTO_CONTINUE_COUNT = 20;
15
13
  const FOLLOW_UP_EVALUATOR_DIRNAME = ".butler-follow-up-evaluator";
16
14
  const RECENT_HISTORY_LIMIT = 40;
17
15
  const FOLLOW_UP_PERMISSION_CHECK_INTERVAL_MS = 10_000;
16
+ const FOLLOW_UP_ASSISTANT_WAIT_TIMEOUT_MS = 20 * 60_000;
17
+ const FOLLOW_UP_ASSISTANT_WAIT_POLL_INTERVAL_MS = 2_000;
18
18
  const FOLLOW_UP_AUTO_APPROVE_ACTION_PREFERENCE = [
19
19
  "acceptForSession",
20
20
  "allow_session",
@@ -43,9 +43,10 @@ export class ButlerFollowUpService {
43
43
  followUpCodexHomeDir;
44
44
  sourceCodexHomeDir;
45
45
  sessionMessageOriginRepository;
46
+ providerUsageLimitGuardService;
46
47
  permissionRequestSweepAtByTaskId = new Map();
47
48
  activeExecutionStateByTaskId = new Map();
48
- constructor(butlerProfileService, butlerProjectService, butlerSessionService, butlerFollowUpTaskRepository, sessionHistoryService, sessionIndexRepository, sessionLiveRuntimeService, workspaceService, providerAdapterRegistry, instructionAdapter, followUpCodexHomeDir = null, sourceCodexHomeDir = null, sessionMessageOriginRepository = null) {
49
+ constructor(butlerProfileService, butlerProjectService, butlerSessionService, butlerFollowUpTaskRepository, sessionHistoryService, sessionIndexRepository, sessionLiveRuntimeService, workspaceService, providerAdapterRegistry, instructionAdapter, followUpCodexHomeDir = null, sourceCodexHomeDir = null, sessionMessageOriginRepository = null, providerUsageLimitGuardService = new SessionProviderUsageLimitGuardService(sessionHistoryService)) {
49
50
  this.butlerProfileService = butlerProfileService;
50
51
  this.butlerProjectService = butlerProjectService;
51
52
  this.butlerSessionService = butlerSessionService;
@@ -59,6 +60,7 @@ export class ButlerFollowUpService {
59
60
  this.followUpCodexHomeDir = followUpCodexHomeDir;
60
61
  this.sourceCodexHomeDir = sourceCodexHomeDir;
61
62
  this.sessionMessageOriginRepository = sessionMessageOriginRepository;
63
+ this.providerUsageLimitGuardService = providerUsageLimitGuardService;
62
64
  }
63
65
  listTasks(filters = {}) {
64
66
  this.butlerProfileService.ensureInitialized();
@@ -85,9 +87,41 @@ export class ButlerFollowUpService {
85
87
  const index = this.sessionIndexRepository.findIndexRecordBySessionId(task.sessionId);
86
88
  return mapTaskView(task, project.workspaceId, project.name, index?.title ?? null);
87
89
  }
90
+ toTaskView(task) {
91
+ const project = this.butlerProjectService.getById(task.projectId);
92
+ const index = this.sessionIndexRepository.findIndexRecordBySessionId(task.sessionId);
93
+ return mapTaskView(task, project.workspaceId, project.name, index?.title ?? null);
94
+ }
95
+ requireTaskForAssistantUpdate(taskId, userId) {
96
+ this.butlerProfileService.ensureInitialized();
97
+ const task = this.butlerFollowUpTaskRepository.findById(taskId);
98
+ if (!task) {
99
+ throw new AppError({
100
+ statusCode: 404,
101
+ errorCode: "BUTLER_FOLLOW_UP_TASK_NOT_FOUND",
102
+ detail: "未找到对应的跟进任务"
103
+ });
104
+ }
105
+ if (task.createdByUserId !== userId) {
106
+ throw new AppError({
107
+ statusCode: 403,
108
+ errorCode: "BUTLER_FOLLOW_UP_TASK_FORBIDDEN",
109
+ detail: "你没有权限更新这个跟进任务"
110
+ });
111
+ }
112
+ if (task.status !== "active") {
113
+ throw new AppError({
114
+ statusCode: 409,
115
+ errorCode: "BUTLER_FOLLOW_UP_TASK_NOT_ACTIVE",
116
+ detail: "当前跟进任务已经不处于可回写状态"
117
+ });
118
+ }
119
+ return task;
120
+ }
88
121
  async createTask(input, userId) {
89
122
  this.butlerProfileService.ensureInitialized();
90
123
  const project = this.butlerProjectService.getById(input.projectId);
124
+ const providerId = normalizeFollowUpProviderId(input.providerId);
91
125
  const objective = normalizeObjective(input.objective);
92
126
  const completionCriteria = normalizeCompletionCriteria(input.completionCriteria, objective);
93
127
  const maxAutoContinueCount = normalizeMaxAutoContinueCount(input.maxAutoContinueCount);
@@ -102,15 +136,38 @@ export class ButlerFollowUpService {
102
136
  });
103
137
  }
104
138
  const session = this.sessionHistoryService.getSession(snapshot.sessionId, userId);
139
+ const inspection = await this.inspectSession(snapshot.sessionId, userId);
140
+ const assistantSession = await this.butlerSessionService.startSession(project.id, {
141
+ providerId,
142
+ role: "adhoc",
143
+ ownershipMode: "managed",
144
+ content: buildFollowUpBootstrapPrompt({
145
+ project,
146
+ sourceButlerSessionId: input.butlerSessionId,
147
+ sourceSessionId: snapshot.sessionId,
148
+ sourceSessionTitle: inspection.sessionTitle,
149
+ objective,
150
+ completionCriteria,
151
+ maxAutoContinueCount,
152
+ latestAssistantText: inspection.latestAssistantText,
153
+ transcriptLines: inspection.transcriptLines
154
+ }),
155
+ model: resolveFollowUpModel(providerId, this.sourceCodexHomeDir),
156
+ reasoningLevel: "low",
157
+ permissionMode: "default"
158
+ }, userId);
105
159
  const timestamp = nowIso();
106
160
  const initialSummary = snapshot.runningState === "starting" || snapshot.runningState === "running"
107
- ? `已开始跟进,先等待当前运行结束,再由后台评估助手决定下一步。默认最多自动推进 ${maxAutoContinueCount} 轮。`
108
- : `已开始跟进,准备由后台评估助手检查当前进展。默认最多自动推进 ${maxAutoContinueCount} 轮。`;
161
+ ? `已创建跟进助手会话,先等待当前运行结束,再由该会话决定是否继续推进。默认最多自动推进 ${maxAutoContinueCount} 轮。`
162
+ : `已创建跟进助手会话,准备由该会话检查当前进展并决定是否继续推进。默认最多自动推进 ${maxAutoContinueCount} 轮。`;
109
163
  const task = this.butlerFollowUpTaskRepository.create({
110
164
  id: createId(),
111
165
  projectId: project.id,
112
166
  butlerSessionId: input.butlerSessionId,
113
167
  sessionId: snapshot.sessionId,
168
+ providerId,
169
+ assistantButlerSessionId: assistantSession.id,
170
+ assistantSessionId: assistantSession.sessionId,
114
171
  createdByUserId: userId,
115
172
  objective,
116
173
  completionCriteria,
@@ -136,6 +193,143 @@ export class ButlerFollowUpService {
136
193
  const processed = await this.processTask(task.id);
137
194
  return mapTaskView(processed, project.workspaceId, project.name, session.title ?? null);
138
195
  }
196
+ async continueTask(taskId, input, userId) {
197
+ const summary = requireNonEmptyFollowUpText(input.summary, "summary", "继续推进必须提供 summary");
198
+ const continuePrompt = requireNonEmptyFollowUpText(input.continuePrompt, "continuePrompt", "继续推进必须提供 continuePrompt");
199
+ const task = this.requireTaskForAssistantUpdate(taskId, userId);
200
+ if (hasReachedAutoContinueLimit(task)) {
201
+ throw new AppError({
202
+ statusCode: 409,
203
+ errorCode: "BUTLER_FOLLOW_UP_TASK_LIMIT_REACHED",
204
+ detail: "当前跟进任务已经达到自动推进上限,不能继续自动推进"
205
+ });
206
+ }
207
+ this.butlerSessionService.captureSessionSnapshot(task.projectId, task.butlerSessionId, task.createdByUserId, { sourceKind: "manual" });
208
+ const inspection = await this.inspectTask(task);
209
+ const timestamp = nowIso();
210
+ const runningState = normalizeRunningState(inspection.runningState);
211
+ const nextAutoContinueCount = task.autoContinueCount + 1;
212
+ const updated = this.persistWithRound({
213
+ ...task,
214
+ status: "active",
215
+ lastCheckedAt: timestamp,
216
+ lastObservedRunningState: runningState,
217
+ lastObservedMessageAt: inspection.messageAt,
218
+ lastObservedMessageCount: inspection.messageCount,
219
+ waitingReason: null,
220
+ nextCheckAt: shiftSeconds(timestamp, task.checkIntervalSeconds),
221
+ lastAutomationSummary: summary,
222
+ lastAutomationAt: timestamp,
223
+ autoContinueCount: nextAutoContinueCount,
224
+ updatedAt: timestamp,
225
+ completedAt: null
226
+ }, {
227
+ kind: "continue",
228
+ status: "active",
229
+ summary,
230
+ waitingReason: null,
231
+ continuePrompt,
232
+ observedRunningState: runningState,
233
+ autoContinueCount: nextAutoContinueCount,
234
+ createdAt: timestamp
235
+ });
236
+ return this.toTaskView(updated);
237
+ }
238
+ async markTaskWaitingUser(taskId, input, userId) {
239
+ const summary = requireNonEmptyFollowUpText(input.summary, "summary", "等待用户必须提供 summary");
240
+ const waitingReason = requireNonEmptyFollowUpText(input.waitingReason, "waitingReason", "等待用户必须提供 waitingReason");
241
+ const task = this.requireTaskForAssistantUpdate(taskId, userId);
242
+ const inspection = await this.inspectTask(task);
243
+ const timestamp = nowIso();
244
+ const runningState = normalizeRunningState(inspection.runningState);
245
+ const updated = this.persistWithRound({
246
+ ...task,
247
+ status: "waiting_user",
248
+ lastCheckedAt: timestamp,
249
+ lastObservedRunningState: runningState,
250
+ lastObservedMessageAt: inspection.messageAt,
251
+ lastObservedMessageCount: inspection.messageCount,
252
+ waitingReason,
253
+ nextCheckAt: null,
254
+ lastAutomationSummary: summary,
255
+ lastAutomationAt: timestamp,
256
+ updatedAt: timestamp,
257
+ completedAt: null
258
+ }, {
259
+ kind: "waiting_user",
260
+ status: "waiting_user",
261
+ summary,
262
+ waitingReason,
263
+ continuePrompt: null,
264
+ observedRunningState: runningState,
265
+ autoContinueCount: task.autoContinueCount,
266
+ createdAt: timestamp
267
+ });
268
+ return this.toTaskView(updated);
269
+ }
270
+ async completeTask(taskId, input, userId) {
271
+ const summary = requireNonEmptyFollowUpText(input.summary, "summary", "完成任务必须提供 summary");
272
+ const task = this.requireTaskForAssistantUpdate(taskId, userId);
273
+ const inspection = await this.inspectTask(task);
274
+ const timestamp = nowIso();
275
+ const runningState = normalizeRunningState(inspection.runningState);
276
+ const updated = this.persistWithRound({
277
+ ...task,
278
+ status: "completed",
279
+ lastCheckedAt: timestamp,
280
+ lastObservedRunningState: runningState,
281
+ lastObservedMessageAt: inspection.messageAt,
282
+ lastObservedMessageCount: inspection.messageCount,
283
+ waitingReason: null,
284
+ nextCheckAt: null,
285
+ lastAutomationSummary: summary,
286
+ lastAutomationAt: timestamp,
287
+ updatedAt: timestamp,
288
+ completedAt: timestamp
289
+ }, {
290
+ kind: "completed",
291
+ status: "completed",
292
+ summary,
293
+ waitingReason: null,
294
+ continuePrompt: null,
295
+ observedRunningState: runningState,
296
+ autoContinueCount: task.autoContinueCount,
297
+ createdAt: timestamp
298
+ });
299
+ return this.toTaskView(updated);
300
+ }
301
+ async failTask(taskId, input, userId) {
302
+ const summary = requireNonEmptyFollowUpText(input.summary, "summary", "标记失败必须提供 summary");
303
+ const reason = normalizeNullableText(input.reason) ?? summary;
304
+ const task = this.requireTaskForAssistantUpdate(taskId, userId);
305
+ const inspection = await this.inspectTask(task);
306
+ const timestamp = nowIso();
307
+ const runningState = normalizeRunningState(inspection.runningState);
308
+ const updated = this.persistWithRound({
309
+ ...task,
310
+ status: "failed",
311
+ lastCheckedAt: timestamp,
312
+ lastObservedRunningState: runningState,
313
+ lastObservedMessageAt: inspection.messageAt,
314
+ lastObservedMessageCount: inspection.messageCount,
315
+ waitingReason: reason,
316
+ nextCheckAt: null,
317
+ lastAutomationSummary: summary,
318
+ lastAutomationAt: timestamp,
319
+ updatedAt: timestamp,
320
+ completedAt: null
321
+ }, {
322
+ kind: "failed",
323
+ status: "failed",
324
+ summary,
325
+ waitingReason: reason,
326
+ continuePrompt: null,
327
+ observedRunningState: runningState,
328
+ autoContinueCount: task.autoContinueCount,
329
+ createdAt: timestamp
330
+ });
331
+ return this.toTaskView(updated);
332
+ }
139
333
  async cancelTask(taskId, userId) {
140
334
  this.butlerProfileService.ensureInitialized();
141
335
  const task = this.butlerFollowUpTaskRepository.findById(taskId);
@@ -181,7 +375,7 @@ export class ButlerFollowUpService {
181
375
  autoContinueCount: task.autoContinueCount,
182
376
  createdAt: timestamp
183
377
  });
184
- await this.stopActiveTaskAutomation(execution);
378
+ await this.stopActiveTaskAutomation(task, execution);
185
379
  const project = this.butlerProjectService.getById(updated.projectId);
186
380
  const index = this.sessionIndexRepository.findIndexRecordBySessionId(updated.sessionId);
187
381
  return mapTaskView(updated, project.workspaceId, project.name, index?.title ?? null);
@@ -236,7 +430,6 @@ export class ButlerFollowUpService {
236
430
  }
237
431
  const execution = this.beginTaskExecution(task.id);
238
432
  try {
239
- const profile = this.butlerProfileService.ensureInitialized();
240
433
  const project = this.butlerProjectService.getById(task.projectId);
241
434
  const inspection = await this.inspectTask(task);
242
435
  this.ensureTaskExecutionActive(task.id, execution);
@@ -282,139 +475,52 @@ export class ButlerFollowUpService {
282
475
  createdAt: referenceAt
283
476
  });
284
477
  }
478
+ const usageLimitBlock = await this.providerUsageLimitGuardService.resolveBlockingInspection([
479
+ {
480
+ sessionId: task.sessionId,
481
+ userId: task.createdByUserId,
482
+ sourceLabel: "跟进目标会话"
483
+ },
484
+ {
485
+ sessionId: task.assistantSessionId,
486
+ userId: task.createdByUserId,
487
+ sourceLabel: "跟进助手会话"
488
+ }
489
+ ], referenceAt);
490
+ if (usageLimitBlock) {
491
+ const nextCheckAt = usageLimitBlock.blockedUntil;
492
+ return this.persistIfExecutionActive(task.id, execution, {
493
+ ...baseUpdate,
494
+ status: "active",
495
+ waitingReason: null,
496
+ nextCheckAt,
497
+ completedAt: null,
498
+ lastAutomationAt: referenceAt,
499
+ lastAutomationSummary: buildFollowUpUsageLimitSummary(usageLimitBlock.inspection.providerUsageLimit, `${usageLimitBlock.inspection.sourceLabel ?? "当前会话"}被 provider 套餐限额暂时挡住。`)
500
+ });
501
+ }
285
502
  try {
286
- const evaluation = await this.evaluateTask(profile, project, task, inspection, runningState, execution);
503
+ const progressBeforeDispatch = snapshotTaskProgress(task);
504
+ await this.requestAssistantEvaluation(project, task, inspection, runningState, execution);
287
505
  this.ensureTaskExecutionActive(task.id, execution);
288
- switch (evaluation.decision) {
289
- case "completed":
290
- return this.persistWithRoundIfExecutionActive(task.id, execution, {
291
- ...baseUpdate,
292
- status: "completed",
293
- waitingReason: null,
294
- nextCheckAt: null,
295
- completedAt: referenceAt,
296
- lastAutomationAt: referenceAt,
297
- lastAutomationSummary: evaluation.summary
298
- }, {
299
- kind: "completed",
300
- status: "completed",
301
- summary: evaluation.summary,
302
- waitingReason: null,
303
- continuePrompt: null,
304
- observedRunningState: runningState,
305
- autoContinueCount: task.autoContinueCount,
306
- createdAt: referenceAt
307
- });
308
- case "waiting_user":
309
- return this.persistWithRoundIfExecutionActive(task.id, execution, {
310
- ...baseUpdate,
311
- status: "waiting_user",
312
- waitingReason: evaluation.waitingReason ?? evaluation.summary,
313
- nextCheckAt: null,
314
- completedAt: null,
315
- lastAutomationAt: referenceAt,
316
- lastAutomationSummary: evaluation.summary
317
- }, {
318
- kind: "waiting_user",
319
- status: "waiting_user",
320
- summary: evaluation.summary,
321
- waitingReason: evaluation.waitingReason ?? evaluation.summary,
322
- continuePrompt: null,
323
- observedRunningState: runningState,
324
- autoContinueCount: task.autoContinueCount,
325
- createdAt: referenceAt
326
- });
327
- case "failed":
328
- return this.persistWithRoundIfExecutionActive(task.id, execution, {
329
- ...baseUpdate,
330
- status: "failed",
331
- waitingReason: evaluation.waitingReason ?? evaluation.summary,
332
- nextCheckAt: null,
333
- completedAt: null,
334
- lastAutomationAt: referenceAt,
335
- lastAutomationSummary: evaluation.summary
336
- }, {
337
- kind: "failed",
338
- status: "failed",
339
- summary: evaluation.summary,
340
- waitingReason: evaluation.waitingReason ?? evaluation.summary,
341
- continuePrompt: null,
342
- observedRunningState: runningState,
343
- autoContinueCount: task.autoContinueCount,
344
- createdAt: referenceAt
345
- });
346
- case "continue":
347
- if (!evaluation.continuePrompt) {
348
- return this.persistWithRoundIfExecutionActive(task.id, execution, {
349
- ...baseUpdate,
350
- status: "failed",
351
- waitingReason: "后台评估助手没有返回可继续推进的指令。",
352
- nextCheckAt: null,
353
- completedAt: null,
354
- lastAutomationAt: referenceAt,
355
- lastAutomationSummary: evaluation.summary
356
- }, {
357
- kind: "failed",
358
- status: "failed",
359
- summary: evaluation.summary,
360
- waitingReason: "后台评估助手没有返回可继续推进的指令。",
361
- continuePrompt: null,
362
- observedRunningState: runningState,
363
- autoContinueCount: task.autoContinueCount,
364
- createdAt: referenceAt
365
- });
366
- }
367
- this.ensureTaskExecutionActive(task.id, execution);
368
- const sendResult = await this.sendContinuePrompt(task, evaluation.continuePrompt, referenceAt);
369
- this.ensureTaskExecutionActive(task.id, execution);
370
- this.butlerSessionService.captureSessionSnapshot(task.projectId, task.butlerSessionId, task.createdByUserId, { sourceKind: "manual" });
371
- const nextAutoContinueCount = task.autoContinueCount + 1;
372
- const nextSummary = sendResult.delivery === "queued"
373
- ? buildQueuedFollowUpSummary(evaluation.summary, sendResult.queueItem)
374
- : evaluation.summary;
375
- return this.persistWithRoundIfExecutionActive(task.id, execution, {
376
- ...baseUpdate,
377
- status: "active",
378
- waitingReason: null,
379
- nextCheckAt: shiftSeconds(referenceAt, task.checkIntervalSeconds),
380
- lastAutomationAt: referenceAt,
381
- autoContinueCount: nextAutoContinueCount,
382
- lastAutomationSummary: nextSummary
383
- }, {
384
- kind: sendResult.delivery === "queued" ? "queued" : "continue",
385
- status: "active",
386
- summary: nextSummary,
387
- waitingReason: null,
388
- continuePrompt: evaluation.continuePrompt,
389
- observedRunningState: runningState,
390
- autoContinueCount: nextAutoContinueCount,
391
- createdAt: referenceAt
392
- });
393
- default:
394
- return this.persistWithRoundIfExecutionActive(task.id, execution, {
395
- ...baseUpdate,
396
- status: "failed",
397
- waitingReason: "后台评估助手返回了不支持的决策。",
398
- nextCheckAt: null,
399
- completedAt: null,
400
- lastAutomationAt: referenceAt,
401
- lastAutomationSummary: "后台评估助手返回了不支持的决策。"
402
- }, {
403
- kind: "failed",
404
- status: "failed",
405
- summary: "后台评估助手返回了不支持的决策。",
406
- waitingReason: "后台评估助手返回了不支持的决策。",
407
- continuePrompt: null,
408
- observedRunningState: runningState,
409
- autoContinueCount: task.autoContinueCount,
410
- createdAt: referenceAt
411
- });
412
- }
506
+ return this.requireAssistantDecisionPersisted(task.id, progressBeforeDispatch);
413
507
  }
414
508
  catch (error) {
415
509
  if (error instanceof FollowUpTaskCancelledError) {
416
510
  return this.butlerFollowUpTaskRepository.findById(task.id) ?? task;
417
511
  }
512
+ const providerUsageLimit = resolveProviderUsageLimitFromError(error, task.providerId, referenceAt);
513
+ if (providerUsageLimit) {
514
+ return this.persistIfExecutionActive(task.id, execution, {
515
+ ...baseUpdate,
516
+ status: "active",
517
+ waitingReason: null,
518
+ nextCheckAt: resolveProviderUsageLimitNextCheckAt(providerUsageLimit, referenceAt, task.checkIntervalSeconds),
519
+ completedAt: null,
520
+ lastAutomationAt: referenceAt,
521
+ lastAutomationSummary: buildFollowUpUsageLimitSummary(providerUsageLimit, "跟进助手会话当前被 provider 额度限制暂时挡住。")
522
+ });
523
+ }
418
524
  if (isDeferredFollowUpSendError(error)) {
419
525
  return this.persistIfExecutionActive(task.id, execution, {
420
526
  ...baseUpdate,
@@ -423,11 +529,11 @@ export class ButlerFollowUpService {
423
529
  nextCheckAt: shiftSeconds(referenceAt, task.checkIntervalSeconds),
424
530
  completedAt: null,
425
531
  lastAutomationAt: referenceAt,
426
- lastAutomationSummary: "当前会话又进入运行态,本轮不插话,等待下一次检查。"
532
+ lastAutomationSummary: "跟进助手会话当前仍在运行,本轮继续等待下一次检查。"
427
533
  });
428
534
  }
429
535
  const detail = error instanceof Error ? error.message : String(error);
430
- const summary = `后台评估助手执行失败:${detail}`;
536
+ const summary = `跟进助手执行失败:${detail}`;
431
537
  return this.persistWithRoundIfExecutionActive(task.id, execution, {
432
538
  ...baseUpdate,
433
539
  status: "failed",
@@ -464,6 +570,16 @@ export class ButlerFollowUpService {
464
570
  }
465
571
  return this.butlerFollowUpTaskRepository.update(normalizedTask) ?? normalizedTask;
466
572
  }
573
+ requireAssistantDecisionPersisted(taskId, before) {
574
+ const updated = this.butlerFollowUpTaskRepository.findById(taskId);
575
+ if (!updated) {
576
+ throw new Error("跟进任务在回写结果前已丢失");
577
+ }
578
+ if (!hasTaskProgressAdvanced(updated, before)) {
579
+ throw new Error("跟进助手没有通过 follow-ups 命令回写本轮结果");
580
+ }
581
+ return updated;
582
+ }
467
583
  persistWithRound(task, round) {
468
584
  const normalizedRounds = normalizeFollowUpRounds(task.rounds);
469
585
  return this.persist({
@@ -533,7 +649,7 @@ export class ButlerFollowUpService {
533
649
  && normalizeNullableIso(session.lastMessageAt) === normalizeNullableIso(task.lastObservedMessageAt)
534
650
  && session.messageCount === task.lastObservedMessageCount);
535
651
  }
536
- async sendContinuePrompt(task, continuePrompt, referenceAt) {
652
+ async sendContinuePrompt(task, providerId, continuePrompt, referenceAt) {
537
653
  const clientRequestId = buildFollowUpClientRequestId(task.id, referenceAt);
538
654
  try {
539
655
  const result = await this.sessionLiveRuntimeService.sendLiveMessage({
@@ -554,6 +670,13 @@ export class ButlerFollowUpService {
554
670
  };
555
671
  }
556
672
  catch (error) {
673
+ const providerUsageLimit = resolveProviderUsageLimitFromError(error, providerId, referenceAt);
674
+ if (providerUsageLimit) {
675
+ return {
676
+ delivery: "cooldown",
677
+ providerUsageLimit
678
+ };
679
+ }
557
680
  if (!isDeferredFollowUpSendError(error)) {
558
681
  throw error;
559
682
  }
@@ -589,32 +712,36 @@ export class ButlerFollowUpService {
589
712
  });
590
713
  }
591
714
  async inspectTask(task) {
592
- const session = this.sessionHistoryService.getSession(task.sessionId, task.createdByUserId);
593
- const runtime = await this.sessionLiveRuntimeService.getSessionRuntime(task.sessionId, task.createdByUserId);
594
- const envelope = await this.sessionHistoryService.readRecentHistoryEnvelope(task.sessionId, RECENT_HISTORY_LIMIT);
715
+ return this.inspectSession(task.sessionId, task.createdByUserId);
716
+ }
717
+ async inspectSession(sessionId, userId) {
718
+ const session = this.sessionHistoryService.getSession(sessionId, userId);
719
+ const runtime = await this.sessionLiveRuntimeService.getSessionRuntime(sessionId, userId);
720
+ const envelope = await this.sessionHistoryService.readRecentHistoryEnvelope(sessionId, RECENT_HISTORY_LIMIT);
721
+ const latestAssistantText = resolveLatestAssistantText(envelope);
595
722
  const sortedMessages = (envelope?.messages ?? [])
596
723
  .slice()
597
724
  .sort((left, right) => left.sequence - right.sequence);
725
+ const providerUsageLimit = resolveInspectionProviderUsageLimit(session.provider, session.lastErrorDetail, latestAssistantText, session.lastMessageAt);
598
726
  return {
727
+ providerId: session.provider,
599
728
  runningState: normalizeRunningState(runtime.runningState),
600
729
  messageAt: session.lastMessageAt,
601
730
  messageCount: session.messageCount,
602
731
  sessionTitle: session.title ?? null,
603
- latestAssistantText: resolveLatestAssistantText(envelope),
732
+ providerUsageLimit,
733
+ latestAssistantText,
604
734
  transcriptLines: sortedMessages.map((message) => renderHistoryLine(message.sequence, message.role, message.kind ?? "text", message.timestamp, message.content))
605
735
  };
606
736
  }
607
- async evaluateTask(profile, project, task, inspection, runningState, execution) {
608
- const evaluatorWorkspacePath = path.join(profile.workspacePath, FOLLOW_UP_EVALUATOR_DIRNAME);
609
- ensureButlerWorkspaceIsolation(evaluatorWorkspacePath);
610
- this.writeEvaluationInstructionFiles(evaluatorWorkspacePath, profile.providerId);
611
- this.syncCodexInstructionConfig(profile.providerId, evaluatorWorkspacePath);
612
- const workspace = this.workspaceService.importWorkspace(evaluatorWorkspacePath, "代码助手");
737
+ async requestAssistantEvaluation(project, task, inspection, runningState, execution) {
613
738
  const instruction = this.instructionAdapter.buildInstruction({
614
- providerId: profile.providerId,
739
+ taskId: task.id,
740
+ providerId: task.providerId,
615
741
  project,
616
742
  sessionId: task.sessionId,
617
743
  butlerSessionId: task.butlerSessionId,
744
+ assistantSessionId: task.assistantSessionId,
618
745
  sessionTitle: inspection.sessionTitle,
619
746
  objective: task.objective,
620
747
  completionCriteria: task.completionCriteria,
@@ -627,34 +754,35 @@ export class ButlerFollowUpService {
627
754
  latestAssistantText: inspection.latestAssistantText,
628
755
  transcriptLines: inspection.transcriptLines
629
756
  });
630
- const adapter = this.providerAdapterRegistry.get(profile.providerId);
631
- const launch = await adapter.startPatrolSession({
632
- workspaceId: workspace.id,
633
- userId: task.createdByUserId,
634
- providerId: profile.providerId,
635
- prompt: instruction.prompt,
636
- model: resolveFollowUpModel(profile.providerId, this.sourceCodexHomeDir),
637
- reasoningLevel: "low",
638
- permissionMode: "default",
639
- instructionFilePath: resolveFollowUpInstructionFilePath(profile.providerId, evaluatorWorkspacePath)
640
- });
641
- execution.evaluatorSessionId = launch.sessionId;
757
+ execution.assistantSessionId = task.assistantSessionId;
642
758
  try {
643
- await adapter.waitForSessionTerminal(launch.sessionId);
759
+ await this.waitForAssistantSessionTerminal(task.assistantSessionId, task.createdByUserId);
760
+ this.ensureTaskExecutionActive(task.id, execution);
761
+ await this.sessionLiveRuntimeService.sendLiveMessage({
762
+ sessionId: task.assistantSessionId,
763
+ userId: task.createdByUserId,
764
+ content: instruction.prompt,
765
+ clientRequestId: null,
766
+ runtimeOptions: {
767
+ model: resolveFollowUpModel(task.providerId, this.sourceCodexHomeDir),
768
+ reasoningLevel: "low",
769
+ permissionMode: "default",
770
+ attachments: []
771
+ }
772
+ });
773
+ await this.waitForAssistantSessionTerminal(task.assistantSessionId, task.createdByUserId);
644
774
  this.ensureTaskExecutionActive(task.id, execution);
645
- const result = await adapter.readPatrolResult(launch.sessionId);
646
- return parseEvaluationResult(result);
647
775
  }
648
776
  finally {
649
- if (execution.evaluatorSessionId === launch.sessionId) {
650
- execution.evaluatorSessionId = null;
777
+ if (execution.assistantSessionId === task.assistantSessionId) {
778
+ execution.assistantSessionId = null;
651
779
  }
652
780
  }
653
781
  }
654
782
  beginTaskExecution(taskId) {
655
783
  const execution = {
656
784
  cancelled: false,
657
- evaluatorSessionId: null
785
+ assistantSessionId: null
658
786
  };
659
787
  this.activeExecutionStateByTaskId.set(taskId, execution);
660
788
  return execution;
@@ -675,61 +803,33 @@ export class ButlerFollowUpService {
675
803
  const current = this.activeExecutionStateByTaskId.get(taskId);
676
804
  return Boolean(current && current === execution && !execution.cancelled);
677
805
  }
678
- async stopActiveTaskAutomation(execution) {
679
- if (!execution?.evaluatorSessionId) {
806
+ async stopActiveTaskAutomation(task, execution) {
807
+ if (!execution?.assistantSessionId) {
680
808
  return;
681
809
  }
682
- const profile = this.butlerProfileService.ensureInitialized();
683
- const adapter = this.providerAdapterRegistry.get(profile.providerId);
684
810
  try {
685
- await adapter.interruptPatrolSession(execution.evaluatorSessionId);
811
+ await this.sessionLiveRuntimeService.interruptSession(execution.assistantSessionId, task.createdByUserId);
686
812
  }
687
813
  catch (error) {
688
- console.warn("[butler-follow-up] interrupt evaluator session failed", {
689
- sessionId: execution.evaluatorSessionId,
814
+ console.warn("[butler-follow-up] interrupt assistant follow-up session failed", {
815
+ sessionId: execution.assistantSessionId,
690
816
  error: error instanceof Error ? error.message : String(error)
691
817
  });
692
818
  }
693
819
  finally {
694
- execution.evaluatorSessionId = null;
820
+ execution.assistantSessionId = null;
695
821
  }
696
822
  }
697
- writeEvaluationInstructionFiles(workspacePath, providerId) {
698
- const content = [
699
- "# 代码助手后台跟进评估规则",
700
- "",
701
- "你不是普通项目会话,也不是面向用户的聊天助手。",
702
- "你的身份是后台跟进评估器,只负责判断某个开发会话现在该继续推进、等用户决定、还是已经完成。",
703
- "如果目标或上下文里提到了 spec,完成标准只能按 spec 明确要求的必做项判断。",
704
- "“建议下一步”“最佳实践”“可以顺手优化”这类内容默认都不是必做项,不能据此继续扩范围。",
705
- "如果没有 spec,就先从目标和最近消息里归纳一句当前核心任务,后续只能围绕这个核心任务判断,不准无限扩展。",
706
- "除非目标本身要求,否则不要把重构、补测试、补体验优化之类建议项升级成必须开发的工作。",
707
- "禁止照搬最后一句回复做草率判断,必须结合用户目标、当前运行态和最近消息一起判断。",
708
- "如果能继续推进,就直接给出下一条要发给开发会话的中文指令,不要空谈。",
709
- "如果确实需要用户决定,要把缺口说清楚,但不要替用户做不存在的决定。",
710
- "输出语言必须是中文,先给结论,再给结构化 JSON。"
711
- ].join("\n");
712
- writeFileIfChanged(path.join(workspacePath, "AGENTS.md"), `${content}\n`);
713
- if (providerId === "claude-code") {
714
- writeFileIfChanged(path.join(workspacePath, "CLAUDE.md"), `${content}\n`);
715
- }
716
- }
717
- syncCodexInstructionConfig(providerId, workspacePath) {
718
- if (providerId !== "codex" || !this.followUpCodexHomeDir?.trim()) {
719
- return;
823
+ async waitForAssistantSessionTerminal(sessionId, userId) {
824
+ const startedAt = Date.now();
825
+ while (Date.now() - startedAt < FOLLOW_UP_ASSISTANT_WAIT_TIMEOUT_MS) {
826
+ const runtime = await this.sessionLiveRuntimeService.getSessionRuntime(sessionId, userId);
827
+ if (isAssistantTerminalRuntimeState(runtime.runningState)) {
828
+ return;
829
+ }
830
+ await delay(FOLLOW_UP_ASSISTANT_WAIT_POLL_INTERVAL_MS);
720
831
  }
721
- const targetHomeDir = path.resolve(this.followUpCodexHomeDir);
722
- const sourceHomeDir = resolveSourceCodexHomeDir(this.sourceCodexHomeDir, targetHomeDir);
723
- const sourceConfigPath = path.join(sourceHomeDir, "config.toml");
724
- const sourceConfigContent = sourceHomeDir !== targetHomeDir && fs.existsSync(sourceConfigPath) && fs.statSync(sourceConfigPath).isFile()
725
- ? fs.readFileSync(sourceConfigPath, "utf8")
726
- : "";
727
- const instructionFilePath = path.join(workspacePath, "AGENTS.md");
728
- fs.mkdirSync(targetHomeDir, { recursive: true });
729
- removeFileIfExists(path.join(targetHomeDir, "AGENTS.md"));
730
- removeFileIfExists(path.join(targetHomeDir, "AGENTS.override.md"));
731
- syncOptionalFile(path.join(sourceHomeDir, "auth.json"), path.join(targetHomeDir, "auth.json"));
732
- writeFileIfChanged(path.join(targetHomeDir, "config.toml"), `${composeCodexConfigContent(sourceConfigContent, instructionFilePath)}\n`);
832
+ throw new Error(`BUTLER_FOLLOW_UP_ASSISTANT_WAIT_TIMEOUT:${sessionId}`);
733
833
  }
734
834
  }
735
835
  function mapTaskView(task, workspaceId, projectName, sessionTitle) {
@@ -741,6 +841,9 @@ function mapTaskView(task, workspaceId, projectName, sessionTitle) {
741
841
  workspaceId,
742
842
  butlerSessionId: task.butlerSessionId,
743
843
  sessionId: task.sessionId,
844
+ providerId: task.providerId,
845
+ assistantButlerSessionId: task.assistantButlerSessionId,
846
+ assistantSessionId: task.assistantSessionId,
744
847
  sessionTitle,
745
848
  objective: task.objective,
746
849
  completionCriteria: task.completionCriteria,
@@ -818,6 +921,24 @@ function normalizeObjective(value) {
818
921
  }
819
922
  return normalized;
820
923
  }
924
+ function normalizeFollowUpProviderId(value) {
925
+ switch (value) {
926
+ case undefined:
927
+ case null:
928
+ case "":
929
+ return "codex";
930
+ case "codex":
931
+ case "claude-code":
932
+ return value;
933
+ default:
934
+ throw new AppError({
935
+ statusCode: 400,
936
+ errorCode: "INVALID_INPUT",
937
+ detail: "会话跟进只允许选择 Codex 或 Claude Code",
938
+ field: "providerId"
939
+ });
940
+ }
941
+ }
821
942
  function normalizeCompletionCriteria(value, objective) {
822
943
  const normalized = value?.trim();
823
944
  return normalized && normalized.length > 0
@@ -843,6 +964,16 @@ function buildFollowUpClientRequestId(taskId, referenceAt) {
843
964
  function buildQueuedFollowUpSummary(summary, queueItem) {
844
965
  return `${summary} 已转入消息队列,等待当前会话空闲后自动补发(队列项 ${queueItem.orderIndex})。`;
845
966
  }
967
+ function buildFollowUpUsageLimitSummary(providerUsageLimit, prefix) {
968
+ return `${prefix} ${providerUsageLimit.summary}`;
969
+ }
970
+ function resolveProviderUsageLimitNextCheckAt(providerUsageLimit, referenceAt, fallbackSeconds) {
971
+ const blockedUntil = resolveProviderUsageLimitBlockedUntil(providerUsageLimit, referenceAt);
972
+ if (blockedUntil && Date.parse(blockedUntil) > Date.parse(referenceAt)) {
973
+ return blockedUntil;
974
+ }
975
+ return shiftSeconds(referenceAt, fallbackSeconds);
976
+ }
846
977
  function isDeferredFollowUpSendError(error) {
847
978
  if (error instanceof AppError) {
848
979
  return (error.errorCode === "ACTIVE_RUN_EXISTS"
@@ -864,6 +995,14 @@ function isDeferredFollowUpSendError(error) {
864
995
  || error.message === "SERVER_TIMEOUT"
865
996
  || error.message.includes("当前会话正在运行"));
866
997
  }
998
+ function isAssistantTerminalRuntimeState(state) {
999
+ return state === "idle" || state === "completed" || state === "failed" || state === "interrupted";
1000
+ }
1001
+ function delay(timeoutMs) {
1002
+ return new Promise((resolve) => {
1003
+ setTimeout(resolve, timeoutMs);
1004
+ });
1005
+ }
867
1006
  function isSyntheticMessageId(messageId) {
868
1007
  return typeof messageId === "string" && messageId.startsWith("synthetic-");
869
1008
  }
@@ -891,6 +1030,24 @@ function normalizeNullableIso(value) {
891
1030
  const normalized = value?.trim();
892
1031
  return normalized && normalized.length > 0 ? normalized : null;
893
1032
  }
1033
+ function resolveInspectionProviderUsageLimit(providerId, lastErrorDetail, latestAssistantText, referenceAt) {
1034
+ const normalizedReferenceAt = normalizeNullableIso(referenceAt) ?? undefined;
1035
+ const fromErrorDetail = normalizeProviderUsageLimit({
1036
+ providerId,
1037
+ text: lastErrorDetail,
1038
+ referenceAt: normalizedReferenceAt,
1039
+ source: "error_detail"
1040
+ });
1041
+ if (fromErrorDetail) {
1042
+ return fromErrorDetail;
1043
+ }
1044
+ return normalizeProviderUsageLimit({
1045
+ providerId,
1046
+ text: latestAssistantText,
1047
+ referenceAt: normalizedReferenceAt,
1048
+ source: "message"
1049
+ });
1050
+ }
894
1051
  function resolveLatestAssistantText(envelope) {
895
1052
  if (!envelope || envelope.messages.length === 0) {
896
1053
  return null;
@@ -912,143 +1069,91 @@ function truncateText(value, maxLength) {
912
1069
  }
913
1070
  return `${value.slice(0, Math.max(0, maxLength - 1))}…`;
914
1071
  }
1072
+ function buildFollowUpBootstrapPrompt(input) {
1073
+ const transcript = input.transcriptLines.length > 0
1074
+ ? input.transcriptLines.slice(-12).join("\n")
1075
+ : "- 暂时没有可用消息,请先按会话现状建立上下文。";
1076
+ return [
1077
+ "你现在是这条开发会话的专用跟进助手,会长期复用当前助手会话推进,不再切回隐藏评估器。",
1078
+ "你的职责只有三件事:",
1079
+ "1. 用 codingns assistant CLI 复核目标项目和目标会话的最新状态。",
1080
+ "2. 判断当前是否真的还需要继续跟进,还是应该等待用户决定,或者已经可以结束。",
1081
+ "3. 用 codingns assistant sessions send 和 codingns assistant follow-ups.* 自己完成发消息与任务回写,不要等后台代发或猜结果。",
1082
+ "",
1083
+ "硬约束:",
1084
+ "- 不要直接改当前仓库代码,这条会话只负责跟进判断和向目标开发会话发消息。",
1085
+ "- 如果决定继续,必须显式使用 `codingns assistant sessions send` 把中文跟进消息发到目标开发会话。",
1086
+ "- 每一轮正式结论都必须用 `codingns assistant follow-ups continue|waiting-user|complete|fail` 之一回写到跟进任务。",
1087
+ "- 如果信息不足或需要用户决策,要明确说明缺口,不要假装已经发消息。",
1088
+ "- 跟进边界只围绕当前目标和结束条件,不准顺手扩范围。",
1089
+ "",
1090
+ `项目名称:${input.project.name}`,
1091
+ `项目路径:${input.project.repoRoot}`,
1092
+ `目标 Butler 会话 ID:${input.sourceButlerSessionId}`,
1093
+ `目标真实会话 ID:${input.sourceSessionId}`,
1094
+ `目标会话标题:${input.sourceSessionTitle ?? "未命名会话"}`,
1095
+ `跟进目标:${input.objective}`,
1096
+ `结束条件:${input.completionCriteria}`,
1097
+ `最多自动推进轮数:${input.maxAutoContinueCount}`,
1098
+ `最近一条助手消息:${input.latestAssistantText?.trim() || "无"}`,
1099
+ "",
1100
+ "最近消息摘录:",
1101
+ transcript,
1102
+ "",
1103
+ "这条消息只用来建立上下文。请先整理当前理解,后续我会继续给你发送正式的跟进检查请求。"
1104
+ ].join("\n");
1105
+ }
915
1106
  function resolveFollowUpModel(providerId, sourceCodexHomeDir) {
916
1107
  if (providerId !== "codex") {
917
1108
  return "haiku";
918
1109
  }
919
1110
  return resolveButlerCodexBackgroundModel("gpt-5.1-codex-mini", sourceCodexHomeDir);
920
1111
  }
921
- function resolveFollowUpInstructionFilePath(providerId, workspacePath) {
922
- return path.join(workspacePath, providerId === "claude-code" ? "CLAUDE.md" : "AGENTS.md");
923
- }
924
- function parseEvaluationResult(result) {
925
- const rawJson = result.structured.rawJson ?? extractJsonFromText(result.latestAssistantMessage);
926
- if (!rawJson) {
927
- throw new Error("后台评估助手没有返回结构化 JSON");
928
- }
929
- let parsed;
930
- try {
931
- parsed = JSON.parse(rawJson);
932
- }
933
- catch (error) {
934
- throw new Error(`后台评估助手返回的 JSON 无法解析:${error instanceof Error ? error.message : String(error)}`);
935
- }
936
- const decision = normalizeDecision(parsed.decision);
937
- if (!decision) {
938
- throw new Error("后台评估助手返回的 decision 不合法");
939
- }
940
- const summary = normalizeNonEmptyString(parsed.summary) ?? result.structured.summary ?? "后台评估助手未提供摘要";
941
- const waitingReason = normalizeNullableString(parsed.waitingReason);
942
- const continuePrompt = normalizeNullableString(parsed.continuePrompt);
943
- const riskLevel = normalizeRiskLevel(parsed.riskLevel);
1112
+ function snapshotTaskProgress(task) {
944
1113
  return {
945
- decision,
946
- summary,
947
- waitingReason,
948
- continuePrompt,
949
- riskLevel
1114
+ roundCount: normalizeFollowUpRounds(task.rounds).length,
1115
+ updatedAt: task.updatedAt,
1116
+ lastAutomationAt: task.lastAutomationAt,
1117
+ autoContinueCount: task.autoContinueCount
950
1118
  };
951
1119
  }
952
- function normalizeDecision(value) {
953
- switch (value) {
954
- case "continue":
955
- case "waiting_user":
956
- case "completed":
957
- case "failed":
958
- return value;
959
- default:
960
- return null;
961
- }
962
- }
963
- function normalizeRiskLevel(value) {
964
- switch (value) {
965
- case "low":
966
- case "medium":
967
- case "high":
968
- return value;
969
- default:
970
- return null;
971
- }
1120
+ function hasTaskProgressAdvanced(task, before) {
1121
+ const roundCount = normalizeFollowUpRounds(task.rounds).length;
1122
+ return (roundCount > before.roundCount
1123
+ || task.updatedAt !== before.updatedAt
1124
+ || task.lastAutomationAt !== before.lastAutomationAt
1125
+ || task.autoContinueCount !== before.autoContinueCount);
972
1126
  }
973
- function normalizeNonEmptyString(value) {
1127
+ function requireNonEmptyFollowUpText(value, field, detail) {
974
1128
  if (typeof value !== "string") {
975
- return null;
1129
+ throw new AppError({
1130
+ statusCode: 400,
1131
+ errorCode: "BUTLER_FOLLOW_UP_TASK_INVALID_INPUT",
1132
+ detail
1133
+ });
976
1134
  }
977
1135
  const normalized = value.trim();
978
- return normalized || null;
979
- }
980
- function normalizeNullableString(value) {
981
- if (value === null || value === undefined) {
982
- return null;
983
- }
984
- return normalizeNonEmptyString(value);
985
- }
986
- function extractJsonFromText(value) {
987
- if (!value) {
988
- return null;
989
- }
990
- const matched = value.match(/```json\s*([\s\S]*?)```/i);
991
- const raw = matched?.[1]?.trim();
992
- return raw || null;
993
- }
994
- function resolveSourceCodexHomeDir(sourceCodexHomeDir, targetHomeDir) {
995
- const configuredSource = sourceCodexHomeDir?.trim();
996
- if (configuredSource) {
997
- const resolvedConfiguredSource = path.resolve(configuredSource);
998
- if (resolvedConfiguredSource !== targetHomeDir) {
999
- return resolvedConfiguredSource;
1000
- }
1001
- }
1002
- const fallbackHomeDir = path.resolve(path.join(os.homedir(), ".codex"));
1003
- if (fallbackHomeDir !== targetHomeDir) {
1004
- return fallbackHomeDir;
1005
- }
1006
- return targetHomeDir;
1007
- }
1008
- function composeCodexConfigContent(sourceConfigContent, instructionFilePath) {
1009
- const normalizedSource = sourceConfigContent
1010
- .split(/\r?\n/)
1011
- .filter((line) => {
1012
- const trimmed = line.trim();
1013
- return trimmed.length > 0 && !trimmed.startsWith("model_instructions_file");
1014
- })
1015
- .join("\n")
1016
- .trim();
1017
- return [
1018
- "# 代码助手跟进评估专用 Codex 配置(系统自动生成)",
1019
- normalizedSource,
1020
- `model_instructions_file = ${toTomlString(path.resolve(instructionFilePath))}`
1021
- ]
1022
- .filter((part) => part.trim().length > 0)
1023
- .join("\n\n");
1024
- }
1025
- function toTomlString(value) {
1026
- return `"${value.replaceAll("\\", "\\\\").replaceAll("\"", "\\\"")}"`;
1027
- }
1028
- function writeFileIfChanged(filePath, content) {
1029
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
1030
- if (fs.existsSync(filePath) && fs.readFileSync(filePath, "utf8") === content) {
1031
- return;
1032
- }
1033
- fs.writeFileSync(filePath, content, "utf8");
1034
- }
1035
- function removeFileIfExists(filePath) {
1036
- if (!fs.existsSync(filePath)) {
1037
- return;
1136
+ if (!normalized) {
1137
+ throw new AppError({
1138
+ statusCode: 400,
1139
+ errorCode: "BUTLER_FOLLOW_UP_TASK_INVALID_INPUT",
1140
+ detail
1141
+ });
1038
1142
  }
1039
- if (fs.statSync(filePath).isFile()) {
1040
- fs.rmSync(filePath, { force: true });
1143
+ if (normalized.length > 4000) {
1144
+ throw new AppError({
1145
+ statusCode: 400,
1146
+ errorCode: "BUTLER_FOLLOW_UP_TASK_INVALID_INPUT",
1147
+ detail: `${field} 长度不能超过 4000 个字符`
1148
+ });
1041
1149
  }
1150
+ return normalized;
1042
1151
  }
1043
- function syncOptionalFile(sourcePath, targetPath) {
1044
- if (!fs.existsSync(sourcePath) || !fs.statSync(sourcePath).isFile()) {
1045
- removeFileIfExists(targetPath);
1046
- return;
1047
- }
1048
- fs.mkdirSync(path.dirname(targetPath), { recursive: true });
1049
- if (fs.existsSync(targetPath) && fs.readFileSync(targetPath).equals(fs.readFileSync(sourcePath))) {
1050
- return;
1152
+ function normalizeNullableText(value) {
1153
+ if (typeof value !== "string") {
1154
+ return null;
1051
1155
  }
1052
- fs.copyFileSync(sourcePath, targetPath);
1156
+ const normalized = value.trim();
1157
+ return normalized || null;
1053
1158
  }
1054
1159
  //# sourceMappingURL=butler-follow-up-service.js.map