agent-tower 0.7.2-beta.7 → 0.7.2-beta.8

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 (550) hide show
  1. package/dist/app.d.ts.map +1 -1
  2. package/dist/app.js +4 -1
  3. package/dist/app.js.map +1 -1
  4. package/dist/app.test.js +80 -1
  5. package/dist/app.test.js.map +1 -1
  6. package/dist/cli.js +4 -10
  7. package/dist/cli.js.map +1 -1
  8. package/dist/core/container.d.ts +8 -0
  9. package/dist/core/container.d.ts.map +1 -1
  10. package/dist/core/container.js +42 -0
  11. package/dist/core/container.js.map +1 -1
  12. package/dist/core/event-bus.d.ts +2 -0
  13. package/dist/core/event-bus.d.ts.map +1 -1
  14. package/dist/core/event-bus.js.map +1 -1
  15. package/dist/executors/__tests__/image-utils.test.d.ts +2 -0
  16. package/dist/executors/__tests__/image-utils.test.d.ts.map +1 -0
  17. package/dist/executors/__tests__/image-utils.test.js +25 -0
  18. package/dist/executors/__tests__/image-utils.test.js.map +1 -0
  19. package/dist/executors/base.executor.d.ts +2 -0
  20. package/dist/executors/base.executor.d.ts.map +1 -1
  21. package/dist/executors/base.executor.js +6 -0
  22. package/dist/executors/base.executor.js.map +1 -1
  23. package/dist/executors/image-utils.d.ts.map +1 -1
  24. package/dist/executors/image-utils.js +10 -8
  25. package/dist/executors/image-utils.js.map +1 -1
  26. package/dist/git/git-cli.d.ts +2 -0
  27. package/dist/git/git-cli.d.ts.map +1 -1
  28. package/dist/git/git-cli.js +2 -0
  29. package/dist/git/git-cli.js.map +1 -1
  30. package/dist/git/worktree.manager.d.ts +2 -1
  31. package/dist/git/worktree.manager.d.ts.map +1 -1
  32. package/dist/git/worktree.manager.js +18 -12
  33. package/dist/git/worktree.manager.js.map +1 -1
  34. package/dist/index.js +16 -31
  35. package/dist/index.js.map +1 -1
  36. package/dist/mcp/http-client.d.ts +1 -0
  37. package/dist/mcp/http-client.d.ts.map +1 -1
  38. package/dist/mcp/http-client.js +3 -0
  39. package/dist/mcp/http-client.js.map +1 -1
  40. package/dist/mcp/server.d.ts.map +1 -1
  41. package/dist/mcp/server.js +15 -0
  42. package/dist/mcp/server.js.map +1 -1
  43. package/dist/mcp/tools/sessions.js +2 -2
  44. package/dist/mcp/tools/sessions.js.map +1 -1
  45. package/dist/mcp/tools/workspaces.d.ts.map +1 -1
  46. package/dist/mcp/tools/workspaces.js +2 -1
  47. package/dist/mcp/tools/workspaces.js.map +1 -1
  48. package/dist/middleware/access-auth.d.ts +1 -0
  49. package/dist/middleware/access-auth.d.ts.map +1 -1
  50. package/dist/middleware/access-auth.js +1 -1
  51. package/dist/middleware/access-auth.js.map +1 -1
  52. package/dist/output/__tests__/text-block-lifecycle.test.d.ts +2 -0
  53. package/dist/output/__tests__/text-block-lifecycle.test.d.ts.map +1 -0
  54. package/dist/output/__tests__/text-block-lifecycle.test.js +241 -0
  55. package/dist/output/__tests__/text-block-lifecycle.test.js.map +1 -0
  56. package/dist/output/claude-code-parser.d.ts +3 -0
  57. package/dist/output/claude-code-parser.d.ts.map +1 -1
  58. package/dist/output/claude-code-parser.js +58 -7
  59. package/dist/output/claude-code-parser.js.map +1 -1
  60. package/dist/output/codex-parser.d.ts.map +1 -1
  61. package/dist/output/codex-parser.js +4 -6
  62. package/dist/output/codex-parser.js.map +1 -1
  63. package/dist/output/cursor-agent-parser.d.ts.map +1 -1
  64. package/dist/output/cursor-agent-parser.js +4 -0
  65. package/dist/output/cursor-agent-parser.js.map +1 -1
  66. package/dist/output/gemini-parser.d.ts +18 -0
  67. package/dist/output/gemini-parser.d.ts.map +1 -0
  68. package/dist/output/gemini-parser.js +91 -0
  69. package/dist/output/gemini-parser.js.map +1 -0
  70. package/dist/output/msg-store.d.ts +6 -0
  71. package/dist/output/msg-store.d.ts.map +1 -1
  72. package/dist/output/msg-store.js +17 -0
  73. package/dist/output/msg-store.js.map +1 -1
  74. package/dist/output/text-block-tracker.d.ts +29 -0
  75. package/dist/output/text-block-tracker.d.ts.map +1 -0
  76. package/dist/output/text-block-tracker.js +149 -0
  77. package/dist/output/text-block-tracker.js.map +1 -0
  78. package/dist/output/types.d.ts +9 -0
  79. package/dist/output/types.d.ts.map +1 -1
  80. package/dist/output/types.js +0 -4
  81. package/dist/output/types.js.map +1 -1
  82. package/dist/routes/__tests__/sessions-runtime.test.js +3 -0
  83. package/dist/routes/__tests__/sessions-runtime.test.js.map +1 -1
  84. package/dist/routes/__tests__/team-run-translations.test.d.ts +2 -0
  85. package/dist/routes/__tests__/team-run-translations.test.d.ts.map +1 -0
  86. package/dist/routes/__tests__/team-run-translations.test.js +418 -0
  87. package/dist/routes/__tests__/team-run-translations.test.js.map +1 -0
  88. package/dist/routes/__tests__/translations.test.d.ts +2 -0
  89. package/dist/routes/__tests__/translations.test.d.ts.map +1 -0
  90. package/dist/routes/__tests__/translations.test.js +286 -0
  91. package/dist/routes/__tests__/translations.test.js.map +1 -0
  92. package/dist/routes/__tests__/workspaces.test.js +140 -0
  93. package/dist/routes/__tests__/workspaces.test.js.map +1 -1
  94. package/dist/routes/conversations.d.ts.map +1 -1
  95. package/dist/routes/conversations.js +19 -2
  96. package/dist/routes/conversations.js.map +1 -1
  97. package/dist/routes/index.d.ts.map +1 -1
  98. package/dist/routes/index.js +2 -0
  99. package/dist/routes/index.js.map +1 -1
  100. package/dist/routes/sessions.d.ts.map +1 -1
  101. package/dist/routes/sessions.js +32 -3
  102. package/dist/routes/sessions.js.map +1 -1
  103. package/dist/routes/team-runs.d.ts.map +1 -1
  104. package/dist/routes/team-runs.js +84 -15
  105. package/dist/routes/team-runs.js.map +1 -1
  106. package/dist/routes/translations.d.ts +4 -0
  107. package/dist/routes/translations.d.ts.map +1 -0
  108. package/dist/routes/translations.js +148 -0
  109. package/dist/routes/translations.js.map +1 -0
  110. package/dist/routes/user-input-origin.d.ts +4 -0
  111. package/dist/routes/user-input-origin.d.ts.map +1 -0
  112. package/dist/routes/user-input-origin.js +7 -0
  113. package/dist/routes/user-input-origin.js.map +1 -0
  114. package/dist/routes/workspaces.d.ts.map +1 -1
  115. package/dist/routes/workspaces.js +32 -2
  116. package/dist/routes/workspaces.js.map +1 -1
  117. package/dist/runtime/__tests__/acp-driver-lifecycle.test.js +2 -2
  118. package/dist/runtime/__tests__/acp-driver-lifecycle.test.js.map +1 -1
  119. package/dist/runtime/__tests__/acp-driver-submission.test.d.ts +2 -0
  120. package/dist/runtime/__tests__/acp-driver-submission.test.d.ts.map +1 -0
  121. package/dist/runtime/__tests__/acp-driver-submission.test.js +88 -0
  122. package/dist/runtime/__tests__/acp-driver-submission.test.js.map +1 -0
  123. package/dist/runtime/__tests__/acp-history-reconciler.test.js +244 -0
  124. package/dist/runtime/__tests__/acp-history-reconciler.test.js.map +1 -1
  125. package/dist/runtime/__tests__/acp-projector.test.js +36 -0
  126. package/dist/runtime/__tests__/acp-projector.test.js.map +1 -1
  127. package/dist/runtime/__tests__/claude-cli-control.test.js +20 -0
  128. package/dist/runtime/__tests__/claude-cli-control.test.js.map +1 -1
  129. package/dist/runtime/__tests__/cli-driver-submission.test.d.ts +2 -0
  130. package/dist/runtime/__tests__/cli-driver-submission.test.d.ts.map +1 -0
  131. package/dist/runtime/__tests__/cli-driver-submission.test.js +61 -0
  132. package/dist/runtime/__tests__/cli-driver-submission.test.js.map +1 -0
  133. package/dist/runtime/__tests__/codex-cli-control.test.js +25 -0
  134. package/dist/runtime/__tests__/codex-cli-control.test.js.map +1 -1
  135. package/dist/runtime/acp/acp-driver.js +38 -1
  136. package/dist/runtime/acp/acp-driver.js.map +1 -1
  137. package/dist/runtime/acp/history-reconciler.d.ts.map +1 -1
  138. package/dist/runtime/acp/history-reconciler.js +95 -36
  139. package/dist/runtime/acp/history-reconciler.js.map +1 -1
  140. package/dist/runtime/acp/projector.d.ts +5 -2
  141. package/dist/runtime/acp/projector.d.ts.map +1 -1
  142. package/dist/runtime/acp/projector.js +50 -23
  143. package/dist/runtime/acp/projector.js.map +1 -1
  144. package/dist/runtime/claude-cli-control.d.ts +4 -1
  145. package/dist/runtime/claude-cli-control.d.ts.map +1 -1
  146. package/dist/runtime/claude-cli-control.js +3 -1
  147. package/dist/runtime/claude-cli-control.js.map +1 -1
  148. package/dist/runtime/cli-driver.js +21 -7
  149. package/dist/runtime/cli-driver.js.map +1 -1
  150. package/dist/runtime/cli-parser.d.ts.map +1 -1
  151. package/dist/runtime/cli-parser.js +3 -0
  152. package/dist/runtime/cli-parser.js.map +1 -1
  153. package/dist/runtime/codex-cli-control.d.ts +4 -1
  154. package/dist/runtime/codex-cli-control.d.ts.map +1 -1
  155. package/dist/runtime/codex-cli-control.js +9 -6
  156. package/dist/runtime/codex-cli-control.js.map +1 -1
  157. package/dist/runtime/codex-cli-projector.d.ts +1 -1
  158. package/dist/runtime/codex-cli-projector.d.ts.map +1 -1
  159. package/dist/runtime/codex-cli-projector.js +4 -2
  160. package/dist/runtime/codex-cli-projector.js.map +1 -1
  161. package/dist/runtime/contracts.d.ts +15 -2
  162. package/dist/runtime/contracts.d.ts.map +1 -1
  163. package/dist/runtime/runtime-coordinator.d.ts.map +1 -1
  164. package/dist/runtime/runtime-coordinator.js +34 -1
  165. package/dist/runtime/runtime-coordinator.js.map +1 -1
  166. package/dist/services/__tests__/conflict-admission.test.d.ts +2 -0
  167. package/dist/services/__tests__/conflict-admission.test.d.ts.map +1 -0
  168. package/dist/services/__tests__/conflict-admission.test.js +37 -0
  169. package/dist/services/__tests__/conflict-admission.test.js.map +1 -0
  170. package/dist/services/__tests__/conflict-recovery-key.test.d.ts +2 -0
  171. package/dist/services/__tests__/conflict-recovery-key.test.d.ts.map +1 -0
  172. package/dist/services/__tests__/conflict-recovery-key.test.js +143 -0
  173. package/dist/services/__tests__/conflict-recovery-key.test.js.map +1 -0
  174. package/dist/services/__tests__/message-delivery.integration.test.d.ts +2 -0
  175. package/dist/services/__tests__/message-delivery.integration.test.d.ts.map +1 -0
  176. package/dist/services/__tests__/message-delivery.integration.test.js +1137 -0
  177. package/dist/services/__tests__/message-delivery.integration.test.js.map +1 -0
  178. package/dist/services/__tests__/review-team-user-counterexamples.test.d.ts +2 -0
  179. package/dist/services/__tests__/review-team-user-counterexamples.test.d.ts.map +1 -0
  180. package/dist/services/__tests__/review-team-user-counterexamples.test.js +200 -0
  181. package/dist/services/__tests__/review-team-user-counterexamples.test.js.map +1 -0
  182. package/dist/services/__tests__/session-manager.lifecycle.test.js +37 -0
  183. package/dist/services/__tests__/session-manager.lifecycle.test.js.map +1 -1
  184. package/dist/services/__tests__/session-manager.team-run.test.js +19 -2
  185. package/dist/services/__tests__/session-manager.team-run.test.js.map +1 -1
  186. package/dist/services/__tests__/team-initial-delivery.integration.test.d.ts +2 -0
  187. package/dist/services/__tests__/team-initial-delivery.integration.test.d.ts.map +1 -0
  188. package/dist/services/__tests__/team-initial-delivery.integration.test.js +469 -0
  189. package/dist/services/__tests__/team-initial-delivery.integration.test.js.map +1 -0
  190. package/dist/services/__tests__/team-reconciler.service.test.js +102 -101
  191. package/dist/services/__tests__/team-reconciler.service.test.js.map +1 -1
  192. package/dist/services/__tests__/team-room-delivery.test.d.ts +2 -0
  193. package/dist/services/__tests__/team-room-delivery.test.d.ts.map +1 -0
  194. package/dist/services/__tests__/team-room-delivery.test.js +670 -0
  195. package/dist/services/__tests__/team-room-delivery.test.js.map +1 -0
  196. package/dist/services/__tests__/team-user-delivery.integration.test.d.ts +2 -0
  197. package/dist/services/__tests__/team-user-delivery.integration.test.d.ts.map +1 -0
  198. package/dist/services/__tests__/team-user-delivery.integration.test.js +622 -0
  199. package/dist/services/__tests__/team-user-delivery.integration.test.js.map +1 -0
  200. package/dist/services/__tests__/translation-client.test.d.ts +2 -0
  201. package/dist/services/__tests__/translation-client.test.d.ts.map +1 -0
  202. package/dist/services/__tests__/translation-client.test.js +240 -0
  203. package/dist/services/__tests__/translation-client.test.js.map +1 -0
  204. package/dist/services/__tests__/translation-protection.test.d.ts +2 -0
  205. package/dist/services/__tests__/translation-protection.test.d.ts.map +1 -0
  206. package/dist/services/__tests__/translation-protection.test.js +156 -0
  207. package/dist/services/__tests__/translation-protection.test.js.map +1 -0
  208. package/dist/services/__tests__/translation-queue.test.d.ts +2 -0
  209. package/dist/services/__tests__/translation-queue.test.d.ts.map +1 -0
  210. package/dist/services/__tests__/translation-queue.test.js +277 -0
  211. package/dist/services/__tests__/translation-queue.test.js.map +1 -0
  212. package/dist/services/__tests__/translation-room-admission.test.d.ts +2 -0
  213. package/dist/services/__tests__/translation-room-admission.test.d.ts.map +1 -0
  214. package/dist/services/__tests__/translation-room-admission.test.js +371 -0
  215. package/dist/services/__tests__/translation-room-admission.test.js.map +1 -0
  216. package/dist/services/__tests__/translation-session-blocks.test.d.ts +2 -0
  217. package/dist/services/__tests__/translation-session-blocks.test.d.ts.map +1 -0
  218. package/dist/services/__tests__/translation-session-blocks.test.js +367 -0
  219. package/dist/services/__tests__/translation-session-blocks.test.js.map +1 -0
  220. package/dist/services/__tests__/translation-settings.service.test.d.ts +2 -0
  221. package/dist/services/__tests__/translation-settings.service.test.d.ts.map +1 -0
  222. package/dist/services/__tests__/translation-settings.service.test.js +154 -0
  223. package/dist/services/__tests__/translation-settings.service.test.js.map +1 -0
  224. package/dist/services/__tests__/translation.service.test.d.ts +2 -0
  225. package/dist/services/__tests__/translation.service.test.d.ts.map +1 -0
  226. package/dist/services/__tests__/translation.service.test.js +480 -0
  227. package/dist/services/__tests__/translation.service.test.js.map +1 -0
  228. package/dist/services/access-auth.service.d.ts +1 -0
  229. package/dist/services/access-auth.service.d.ts.map +1 -1
  230. package/dist/services/access-auth.service.js +8 -0
  231. package/dist/services/access-auth.service.js.map +1 -1
  232. package/dist/services/attachment-context.d.ts +7 -1
  233. package/dist/services/attachment-context.d.ts.map +1 -1
  234. package/dist/services/attachment-context.js +23 -2
  235. package/dist/services/attachment-context.js.map +1 -1
  236. package/dist/services/conflict-admission.d.ts +37 -0
  237. package/dist/services/conflict-admission.d.ts.map +1 -0
  238. package/dist/services/conflict-admission.js +97 -0
  239. package/dist/services/conflict-admission.js.map +1 -0
  240. package/dist/services/conflict-resolution-context.d.ts +33 -0
  241. package/dist/services/conflict-resolution-context.d.ts.map +1 -0
  242. package/dist/services/conflict-resolution-context.js +434 -0
  243. package/dist/services/conflict-resolution-context.js.map +1 -0
  244. package/dist/services/message-delivery.service.d.ts +195 -0
  245. package/dist/services/message-delivery.service.d.ts.map +1 -0
  246. package/dist/services/message-delivery.service.js +956 -0
  247. package/dist/services/message-delivery.service.js.map +1 -0
  248. package/dist/services/project.service.d.ts +1 -1
  249. package/dist/services/session-manager.d.ts +94 -74
  250. package/dist/services/session-manager.d.ts.map +1 -1
  251. package/dist/services/session-manager.js +91 -17
  252. package/dist/services/session-manager.js.map +1 -1
  253. package/dist/services/task.service.d.ts +26 -20
  254. package/dist/services/task.service.d.ts.map +1 -1
  255. package/dist/services/task.service.js +9 -26
  256. package/dist/services/task.service.js.map +1 -1
  257. package/dist/services/team-room-delivery.d.ts +541 -0
  258. package/dist/services/team-room-delivery.d.ts.map +1 -0
  259. package/dist/services/team-room-delivery.js +123 -0
  260. package/dist/services/team-room-delivery.js.map +1 -0
  261. package/dist/services/team-run.service.d.ts +83 -0
  262. package/dist/services/team-run.service.d.ts.map +1 -1
  263. package/dist/services/team-run.service.js +207 -18
  264. package/dist/services/team-run.service.js.map +1 -1
  265. package/dist/services/team-scheduler.service.d.ts +3 -0
  266. package/dist/services/team-scheduler.service.d.ts.map +1 -1
  267. package/dist/services/team-scheduler.service.js +65 -11
  268. package/dist/services/team-scheduler.service.js.map +1 -1
  269. package/dist/services/team-task-input.d.ts +37 -0
  270. package/dist/services/team-task-input.d.ts.map +1 -0
  271. package/dist/services/team-task-input.js +98 -0
  272. package/dist/services/team-task-input.js.map +1 -0
  273. package/dist/services/team-task-input.service.d.ts +72 -0
  274. package/dist/services/team-task-input.service.d.ts.map +1 -0
  275. package/dist/services/team-task-input.service.js +205 -0
  276. package/dist/services/team-task-input.service.js.map +1 -0
  277. package/dist/services/translation-client.d.ts +31 -0
  278. package/dist/services/translation-client.d.ts.map +1 -0
  279. package/dist/services/translation-client.js +169 -0
  280. package/dist/services/translation-client.js.map +1 -0
  281. package/dist/services/translation-errors.d.ts +14 -0
  282. package/dist/services/translation-errors.d.ts.map +1 -0
  283. package/dist/services/translation-errors.js +16 -0
  284. package/dist/services/translation-errors.js.map +1 -0
  285. package/dist/services/translation-protection.d.ts +27 -0
  286. package/dist/services/translation-protection.d.ts.map +1 -0
  287. package/dist/services/translation-protection.js +547 -0
  288. package/dist/services/translation-protection.js.map +1 -0
  289. package/dist/services/translation-queue.d.ts +49 -0
  290. package/dist/services/translation-queue.d.ts.map +1 -0
  291. package/dist/services/translation-queue.js +211 -0
  292. package/dist/services/translation-queue.js.map +1 -0
  293. package/dist/services/translation-settings.service.d.ts +152 -0
  294. package/dist/services/translation-settings.service.d.ts.map +1 -0
  295. package/dist/services/translation-settings.service.js +174 -0
  296. package/dist/services/translation-settings.service.js.map +1 -0
  297. package/dist/services/translation.service.d.ts +168 -0
  298. package/dist/services/translation.service.d.ts.map +1 -0
  299. package/dist/services/translation.service.js +861 -0
  300. package/dist/services/translation.service.js.map +1 -0
  301. package/dist/services/workspace.service.d.ts +84 -93
  302. package/dist/services/workspace.service.d.ts.map +1 -1
  303. package/dist/services/workspace.service.js +69 -5
  304. package/dist/services/workspace.service.js.map +1 -1
  305. package/dist/socket/__tests__/socket-gateway.test.js +131 -1
  306. package/dist/socket/__tests__/socket-gateway.test.js.map +1 -1
  307. package/dist/socket/socket-gateway.d.ts +6 -0
  308. package/dist/socket/socket-gateway.d.ts.map +1 -1
  309. package/dist/socket/socket-gateway.js +312 -6
  310. package/dist/socket/socket-gateway.js.map +1 -1
  311. package/dist/utils/database-schema.d.ts +9 -0
  312. package/dist/utils/database-schema.d.ts.map +1 -0
  313. package/dist/utils/database-schema.js +434 -0
  314. package/dist/utils/database-schema.js.map +1 -0
  315. package/dist/utils/database-schema.test.d.ts +2 -0
  316. package/dist/utils/database-schema.test.d.ts.map +1 -0
  317. package/dist/utils/database-schema.test.js +359 -0
  318. package/dist/utils/database-schema.test.js.map +1 -0
  319. package/dist/utils/database-team-foundation.test.d.ts +2 -0
  320. package/dist/utils/database-team-foundation.test.d.ts.map +1 -0
  321. package/dist/utils/database-team-foundation.test.js +467 -0
  322. package/dist/utils/database-team-foundation.test.js.map +1 -0
  323. package/dist/utils/owned-child-process.test.js +6 -3
  324. package/dist/utils/owned-child-process.test.js.map +1 -1
  325. package/dist/utils/text-preview.d.ts +4 -0
  326. package/dist/utils/text-preview.d.ts.map +1 -0
  327. package/dist/utils/text-preview.js +21 -0
  328. package/dist/utils/text-preview.js.map +1 -0
  329. package/dist/web/assets/AgentDemoPage-CtCYXopw.js +1 -0
  330. package/dist/web/assets/{AgentEnvironmentSettingsPage-SnxiaHvN.js → AgentEnvironmentSettingsPage-DSWN546Y.js} +1 -1
  331. package/dist/web/assets/AttachmentPreview-DRG5_cfN.js +2 -0
  332. package/dist/web/assets/BranchSelect-Dxs7HnMM.js +1 -0
  333. package/dist/web/assets/BrandLogo-Ilm880Xs.js +1 -0
  334. package/dist/web/assets/ConversationPage-B55Bj_ft.js +1 -0
  335. package/dist/web/assets/CreateTaskInput-CxWPeToI.js +1 -0
  336. package/dist/web/assets/{DemoPage-BHbnnZFF.js → DemoPage-Dyn-U9JO.js} +3 -3
  337. package/dist/web/assets/DesignSystemPage-BxypsRUZ.js +12 -0
  338. package/dist/web/assets/EditableTaskTitle-Dh-SmvFW.js +70 -0
  339. package/dist/web/assets/{ExecutionPermissionSelector-DcC4IBOF.js → ExecutionPermissionSelector-CQ_GNxR0.js} +1 -1
  340. package/dist/web/assets/GeneralSettingsPage-CzoejPZP.js +1 -0
  341. package/dist/web/assets/{LoadingPreviewPage-dE_jyl19.js → LoadingPreviewPage-BlW2PJhI.js} +1 -1
  342. package/dist/web/assets/{McpSettingsPage-CIuQ4bvy.js → McpSettingsPage-DD7EqD4L.js} +1 -1
  343. package/dist/web/assets/{MemberAvatar-BG0VDZKf.js → MemberAvatar-B2pAdWUi.js} +1 -1
  344. package/dist/web/assets/MobileTaskDetail-CvmNW0BQ.js +3 -0
  345. package/dist/web/assets/NotificationSettingsPage-BIqldIRf.js +1 -0
  346. package/dist/web/assets/ProfileSettingsPage-DMxpHmF1.js +3 -0
  347. package/dist/web/assets/ProjectKanbanPage-CHW79td4.js +12 -0
  348. package/dist/web/assets/ProjectSettingsPage-DKiEUkoa.js +2 -0
  349. package/dist/web/assets/{ProviderSettingsPage-DLq-GBxp.js → ProviderSettingsPage-DZT7wyE2.js} +2 -2
  350. package/dist/web/assets/{SettingsMasterDetail-UfyoKAum.js → SettingsMasterDetail-DnF0lsV1.js} +1 -1
  351. package/dist/web/assets/TaskDetail-D_970BDY.js +1 -0
  352. package/dist/web/assets/TeamSettingsPage-BJIEZ7xL.js +1 -0
  353. package/dist/web/assets/TranslationSettingsPage-D3mazqEW.js +1 -0
  354. package/dist/web/assets/arc-hUh7ykl8.js +1 -0
  355. package/dist/web/assets/{architectureDiagram-3BPJPVTR-BCNj6ZWE.js → architectureDiagram-3BPJPVTR-CcV9CLkm.js} +1 -1
  356. package/dist/web/assets/{at-sign-C4VhWike.js → at-sign-B-nhoGyH.js} +1 -1
  357. package/dist/web/assets/{blockDiagram-GPEHLZMM-D1kmB9gz.js → blockDiagram-GPEHLZMM-5q3Y9y0e.js} +1 -1
  358. package/dist/web/assets/{c4Diagram-AAUBKEIU-DbWKTkSe.js → c4Diagram-AAUBKEIU-CorBXi06.js} +1 -1
  359. package/dist/web/assets/channel-By24WpLg.js +1 -0
  360. package/dist/web/assets/check-DqPSoX0r.js +1 -0
  361. package/dist/web/assets/chevron-down-CPnvItQw.js +1 -0
  362. package/dist/web/assets/{chevron-up-DOkDFvIc.js → chevron-up-B8PE5N0G.js} +1 -1
  363. package/dist/web/assets/{chunk-2J33WTMH-B3TA0-LD.js → chunk-2J33WTMH-BUf5B4z4.js} +1 -1
  364. package/dist/web/assets/{chunk-4BX2VUAB-CvPTiBSV.js → chunk-4BX2VUAB-CdnOHkro.js} +1 -1
  365. package/dist/web/assets/{chunk-55IACEB6-xGe9Fu9Z.js → chunk-55IACEB6-U7edZDpP.js} +1 -1
  366. package/dist/web/assets/{chunk-727SXJPM-H8pz3amK.js → chunk-727SXJPM-B64Tuhla.js} +1 -1
  367. package/dist/web/assets/{chunk-AQP2D5EJ-BmYYNxcL.js → chunk-AQP2D5EJ-dXfe56jB.js} +1 -1
  368. package/dist/web/assets/{chunk-FMBD7UC4-BmOEsLTE.js → chunk-FMBD7UC4-DDYbxc1Q.js} +1 -1
  369. package/dist/web/assets/{chunk-ND2GUHAM-C5MfA0xc.js → chunk-ND2GUHAM-BQDGMv0R.js} +1 -1
  370. package/dist/web/assets/{chunk-QZHKN3VN-BcrGYZ7V.js → chunk-QZHKN3VN-DB8q4xAj.js} +1 -1
  371. package/dist/web/assets/classDiagram-4FO5ZUOK-DxHoimtR.js +1 -0
  372. package/dist/web/assets/classDiagram-v2-Q7XG4LA2-DxHoimtR.js +1 -0
  373. package/dist/web/assets/code-block-OCS4YCEC-BSdICyOa.js +2 -0
  374. package/dist/web/assets/confirm-dialog-DxEBZEbV.js +1 -0
  375. package/dist/web/assets/content-CMUL3HGx.js +2 -0
  376. package/dist/web/assets/{copy-CdzVE3kf.js → copy-DyxlXxiU.js} +1 -1
  377. package/dist/web/assets/{cose-bilkent-S5V4N54A-CLQ-m71G.js → cose-bilkent-S5V4N54A-Dx6eMnll.js} +1 -1
  378. package/dist/web/assets/cssMode-ZnNWfjZ6.js +1 -0
  379. package/dist/web/assets/{dagre-BM42HDAG-CmDqxOfn.js → dagre-BM42HDAG-Cxe8pLgD.js} +1 -1
  380. package/dist/web/assets/{diagram-2AECGRRQ-D-fSbEll.js → diagram-2AECGRRQ-RMsswEBe.js} +1 -1
  381. package/dist/web/assets/{diagram-5GNKFQAL-HPywThAf.js → diagram-5GNKFQAL-Bi4E0hzx.js} +1 -1
  382. package/dist/web/assets/{diagram-KO2AKTUF-DAzLhllz.js → diagram-KO2AKTUF-Mv6CDCyn.js} +1 -1
  383. package/dist/web/assets/{diagram-LMA3HP47-DXYB9h8L.js → diagram-LMA3HP47-CJ3qKNzK.js} +1 -1
  384. package/dist/web/assets/{diagram-OG6HWLK6-vv4kz4Zo.js → diagram-OG6HWLK6-D8SBY4ft.js} +1 -1
  385. package/dist/web/assets/dialog-BYdeYC2Y.js +5 -0
  386. package/dist/web/assets/{erDiagram-TEJ5UH35-Cu6aVKAa.js → erDiagram-TEJ5UH35-DwVSCU6X.js} +1 -1
  387. package/dist/web/assets/{flowDiagram-I6XJVG4X-BKDe8WSQ.js → flowDiagram-I6XJVG4X-CyipYAWd.js} +1 -1
  388. package/dist/web/assets/{freemarker2-DI2giuAo.js → freemarker2-DbH_yMCO.js} +2 -2
  389. package/dist/web/assets/{ganttDiagram-6RSMTGT7-DMVLzmAE.js → ganttDiagram-6RSMTGT7-CsC6Wjt4.js} +1 -1
  390. package/dist/web/assets/{gitGraphDiagram-PVQCEYII-vogzYdKn.js → gitGraphDiagram-PVQCEYII-E5jNbqlq.js} +1 -1
  391. package/dist/web/assets/{globe-Rnn-nKw-.js → globe-B2rgTPH3.js} +1 -1
  392. package/dist/web/assets/handlebars-C5VY_S-z.js +1 -0
  393. package/dist/web/assets/hooks-Ct0srfV-.js +1 -0
  394. package/dist/web/assets/html-e5uLnAgN.js +1 -0
  395. package/dist/web/assets/htmlMode-BeNiGozX.js +1 -0
  396. package/dist/web/assets/{icon-button-CuSuXgr_.js → icon-button-BEMJj_6T.js} +1 -1
  397. package/dist/web/assets/index-0XPZayUU.css +1 -0
  398. package/dist/web/assets/index-BYPOXP6w.js +1 -0
  399. package/dist/web/assets/{index-B88xAivn.js → index-C-M0QDqc.js} +5 -5
  400. package/dist/web/assets/index-CAjqC8pT.js +41 -0
  401. package/dist/web/assets/index-CPGzLjEC.js +1 -0
  402. package/dist/web/assets/index-Cm6snkmR.js +122 -0
  403. package/dist/web/assets/index-DNdMmXBr.js +1 -0
  404. package/dist/web/assets/{index-CMjLMilU.js → index-DppwrK2G.js} +1 -1
  405. package/dist/web/assets/index-IMdA08Ok.js +1 -0
  406. package/dist/web/assets/index-kr6U6_pi.js +1 -0
  407. package/dist/web/assets/{infoDiagram-5YYISTIA-CEfSHkNq.js → infoDiagram-5YYISTIA-5_0wrRXl.js} +1 -1
  408. package/dist/web/assets/{ishikawaDiagram-YF4QCWOH-BXKZuWtD.js → ishikawaDiagram-YF4QCWOH-aQujIFZw.js} +5 -5
  409. package/dist/web/assets/javascript-D7QVj9jR.js +1 -0
  410. package/dist/web/assets/{journeyDiagram-JHISSGLW-wsOUYlu7.js → journeyDiagram-JHISSGLW-ClMzHdqI.js} +1 -1
  411. package/dist/web/assets/jsonMode-nHmLVf-n.js +7 -0
  412. package/dist/web/assets/{kanban-definition-UN3LZRKU-DNsuvOrU.js → kanban-definition-UN3LZRKU-BFVlt0tz.js} +1 -1
  413. package/dist/web/assets/{kbd-B3m64lRd.js → kbd-Bfq5K6Ux.js} +1 -1
  414. package/dist/web/assets/{key-round-DpYSyeDH.js → key-round-BqsaQdce.js} +1 -1
  415. package/dist/web/assets/{layers-CxqMQVmH.js → layers-DI4vFJ4F.js} +1 -1
  416. package/dist/web/assets/{linear-BmmCZdOQ.js → linear-DZ-6PAPS.js} +1 -1
  417. package/dist/web/assets/{link-DbSpzB1w.js → link-BU4PN723.js} +1 -1
  418. package/dist/web/assets/liquid-BPIyogoj.js +1 -0
  419. package/dist/web/assets/{lspLanguageFeatures-WhdC7zdf.js → lspLanguageFeatures-DH4Z8jiM.js} +1 -1
  420. package/dist/web/assets/mdx-CI8yO2pV.js +1 -0
  421. package/dist/web/assets/menu-C3uB3M7p.js +1 -0
  422. package/dist/web/assets/{mermaid-NOHMQCX5-DmKVQY9m.js → mermaid-NOHMQCX5-BqeX1osS.js} +71 -71
  423. package/dist/web/assets/{message-square-CfNb-MTd.js → message-square-E5K8E3A-.js} +1 -1
  424. package/dist/web/assets/{mindmap-definition-RKZ34NQL-0RzXjZ9G.js → mindmap-definition-RKZ34NQL-C7dea0db.js} +1 -1
  425. package/dist/web/assets/{modal-Kpqn-nDO.js → modal-Cz1iIVeD.js} +1 -1
  426. package/dist/web/assets/{monaco-I1fXAMIt.js → monaco-DM3DuGN5.js} +3 -3
  427. package/dist/web/assets/{paperclip-BBrzKZQC.js → paperclip-COXNce_v.js} +1 -1
  428. package/dist/web/assets/{pencil-pzVKblxj.js → pencil-BFc4n44f.js} +1 -1
  429. package/dist/web/assets/{people-C6b3eeLq.js → people-BaCetIqh.js} +1 -1
  430. package/dist/web/assets/{pieDiagram-4H26LBE5-Bcvzb8TW.js → pieDiagram-4H26LBE5-2q5medug.js} +2 -2
  431. package/dist/web/assets/popover-CJCQLoGu.js +1 -0
  432. package/dist/web/assets/python-CSxVPG9p.js +1 -0
  433. package/dist/web/assets/{quadrantDiagram-W4KKPZXB-DwNhxMxj.js → quadrantDiagram-W4KKPZXB-DHPEM56s.js} +1 -1
  434. package/dist/web/assets/razor-CVoIieeS.js +1 -0
  435. package/dist/web/assets/{requirementDiagram-4Y6WPE33-D2wSGYn2.js → requirementDiagram-4Y6WPE33-B_KyH7PP.js} +1 -1
  436. package/dist/web/assets/{sankeyDiagram-5OEKKPKP-l3LLF--T.js → sankeyDiagram-5OEKKPKP-CrqkZAYW.js} +1 -1
  437. package/dist/web/assets/search-CkiBPir_.js +1 -0
  438. package/dist/web/assets/search-field-CbM2P2KE.js +1 -0
  439. package/dist/web/assets/{select-DGwKE4l0.js → select-Br0TAs7A.js} +1 -1
  440. package/dist/web/assets/select-DJsEG0jq.js +1 -0
  441. package/dist/web/assets/{sequenceDiagram-3UESZ5HK-BAGsbTS_.js → sequenceDiagram-3UESZ5HK-BRTm1WQA.js} +1 -1
  442. package/dist/web/assets/{server-BIPZ9Ka-.js → server-DYeSE1wn.js} +1 -1
  443. package/dist/web/assets/stateDiagram-AJRCARHV-qN49iyR2.js +1 -0
  444. package/dist/web/assets/stateDiagram-v2-BHNVJYJU-B1clBPkJ.js +1 -0
  445. package/dist/web/assets/{textarea-Bnfq7adW.js → textarea-B7vtoRE3.js} +1 -1
  446. package/dist/web/assets/{timeline-definition-PNZ67QCA-Dt-DcRrG.js → timeline-definition-PNZ67QCA-BzarcDi3.js} +1 -1
  447. package/dist/web/assets/toggles-jo27o0AH.js +1 -0
  448. package/dist/web/assets/tooltip-DbVtCXcO.js +1 -0
  449. package/dist/web/assets/{track-B_K578io.js → track-C0vxnITw.js} +1 -1
  450. package/dist/web/assets/{trash-2-ALAYDudv.js → trash-2-CUM-XX8B.js} +1 -1
  451. package/dist/web/assets/tsMode-7D3gqep6.js +11 -0
  452. package/dist/web/assets/typescript-CbOVEQNg.js +1 -0
  453. package/dist/web/assets/{upload-D7hKT2VA.js → upload-CJ6jVmte.js} +1 -1
  454. package/dist/web/assets/{use-profiles-Ncqxdw7S.js → use-profiles-B0xXAf7Z.js} +1 -1
  455. package/dist/web/assets/{use-projects-Dwy9AfAG.js → use-projects-BlTwWxkY.js} +1 -1
  456. package/dist/web/assets/{use-providers-BRyZjuBG.js → use-providers-FMSfyTD1.js} +1 -1
  457. package/dist/web/assets/use-translation-settings-BOlszecX.js +1 -0
  458. package/dist/web/assets/use-workspaces-DYrEp0Lw.js +1 -0
  459. package/dist/web/assets/useNormalizedLogs-D0N8MJdH.js +1 -0
  460. package/dist/web/assets/useTokenUsage-WB3ZQ25v.js +1 -0
  461. package/dist/web/assets/{vennDiagram-CIIHVFJN-Cr55FtiY.js → vennDiagram-CIIHVFJN-B6oZetvf.js} +1 -1
  462. package/dist/web/assets/{wardley-L42UT6IY-CtwMk9MJ.js → wardley-L42UT6IY-Bzuhm76f.js} +1 -1
  463. package/dist/web/assets/{wardleyDiagram-YWT4CUSO-BeyHmTu3.js → wardleyDiagram-YWT4CUSO-DaC-FUf5.js} +1 -1
  464. package/dist/web/assets/xml-Cfa9tkdT.js +1 -0
  465. package/dist/web/assets/{xychartDiagram-2RQKCTM6-9g34Lq8E.js → xychartDiagram-2RQKCTM6-CCvRIJRL.js} +1 -1
  466. package/dist/web/assets/yaml-C86f7VLL.js +1 -0
  467. package/dist/web/index.html +2 -2
  468. package/node_modules/@agent-tower/shared/dist/index.d.ts +1 -0
  469. package/node_modules/@agent-tower/shared/dist/index.d.ts.map +1 -1
  470. package/node_modules/@agent-tower/shared/dist/index.js +1 -0
  471. package/node_modules/@agent-tower/shared/dist/index.js.map +1 -1
  472. package/node_modules/@agent-tower/shared/dist/log-adapter.d.ts +11 -0
  473. package/node_modules/@agent-tower/shared/dist/log-adapter.d.ts.map +1 -1
  474. package/node_modules/@agent-tower/shared/dist/log-adapter.js +2 -0
  475. package/node_modules/@agent-tower/shared/dist/log-adapter.js.map +1 -1
  476. package/node_modules/@agent-tower/shared/dist/socket/events.d.ts +5 -2
  477. package/node_modules/@agent-tower/shared/dist/socket/events.d.ts.map +1 -1
  478. package/node_modules/@agent-tower/shared/dist/socket/events.js +2 -0
  479. package/node_modules/@agent-tower/shared/dist/socket/events.js.map +1 -1
  480. package/node_modules/@agent-tower/shared/dist/translation.d.ts +306 -0
  481. package/node_modules/@agent-tower/shared/dist/translation.d.ts.map +1 -0
  482. package/node_modules/@agent-tower/shared/dist/translation.js +8 -0
  483. package/node_modules/@agent-tower/shared/dist/translation.js.map +1 -0
  484. package/node_modules/@agent-tower/shared/dist/types.d.ts +23 -0
  485. package/node_modules/@agent-tower/shared/dist/types.d.ts.map +1 -1
  486. package/node_modules/@agent-tower/shared/dist/types.js.map +1 -1
  487. package/package.json +1 -1
  488. package/prisma/migrations/20261016000000_add_translation_foundation/migration.sql +104 -0
  489. package/prisma/migrations/20261016010000_add_message_deliveries/migration.sql +158 -0
  490. package/prisma/migrations/20261019000000_add_room_translation_eligibility/migration.sql +3 -0
  491. package/prisma/migrations/20261019100000_add_team_delivery_foundation/migration.sql +217 -0
  492. package/prisma/schema.prisma +224 -0
  493. package/dist/web/assets/AgentDemoPage-CaR_Yk9_.js +0 -1
  494. package/dist/web/assets/BranchSelect-D-kqavDG.js +0 -1
  495. package/dist/web/assets/BrandLogo-CC1alVsd.js +0 -1
  496. package/dist/web/assets/ConversationPage-RWKLxe1j.js +0 -3
  497. package/dist/web/assets/CreateTaskInput-SHJ1YpRe.js +0 -1
  498. package/dist/web/assets/DesignSystemPage-ChCrzeKT.js +0 -12
  499. package/dist/web/assets/EditableTaskTitle-CGVeZUSt.js +0 -70
  500. package/dist/web/assets/GeneralSettingsPage-C_tkwpmF.js +0 -1
  501. package/dist/web/assets/MobileTaskDetail-DxE6ZDAS.js +0 -5
  502. package/dist/web/assets/NotificationSettingsPage-B-Ot2OMg.js +0 -1
  503. package/dist/web/assets/ProfileSettingsPage-Dn1lrjJr.js +0 -3
  504. package/dist/web/assets/ProjectKanbanPage-jcW8sA7x.js +0 -12
  505. package/dist/web/assets/ProjectSettingsPage-BFPty2eA.js +0 -2
  506. package/dist/web/assets/TaskDetail-a1jalILy.js +0 -3
  507. package/dist/web/assets/TeamSettingsPage-BxGc4W7K.js +0 -1
  508. package/dist/web/assets/arc-dhk0OSs5.js +0 -1
  509. package/dist/web/assets/channel-K0AG6Tl1.js +0 -1
  510. package/dist/web/assets/check-DYlb0A_N.js +0 -1
  511. package/dist/web/assets/classDiagram-4FO5ZUOK-BTJ44hwx.js +0 -1
  512. package/dist/web/assets/classDiagram-v2-Q7XG4LA2-BTJ44hwx.js +0 -1
  513. package/dist/web/assets/code-block-OCS4YCEC-DLPrDBDy.js +0 -2
  514. package/dist/web/assets/confirm-dialog-BRkrtULz.js +0 -1
  515. package/dist/web/assets/content-DPCGELMz.js +0 -2
  516. package/dist/web/assets/cssMode-ChMyv9Nj.js +0 -1
  517. package/dist/web/assets/dialog-B_vC5R4B.js +0 -5
  518. package/dist/web/assets/handlebars-B9tmKQPV.js +0 -1
  519. package/dist/web/assets/hooks-DK4m7-FN.js +0 -1
  520. package/dist/web/assets/html-B_E2U0g5.js +0 -1
  521. package/dist/web/assets/htmlMode-DRHM_Cwq.js +0 -1
  522. package/dist/web/assets/index-B6Lvy5Sr.js +0 -1
  523. package/dist/web/assets/index-BCV57B4O.js +0 -41
  524. package/dist/web/assets/index-CTG-4w90.js +0 -1
  525. package/dist/web/assets/index-Cz73HpYH.js +0 -122
  526. package/dist/web/assets/index-CzvAGoau.css +0 -1
  527. package/dist/web/assets/index-DENY_U7o.js +0 -1
  528. package/dist/web/assets/index-ZO2tqdha.js +0 -1
  529. package/dist/web/assets/javascript-ClWqjgn3.js +0 -1
  530. package/dist/web/assets/jsonMode-DVAoDfae.js +0 -7
  531. package/dist/web/assets/liquid-CZf3xdoq.js +0 -1
  532. package/dist/web/assets/mdx-xzVEPP01.js +0 -1
  533. package/dist/web/assets/menu-0A0v_lRV.js +0 -1
  534. package/dist/web/assets/popover-BYz89aiY.js +0 -1
  535. package/dist/web/assets/python-DL05HvW4.js +0 -1
  536. package/dist/web/assets/razor-BCZ3_uDf.js +0 -1
  537. package/dist/web/assets/search-field-B5czD2Pl.js +0 -1
  538. package/dist/web/assets/select-yem-xN9Z.js +0 -1
  539. package/dist/web/assets/stateDiagram-AJRCARHV-C0BHmXYX.js +0 -1
  540. package/dist/web/assets/stateDiagram-v2-BHNVJYJU-Be3UCkeQ.js +0 -1
  541. package/dist/web/assets/toggles-DxNDJZsN.js +0 -1
  542. package/dist/web/assets/tooltip-CHOgkFbE.js +0 -1
  543. package/dist/web/assets/tsMode-kXxq-Lw7.js +0 -11
  544. package/dist/web/assets/typescript-E3_QcT_u.js +0 -1
  545. package/dist/web/assets/use-attachments-CK7y5KsN.js +0 -2
  546. package/dist/web/assets/use-workspaces-BBbahdwU.js +0 -1
  547. package/dist/web/assets/useNormalizedLogs-CRHRedwp.js +0 -1
  548. package/dist/web/assets/useTokenUsage-YwiF0xnT.js +0 -1
  549. package/dist/web/assets/xml-DxKND8qX.js +0 -1
  550. package/dist/web/assets/yaml-BqpCqTY6.js +0 -1
@@ -0,0 +1,861 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import { Prisma } from '@prisma/client';
3
+ import { sessionMsgStoreManager } from '../output/msg-store.js';
4
+ import { TEAM_RUN_TRANSLATION_MAX_MESSAGE_IDS, TEAM_RUN_TRANSLATION_PREVIEW_LENGTH } from '@agent-tower/shared';
5
+ import { ServiceError } from '../errors.js';
6
+ import { TranslationClient, TranslationError } from './translation-client.js';
7
+ import { prepareTranslation, TRANSLATION_PROTECTION_VERSION } from './translation-protection.js';
8
+ import { readTeamDeliveryIntent } from './team-room-delivery.js';
9
+ import { buildTaskUserText, assertTaskInputSnapshot } from './team-task-input.js';
10
+ import { TranslationQueue } from './translation-queue.js';
11
+ const enabledAdapters = { OR: [{ sourceKind: { in: ['SESSION_BLOCK', 'ROOM_MESSAGE', 'TASK_INPUT'] } }, { sourceKind: 'USER_DELIVERY', delivery: { origin: { in: ['TASK_INITIAL', 'TASK_FOLLOWUP', 'CONVERSATION', 'TEAM_USER', 'TEAM_INITIAL'] } } }] };
12
+ const ACTIVE = ['PENDING', 'RUNNING', 'RETRY_WAIT'];
13
+ const RETRYABLE = ['FAILED', 'CANCELLED', 'INTERRUPTED', 'QUEUE_FULL', 'PAUSED_DISABLED'];
14
+ const DEFAULT_LIMITS = { sources: 512, displaySources: 384, laneSources: 64, attemptMs: 180_000, chainMs: 300_000, retryDelays: [1_000, 4_000], tickMs: 1_000, leaseMs: 30_000, renewMs: 10_000 };
15
+ /** App-owned translation state machine; transcript facts remain in MsgStore/Session snapshots. */
16
+ export class TranslationService {
17
+ db;
18
+ settings;
19
+ owner = randomUUID();
20
+ queue;
21
+ client;
22
+ limits;
23
+ running = new Map();
24
+ drafts = new Set();
25
+ draftControllers = new Set();
26
+ circuits = new Set();
27
+ timer;
28
+ unsubscribe;
29
+ closed = false;
30
+ started = false;
31
+ pumping;
32
+ checkpointOverflow = false;
33
+ pendingCheckpoints = new Set();
34
+ checkpointDrain;
35
+ pendingRoomMessages = new Set();
36
+ roomDrain;
37
+ roomCursor;
38
+ roomUpperBound;
39
+ /** No text/config or unbounded promises enter the original publication path. */
40
+ roomMessageCommitted(messageId) {
41
+ if (this.closed)
42
+ return;
43
+ if (this.pendingRoomMessages.size < 512)
44
+ this.pendingRoomMessages.add(messageId);
45
+ if (this.started && !this.roomDrain)
46
+ void this.drainRoomMessages().catch(() => undefined);
47
+ }
48
+ async registerRoomCandidate(messageId) {
49
+ // registerSource repeats both checks in its serialized transaction. This
50
+ // cheap path also avoids redundant invalidation for existing/tombstoned rows.
51
+ const message = await this.db.roomMessage.findUnique({ where: { id: messageId }, select: { translationEligibilityEpoch: true } });
52
+ if (message?.translationEligibilityEpoch == null)
53
+ return;
54
+ const old = await this.db.translationSource.findUnique({ where: { sourceKind_sourceKey_direction: { sourceKind: 'ROOM_MESSAGE', sourceKey: messageId, direction: 'DISPLAY' } }, select: { id: true } });
55
+ if (!old && !this.closed)
56
+ await this.registerSource({ kind: 'ROOM_MESSAGE', messageId });
57
+ }
58
+ async recoverRoomMessages() {
59
+ if (!this.roomUpperBound) {
60
+ const last = await this.db.roomMessage.findFirst({ where: { translationEligibilityEpoch: { not: null } }, orderBy: [{ translationEligibilityEpoch: 'desc' }, { id: 'desc' }], select: { id: true, translationEligibilityEpoch: true } });
61
+ if (!last || last.translationEligibilityEpoch == null)
62
+ return;
63
+ this.roomUpperBound = { id: last.id, translationEligibilityEpoch: last.translationEligibilityEpoch };
64
+ }
65
+ const upper = this.roomUpperBound;
66
+ const cursor = this.roomCursor;
67
+ // Index keyset scan, bounded BEFORE the missing-source lookup. Never scan
68
+ // existing history until enough missing sources have been found.
69
+ const candidates = await this.db.$queryRaw(Prisma.sql `
70
+ SELECT id, translationEligibilityEpoch FROM RoomMessage
71
+ WHERE translationEligibilityEpoch IS NOT NULL
72
+ AND (translationEligibilityEpoch, id) <= (${upper.translationEligibilityEpoch}, ${upper.id})
73
+ ${cursor ? Prisma.sql `AND (translationEligibilityEpoch, id) > (${cursor.translationEligibilityEpoch}, ${cursor.id})` : Prisma.empty}
74
+ ORDER BY translationEligibilityEpoch ASC, id ASC LIMIT 64`);
75
+ const known = await this.db.translationSource.findMany({ where: { sourceKind: 'ROOM_MESSAGE', direction: 'DISPLAY', sourceKey: { in: candidates.map(row => row.id) } }, select: { sourceKey: true } });
76
+ const present = new Set(known.map(row => row.sourceKey));
77
+ for (const candidate of candidates) {
78
+ if (this.closed)
79
+ return;
80
+ if (!present.has(candidate.id)) {
81
+ try {
82
+ await this.registerRoomCandidate(candidate.id);
83
+ }
84
+ catch { /* A poison row must not starve later candidates. */ }
85
+ }
86
+ }
87
+ const last = candidates.at(-1);
88
+ if (!last || candidates.length < 64 || last.id === upper.id && last.translationEligibilityEpoch === upper.translationEligibilityEpoch) {
89
+ this.roomCursor = this.roomUpperBound = undefined;
90
+ }
91
+ else
92
+ this.roomCursor = last;
93
+ }
94
+ async drainRoomMessages() {
95
+ if (this.closed)
96
+ return;
97
+ if (this.roomDrain)
98
+ return this.roomDrain;
99
+ this.roomDrain = (async () => {
100
+ // Both lanes receive a fixed budget even under continuous hot traffic.
101
+ for (const id of [...this.pendingRoomMessages].slice(0, 32)) {
102
+ if (this.closed)
103
+ return;
104
+ this.pendingRoomMessages.delete(id);
105
+ try {
106
+ await this.registerRoomCandidate(id);
107
+ }
108
+ catch { /* Persistent scan repairs lost/failed admission. */ }
109
+ }
110
+ if (!this.closed)
111
+ await this.recoverRoomMessages();
112
+ })().finally(() => { this.roomDrain = undefined; });
113
+ return this.roomDrain;
114
+ }
115
+ checkpointCommitted(sessionId) {
116
+ if (this.closed)
117
+ return;
118
+ if (this.pendingCheckpoints.size < 512)
119
+ this.pendingCheckpoints.add(sessionId);
120
+ else
121
+ this.checkpointOverflow = true;
122
+ void this.drainCheckpoints();
123
+ }
124
+ async drainCheckpoints() {
125
+ if (this.closed || this.checkpointDrain)
126
+ return;
127
+ this.checkpointDrain = (async () => {
128
+ for (const id of [...this.pendingCheckpoints].slice(0, 64)) {
129
+ this.pendingCheckpoints.delete(id);
130
+ try {
131
+ await this.syncSessionCheckpoint(id);
132
+ }
133
+ catch {
134
+ if (this.pendingCheckpoints.size < 512)
135
+ this.pendingCheckpoints.add(id);
136
+ else
137
+ this.checkpointOverflow = true;
138
+ }
139
+ }
140
+ if (this.checkpointOverflow && this.pendingCheckpoints.size < 448) {
141
+ this.checkpointOverflow = false;
142
+ try {
143
+ await this.recoverSessionCheckpoints();
144
+ }
145
+ catch {
146
+ this.checkpointOverflow = true;
147
+ }
148
+ }
149
+ })().finally(() => { this.checkpointDrain = undefined; });
150
+ await this.checkpointDrain;
151
+ }
152
+ enabledEpoch;
153
+ listeners = new Set();
154
+ getAdmissionEpoch = () => this.enabledEpoch;
155
+ onInvalidated(listener) {
156
+ this.listeners.add(listener);
157
+ return () => this.listeners.delete(listener);
158
+ }
159
+ async notify(sourceId) {
160
+ const source = await this.db.translationSource.findUnique({ where: { id: sourceId } });
161
+ if (!source)
162
+ return;
163
+ const revisions = { sourceId, sourceRevision: source.sourceRevision, overlayRevision: source.overlayRevision };
164
+ let payload;
165
+ if (source.sourceKind === 'ROOM_MESSAGE' && source.messageId && source.scopeType === 'TEAM_RUN') {
166
+ payload = { teamRunId: source.scopeId, messageId: source.messageId, ...revisions };
167
+ }
168
+ else if (source.sessionId)
169
+ payload = { sessionId: source.sessionId, ...revisions };
170
+ else
171
+ return;
172
+ for (const listener of this.listeners) {
173
+ try {
174
+ listener(payload);
175
+ }
176
+ catch { /* Display invalidation is best effort. */ }
177
+ }
178
+ }
179
+ mutation = Promise.resolve();
180
+ constructor(db, settings, options = {}) {
181
+ this.db = db;
182
+ this.settings = settings;
183
+ this.client = options.client ?? new TranslationClient();
184
+ this.queue = options.queue ?? new TranslationQueue({ maxConcurrency: 4 });
185
+ this.limits = { ...DEFAULT_LIMITS, ...options.limits };
186
+ this.unsubscribe = settings.onChange((next) => {
187
+ this.enabledEpoch = next.enabled ? next.enableEpoch : undefined;
188
+ this.circuits.clear();
189
+ this.queue.setConcurrency(next.maxConcurrency);
190
+ if (!next.enabled) {
191
+ for (const handle of this.running.values())
192
+ handle.controller.abort();
193
+ void this.serial(() => this.pauseRevoked()).catch(() => undefined);
194
+ }
195
+ this.wake();
196
+ });
197
+ }
198
+ serial(action) {
199
+ const result = this.mutation.then(action);
200
+ this.mutation = result.catch(() => undefined);
201
+ return result;
202
+ }
203
+ async schedule(request, execute) {
204
+ let execution;
205
+ try {
206
+ return await this.queue.run(request, (signal) => { execution = execute(signal); return execution; });
207
+ }
208
+ finally {
209
+ // Queue cancellation rejects the caller promptly, but the transport/claim still
210
+ // owns its resources until it settles. Keep attempt timers and identity alive.
211
+ await execution?.catch(() => undefined);
212
+ }
213
+ }
214
+ async start() {
215
+ if (this.closed)
216
+ throw new ServiceError('Translation service is closed', 'TRANSLATION_CLOSED', 409);
217
+ if (this.started)
218
+ return;
219
+ this.started = true;
220
+ const config = await this.settings.getPublic();
221
+ this.queue.setConcurrency(config.maxConcurrency);
222
+ this.enabledEpoch = config.enabled ? config.enableEpoch : undefined;
223
+ await this.db.$transaction(async (tx) => {
224
+ await tx.translationAttempt.updateMany({ where: { source: enabledAdapters, status: 'RUNNING' }, data: { status: 'INTERRUPTED', errorCode: 'INTERRUPTED', completedAt: new Date(), leaseOwner: null, leaseExpiresAt: null } });
225
+ // Preserve retry deadlines and budgets; an interrupted request is a new attempt with current configuration.
226
+ await tx.translationSource.updateMany({ where: { AND: [enabledAdapters], jobState: 'RUNNING', deletedAt: null }, data: { overlayRevision: { increment: 1 }, jobState: 'PENDING', generation: { increment: 1 } } });
227
+ await tx.translationSource.updateMany({ where: { AND: [enabledAdapters], jobState: { in: ACTIVE }, deletedAt: null, OR: [{ enableEpoch: { not: config.enableEpoch } }, ...(!config.enabled ? [{}] : [])] }, data: { overlayRevision: { increment: 1 }, jobState: 'PAUSED_DISABLED', nextRetryAt: null, generation: { increment: 1 }, errorCode: 'DISABLED' } });
228
+ });
229
+ await this.recoverSessionCheckpoints();
230
+ await this.drainRoomMessages();
231
+ this.timer = setInterval(() => { void this.drainCheckpoints(); if (!this.roomDrain)
232
+ void this.drainRoomMessages().catch(() => undefined); this.wake(); }, this.limits.tickMs);
233
+ this.timer.unref();
234
+ await this.pump();
235
+ }
236
+ async pauseRevoked() {
237
+ const config = await this.settings.getPublic();
238
+ const ids = await this.db.$transaction(async (tx) => {
239
+ const sources = await tx.translationSource.findMany({ where: { AND: [enabledAdapters], jobState: { in: ACTIVE }, deletedAt: null, OR: [{ enableEpoch: { not: config.enableEpoch } }, ...(!config.enabled ? [{}] : [])] }, select: { id: true }, take: 512 });
240
+ const ids = sources.map((source) => source.id);
241
+ await tx.translationSource.updateMany({ where: { id: { in: ids } }, data: { overlayRevision: { increment: 1 }, jobState: 'PAUSED_DISABLED', nextRetryAt: null, generation: { increment: 1 }, errorCode: 'DISABLED' } });
242
+ await tx.translationAttempt.updateMany({ where: { sourceId: { in: ids }, status: 'RUNNING' }, data: { status: 'CANCELLED', errorCode: 'DISABLED', completedAt: new Date(), leaseOwner: null, leaseExpiresAt: null } });
243
+ return ids;
244
+ });
245
+ await Promise.all(ids.map(id => this.notify(id)));
246
+ }
247
+ /** Identity and lane are derived here, never accepted as untrusted scope/source keys. */
248
+ async resolve(input, tx) {
249
+ if (input.kind === 'SESSION_BLOCK') {
250
+ if (input.semanticType !== 'assistant_message' || !input.turnId || !input.blockId)
251
+ throw new ServiceError('Only assistant message bodies are eligible', 'INELIGIBLE_SOURCE', 400);
252
+ const session = await this.readableSession(input.sessionId, tx);
253
+ return { scopeType: session.conversationId ? 'CONVERSATION' : 'SESSION', scopeId: session.conversationId ?? session.id, laneKey: session.conversationId ? `conversation:${session.conversationId}` : `session:${session.id}`, sourceKind: input.kind, sourceKey: JSON.stringify([session.id, input.turnId, input.blockId]), sessionId: session.id, turnId: input.turnId, blockId: input.blockId, messageId: null, direction: 'DISPLAY', originalText: input.text, blockState: input.state };
254
+ }
255
+ if (input.kind === 'TASK_INPUT') {
256
+ if (typeof input.snapshotId !== 'string' || !input.snapshotId)
257
+ throw new ServiceError('Invalid task input source', 'INELIGIBLE_SOURCE', 400);
258
+ const snapshot = await tx.teamTaskInputSnapshot.findUnique({ where: { id: input.snapshotId } });
259
+ if (!snapshot || snapshot.state !== 'PENDING')
260
+ throw new ServiceError('Input source not found', 'NOT_FOUND', 404);
261
+ await this.assertReadableTeamRun(snapshot.teamRunId, tx);
262
+ if (!await tx.workRequest.count({ where: { taskInputSnapshotId: snapshot.id, status: 'QUEUED' } }) && !await tx.messageDelivery.count({ where: { taskInputSnapshotId: snapshot.id, state: { notIn: ['SENT', 'CANCELLED'] } } }))
263
+ throw new ServiceError('Input has no active consumer', 'NOT_FOUND', 404);
264
+ await assertTaskInputSnapshot(tx, snapshot);
265
+ return { scopeType: 'TEAM_RUN', scopeId: snapshot.teamRunId, laneKey: `team:${snapshot.teamRunId}`, sourceKind: input.kind, sourceKey: JSON.stringify([snapshot.teamRunId, snapshot.taskSourceHash]), sessionId: null, turnId: null, blockId: null, messageId: null, deliveryId: null, direction: 'SEND', originalText: buildTaskUserText(snapshot.originalTitle, snapshot.originalDescription), blockState: 'SEALED' };
266
+ }
267
+ if (input.kind === 'USER_DELIVERY') {
268
+ const delivery = await tx.messageDelivery.findUnique({ where: { id: input.deliveryId } });
269
+ if (!delivery || JSON.parse(delivery.intent).systemAction || ['CANCELLED', 'SENT'].includes(delivery.state))
270
+ throw new ServiceError('Delivery not found', 'NOT_FOUND', 404);
271
+ if (delivery.origin === 'TEAM_INITIAL')
272
+ readTeamDeliveryIntent(delivery);
273
+ if (delivery.origin === 'TEAM_INITIAL' && delivery.taskId && !delivery.sessionId && !delivery.teamRunId) {
274
+ if (!await tx.task.findFirst({ where: { id: delivery.taskId, deletedAt: null, project: { archivedAt: null } } }))
275
+ throw new ServiceError('Delivery not found', 'NOT_FOUND', 404);
276
+ return { sourceRevision: delivery.sourceRevision, scopeType: 'TASK', scopeId: delivery.taskId, laneKey: delivery.channelKey, sourceKind: input.kind, sourceKey: delivery.id, deliveryId: delivery.id, sessionId: null, turnId: null, blockId: null, messageId: null, direction: 'SEND', originalText: delivery.originalText, blockState: 'SEALED' };
277
+ }
278
+ if (delivery.origin === 'TEAM_USER' && delivery.taskId && delivery.teamRunId && !delivery.sessionId) {
279
+ await this.assertReadableTeamRun(delivery.teamRunId, tx);
280
+ return { scopeType: 'TEAM_RUN', scopeId: delivery.teamRunId, laneKey: `team:${delivery.teamRunId}`, sourceKind: input.kind, sourceKey: delivery.id, deliveryId: delivery.id, sessionId: null, turnId: null, blockId: null, messageId: null, direction: 'SEND', originalText: delivery.originalText, blockState: 'SEALED' };
281
+ }
282
+ if (!['TASK_INITIAL', 'TASK_FOLLOWUP', 'CONVERSATION'].includes(delivery.origin) || !delivery.sessionId || delivery.taskId || delivery.teamRunId || delivery.taskInputSnapshotId)
283
+ throw new ServiceError('Delivery destination is not enabled', 'DELIVERY_DESTINATION_DISABLED', 409);
284
+ const session = await this.readableSession(delivery.sessionId, tx);
285
+ return { scopeType: session.conversationId ? 'CONVERSATION' : 'SESSION', scopeId: session.conversationId ?? session.id, laneKey: delivery.channelKey, sourceKind: input.kind, sourceKey: delivery.id, deliveryId: delivery.id, sessionId: session.id, turnId: null, blockId: null, messageId: null, direction: 'SEND', originalText: delivery.originalText, blockState: 'SEALED' };
286
+ }
287
+ if (input.kind !== 'ROOM_MESSAGE')
288
+ throw new ServiceError('Translation adapter is not enabled', 'INELIGIBLE_SOURCE', 400);
289
+ const message = await tx.roomMessage.findUnique({ where: { id: input.messageId }, include: { teamRun: { include: { task: { include: { project: true } } } } } });
290
+ if (!message || message.senderType !== 'agent' || message.visibility !== 'PUBLIC' || message.teamRun.task.deletedAt || message.teamRun.task.project.archivedAt)
291
+ throw new ServiceError('Translation source not found', 'NOT_FOUND', 404);
292
+ return { scopeType: 'TEAM_RUN', scopeId: message.teamRunId, laneKey: `team:${message.teamRunId}`, sourceKind: input.kind, sourceKey: message.id, sessionId: null, turnId: null, blockId: null, messageId: message.id, direction: 'DISPLAY', originalText: message.content, blockState: 'SEALED', enableEpoch: message.translationEligibilityEpoch };
293
+ }
294
+ async assertReadableTeamTask(taskId) {
295
+ if (!await this.db.task.findFirst({ where: { id: taskId, deletedAt: null, project: { archivedAt: null } } }))
296
+ throw new ServiceError('Task not found', 'NOT_FOUND', 404);
297
+ }
298
+ async sourceState(source, tx) {
299
+ try {
300
+ if (source.sourceKind === 'SESSION_BLOCK' && source.sessionId) {
301
+ const session = await this.readableSession(source.sessionId, tx);
302
+ const snapshot = this.sessionSnapshot(session);
303
+ if (snapshot && !snapshot.entries?.some(e => e.metadata?.textBlock?.turnId === source.turnId && e.metadata.textBlock.blockId === source.blockId))
304
+ return 'DELETED';
305
+ return this.matchesSnapshot(source, snapshot) ? 'CURRENT' : 'STALE';
306
+ }
307
+ else if (source.sourceKind === 'ROOM_MESSAGE' && source.messageId) {
308
+ const live = await this.resolve({ kind: 'ROOM_MESSAGE', messageId: source.messageId }, tx);
309
+ if (source.scopeType !== live.scopeType || source.scopeId !== live.scopeId || source.sourceKey !== live.sourceKey || source.laneKey !== live.laneKey || source.direction !== 'DISPLAY' || source.sessionId !== null || source.deliveryId !== null || source.turnId !== null || source.blockId !== null || source.blockState !== 'SEALED')
310
+ return 'DELETED';
311
+ if (live.originalText !== source.originalText)
312
+ return 'STALE';
313
+ }
314
+ else if (source.sourceKind === 'USER_DELIVERY') {
315
+ const live = await this.resolve({ kind: 'USER_DELIVERY', deliveryId: source.sourceKey }, tx);
316
+ if (live.originalText !== source.originalText || 'sourceRevision' in live && live.sourceRevision !== source.sourceRevision)
317
+ return 'STALE';
318
+ }
319
+ else if (source.sourceKind === 'TASK_INPUT') {
320
+ const snapshot = await tx.teamTaskInputSnapshot.findUnique({ where: { translationSourceId: source.id } });
321
+ if (!snapshot || snapshot.state !== 'PENDING')
322
+ return 'DELETED';
323
+ const live = await this.resolve({ kind: 'TASK_INPUT', snapshotId: snapshot.id }, tx);
324
+ if (live.originalText !== source.originalText || live.sourceKey !== source.sourceKey || live.scopeId !== source.scopeId || live.laneKey !== source.laneKey || source.direction !== 'SEND')
325
+ return 'DELETED';
326
+ }
327
+ else
328
+ return 'DELETED'; // No permissive fallback for unknown adapters.
329
+ return 'CURRENT';
330
+ }
331
+ catch (error) {
332
+ if (error instanceof ServiceError && error.code === 'NOT_FOUND')
333
+ return 'DELETED';
334
+ throw error;
335
+ }
336
+ }
337
+ async readableSession(sessionId, tx) {
338
+ const session = await tx.session.findUnique({ where: { id: sessionId }, include: { workspace: { include: { task: { include: { project: true } } } }, conversation: true } });
339
+ if (!session || session.purpose !== 'CHAT' || session.conversation?.deletedAt || session.workspace?.task.deletedAt || session.workspace?.task.project.archivedAt || (!session.workspace && !session.conversation))
340
+ throw new ServiceError('Translation source not found', 'NOT_FOUND', 404);
341
+ return session;
342
+ }
343
+ sessionSnapshot(session) {
344
+ const live = sessionMsgStoreManager.get(session.id)?.getSnapshot();
345
+ if (live)
346
+ return live;
347
+ if (!session.logSnapshot)
348
+ return null;
349
+ try {
350
+ return JSON.parse(session.logSnapshot);
351
+ }
352
+ catch {
353
+ return { entries: [] };
354
+ }
355
+ }
356
+ matchesSnapshot(source, snapshot) {
357
+ // Internal pre-lifecycle callers have no snapshot. Runtime sources always have a durable checkpoint.
358
+ if (!snapshot)
359
+ return true;
360
+ const entry = snapshot.entries?.find(e => e.metadata?.textBlock?.turnId === source.turnId && e.metadata.textBlock.blockId === source.blockId);
361
+ return !!entry && entry.entryType === 'assistant_message' && entry.content === source.originalText && entry.metadata?.textBlock?.sourceRevision === source.sourceRevision && entry.metadata.textBlock.state === source.blockState;
362
+ }
363
+ async assertReadableSession(sessionId) {
364
+ await this.readableSession(sessionId, this.db);
365
+ }
366
+ /** Called only after the single Session snapshot writer has committed a boundary. */
367
+ async syncSessionCheckpoint(sessionId) {
368
+ const session = await this.db.session.findUnique({ where: { id: sessionId }, select: { logSnapshot: true, purpose: true } });
369
+ if (!session?.logSnapshot || session.purpose !== 'CHAT')
370
+ return;
371
+ let snapshot;
372
+ try {
373
+ snapshot = JSON.parse(session.logSnapshot);
374
+ }
375
+ catch {
376
+ return;
377
+ }
378
+ if (!Array.isArray(snapshot.entries))
379
+ return;
380
+ const known = await this.db.translationSource.findMany({ where: { sessionId, sourceKind: 'SESSION_BLOCK' }, select: { id: true, turnId: true, blockId: true, sourceRevision: true, sourceHash: true, blockState: true, deletedAt: true } });
381
+ const versions = new Map(known.map(row => [JSON.stringify([row.turnId, row.blockId]), row]));
382
+ const present = new Set(snapshot.entries.flatMap(entry => entry.metadata?.textBlock ? [entry.metadata.textBlock.blockId] : []));
383
+ for (const row of known)
384
+ if (!row.deletedAt && row.blockId && !present.has(row.blockId))
385
+ await this.cancel(row.id, true);
386
+ for (const entry of snapshot.entries) {
387
+ const block = entry.metadata?.textBlock;
388
+ if (entry.entryType !== 'assistant_message' || block?.translationEpoch === undefined)
389
+ continue;
390
+ const old = versions.get(JSON.stringify([block.turnId, block.blockId]));
391
+ if (old?.deletedAt || old && old.sourceRevision === block.sourceRevision && old.sourceHash === createHash('sha256').update(entry.content).digest('hex') && old.blockState === block.state)
392
+ continue;
393
+ await this.registerSource({ kind: 'SESSION_BLOCK', sessionId, turnId: block.turnId, blockId: block.blockId, semanticType: 'assistant_message', text: entry.content, state: block.state, sourceRevision: block.sourceRevision, enableEpoch: block.translationEpoch });
394
+ }
395
+ }
396
+ async recoverSessionCheckpoints() {
397
+ let cursor;
398
+ while (true) {
399
+ // Only explicitly marked live output, never static history.
400
+ const sessions = await this.db.session.findMany({ where: { purpose: 'CHAT', logSnapshot: { contains: '"translationEpoch":' }, ...(cursor ? { id: { gt: cursor } } : {}) }, select: { id: true }, orderBy: { id: 'asc' }, take: 64 });
401
+ for (const session of sessions) {
402
+ try {
403
+ await this.syncSessionCheckpoint(session.id);
404
+ }
405
+ catch (error) {
406
+ if (!(error instanceof ServiceError && error.code === 'NOT_FOUND') && this.pendingCheckpoints.size < 512)
407
+ this.pendingCheckpoints.add(session.id);
408
+ }
409
+ }
410
+ if (sessions.length < 64)
411
+ break;
412
+ cursor = sessions[sessions.length - 1].id;
413
+ }
414
+ }
415
+ async listSessionOverlays(sessionId, options = {}) {
416
+ const session = await this.readableSession(sessionId, this.db);
417
+ const snapshot = this.sessionSnapshot(session);
418
+ const rows = await this.db.translationSource.findMany({ where: { sessionId, sourceKind: 'SESSION_BLOCK', deletedAt: null, ...(options.blockIds ? { blockId: { in: options.blockIds } } : {}), ...(options.cursor ? { id: { gt: options.cursor } } : {}) }, include: { attempts: { where: { status: 'SUCCESS' }, orderBy: { attemptVersion: 'desc' }, take: 1 } }, orderBy: { id: 'asc' }, take: 100 });
419
+ const items = [];
420
+ for (const row of rows) {
421
+ if (!this.matchesSnapshot(row, snapshot))
422
+ continue;
423
+ const attempt = row.attempts.find(a => a.id === row.effectiveAttemptId && a.sourceRevision === row.sourceRevision && a.sourceHash === row.sourceHash);
424
+ items.push({ sourceId: row.id, sessionId, turnId: row.turnId, blockId: row.blockId, sourceRevision: row.sourceRevision, overlayRevision: row.overlayRevision, state: row.jobState, originalText: row.originalText, translatedText: attempt?.resultText ?? null, targetLanguage: attempt?.targetLanguage ?? null, errorCode: row.errorCode });
425
+ }
426
+ return { items, nextCursor: rows.length === 100 ? rows[rows.length - 1].id : null };
427
+ }
428
+ async retrySessionOverlay(sessionId, sourceId, revision) {
429
+ await this.assertReadableSession(sessionId);
430
+ const source = await this.db.translationSource.findFirst({ where: { id: sourceId, sessionId, sourceKind: 'SESSION_BLOCK', deletedAt: null } });
431
+ if (!source)
432
+ throw new ServiceError('Translation source not found', 'NOT_FOUND', 404);
433
+ await this.retry(sourceId, revision);
434
+ }
435
+ async assertReadableTeamRun(teamRunId, tx = this.db) {
436
+ const room = await tx.teamRun.findFirst({ where: { id: teamRunId, task: { deletedAt: null, project: { archivedAt: null } } }, select: { id: true } });
437
+ if (!room)
438
+ throw new ServiceError('Translation source not found', 'NOT_FOUND', 404);
439
+ }
440
+ /** One bounded projection over loaded message IDs; never registers or recovers history. */
441
+ async roomOverlays(tx, teamRunId, messageIds, full = false, sourceId) {
442
+ if (!messageIds.length)
443
+ return [];
444
+ const limit = TEAM_RUN_TRANSLATION_PREVIEW_LENGTH;
445
+ return tx.$queryRaw(Prisma.sql `
446
+ SELECT s.id AS sourceId, m.teamRunId, m.id AS messageId, s.sourceRevision, s.overlayRevision,
447
+ s.jobState AS state, s.errorCode,
448
+ ${full ? Prisma.sql `s.originalText` : Prisma.sql `substr(s.originalText, 1, ${limit})`} AS originalText,
449
+ ${full ? Prisma.sql `a.resultText` : Prisma.sql `substr(a.resultText, 1, ${limit})`} AS translatedText,
450
+ length(s.originalText) > ${limit} AS originalTextTruncated,
451
+ coalesce(length(a.resultText), 0) > ${limit} AS translatedTextTruncated,
452
+ a.targetLanguage
453
+ FROM TranslationSource s
454
+ JOIN RoomMessage m ON m.id = s.messageId AND m.id = s.sourceKey AND m.teamRunId = s.scopeId
455
+ JOIN TeamRun r ON r.id = m.teamRunId
456
+ JOIN Task t ON t.id = r.taskId
457
+ JOIN Project p ON p.id = t.projectId
458
+ LEFT JOIN TranslationAttempt a ON a.id = s.effectiveAttemptId AND a.sourceId = s.id
459
+ AND a.sourceRevision = s.sourceRevision AND a.sourceHash = s.sourceHash AND a.status = 'SUCCESS'
460
+ WHERE m.teamRunId = ${teamRunId} AND m.id IN (${Prisma.join(messageIds)})
461
+ AND m.visibility = 'PUBLIC' AND m.senderType = 'agent' AND t.deletedAt IS NULL AND p.archivedAt IS NULL
462
+ AND s.scopeType = 'TEAM_RUN' AND s.sourceKind = 'ROOM_MESSAGE' AND s.direction = 'DISPLAY'
463
+ AND s.laneKey = ${`team:${teamRunId}`} AND s.sessionId IS NULL AND s.deliveryId IS NULL
464
+ AND s.turnId IS NULL AND s.blockId IS NULL AND s.blockState = 'SEALED' AND s.deletedAt IS NULL
465
+ AND s.originalText = m.content ${sourceId ? Prisma.sql `AND s.id = ${sourceId}` : Prisma.empty}
466
+ ORDER BY s.id LIMIT ${TEAM_RUN_TRANSLATION_MAX_MESSAGE_IDS}
467
+ `);
468
+ }
469
+ async listTeamRunOverlays(teamRunId, messageIds) {
470
+ const ids = [...new Set(messageIds)];
471
+ if (ids.length > TEAM_RUN_TRANSLATION_MAX_MESSAGE_IDS)
472
+ throw new ServiceError('Too many message IDs', 'VALIDATION_ERROR', 400);
473
+ return this.db.$transaction(async (tx) => {
474
+ await this.assertReadableTeamRun(teamRunId, tx);
475
+ const rows = await this.roomOverlays(tx, teamRunId, ids);
476
+ return { items: rows.map(({ originalText, translatedText, originalTextTruncated, translatedTextTruncated, ...metadata }) => ({
477
+ ...metadata, originalTextPreview: originalText, translatedTextPreview: translatedText,
478
+ originalTextTruncated: Boolean(originalTextTruncated), translatedTextTruncated: Boolean(translatedTextTruncated),
479
+ })) };
480
+ });
481
+ }
482
+ async getTeamRunOverlay(teamRunId, messageId) {
483
+ return this.db.$transaction(async (tx) => {
484
+ await this.assertReadableTeamRun(teamRunId, tx);
485
+ const [row] = await this.roomOverlays(tx, teamRunId, [messageId], true);
486
+ if (!row)
487
+ throw new ServiceError('Translation source not found', 'NOT_FOUND', 404);
488
+ const { originalTextTruncated: _original, translatedTextTruncated: _translated, ...overlay } = row;
489
+ return overlay;
490
+ });
491
+ }
492
+ async assertReadableRoomTranslation(teamRunId, messageId, sourceId) {
493
+ const [row] = await this.roomOverlays(this.db, teamRunId, [messageId], false, sourceId);
494
+ if (!row)
495
+ throw new ServiceError('Translation source not found', 'NOT_FOUND', 404);
496
+ }
497
+ async retryTeamRunOverlay(teamRunId, sourceId, revision) {
498
+ await this.retry(sourceId, revision, teamRunId);
499
+ }
500
+ async hasCapacity(tx, laneKey, direction, excluding) {
501
+ const where = { AND: [enabledAdapters], deletedAt: null, jobState: { in: ACTIVE }, ...(excluding ? { id: { not: excluding } } : {}) };
502
+ const total = await tx.translationSource.count({ where });
503
+ if (total >= this.limits.sources || await tx.translationSource.count({ where: { ...where, laneKey } }) >= this.limits.laneSources)
504
+ return false;
505
+ return direction !== 'DISPLAY' || await tx.translationSource.count({ where: { ...where, direction: 'DISPLAY' } }) < this.limits.displaySources;
506
+ }
507
+ async retryDeliverySource(tx, sourceId, enableEpoch) {
508
+ const source = await tx.translationSource.findUnique({ where: { id: sourceId } });
509
+ if (!source || !['USER_DELIVERY', 'TASK_INPUT'].includes(source.sourceKind) || source.deletedAt)
510
+ throw new ServiceError('Delivery source not found', 'NOT_FOUND', 404);
511
+ if (source.sourceKind === 'USER_DELIVERY')
512
+ await this.resolve({ kind: 'USER_DELIVERY', deliveryId: source.deliveryId ?? source.sourceKey }, tx);
513
+ else {
514
+ const snapshot = await tx.teamTaskInputSnapshot.findUnique({ where: { translationSourceId: source.id } });
515
+ if (!snapshot)
516
+ throw new ServiceError('Input not found', 'NOT_FOUND', 404);
517
+ await this.resolve({ kind: 'TASK_INPUT', snapshotId: snapshot.id }, tx);
518
+ }
519
+ if (!await this.hasCapacity(tx, source.laneKey, 'SEND', source.id))
520
+ throw new ServiceError('Translation queue is full', 'QUEUE_FULL', 429);
521
+ await tx.translationSource.update({ where: { id: sourceId }, data: { jobState: 'PENDING', enableEpoch, generation: { increment: 1 }, currentAttemptVersion: { increment: 1 }, effectiveAttemptId: null, retryChainId: randomUUID(), retryCount: 0, budgetStartedAt: null, nextRetryAt: null, errorCode: null, overlayRevision: { increment: 1 } } });
522
+ }
523
+ async registerSource(input) {
524
+ if (this.closed)
525
+ throw new ServiceError('Translation service is closed', 'TRANSLATION_CLOSED', 409);
526
+ const result = await this.serial(() => this.db.$transaction(async (tx) => {
527
+ return this.registerSourceInTransaction(tx, input);
528
+ }));
529
+ await this.notify(result.id);
530
+ this.wake();
531
+ return result;
532
+ }
533
+ async registerDeliveryInTransaction(tx, deliveryId) { return this.registerSourceInTransaction(tx, { kind: 'USER_DELIVERY', deliveryId }); }
534
+ /** Candidate creation and translation eligibility share one durable transaction. */
535
+ async registerTaskInputInTransaction(tx, snapshotId) {
536
+ if (this.closed)
537
+ throw new ServiceError('Translation service is closed', 'TRANSLATION_CLOSED', 409);
538
+ return this.registerSourceInTransaction(tx, { kind: 'TASK_INPUT', snapshotId });
539
+ }
540
+ async registerSourceInTransaction(tx, input) {
541
+ const data = await this.resolve(input, tx);
542
+ const linkDelivery = async (source) => {
543
+ if (input.kind === 'USER_DELIVERY') {
544
+ const linked = await tx.messageDelivery.updateMany({ where: { id: input.deliveryId, state: 'WAITING_TRANSLATION' }, data: { translationSourceId: source.id } });
545
+ if (!linked.count)
546
+ throw new ServiceError('Delivery changed', 'DELIVERY_CONFLICT', 409);
547
+ }
548
+ if (input.kind === 'TASK_INPUT') {
549
+ const linked = await tx.teamTaskInputSnapshot.updateMany({ where: { id: input.snapshotId, state: 'PENDING' }, data: { translationSourceId: source.id } });
550
+ if (!linked.count)
551
+ throw new ServiceError('Task input changed', 'TASK_INPUT_CHANGED', 409);
552
+ }
553
+ return source;
554
+ };
555
+ const config = await tx.translationSettings.findUnique({ where: { id: 'singleton' } });
556
+ const key = { sourceKind: data.sourceKind, sourceKey: data.sourceKey, direction: data.direction };
557
+ const old = await tx.translationSource.findUnique({ where: { sourceKind_sourceKey_direction: key } });
558
+ // Room messages are immutable publication facts. Automatic admission must
559
+ // never reset failure/capacity/success state or resurrect a tombstone.
560
+ if (input.kind === 'TASK_INPUT' && old)
561
+ return linkDelivery(old);
562
+ if (input.kind === 'ROOM_MESSAGE') {
563
+ if (old)
564
+ return old;
565
+ if (!('enableEpoch' in data) || data.enableEpoch == null)
566
+ throw new ServiceError('Message has no translation eligibility', 'INELIGIBLE_SOURCE', 400);
567
+ }
568
+ if (old && input.kind === 'SESSION_BLOCK' && input.sourceRevision !== undefined && input.sourceRevision < old.sourceRevision)
569
+ return old;
570
+ if (old?.deletedAt)
571
+ throw new ServiceError('Translation source was deleted', 'NOT_FOUND', 404);
572
+ const sourceHash = createHash('sha256').update(data.originalText).digest('hex');
573
+ if (old && old.sourceHash === sourceHash && old.blockState === data.blockState && (!('sourceRevision' in data) || old.sourceRevision === data.sourceRevision) && (input.kind !== 'SESSION_BLOCK' || input.sourceRevision === undefined || old.sourceRevision === input.sourceRevision))
574
+ return linkDelivery(old);
575
+ const eligible = data.blockState === 'SEALED';
576
+ const delivery = input.kind === 'USER_DELIVERY' ? await tx.messageDelivery.findUnique({ where: { id: input.deliveryId } }) : null;
577
+ const roomEpoch = 'enableEpoch' in data ? data.enableEpoch : undefined;
578
+ const epochValid = input.kind === 'USER_DELIVERY' ? delivery != null && delivery.enableEpoch === config?.enableEpoch && delivery.state === 'WAITING_TRANSLATION' : input.kind === 'ROOM_MESSAGE' ? roomEpoch === config?.enableEpoch : input.kind === 'TASK_INPUT' || input.enableEpoch === undefined || input.enableEpoch === config?.enableEpoch;
579
+ const admitted = eligible && config?.enabled && epochValid && await this.hasCapacity(tx, data.laneKey, data.direction, old?.id);
580
+ const jobState = data.blockState === 'ABORTED' ? 'CANCELLED' : !eligible ? 'OPEN' : !config?.enabled || !epochValid ? 'PAUSED_DISABLED' : admitted ? 'PENDING' : 'QUEUE_FULL';
581
+ if (old) {
582
+ this.running.get(old.id)?.controller.abort();
583
+ await tx.translationAttempt.updateMany({ where: { sourceId: old.id, status: 'RUNNING' }, data: { status: 'SUPERSEDED', completedAt: new Date(), leaseOwner: null, leaseExpiresAt: null } });
584
+ }
585
+ const reset = { ...data, ...(input.kind === 'SESSION_BLOCK' && input.sourceRevision !== undefined ? { sourceRevision: input.sourceRevision } : {}), sourceHash, jobState, enableEpoch: input.kind === 'ROOM_MESSAGE' ? roomEpoch : input.kind === 'SESSION_BLOCK' ? input.enableEpoch ?? config?.enableEpoch ?? 0 : config?.enableEpoch ?? 0, effectiveAttemptId: null, retryChainId: randomUUID(), retryCount: 0, budgetStartedAt: null, nextRetryAt: null, errorCode: jobState === 'QUEUE_FULL' ? 'QUEUE_FULL' : null };
586
+ const saved = await (old ? tx.translationSource.update({ where: { id: old.id }, data: { ...reset, sourceRevision: input.kind === 'SESSION_BLOCK' && input.sourceRevision !== undefined ? input.sourceRevision : 'sourceRevision' in data ? data.sourceRevision : { increment: 1 }, generation: { increment: 1 }, currentAttemptVersion: { increment: 1 }, overlayRevision: { increment: 1 } } }) : tx.translationSource.create({ data: reset }));
587
+ return linkDelivery(saved);
588
+ }
589
+ async retry(sourceId, expectedRevision, teamRunId) {
590
+ const result = await this.serial(() => this.db.$transaction(async (tx) => {
591
+ const source = await tx.translationSource.findUnique({ where: { id: sourceId } });
592
+ if (teamRunId !== undefined && (!source || source.sourceKind !== 'ROOM_MESSAGE' || source.scopeType !== 'TEAM_RUN' || source.scopeId !== teamRunId))
593
+ throw new ServiceError('Translation source not found', 'NOT_FOUND', 404);
594
+ if (!source || source.deletedAt || await this.sourceState(source, tx) !== 'CURRENT')
595
+ throw new ServiceError('Translation source not found', 'NOT_FOUND', 404);
596
+ if (source.sourceRevision !== expectedRevision || source.blockState !== 'SEALED' || !RETRYABLE.includes(source.jobState))
597
+ throw new ServiceError('Translation source changed', 'TRANSLATION_CONFLICT', 409);
598
+ const config = await tx.translationSettings.findUnique({ where: { id: 'singleton' } });
599
+ if (!config?.enabled)
600
+ throw new ServiceError('Translation is disabled', 'TRANSLATION_DISABLED', 409);
601
+ if (!await this.hasCapacity(tx, source.laneKey, source.direction, source.id))
602
+ throw new ServiceError('Translation queue is full', 'QUEUE_FULL', 429);
603
+ return tx.translationSource.update({ where: { id: source.id }, data: { overlayRevision: { increment: 1 }, jobState: 'PENDING', enableEpoch: config.enableEpoch, generation: { increment: 1 }, currentAttemptVersion: { increment: 1 }, effectiveAttemptId: null, retryChainId: randomUUID(), retryCount: 0, budgetStartedAt: null, nextRetryAt: null, errorCode: null } });
604
+ }));
605
+ await this.notify(result.id);
606
+ this.wake();
607
+ return result;
608
+ }
609
+ abortSource(sourceId, beforeGeneration) {
610
+ const handle = this.running.get(sourceId);
611
+ if (handle && handle.generation < beforeGeneration)
612
+ handle.controller.abort();
613
+ }
614
+ async cancel(sourceId, deleted = false) {
615
+ await this.serial(() => this.db.$transaction(async (tx) => {
616
+ await tx.translationSource.updateMany({ where: { id: sourceId, deletedAt: null }, data: { overlayRevision: { increment: 1 }, jobState: 'CANCELLED', generation: { increment: 1 }, currentAttemptVersion: { increment: 1 }, effectiveAttemptId: null, nextRetryAt: null, errorCode: 'CANCELLED', ...(deleted ? { deletedAt: new Date() } : {}) } });
617
+ await tx.translationAttempt.updateMany({ where: { sourceId, status: 'RUNNING' }, data: { status: 'CANCELLED', errorCode: 'CANCELLED', completedAt: new Date(), leaseOwner: null, leaseExpiresAt: null } });
618
+ }));
619
+ this.running.get(sourceId)?.controller.abort();
620
+ await this.notify(sourceId);
621
+ this.wake();
622
+ }
623
+ wake() {
624
+ if (!this.closed && this.started)
625
+ void this.pump().catch(() => undefined); // Tick retries storage failures; never log text/config.
626
+ }
627
+ async pump() {
628
+ if (this.closed || !this.started)
629
+ return;
630
+ if (this.pumping)
631
+ return this.pumping;
632
+ this.pumping = (async () => {
633
+ const config = await this.settings.getPublic();
634
+ if (!config.enabled)
635
+ return;
636
+ this.queue.setConcurrency(config.maxConcurrency);
637
+ await this.recoverExpiredLeases();
638
+ // Queued claims remain PENDING until a request slot is acquired. Exclude
639
+ // these bounded, already-owned sources before paging so later lanes enter
640
+ // the fair queue even while earlier lanes keep their requests open.
641
+ const heldSourceIds = [...this.running.keys()];
642
+ const sources = await this.db.translationSource.findMany({ where: { AND: [enabledAdapters], deletedAt: null, jobState: { in: ['PENDING', 'RETRY_WAIT'] }, ...(heldSourceIds.length ? { id: { notIn: heldSourceIds } } : {}), OR: [{ nextRetryAt: null }, { nextRetryAt: { lte: new Date() } }] }, orderBy: [{ updatedAt: 'asc' }, { id: 'asc' }], take: 64 });
643
+ for (const source of sources) {
644
+ if (this.running.has(source.id))
645
+ continue;
646
+ const controller = new AbortController();
647
+ const handle = { generation: source.generation, controller, done: Promise.resolve() };
648
+ this.running.set(source.id, handle);
649
+ handle.done = this.runSource(source, controller).catch(() => undefined).finally(() => {
650
+ if (this.running.get(source.id) === handle)
651
+ this.running.delete(source.id);
652
+ this.wake();
653
+ });
654
+ }
655
+ })().finally(() => { this.pumping = undefined; });
656
+ return this.pumping;
657
+ }
658
+ async recoverExpiredLeases() {
659
+ const expired = await this.db.translationAttempt.findMany({ where: { source: enabledAdapters, status: 'RUNNING', leaseExpiresAt: { lte: new Date() } }, orderBy: [{ leaseExpiresAt: 'asc' }, { id: 'asc' }], take: 64 });
660
+ for (const attempt of expired) {
661
+ const revoked = await this.serial(() => this.db.$transaction(async (tx) => {
662
+ const changed = await tx.translationAttempt.updateMany({ where: { id: attempt.id, status: 'RUNNING', leaseExpiresAt: { lte: new Date() } }, data: { status: 'INTERRUPTED', errorCode: 'LEASE_EXPIRED', completedAt: new Date(), leaseOwner: null, leaseExpiresAt: null } });
663
+ if (!changed.count)
664
+ return false;
665
+ await tx.translationSource.updateMany({ where: { id: attempt.sourceId, deletedAt: null, jobState: 'RUNNING', generation: attempt.generation, currentAttemptVersion: attempt.attemptVersion }, data: { overlayRevision: { increment: 1 }, jobState: 'PENDING', generation: { increment: 1 }, errorCode: 'LEASE_EXPIRED' } });
666
+ return true;
667
+ }));
668
+ if (revoked)
669
+ this.running.get(attempt.sourceId)?.controller.abort();
670
+ }
671
+ }
672
+ async runSource(source, controller) {
673
+ let attempt;
674
+ let attemptTimer;
675
+ let renewTimer;
676
+ let timedOut = false;
677
+ try {
678
+ // Queue admission precedes attempt creation and configuration snapshotting.
679
+ const execution = await this.schedule({ id: `${source.id}:claim:${source.generation}`, laneKey: source.laneKey, direction: source.direction, signal: controller.signal }, async () => {
680
+ if (this.closed || controller.signal.aborted)
681
+ throw new TranslationError('CANCELLED');
682
+ const config = await this.settings.getExecutionConfig();
683
+ const claimed = await this.serial(() => this.db.$transaction(async (tx) => {
684
+ const live = await tx.translationSource.findUnique({ where: { id: source.id } });
685
+ const settings = await tx.translationSettings.findUnique({ where: { id: 'singleton' } });
686
+ if (!live || live.deletedAt || live.generation !== source.generation || !['PENDING', 'RETRY_WAIT'].includes(live.jobState))
687
+ return null;
688
+ if (!settings?.enabled || settings.enableEpoch !== live.enableEpoch) {
689
+ await tx.translationSource.update({ where: { id: live.id }, data: { overlayRevision: { increment: 1 }, jobState: 'PAUSED_DISABLED', errorCode: 'DISABLED', nextRetryAt: null } });
690
+ return null;
691
+ }
692
+ if (settings.revision !== config.revision)
693
+ return null; // Retry claim with the current configuration, never an obsolete queued snapshot.
694
+ const sourceState = await this.sourceState(live, tx);
695
+ if (sourceState !== 'CURRENT') {
696
+ // Memory can advance before the serialized original checkpoint. Retire
697
+ // this version without tombstoning the canonical source: its next
698
+ // checkpoint must still be able to register the revised block.
699
+ await tx.translationSource.update({ where: { id: live.id }, data: { overlayRevision: { increment: 1 }, jobState: 'CANCELLED', ...(sourceState === 'DELETED' ? { deletedAt: new Date() } : {}), generation: { increment: 1 }, effectiveAttemptId: null, nextRetryAt: null, errorCode: sourceState === 'DELETED' ? 'SOURCE_DELETED' : 'SOURCE_CHANGED' } });
700
+ return null;
701
+ }
702
+ if (live.budgetStartedAt && Date.now() - live.budgetStartedAt.getTime() >= this.limits.chainMs || live.retryCount >= 3) {
703
+ await tx.translationSource.update({ where: { id: live.id }, data: { overlayRevision: { increment: 1 }, jobState: 'FAILED', errorCode: 'RETRY_BUDGET_EXHAUSTED', nextRetryAt: null } });
704
+ return null;
705
+ }
706
+ if (this.circuits.has(config.revision)) {
707
+ await tx.translationSource.update({ where: { id: live.id }, data: { overlayRevision: { increment: 1 }, jobState: 'FAILED', errorCode: 'CONFIG_CIRCUIT_OPEN', nextRetryAt: null } });
708
+ return null;
709
+ }
710
+ const { apiKey: _secret, ...sanitized } = config;
711
+ const row = await tx.translationAttempt.create({ data: { sourceId: live.id, sourceRevision: live.sourceRevision, sourceHash: live.sourceHash, attemptVersion: live.currentAttemptVersion + 1, configRevision: config.revision, credentialRevision: config.credentialRevision, targetLanguage: live.direction === 'SEND' ? config.sendTargetLanguage : config.displayTargetLanguage, sanitizedConfigSnapshot: JSON.stringify(sanitized), protectionVersion: String(TRANSLATION_PROTECTION_VERSION), status: 'RUNNING', generation: live.generation, enableEpoch: live.enableEpoch, retryIndex: live.retryCount, leaseOwner: this.owner, leaseExpiresAt: new Date(Date.now() + this.limits.leaseMs) } });
712
+ await tx.translationSource.update({ where: { id: live.id }, data: { overlayRevision: { increment: 1 }, jobState: 'RUNNING', currentAttemptVersion: row.attemptVersion, effectiveAttemptId: null, budgetStartedAt: live.budgetStartedAt ?? new Date(), retryCount: { increment: 1 }, nextRetryAt: null } });
713
+ return row;
714
+ }));
715
+ if (!claimed)
716
+ return null;
717
+ attempt = claimed;
718
+ await this.notify(source.id);
719
+ if (controller.signal.aborted || this.closed)
720
+ throw new TranslationError('CANCELLED');
721
+ attemptTimer = setTimeout(() => { timedOut = true; controller.abort(); }, this.limits.attemptMs);
722
+ renewTimer = setInterval(() => {
723
+ void this.db.translationAttempt.updateMany({ where: { id: claimed.id, status: 'RUNNING', leaseOwner: this.owner, leaseExpiresAt: { gt: new Date() } }, data: { leaseExpiresAt: new Date(Date.now() + this.limits.leaseMs) } }).then((result) => { if (!result.count)
724
+ controller.abort(); }).catch(() => controller.abort());
725
+ }, this.limits.renewMs);
726
+ renewTimer.unref();
727
+ const prepared = prepareTranslation(source.originalText);
728
+ const results = [];
729
+ if (prepared.hasTranslatableText)
730
+ results.push(await this.translateSegment(config, claimed.targetLanguage, prepared.segments[0], controller.signal));
731
+ return { config, prepared, results };
732
+ });
733
+ if (!execution || !attempt) {
734
+ await this.notify(source.id);
735
+ return;
736
+ }
737
+ if (execution.prepared.hasTranslatableText) {
738
+ for (let index = 1; index < execution.prepared.segments.length; index++) {
739
+ const text = execution.prepared.segments[index];
740
+ const config = execution.config;
741
+ const target = attempt.targetLanguage;
742
+ execution.results.push(await this.schedule({ id: `${attempt.id}:${index}`, laneKey: source.laneKey, direction: source.direction, signal: controller.signal }, (signal) => this.translateSegment(config, target, text, signal)));
743
+ }
744
+ }
745
+ const resultText = execution.prepared.hasTranslatableText ? execution.prepared.restore(execution.results) : source.originalText;
746
+ if (controller.signal.aborted || this.closed)
747
+ throw new TranslationError(timedOut ? 'TIMEOUT' : 'CANCELLED');
748
+ await this.complete(source, attempt, resultText, execution.prepared.hasTranslatableText ? 'SUCCESS' : 'NOT_REQUIRED');
749
+ }
750
+ catch (error) {
751
+ if (attempt)
752
+ await this.fail(source, attempt, timedOut ? new TranslationError('TIMEOUT', { retryable: true }) : error, controller.signal.aborted);
753
+ }
754
+ finally {
755
+ if (attemptTimer)
756
+ clearTimeout(attemptTimer);
757
+ if (renewTimer)
758
+ clearInterval(renewTimer);
759
+ }
760
+ }
761
+ translateSegment(config, targetLanguage, text, signal) {
762
+ return this.client.translate({ apiBaseUrl: config.apiBaseUrl, model: config.model, apiKey: config.apiKey, promptTemplate: config.promptTemplate, targetLanguage, text, signal });
763
+ }
764
+ async owns(tx, source, attempt) {
765
+ const live = await tx.translationSource.findUnique({ where: { id: source.id } });
766
+ const lease = await tx.translationAttempt.findUnique({ where: { id: attempt.id } });
767
+ const config = await tx.translationSettings.findUnique({ where: { id: 'singleton' } });
768
+ if (!live || live.deletedAt || live.jobState !== 'RUNNING' || live.sourceRevision !== attempt.sourceRevision || live.sourceHash !== attempt.sourceHash || live.currentAttemptVersion !== attempt.attemptVersion || live.generation !== attempt.generation || live.enableEpoch !== attempt.enableEpoch || !config?.enabled || config.enableEpoch !== attempt.enableEpoch || lease?.status !== 'RUNNING' || lease.leaseOwner !== this.owner || !lease.leaseExpiresAt || lease.leaseExpiresAt.getTime() <= Date.now() || await this.sourceState(live, tx) !== 'CURRENT')
769
+ return null;
770
+ return live;
771
+ }
772
+ async complete(source, attempt, resultText, state) {
773
+ await this.serial(() => this.db.$transaction(async (tx) => {
774
+ if (!await this.owns(tx, source, attempt)) {
775
+ await this.retireStale(tx, source, attempt);
776
+ return;
777
+ }
778
+ await tx.translationAttempt.update({ where: { id: attempt.id }, data: { status: 'SUCCESS', resultText, completedAt: new Date(), leaseOwner: null, leaseExpiresAt: null } });
779
+ await tx.translationSource.update({ where: { id: source.id }, data: { overlayRevision: { increment: 1 }, jobState: state, effectiveAttemptId: attempt.id, errorCode: null } });
780
+ }));
781
+ await this.notify(source.id);
782
+ }
783
+ async retireStale(tx, source, attempt) {
784
+ await tx.translationAttempt.updateMany({ where: { id: attempt.id, status: 'RUNNING', leaseOwner: this.owner }, data: { status: 'SUPERSEDED', completedAt: new Date(), leaseOwner: null, leaseExpiresAt: null } });
785
+ const config = await tx.translationSettings.findUnique({ where: { id: 'singleton' } });
786
+ const paused = !config?.enabled || config.enableEpoch !== attempt.enableEpoch;
787
+ await tx.translationSource.updateMany({ where: { id: source.id, jobState: 'RUNNING', generation: attempt.generation, sourceRevision: attempt.sourceRevision, currentAttemptVersion: attempt.attemptVersion }, data: { overlayRevision: { increment: 1 }, jobState: paused ? 'PAUSED_DISABLED' : 'FAILED', nextRetryAt: null, errorCode: paused ? 'DISABLED' : 'SOURCE_OR_LEASE_REVOKED' } });
788
+ }
789
+ async fail(source, attempt, error, aborted) {
790
+ const failure = error instanceof TranslationError ? error : new TranslationError(aborted ? 'CANCELLED' : 'INTERNAL_ERROR');
791
+ if (['AUTH_FAILED', 'MODEL_CONFIG_ERROR'].includes(failure.errorCode) && (await this.settings.getPublic()).revision === attempt.configRevision)
792
+ this.circuits.add(attempt.configRevision);
793
+ await this.serial(() => this.db.$transaction(async (tx) => {
794
+ const live = await this.owns(tx, source, attempt);
795
+ if (!live) {
796
+ await this.retireStale(tx, source, attempt);
797
+ return;
798
+ }
799
+ const canRetry = !this.closed && failure.retryable && live.retryCount < 3 && Date.now() - live.budgetStartedAt.getTime() < this.limits.chainMs;
800
+ const delay = Math.max((this.limits.retryDelays[live.retryCount - 1] ?? 4_000) * (1 + Math.random() * .2), Math.min(failure.retryAfterMs ?? 0, 30_000));
801
+ const nextRetryAt = canRetry ? new Date(Date.now() + delay) : null;
802
+ await tx.translationAttempt.update({ where: { id: attempt.id }, data: { status: this.closed ? 'INTERRUPTED' : 'FAILED', errorCode: this.closed ? 'INTERRUPTED' : failure.errorCode, nextRetryAt, completedAt: new Date(), leaseOwner: null, leaseExpiresAt: null } });
803
+ await tx.translationSource.update({ where: { id: source.id }, data: { overlayRevision: { increment: 1 }, jobState: this.closed ? 'INTERRUPTED' : canRetry ? 'RETRY_WAIT' : 'FAILED', nextRetryAt, errorCode: this.closed ? 'INTERRUPTED' : failure.errorCode } });
804
+ }));
805
+ await this.notify(source.id);
806
+ }
807
+ async testDraft(identity, draft, text, direction) {
808
+ if (this.closed)
809
+ throw new ServiceError('Translation service is closed', 'TRANSLATION_CLOSED', 409);
810
+ if (this.drafts.has(identity) || this.drafts.size >= 8)
811
+ throw new ServiceError('Translation test queue is full', 'QUEUE_FULL', 429);
812
+ this.drafts.add(identity);
813
+ const controller = new AbortController();
814
+ this.draftControllers.add(controller);
815
+ const timer = setTimeout(() => controller.abort(), this.limits.attemptMs);
816
+ try {
817
+ const config = await this.settings.resolveDraft(draft);
818
+ const prepared = prepareTranslation(text);
819
+ if (!prepared.hasTranslatableText)
820
+ return { resultText: text };
821
+ const translated = [];
822
+ const id = randomUUID();
823
+ for (let index = 0; index < prepared.segments.length; index++) {
824
+ translated.push(await this.schedule({ id: `test:${id}:${index}`, laneKey: `test:${identity}`, direction: 'SEND', signal: controller.signal }, (signal) => this.translateSegment(config, direction === 'SEND' ? config.sendTargetLanguage : config.displayTargetLanguage, prepared.segments[index], signal)));
825
+ }
826
+ const saved = await this.settings.getExecutionConfig();
827
+ if (saved.revision === config.revision && saved.apiBaseUrl === config.apiBaseUrl && saved.model === config.model && saved.apiKey === config.apiKey)
828
+ this.circuits.delete(config.revision);
829
+ return { resultText: prepared.restore(translated) };
830
+ }
831
+ finally {
832
+ clearTimeout(timer);
833
+ this.drafts.delete(identity);
834
+ this.draftControllers.delete(controller);
835
+ }
836
+ }
837
+ async shutdown() {
838
+ this.closed = true;
839
+ this.enabledEpoch = undefined;
840
+ this.listeners.clear();
841
+ if (this.timer)
842
+ clearInterval(this.timer);
843
+ this.unsubscribe?.();
844
+ for (const handle of this.running.values())
845
+ handle.controller.abort();
846
+ for (const controller of this.draftControllers)
847
+ controller.abort();
848
+ await this.checkpointDrain;
849
+ this.pendingCheckpoints.clear();
850
+ await this.roomDrain?.catch(() => undefined);
851
+ this.pendingRoomMessages.clear();
852
+ this.roomCursor = this.roomUpperBound = undefined;
853
+ await this.pumping;
854
+ await this.queue.shutdown();
855
+ await Promise.allSettled([...this.running.values()].map((handle) => handle.done));
856
+ await this.mutation;
857
+ // Interrupted attempts with a valid epoch resume on the next application instance.
858
+ await this.db.translationSource.updateMany({ where: { AND: [enabledAdapters], jobState: 'INTERRUPTED', deletedAt: null }, data: { overlayRevision: { increment: 1 }, jobState: 'PENDING', generation: { increment: 1 } } });
859
+ }
860
+ }
861
+ //# sourceMappingURL=translation.service.js.map