agent-tower 0.7.2-beta.7 → 0.7.2

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 (555) 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-runtime.test.js +82 -5
  312. package/dist/utils/database-runtime.test.js.map +1 -1
  313. package/dist/utils/database-schema.d.ts +9 -0
  314. package/dist/utils/database-schema.d.ts.map +1 -0
  315. package/dist/utils/database-schema.js +434 -0
  316. package/dist/utils/database-schema.js.map +1 -0
  317. package/dist/utils/database-schema.test.d.ts +2 -0
  318. package/dist/utils/database-schema.test.d.ts.map +1 -0
  319. package/dist/utils/database-schema.test.js +359 -0
  320. package/dist/utils/database-schema.test.js.map +1 -0
  321. package/dist/utils/database-team-foundation.test.d.ts +2 -0
  322. package/dist/utils/database-team-foundation.test.d.ts.map +1 -0
  323. package/dist/utils/database-team-foundation.test.js +467 -0
  324. package/dist/utils/database-team-foundation.test.js.map +1 -0
  325. package/dist/utils/index.d.ts.map +1 -1
  326. package/dist/utils/index.js +29 -1
  327. package/dist/utils/index.js.map +1 -1
  328. package/dist/utils/owned-child-process.test.js +6 -3
  329. package/dist/utils/owned-child-process.test.js.map +1 -1
  330. package/dist/utils/text-preview.d.ts +4 -0
  331. package/dist/utils/text-preview.d.ts.map +1 -0
  332. package/dist/utils/text-preview.js +21 -0
  333. package/dist/utils/text-preview.js.map +1 -0
  334. package/dist/web/assets/AgentDemoPage-BfXkNCdN.js +1 -0
  335. package/dist/web/assets/{AgentEnvironmentSettingsPage-SnxiaHvN.js → AgentEnvironmentSettingsPage-GHjKIWpb.js} +1 -1
  336. package/dist/web/assets/AttachmentPreview-CLN1lnLe.js +2 -0
  337. package/dist/web/assets/BranchSelect-Cofp8vYf.js +1 -0
  338. package/dist/web/assets/BrandLogo-BUimEX-9.js +1 -0
  339. package/dist/web/assets/ConversationPage-R7vNaOQv.js +1 -0
  340. package/dist/web/assets/CreateTaskInput-CasITF8y.js +1 -0
  341. package/dist/web/assets/{DemoPage-BHbnnZFF.js → DemoPage-DK-ikg1_.js} +3 -3
  342. package/dist/web/assets/DesignSystemPage-BhjNs2vP.js +12 -0
  343. package/dist/web/assets/EditableTaskTitle-BJ9Rxggf.js +70 -0
  344. package/dist/web/assets/{ExecutionPermissionSelector-DcC4IBOF.js → ExecutionPermissionSelector-B8befbxJ.js} +1 -1
  345. package/dist/web/assets/GeneralSettingsPage-RMN2al8I.js +1 -0
  346. package/dist/web/assets/{LoadingPreviewPage-dE_jyl19.js → LoadingPreviewPage-BDlf89rJ.js} +1 -1
  347. package/dist/web/assets/{McpSettingsPage-CIuQ4bvy.js → McpSettingsPage-CPNxFF67.js} +1 -1
  348. package/dist/web/assets/{MemberAvatar-BG0VDZKf.js → MemberAvatar-YVr_erAr.js} +1 -1
  349. package/dist/web/assets/MobileTaskDetail-QNUHMq7u.js +3 -0
  350. package/dist/web/assets/NotificationSettingsPage-BEyuyu3Z.js +1 -0
  351. package/dist/web/assets/ProfileSettingsPage-CHe4LlRp.js +3 -0
  352. package/dist/web/assets/ProjectKanbanPage-Bt_01giW.js +12 -0
  353. package/dist/web/assets/ProjectSettingsPage-cus6Ns97.js +2 -0
  354. package/dist/web/assets/{ProviderSettingsPage-DLq-GBxp.js → ProviderSettingsPage-DqA_B1Ey.js} +2 -2
  355. package/dist/web/assets/{SettingsMasterDetail-UfyoKAum.js → SettingsMasterDetail-DjWR9Sqk.js} +1 -1
  356. package/dist/web/assets/TaskDetail-rEXNe2h3.js +1 -0
  357. package/dist/web/assets/TeamSettingsPage-DwWROtW6.js +1 -0
  358. package/dist/web/assets/TranslationSettingsPage-Cod9c4Ug.js +1 -0
  359. package/dist/web/assets/arc-DEy33vTc.js +1 -0
  360. package/dist/web/assets/{architectureDiagram-3BPJPVTR-BCNj6ZWE.js → architectureDiagram-3BPJPVTR-Ky1D1LBk.js} +1 -1
  361. package/dist/web/assets/{at-sign-C4VhWike.js → at-sign-C48_y345.js} +1 -1
  362. package/dist/web/assets/{blockDiagram-GPEHLZMM-D1kmB9gz.js → blockDiagram-GPEHLZMM-BNgB7kNT.js} +1 -1
  363. package/dist/web/assets/{c4Diagram-AAUBKEIU-DbWKTkSe.js → c4Diagram-AAUBKEIU-4GMrPMgQ.js} +1 -1
  364. package/dist/web/assets/channel-DTP-syjA.js +1 -0
  365. package/dist/web/assets/check-BmiJ7qMY.js +1 -0
  366. package/dist/web/assets/chevron-down-3y5Aiolp.js +1 -0
  367. package/dist/web/assets/{chevron-up-DOkDFvIc.js → chevron-up-DqaYcjpq.js} +1 -1
  368. package/dist/web/assets/{chunk-2J33WTMH-B3TA0-LD.js → chunk-2J33WTMH-C439hRe_.js} +1 -1
  369. package/dist/web/assets/{chunk-4BX2VUAB-CvPTiBSV.js → chunk-4BX2VUAB-D8GK3M3t.js} +1 -1
  370. package/dist/web/assets/{chunk-55IACEB6-xGe9Fu9Z.js → chunk-55IACEB6-Dl4UhJ8X.js} +1 -1
  371. package/dist/web/assets/{chunk-727SXJPM-H8pz3amK.js → chunk-727SXJPM-BSEeayBq.js} +1 -1
  372. package/dist/web/assets/{chunk-AQP2D5EJ-BmYYNxcL.js → chunk-AQP2D5EJ-B6chbZfT.js} +1 -1
  373. package/dist/web/assets/{chunk-FMBD7UC4-BmOEsLTE.js → chunk-FMBD7UC4-DaLkiLdQ.js} +1 -1
  374. package/dist/web/assets/{chunk-ND2GUHAM-C5MfA0xc.js → chunk-ND2GUHAM-DeQwZAAT.js} +1 -1
  375. package/dist/web/assets/{chunk-QZHKN3VN-BcrGYZ7V.js → chunk-QZHKN3VN-CLRr4YRM.js} +1 -1
  376. package/dist/web/assets/classDiagram-4FO5ZUOK-CUKgurLa.js +1 -0
  377. package/dist/web/assets/classDiagram-v2-Q7XG4LA2-CUKgurLa.js +1 -0
  378. package/dist/web/assets/code-block-OCS4YCEC-CnoxUpOx.js +2 -0
  379. package/dist/web/assets/confirm-dialog-ltNBFYCs.js +1 -0
  380. package/dist/web/assets/content-DPQMfMae.js +2 -0
  381. package/dist/web/assets/{copy-CdzVE3kf.js → copy-HRMhTkIG.js} +1 -1
  382. package/dist/web/assets/{cose-bilkent-S5V4N54A-CLQ-m71G.js → cose-bilkent-S5V4N54A-C1UiYzfk.js} +1 -1
  383. package/dist/web/assets/cssMode-DkuSsGeg.js +1 -0
  384. package/dist/web/assets/{dagre-BM42HDAG-CmDqxOfn.js → dagre-BM42HDAG-CTJpBgye.js} +1 -1
  385. package/dist/web/assets/{diagram-2AECGRRQ-D-fSbEll.js → diagram-2AECGRRQ-Dj1_TEYp.js} +1 -1
  386. package/dist/web/assets/{diagram-5GNKFQAL-HPywThAf.js → diagram-5GNKFQAL-V-KUpwqh.js} +1 -1
  387. package/dist/web/assets/{diagram-KO2AKTUF-DAzLhllz.js → diagram-KO2AKTUF-CXG1ZIeX.js} +1 -1
  388. package/dist/web/assets/{diagram-LMA3HP47-DXYB9h8L.js → diagram-LMA3HP47-BZSJUwk9.js} +1 -1
  389. package/dist/web/assets/{diagram-OG6HWLK6-vv4kz4Zo.js → diagram-OG6HWLK6-CCr4gq4u.js} +1 -1
  390. package/dist/web/assets/dialog-CuFEdqLg.js +5 -0
  391. package/dist/web/assets/{erDiagram-TEJ5UH35-Cu6aVKAa.js → erDiagram-TEJ5UH35-Djtyzh6z.js} +1 -1
  392. package/dist/web/assets/{flowDiagram-I6XJVG4X-BKDe8WSQ.js → flowDiagram-I6XJVG4X-BmSVSiXJ.js} +1 -1
  393. package/dist/web/assets/{freemarker2-DI2giuAo.js → freemarker2-DdptX5f1.js} +2 -2
  394. package/dist/web/assets/{ganttDiagram-6RSMTGT7-DMVLzmAE.js → ganttDiagram-6RSMTGT7-r5vQX-b5.js} +1 -1
  395. package/dist/web/assets/{gitGraphDiagram-PVQCEYII-vogzYdKn.js → gitGraphDiagram-PVQCEYII-Zj9aloh9.js} +1 -1
  396. package/dist/web/assets/{globe-Rnn-nKw-.js → globe-B9I-jHBB.js} +1 -1
  397. package/dist/web/assets/handlebars-B34f-LAf.js +1 -0
  398. package/dist/web/assets/hooks-BLxb16IA.js +1 -0
  399. package/dist/web/assets/html-CnvOQ63o.js +1 -0
  400. package/dist/web/assets/htmlMode-RdxZ8Svp.js +1 -0
  401. package/dist/web/assets/{icon-button-CuSuXgr_.js → icon-button-CYiKa-7C.js} +1 -1
  402. package/dist/web/assets/index-0XPZayUU.css +1 -0
  403. package/dist/web/assets/index-B1oVmCie.js +1 -0
  404. package/dist/web/assets/index-BJObbxud.js +122 -0
  405. package/dist/web/assets/index-Bhvt-E6d.js +41 -0
  406. package/dist/web/assets/index-BtUQYU5l.js +1 -0
  407. package/dist/web/assets/index-CThFsQNP.js +1 -0
  408. package/dist/web/assets/index-DZLnTISJ.js +1 -0
  409. package/dist/web/assets/index-DcOU2m2F.js +1 -0
  410. package/dist/web/assets/{index-CMjLMilU.js → index-Dk3bRQAK.js} +1 -1
  411. package/dist/web/assets/{index-B88xAivn.js → index-DkRy6eNX.js} +5 -5
  412. package/dist/web/assets/{infoDiagram-5YYISTIA-CEfSHkNq.js → infoDiagram-5YYISTIA-BHtVaxZx.js} +1 -1
  413. package/dist/web/assets/{ishikawaDiagram-YF4QCWOH-BXKZuWtD.js → ishikawaDiagram-YF4QCWOH-BYmwjkbC.js} +5 -5
  414. package/dist/web/assets/javascript-BhombDEO.js +1 -0
  415. package/dist/web/assets/{journeyDiagram-JHISSGLW-wsOUYlu7.js → journeyDiagram-JHISSGLW-c8BBqqRS.js} +1 -1
  416. package/dist/web/assets/jsonMode-BJGOUeaV.js +7 -0
  417. package/dist/web/assets/{kanban-definition-UN3LZRKU-DNsuvOrU.js → kanban-definition-UN3LZRKU-D4Vr-oFT.js} +1 -1
  418. package/dist/web/assets/{kbd-B3m64lRd.js → kbd-C88ut3t9.js} +1 -1
  419. package/dist/web/assets/{key-round-DpYSyeDH.js → key-round-Cjbj2qNa.js} +1 -1
  420. package/dist/web/assets/{layers-CxqMQVmH.js → layers-eaHAVcc3.js} +1 -1
  421. package/dist/web/assets/{linear-BmmCZdOQ.js → linear-opi90s0L.js} +1 -1
  422. package/dist/web/assets/{link-DbSpzB1w.js → link-BSGb85gq.js} +1 -1
  423. package/dist/web/assets/liquid-Czy1WpPz.js +1 -0
  424. package/dist/web/assets/{lspLanguageFeatures-WhdC7zdf.js → lspLanguageFeatures-DWu2Ubf0.js} +1 -1
  425. package/dist/web/assets/mdx-DhXfoSnM.js +1 -0
  426. package/dist/web/assets/menu--sjyisSg.js +1 -0
  427. package/dist/web/assets/{mermaid-NOHMQCX5-DmKVQY9m.js → mermaid-NOHMQCX5-BSNbBj78.js} +71 -71
  428. package/dist/web/assets/{message-square-CfNb-MTd.js → message-square-BLEJN6p7.js} +1 -1
  429. package/dist/web/assets/{mindmap-definition-RKZ34NQL-0RzXjZ9G.js → mindmap-definition-RKZ34NQL-BN3ZjBh-.js} +1 -1
  430. package/dist/web/assets/{modal-Kpqn-nDO.js → modal-CSGOqphK.js} +1 -1
  431. package/dist/web/assets/{monaco-I1fXAMIt.js → monaco-BkuNyOkP.js} +3 -3
  432. package/dist/web/assets/{paperclip-BBrzKZQC.js → paperclip-CTNoXpUW.js} +1 -1
  433. package/dist/web/assets/{pencil-pzVKblxj.js → pencil-Cp70ZwtG.js} +1 -1
  434. package/dist/web/assets/{people-C6b3eeLq.js → people-DT78SebR.js} +1 -1
  435. package/dist/web/assets/{pieDiagram-4H26LBE5-Bcvzb8TW.js → pieDiagram-4H26LBE5-CfmFHpPI.js} +2 -2
  436. package/dist/web/assets/popover-BiVxGGgp.js +1 -0
  437. package/dist/web/assets/python-DIcQDjcf.js +1 -0
  438. package/dist/web/assets/{quadrantDiagram-W4KKPZXB-DwNhxMxj.js → quadrantDiagram-W4KKPZXB-CeaumJAa.js} +1 -1
  439. package/dist/web/assets/razor-DYK6Rxer.js +1 -0
  440. package/dist/web/assets/{requirementDiagram-4Y6WPE33-D2wSGYn2.js → requirementDiagram-4Y6WPE33-BWWCDXCE.js} +1 -1
  441. package/dist/web/assets/{sankeyDiagram-5OEKKPKP-l3LLF--T.js → sankeyDiagram-5OEKKPKP-DAJut743.js} +1 -1
  442. package/dist/web/assets/search-DniD_AGC.js +1 -0
  443. package/dist/web/assets/search-field-CQLDXLeA.js +1 -0
  444. package/dist/web/assets/select-B5RYunTs.js +1 -0
  445. package/dist/web/assets/{select-DGwKE4l0.js → select-BPdJrr5M.js} +1 -1
  446. package/dist/web/assets/{sequenceDiagram-3UESZ5HK-BAGsbTS_.js → sequenceDiagram-3UESZ5HK-CynjRGW4.js} +1 -1
  447. package/dist/web/assets/{server-BIPZ9Ka-.js → server-xswxWpKD.js} +1 -1
  448. package/dist/web/assets/stateDiagram-AJRCARHV-amAtpx9w.js +1 -0
  449. package/dist/web/assets/stateDiagram-v2-BHNVJYJU-V-lSceg1.js +1 -0
  450. package/dist/web/assets/{textarea-Bnfq7adW.js → textarea-BVyPl9rM.js} +1 -1
  451. package/dist/web/assets/{timeline-definition-PNZ67QCA-Dt-DcRrG.js → timeline-definition-PNZ67QCA-DZ9W7WlP.js} +1 -1
  452. package/dist/web/assets/toggles-QcIz43DP.js +1 -0
  453. package/dist/web/assets/tooltip-CPqGK_WZ.js +1 -0
  454. package/dist/web/assets/{track-B_K578io.js → track-Ckye0-tt.js} +1 -1
  455. package/dist/web/assets/{trash-2-ALAYDudv.js → trash-2-CvUJsbtT.js} +1 -1
  456. package/dist/web/assets/tsMode-c5x0d47n.js +11 -0
  457. package/dist/web/assets/typescript-i7eYblLt.js +1 -0
  458. package/dist/web/assets/{upload-D7hKT2VA.js → upload-CsXAmAun.js} +1 -1
  459. package/dist/web/assets/{use-profiles-Ncqxdw7S.js → use-profiles-DuXaBRju.js} +1 -1
  460. package/dist/web/assets/{use-projects-Dwy9AfAG.js → use-projects-Ddz0knyr.js} +1 -1
  461. package/dist/web/assets/{use-providers-BRyZjuBG.js → use-providers-Td1-gyrN.js} +1 -1
  462. package/dist/web/assets/use-translation-settings-NYM0lmBx.js +1 -0
  463. package/dist/web/assets/use-workspaces-CdJZLmGh.js +1 -0
  464. package/dist/web/assets/useNormalizedLogs--RbVywYa.js +1 -0
  465. package/dist/web/assets/useTokenUsage-DDXf7E4p.js +1 -0
  466. package/dist/web/assets/{vennDiagram-CIIHVFJN-Cr55FtiY.js → vennDiagram-CIIHVFJN-CB2Ubvy9.js} +1 -1
  467. package/dist/web/assets/{wardley-L42UT6IY-CtwMk9MJ.js → wardley-L42UT6IY-BXOkk77I.js} +1 -1
  468. package/dist/web/assets/{wardleyDiagram-YWT4CUSO-BeyHmTu3.js → wardleyDiagram-YWT4CUSO-VA6ywdQQ.js} +1 -1
  469. package/dist/web/assets/xml-D2EGActB.js +1 -0
  470. package/dist/web/assets/{xychartDiagram-2RQKCTM6-9g34Lq8E.js → xychartDiagram-2RQKCTM6-FNcvDj5G.js} +1 -1
  471. package/dist/web/assets/yaml-jeiIZSL4.js +1 -0
  472. package/dist/web/index.html +2 -2
  473. package/node_modules/@agent-tower/shared/dist/index.d.ts +1 -0
  474. package/node_modules/@agent-tower/shared/dist/index.d.ts.map +1 -1
  475. package/node_modules/@agent-tower/shared/dist/index.js +1 -0
  476. package/node_modules/@agent-tower/shared/dist/index.js.map +1 -1
  477. package/node_modules/@agent-tower/shared/dist/log-adapter.d.ts +11 -0
  478. package/node_modules/@agent-tower/shared/dist/log-adapter.d.ts.map +1 -1
  479. package/node_modules/@agent-tower/shared/dist/log-adapter.js +2 -0
  480. package/node_modules/@agent-tower/shared/dist/log-adapter.js.map +1 -1
  481. package/node_modules/@agent-tower/shared/dist/socket/events.d.ts +5 -2
  482. package/node_modules/@agent-tower/shared/dist/socket/events.d.ts.map +1 -1
  483. package/node_modules/@agent-tower/shared/dist/socket/events.js +2 -0
  484. package/node_modules/@agent-tower/shared/dist/socket/events.js.map +1 -1
  485. package/node_modules/@agent-tower/shared/dist/translation.d.ts +306 -0
  486. package/node_modules/@agent-tower/shared/dist/translation.d.ts.map +1 -0
  487. package/node_modules/@agent-tower/shared/dist/translation.js +8 -0
  488. package/node_modules/@agent-tower/shared/dist/translation.js.map +1 -0
  489. package/node_modules/@agent-tower/shared/dist/types.d.ts +23 -0
  490. package/node_modules/@agent-tower/shared/dist/types.d.ts.map +1 -1
  491. package/node_modules/@agent-tower/shared/dist/types.js.map +1 -1
  492. package/package.json +1 -1
  493. package/prisma/migrations/20261016000000_add_translation_foundation/migration.sql +104 -0
  494. package/prisma/migrations/20261016010000_add_message_deliveries/migration.sql +158 -0
  495. package/prisma/migrations/20261019000000_add_room_translation_eligibility/migration.sql +3 -0
  496. package/prisma/migrations/20261019100000_add_team_delivery_foundation/migration.sql +217 -0
  497. package/prisma/schema.prisma +224 -0
  498. package/dist/web/assets/AgentDemoPage-CaR_Yk9_.js +0 -1
  499. package/dist/web/assets/BranchSelect-D-kqavDG.js +0 -1
  500. package/dist/web/assets/BrandLogo-CC1alVsd.js +0 -1
  501. package/dist/web/assets/ConversationPage-RWKLxe1j.js +0 -3
  502. package/dist/web/assets/CreateTaskInput-SHJ1YpRe.js +0 -1
  503. package/dist/web/assets/DesignSystemPage-ChCrzeKT.js +0 -12
  504. package/dist/web/assets/EditableTaskTitle-CGVeZUSt.js +0 -70
  505. package/dist/web/assets/GeneralSettingsPage-C_tkwpmF.js +0 -1
  506. package/dist/web/assets/MobileTaskDetail-DxE6ZDAS.js +0 -5
  507. package/dist/web/assets/NotificationSettingsPage-B-Ot2OMg.js +0 -1
  508. package/dist/web/assets/ProfileSettingsPage-Dn1lrjJr.js +0 -3
  509. package/dist/web/assets/ProjectKanbanPage-jcW8sA7x.js +0 -12
  510. package/dist/web/assets/ProjectSettingsPage-BFPty2eA.js +0 -2
  511. package/dist/web/assets/TaskDetail-a1jalILy.js +0 -3
  512. package/dist/web/assets/TeamSettingsPage-BxGc4W7K.js +0 -1
  513. package/dist/web/assets/arc-dhk0OSs5.js +0 -1
  514. package/dist/web/assets/channel-K0AG6Tl1.js +0 -1
  515. package/dist/web/assets/check-DYlb0A_N.js +0 -1
  516. package/dist/web/assets/classDiagram-4FO5ZUOK-BTJ44hwx.js +0 -1
  517. package/dist/web/assets/classDiagram-v2-Q7XG4LA2-BTJ44hwx.js +0 -1
  518. package/dist/web/assets/code-block-OCS4YCEC-DLPrDBDy.js +0 -2
  519. package/dist/web/assets/confirm-dialog-BRkrtULz.js +0 -1
  520. package/dist/web/assets/content-DPCGELMz.js +0 -2
  521. package/dist/web/assets/cssMode-ChMyv9Nj.js +0 -1
  522. package/dist/web/assets/dialog-B_vC5R4B.js +0 -5
  523. package/dist/web/assets/handlebars-B9tmKQPV.js +0 -1
  524. package/dist/web/assets/hooks-DK4m7-FN.js +0 -1
  525. package/dist/web/assets/html-B_E2U0g5.js +0 -1
  526. package/dist/web/assets/htmlMode-DRHM_Cwq.js +0 -1
  527. package/dist/web/assets/index-B6Lvy5Sr.js +0 -1
  528. package/dist/web/assets/index-BCV57B4O.js +0 -41
  529. package/dist/web/assets/index-CTG-4w90.js +0 -1
  530. package/dist/web/assets/index-Cz73HpYH.js +0 -122
  531. package/dist/web/assets/index-CzvAGoau.css +0 -1
  532. package/dist/web/assets/index-DENY_U7o.js +0 -1
  533. package/dist/web/assets/index-ZO2tqdha.js +0 -1
  534. package/dist/web/assets/javascript-ClWqjgn3.js +0 -1
  535. package/dist/web/assets/jsonMode-DVAoDfae.js +0 -7
  536. package/dist/web/assets/liquid-CZf3xdoq.js +0 -1
  537. package/dist/web/assets/mdx-xzVEPP01.js +0 -1
  538. package/dist/web/assets/menu-0A0v_lRV.js +0 -1
  539. package/dist/web/assets/popover-BYz89aiY.js +0 -1
  540. package/dist/web/assets/python-DL05HvW4.js +0 -1
  541. package/dist/web/assets/razor-BCZ3_uDf.js +0 -1
  542. package/dist/web/assets/search-field-B5czD2Pl.js +0 -1
  543. package/dist/web/assets/select-yem-xN9Z.js +0 -1
  544. package/dist/web/assets/stateDiagram-AJRCARHV-C0BHmXYX.js +0 -1
  545. package/dist/web/assets/stateDiagram-v2-BHNVJYJU-Be3UCkeQ.js +0 -1
  546. package/dist/web/assets/toggles-DxNDJZsN.js +0 -1
  547. package/dist/web/assets/tooltip-CHOgkFbE.js +0 -1
  548. package/dist/web/assets/tsMode-kXxq-Lw7.js +0 -11
  549. package/dist/web/assets/typescript-E3_QcT_u.js +0 -1
  550. package/dist/web/assets/use-attachments-CK7y5KsN.js +0 -2
  551. package/dist/web/assets/use-workspaces-BBbahdwU.js +0 -1
  552. package/dist/web/assets/useNormalizedLogs-CRHRedwp.js +0 -1
  553. package/dist/web/assets/useTokenUsage-YwiF0xnT.js +0 -1
  554. package/dist/web/assets/xml-DxKND8qX.js +0 -1
  555. package/dist/web/assets/yaml-BqpCqTY6.js +0 -1
@@ -0,0 +1,670 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import { createHash, randomUUID } from 'node:crypto';
3
+ import fs from 'node:fs';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+ import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
8
+ import { buildTaskUserText, taskInputHash } from '../team-task-input.js';
9
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'tower-room-delivery-'));
10
+ const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../..');
11
+ process.env.AGENT_TOWER_DATABASE_URL = `file:${path.join(dir, 'test.db')}`;
12
+ process.env.AGENT_TOWER_DATA_DIR = dir;
13
+ const runtime = vi.hoisted(() => ({ start: vi.fn(), sendMessage: vi.fn(), setMessageDeliveryService: vi.fn(), getEventBus: vi.fn(), getTranslationService: vi.fn() }));
14
+ vi.mock('../../core/container.js', () => ({ getEventBus: runtime.getEventBus, getSessionManager: () => runtime, getTranslationService: runtime.getTranslationService }));
15
+ let db;
16
+ let service;
17
+ let settings;
18
+ let translation;
19
+ let delivery;
20
+ let fault;
21
+ const hash = (text) => createHash('sha256').update(text).digest('hex');
22
+ async function addMember(teamRunId, name) {
23
+ return db.teamMember.create({ data: { teamRunId, name, aliases: '[]', providerId: 'never-executed', rolePrompt: 'Fixture only', capabilities: '{}', workspacePolicy: 'shared', triggerPolicy: 'USER_MESSAGES' } });
24
+ }
25
+ async function fixture(options = {}) {
26
+ const project = await db.project.create({ data: { name: 'Isolated publication', repoPath: dir } });
27
+ const task = await db.task.create({ data: { projectId: project.id, title: '完整原始任务标题', description: ' Full task description\nincluding exact whitespace. ' } });
28
+ const team = await db.teamRun.create({ data: { taskId: task.id, mode: options.mode ?? 'CONFIRM' } });
29
+ const members = await Promise.all([addMember(team.id, 'First'), addMember(team.id, 'Second')]);
30
+ const taskSourceHash = taskInputHash(task.title, task.description);
31
+ const state = options.state ?? 'ACCEPTED';
32
+ const effectiveUserText = buildTaskUserText(task.title, task.description);
33
+ const snapshot = await db.teamTaskInputSnapshot.create({ data: {
34
+ teamRunId: team.id, taskId: task.id, taskSourceHash, originalTitle: task.title, originalDescription: task.description,
35
+ state, ...(state === 'ACCEPTED' ? { effectiveUserText, sentMode: 'ORIGINAL', acceptedAt: new Date() } : {}),
36
+ } });
37
+ const intent = { version: 1, taskSourceHash, kind: 'work_request', mentions: [], artifactRefs: ['artifact:fixture'], resolvedRecipients: members.slice(0, options.recipients ?? 2).map((member, index) => ({ memberId: member.id, ifBusy: index ? 'cancel_current_and_start' : 'queue', cancelQueued: !!index, target: null, targetSourceMemberId: null })) };
38
+ const row = await db.messageDelivery.create({ data: {
39
+ clientSubmissionId: randomUUID(), requestFingerprint: hash(randomUUID()), taskId: task.id, teamRunId: team.id,
40
+ channelKey: `task:${task.id}:team-room`, sequence: 1, origin: 'TEAM_USER', originalText: '原始跟进正文', sourceHash: hash('原始跟进正文'),
41
+ attachmentIds: '[]', intent: JSON.stringify(intent), enableEpoch: 0, requiresTranslation: false, state: 'READY', sentMode: 'ORIGINAL', acceptedText: '原始跟进正文',
42
+ } });
43
+ const taskInput = { snapshotId: snapshot.id, revision: snapshot.revision, taskSourceHash, effectiveUserText, sentMode: 'ORIGINAL', acceptedAttemptId: null };
44
+ const envelope = { deliveryId: row.id, expectedRevision: row.revision, enableEpoch: row.enableEpoch, acceptedText: row.acceptedText, taskInput };
45
+ return { project, task, team, members, snapshot, row, envelope, intent };
46
+ }
47
+ const publish = (f) => db.$transaction(tx => service.publishAcceptedUserDelivery(tx, f.envelope));
48
+ async function translatedTaskInput(f) {
49
+ await db.translationSettings.update({ where: { id: 'singleton' }, data: { enabled: true, apiBaseUrl: 'http://fixture.invalid/v1', model: 'fixture', apiKey: '' } });
50
+ const original = buildTaskUserText(f.task.title, f.task.description);
51
+ const source = await db.translationSource.create({ data: { scopeType: 'TEAM_RUN', scopeId: f.team.id, sourceKind: 'TASK_INPUT', sourceKey: JSON.stringify([f.team.id, f.envelope.taskInput.taskSourceHash]), laneKey: `team:${f.team.id}`, direction: 'SEND', originalText: original, sourceHash: hash(original), blockState: 'SEALED', jobState: 'SUCCESS', currentAttemptVersion: 1, generation: 1, enableEpoch: 0 } });
52
+ const attempt = await db.translationAttempt.create({ data: { sourceId: source.id, sourceRevision: source.sourceRevision, sourceHash: source.sourceHash, attemptVersion: 1, configRevision: 0, credentialRevision: 0, targetLanguage: 'English', sanitizedConfigSnapshot: '{}', protectionVersion: 'fixture', status: 'SUCCESS', resultText: 'Accepted translated complete task input', generation: 1, enableEpoch: 0, completedAt: new Date() } });
53
+ await db.translationSource.update({ where: { id: source.id }, data: { effectiveAttemptId: attempt.id } });
54
+ await db.teamTaskInputSnapshot.update({ where: { id: f.snapshot.id }, data: { translationSourceId: source.id } });
55
+ f.envelope.taskInput = { ...f.envelope.taskInput, effectiveUserText: attempt.resultText, sentMode: 'TRANSLATED', acceptedAttemptId: attempt.id };
56
+ return { source, attempt };
57
+ }
58
+ async function unchanged(f) {
59
+ expect(await db.roomMessage.count({ where: { deliveryId: f.row.id } })).toBe(0);
60
+ expect(await db.deliveryReceipt.count({ where: { deliveryId: f.row.id } })).toBe(0);
61
+ expect(await db.workRequest.count({ where: { teamRunId: f.team.id } })).toBe(0);
62
+ expect(await db.messageDelivery.findUniqueOrThrow({ where: { id: f.row.id } })).toEqual(f.row);
63
+ expect(await db.teamTaskInputSnapshot.findUniqueOrThrow({ where: { id: f.snapshot.id } })).toEqual(f.snapshot);
64
+ expect((await db.teamRun.findUniqueOrThrow({ where: { id: f.team.id } })).currentTaskInputSnapshotId).toBeNull();
65
+ }
66
+ describe('isolated atomic TeamRun publication foundation', () => {
67
+ beforeAll(async () => {
68
+ execFileSync('pnpm', ['exec', 'prisma', 'db', 'push', '--skip-generate', '--schema=prisma/schema.prisma'], { cwd: root, env: process.env, stdio: 'pipe' });
69
+ const utils = await import('../../utils/index.js');
70
+ db = utils.prisma;
71
+ await utils.initializeDatabaseRuntime();
72
+ db.$use(async (params, next) => {
73
+ const result = await next(params);
74
+ if (fault && params.model === fault.model && params.action === fault.action && ++fault.seen === fault.occurrence) {
75
+ fault.fired = true;
76
+ throw new Error(`injected after ${fault.model}.${fault.action} #${fault.occurrence}`);
77
+ }
78
+ return result;
79
+ });
80
+ }, 30_000);
81
+ beforeEach(async () => {
82
+ fault = undefined;
83
+ await db.messageDelivery.deleteMany();
84
+ await db.translationSource.deleteMany();
85
+ await db.project.deleteMany();
86
+ await db.translationSettings.deleteMany();
87
+ vi.clearAllMocks();
88
+ const { TeamRunService: Service } = await import('../team-run.service.js');
89
+ const { TranslationSettingsService: Settings } = await import('../translation-settings.service.js');
90
+ const { TranslationService: Translation } = await import('../translation.service.js');
91
+ const { MessageDeliveryService: Delivery } = await import('../message-delivery.service.js');
92
+ service = new Service();
93
+ settings = new Settings(db);
94
+ translation = new Translation(db, settings);
95
+ delivery = new Delivery(db, settings, translation, runtime, { tickMs: 100_000 });
96
+ await settings.getPublic();
97
+ });
98
+ afterEach(async () => { fault = undefined; await delivery.shutdown(); await translation.shutdown(); });
99
+ afterAll(async () => { await db.$disconnect(); fs.rmSync(dir, { recursive: true, force: true }); });
100
+ it.each(['AUTO', 'CONFIRM'])('publishes all frozen recipients in %s and preserves originals', async (mode) => {
101
+ const f = await fixture({ mode });
102
+ await addMember(f.team.id, 'Joined after acceptance');
103
+ await db.teamMember.updateMany({ where: { id: { in: f.members.map(m => m.id) } }, data: { triggerPolicy: 'MENTION_ONLY' } });
104
+ const result = await publish(f);
105
+ const message = await db.roomMessage.findUniqueOrThrow({ where: { id: result.messageId } });
106
+ const requests = await db.workRequest.findMany({ where: { triggerMessageId: message.id }, orderBy: { targetMemberId: 'asc' } });
107
+ expect(requests.map(r => r.targetMemberId)).toEqual(f.members.map(m => m.id).sort());
108
+ expect(requests.every(r => r.status === (mode === 'CONFIRM' ? 'PENDING_APPROVAL' : 'QUEUED') && r.taskInputSnapshotId === f.snapshot.id)).toBe(true);
109
+ expect(requests.map(r => [r.targetMemberId, r.ifBusy, r.cancelQueued]).sort()).toEqual(f.intent.resolvedRecipients.map(r => [r.memberId, r.ifBusy, r.cancelQueued]).sort());
110
+ const sent = await db.messageDelivery.findUniqueOrThrow({ where: { id: f.row.id } });
111
+ const receipt = await db.deliveryReceipt.findUniqueOrThrow({ where: { deliveryId: f.row.id } });
112
+ expect(sent).toMatchObject({ state: 'SENT', revision: 2, acceptedDestinationId: message.id, actualSentText: f.row.acceptedText, taskInputSnapshotId: f.snapshot.id });
113
+ expect(receipt).toMatchObject({ phase: 'SUBMITTED', evidence: 'ROOM_PUBLISHED', generation: sent.revision, runtimeInstanceId: null, turnId: null, submittedAt: sent.sentAt });
114
+ expect(message).toMatchObject({ deliveryId: f.row.id, content: f.row.acceptedText, senderType: 'user', senderId: null, senderInvocationId: null, visibility: 'PUBLIC', translationEligibilityEpoch: null });
115
+ expect(await db.roomMessageParticipant.count()).toBe(0);
116
+ expect(await db.task.findUniqueOrThrow({ where: { id: f.task.id } })).toEqual(f.task);
117
+ expect(runtime.start).not.toHaveBeenCalled();
118
+ expect(runtime.sendMessage).not.toHaveBeenCalled();
119
+ });
120
+ it('atomically adopts a PENDING original snapshot and keeps a zero-recipient publication empty', async () => {
121
+ const f = await fixture({ state: 'PENDING', recipients: 0 });
122
+ await addMember(f.team.id, 'Would receive an unfrozen broadcast');
123
+ const result = await publish(f);
124
+ expect(result.workRequestIds).toEqual([]);
125
+ expect(await db.workRequest.count()).toBe(0);
126
+ expect(await db.teamTaskInputSnapshot.findUniqueOrThrow({ where: { id: f.snapshot.id } })).toMatchObject({ state: 'ACCEPTED', revision: 2, effectiveUserText: f.envelope.taskInput.effectiveUserText, sentMode: 'ORIGINAL', acceptedAttemptId: null, acceptedAt: expect.any(Date) });
127
+ expect((await db.teamRun.findUniqueOrThrow({ where: { id: f.team.id } })).currentTaskInputSnapshotId).toBe(f.snapshot.id);
128
+ });
129
+ it('publishes the normalized target and retains its frozen owner after ownership changes', async () => {
130
+ const f = await fixture();
131
+ const source = await db.workspace.create({ data: { taskId: f.task.id, ownerMemberId: f.members[0].id, branchName: 'source', worktreePath: path.join(dir, 'target-fixture') } });
132
+ f.intent.resolvedRecipients[1].target = { kind: 'WORKSPACE_COMMIT', purpose: 'REVIEW', sourceWorkspaceId: source.id, headSha: ` ${'a'.repeat(40)} `, branchName: ' source ', planItemId: ' plan-1 ' };
133
+ f.intent.resolvedRecipients[1].targetSourceMemberId = f.members[0].id;
134
+ await db.messageDelivery.update({ where: { id: f.row.id }, data: { intent: JSON.stringify(f.intent) } });
135
+ const result = await publish(f);
136
+ expect(await db.workRequest.findFirstOrThrow({ where: { triggerMessageId: result.messageId, targetMemberId: f.members[1].id } })).toMatchObject({ targetKind: 'WORKSPACE_COMMIT', targetPurpose: 'REVIEW', targetSourceWorkspaceId: source.id, targetSourceMemberId: f.members[0].id, targetHeadSha: 'a'.repeat(40), targetBranchName: 'source', targetPlanItemId: 'plan-1' });
137
+ await db.workspace.update({ where: { id: source.id }, data: { ownerMemberId: f.members[1].id } });
138
+ expect(await publish(f)).toEqual(result);
139
+ });
140
+ it('adopts a PENDING translated snapshot from exact current source evidence and preserves accepted history', async () => {
141
+ const f = await fixture({ state: 'PENDING' });
142
+ const evidence = await translatedTaskInput(f);
143
+ const result = await publish(f);
144
+ expect(await db.teamTaskInputSnapshot.findUniqueOrThrow({ where: { id: f.snapshot.id } })).toMatchObject({ state: 'ACCEPTED', revision: 2, effectiveUserText: evidence.attempt.resultText, sentMode: 'TRANSLATED', acceptedAttemptId: evidence.attempt.id });
145
+ // Current config/source advancement does not invalidate the immutable accepted attempt.
146
+ await db.translationSettings.update({ where: { id: 'singleton' }, data: { enabled: false, enableEpoch: 1 } });
147
+ await db.translationSource.update({ where: { id: evidence.source.id }, data: { generation: 9, sourceRevision: 8, enableEpoch: 1, currentAttemptVersion: 2, effectiveAttemptId: null, jobState: 'SUPERSEDED' } });
148
+ expect(await publish(f)).toEqual(result);
149
+ });
150
+ it.each(['source-key', 'source-scope', 'source-lane', 'source-kind', 'source-text', 'source-hash', 'source-open', 'source-deleted', 'source-not-success', 'effective-attempt', 'attempt-hash', 'attempt-revision', 'attempt-version', 'attempt-generation', 'source-epoch', 'attempt-epoch', 'config-disabled', 'result-text', 'attempt-status'])('rejects PENDING translated evidence with stale or foreign %s', async (invalid) => {
151
+ const f = await fixture({ state: 'PENDING' });
152
+ const { source, attempt } = await translatedTaskInput(f);
153
+ if (invalid === 'source-key')
154
+ await db.translationSource.update({ where: { id: source.id }, data: { sourceKey: JSON.stringify([f.team.id, 'other-input']) } });
155
+ else if (invalid === 'source-scope')
156
+ await db.translationSource.update({ where: { id: source.id }, data: { scopeId: 'another-team' } });
157
+ else if (invalid === 'source-lane')
158
+ await db.translationSource.update({ where: { id: source.id }, data: { laneKey: 'team:another-team' } });
159
+ else if (invalid === 'source-kind')
160
+ await db.translationSource.update({ where: { id: source.id }, data: { sourceKind: 'ROOM_MESSAGE' } });
161
+ else if (invalid === 'source-text')
162
+ await db.translationSource.update({ where: { id: source.id }, data: { originalText: 'Another original' } });
163
+ else if (invalid === 'source-hash') {
164
+ await db.translationSource.update({ where: { id: source.id }, data: { sourceHash: hash('Another original') } });
165
+ await db.translationAttempt.update({ where: { id: attempt.id }, data: { sourceHash: hash('Another original') } });
166
+ }
167
+ else if (invalid === 'source-open')
168
+ await db.translationSource.update({ where: { id: source.id }, data: { blockState: 'OPEN' } });
169
+ else if (invalid === 'source-deleted')
170
+ await db.translationSource.update({ where: { id: source.id }, data: { deletedAt: new Date() } });
171
+ else if (invalid === 'source-not-success')
172
+ await db.translationSource.update({ where: { id: source.id }, data: { jobState: 'FAILED' } });
173
+ else if (invalid === 'effective-attempt')
174
+ await db.translationSource.update({ where: { id: source.id }, data: { effectiveAttemptId: null } });
175
+ else if (invalid === 'attempt-hash')
176
+ await db.translationAttempt.update({ where: { id: attempt.id }, data: { sourceHash: 'wrong-hash' } });
177
+ else if (invalid === 'attempt-revision')
178
+ await db.translationAttempt.update({ where: { id: attempt.id }, data: { sourceRevision: 7 } });
179
+ else if (invalid === 'attempt-version')
180
+ await db.translationAttempt.update({ where: { id: attempt.id }, data: { attemptVersion: 7 } });
181
+ else if (invalid === 'attempt-generation')
182
+ await db.translationAttempt.update({ where: { id: attempt.id }, data: { generation: 7 } });
183
+ else if (invalid === 'source-epoch')
184
+ await db.translationSource.update({ where: { id: source.id }, data: { enableEpoch: 7 } });
185
+ else if (invalid === 'attempt-epoch')
186
+ await db.translationAttempt.update({ where: { id: attempt.id }, data: { enableEpoch: 7 } });
187
+ else if (invalid === 'config-disabled')
188
+ await db.translationSettings.update({ where: { id: 'singleton' }, data: { enabled: false } });
189
+ else if (invalid === 'result-text')
190
+ await db.translationAttempt.update({ where: { id: attempt.id }, data: { resultText: 'Unaccepted text' } });
191
+ else
192
+ await db.translationAttempt.update({ where: { id: attempt.id }, data: { status: 'FAILED' } });
193
+ const before = await db.teamTaskInputSnapshot.findUniqueOrThrow({ where: { id: f.snapshot.id } });
194
+ await expect(publish(f)).rejects.toMatchObject({ code: 'TASK_INPUT_CHANGED' });
195
+ expect(await db.teamTaskInputSnapshot.findUniqueOrThrow({ where: { id: f.snapshot.id } })).toEqual(before);
196
+ expect(await db.roomMessage.count()).toBe(0);
197
+ expect(await db.workRequest.count()).toBe(0);
198
+ expect(await db.deliveryReceipt.count()).toBe(0);
199
+ expect((await db.teamRun.findUniqueOrThrow({ where: { id: f.team.id } })).currentTaskInputSnapshotId).toBeNull();
200
+ });
201
+ it.each([
202
+ ['TeamTaskInputSnapshot', 'updateMany', 1], ['TeamRun', 'update', 1], ['RoomMessage', 'create', 1],
203
+ ['WorkRequest', 'create', 1], ['WorkRequest', 'create', 2], ['RoomMessage', 'update', 1],
204
+ ['DeliveryReceipt', 'create', 1], ['MessageDelivery', 'updateMany', 1],
205
+ ])('rolls back every publication fact after %s.%s #%i fails', async (model, action, occurrence) => {
206
+ const f = await fixture({ state: 'PENDING' });
207
+ fault = { model, action, occurrence, seen: 0, fired: false };
208
+ await expect(publish(f)).rejects.toThrow('injected after');
209
+ expect(fault.fired).toBe(true);
210
+ fault = undefined;
211
+ await unchanged(f);
212
+ await publish(f);
213
+ expect(await db.workRequest.count({ where: { teamRunId: f.team.id } })).toBe(2);
214
+ });
215
+ it('recovers a committed receipt after a lost response and later task/member changes without replay', async () => {
216
+ const f = await fixture();
217
+ const result = await publish(f); // Treat the successful response as lost by the caller.
218
+ await db.task.update({ where: { id: f.task.id }, data: { title: 'Edited after publication' } });
219
+ await db.teamMember.updateMany({ where: { teamRunId: f.team.id }, data: { membershipStatus: 'REMOVED' } });
220
+ await db.workRequest.updateMany({ where: { triggerMessageId: result.messageId }, data: { status: 'COMPLETED' } });
221
+ expect(await publish(f)).toEqual(result);
222
+ await delivery.start();
223
+ await delivery.pump();
224
+ expect(await db.roomMessage.count({ where: { deliveryId: f.row.id } })).toBe(1);
225
+ expect(await db.workRequest.count({ where: { teamRunId: f.team.id } })).toBe(2);
226
+ expect((await db.deliveryReceipt.findUniqueOrThrow({ where: { deliveryId: f.row.id } })).evidence).toBe('ROOM_PUBLISHED');
227
+ });
228
+ it.each(['extra-request', 'missing-request', 'receipt-evidence', 'receipt-generation', 'receipt-runtime', 'message-body', 'message-request-ids', 'participant', 'target-source-owner', 'snapshot-text', 'snapshot-mode', 'snapshot-original-hash'])('fails closed when recovery detects %s', async (corrupt) => {
229
+ const f = await fixture();
230
+ if (corrupt === 'target-source-owner') {
231
+ const source = await db.workspace.create({ data: { taskId: f.task.id, ownerMemberId: f.members[0].id, branchName: 'source', worktreePath: path.join(dir, 'tampered-target-fixture') } });
232
+ f.intent.resolvedRecipients[0].target = { kind: 'WORKSPACE_COMMIT', purpose: 'REVIEW', sourceWorkspaceId: source.id, headSha: 'a'.repeat(40), branchName: 'source' };
233
+ f.intent.resolvedRecipients[0].targetSourceMemberId = f.members[0].id;
234
+ await db.messageDelivery.update({ where: { id: f.row.id }, data: { intent: JSON.stringify(f.intent) } });
235
+ }
236
+ const result = await publish(f);
237
+ const requests = await db.workRequest.findMany({ where: { triggerMessageId: result.messageId } });
238
+ if (corrupt === 'extra-request') {
239
+ const { id: _id, createdAt: _created, updatedAt: _updated, ...data } = requests[0];
240
+ await db.workRequest.create({ data });
241
+ }
242
+ else if (corrupt === 'missing-request')
243
+ await db.workRequest.delete({ where: { id: requests[0].id } });
244
+ else if (corrupt === 'receipt-evidence')
245
+ await db.deliveryReceipt.update({ where: { deliveryId: f.row.id }, data: { evidence: 'CLI_WRITE' } });
246
+ else if (corrupt === 'receipt-generation')
247
+ await db.deliveryReceipt.update({ where: { deliveryId: f.row.id }, data: { generation: 99 } });
248
+ else if (corrupt === 'receipt-runtime')
249
+ await db.deliveryReceipt.update({ where: { deliveryId: f.row.id }, data: { runtimeInstanceId: 'unexpected' } });
250
+ else if (corrupt === 'message-body')
251
+ await db.roomMessage.update({ where: { id: result.messageId }, data: { content: 'Corrupted body' } });
252
+ else if (corrupt === 'message-request-ids')
253
+ await db.roomMessage.update({ where: { id: result.messageId }, data: { workRequestIds: JSON.stringify([requests[0].id, requests[0].id]) } });
254
+ else if (corrupt === 'participant')
255
+ await db.roomMessageParticipant.create({ data: { teamRunId: f.team.id, roomMessageId: result.messageId, memberId: f.members[0].id, role: 'recipient' } });
256
+ else if (corrupt === 'target-source-owner')
257
+ await db.workRequest.update({ where: { id: requests.find(r => r.targetMemberId === f.members[0].id).id }, data: { targetSourceMemberId: f.members[1].id } });
258
+ else if (corrupt === 'snapshot-text')
259
+ await db.teamTaskInputSnapshot.update({ where: { id: f.snapshot.id }, data: { effectiveUserText: 'Corrupted accepted text' } });
260
+ else if (corrupt === 'snapshot-mode')
261
+ await db.teamTaskInputSnapshot.update({ where: { id: f.snapshot.id }, data: { sentMode: null } });
262
+ else
263
+ await db.teamTaskInputSnapshot.update({ where: { id: f.snapshot.id }, data: { originalTitle: 'Corrupted saved original' } });
264
+ const counts = [await db.roomMessage.count(), await db.workRequest.count(), await db.deliveryReceipt.count()];
265
+ await expect(publish(f)).rejects.toMatchObject({ code: 'ROOM_DELIVERY_INTEGRITY' });
266
+ await expect(delivery.start()).rejects.toMatchObject({ code: 'ROOM_DELIVERY_INTEGRITY' });
267
+ expect([await db.roomMessage.count(), await db.workRequest.count(), await db.deliveryReceipt.count()]).toEqual(counts);
268
+ });
269
+ it.each(['revision', 'accepted-text', 'envelope-epoch', 'current-epoch', 'task-hash', 'snapshot-revision', 'snapshot-original', 'removed-member', 'foreign-target', 'target-owner', 'predecessor', 'deleted-task', 'archived-project'])('rejects stale or unavailable %s before publication', async (invalid) => {
270
+ const f = await fixture({ state: 'PENDING' });
271
+ if (invalid === 'revision')
272
+ f.envelope.expectedRevision++;
273
+ else if (invalid === 'accepted-text')
274
+ f.envelope.acceptedText = 'Not accepted';
275
+ else if (invalid === 'envelope-epoch')
276
+ f.envelope.enableEpoch++;
277
+ else if (invalid === 'current-epoch')
278
+ await db.translationSettings.update({ where: { id: 'singleton' }, data: { enableEpoch: 1 } });
279
+ else if (invalid === 'task-hash')
280
+ await db.task.update({ where: { id: f.task.id }, data: { description: 'Changed input' } });
281
+ else if (invalid === 'snapshot-revision')
282
+ f.envelope.taskInput.revision++;
283
+ else if (invalid === 'snapshot-original')
284
+ await db.teamTaskInputSnapshot.update({ where: { id: f.snapshot.id }, data: { originalTitle: 'Wrong original' } });
285
+ else if (invalid === 'removed-member')
286
+ await db.teamMember.update({ where: { id: f.members[1].id }, data: { membershipStatus: 'REMOVED' } });
287
+ else if (invalid === 'foreign-target') {
288
+ f.intent.resolvedRecipients[1].target = { kind: 'WORKSPACE_COMMIT', purpose: 'REVIEW', sourceWorkspaceId: 'not-in-this-team', headSha: 'a'.repeat(40), branchName: 'feature' };
289
+ await db.messageDelivery.update({ where: { id: f.row.id }, data: { intent: JSON.stringify(f.intent) } });
290
+ }
291
+ else if (invalid === 'target-owner') {
292
+ const source = await db.workspace.create({ data: { taskId: f.task.id, ownerMemberId: f.members[0].id, branchName: 'source', worktreePath: path.join(dir, 'changed-owner-fixture') } });
293
+ f.intent.resolvedRecipients[1].target = { kind: 'WORKSPACE_COMMIT', purpose: 'TEST', sourceWorkspaceId: source.id, headSha: 'a'.repeat(40), branchName: 'source' };
294
+ f.intent.resolvedRecipients[1].targetSourceMemberId = f.members[1].id;
295
+ await db.messageDelivery.update({ where: { id: f.row.id }, data: { intent: JSON.stringify(f.intent) } });
296
+ }
297
+ else if (invalid === 'predecessor') {
298
+ await db.messageDelivery.update({ where: { id: f.row.id }, data: { sequence: 2 } });
299
+ const { id: _id, createdAt: _created, updatedAt: _updated, ...data } = f.row;
300
+ await db.messageDelivery.create({ data: { ...data, clientSubmissionId: randomUUID(), sequence: 1, state: 'DELIVERY_UNKNOWN' } });
301
+ }
302
+ else if (invalid === 'deleted-task')
303
+ await db.task.update({ where: { id: f.task.id }, data: { deletedAt: new Date() } });
304
+ else
305
+ await db.project.update({ where: { id: f.project.id }, data: { archivedAt: new Date() } });
306
+ const snapshotBefore = await db.teamTaskInputSnapshot.findUniqueOrThrow({ where: { id: f.snapshot.id } });
307
+ await expect(publish(f)).rejects.toMatchObject({ statusCode: expect.any(Number) });
308
+ expect(await db.roomMessage.count()).toBe(0);
309
+ expect(await db.workRequest.count()).toBe(0);
310
+ expect(await db.deliveryReceipt.count()).toBe(0);
311
+ expect(await db.teamTaskInputSnapshot.findUniqueOrThrow({ where: { id: f.snapshot.id } })).toEqual(snapshotBefore);
312
+ expect((await db.teamRun.findUniqueOrThrow({ where: { id: f.team.id } })).currentTaskInputSnapshotId).toBeNull();
313
+ expect((await db.messageDelivery.findUniqueOrThrow({ where: { id: f.row.id } })).state).toBe('READY');
314
+ });
315
+ it('concurrent attempts and their retries produce one complete publication', async () => {
316
+ const f = await fixture();
317
+ await db.$queryRawUnsafe('PRAGMA busy_timeout = 100');
318
+ try {
319
+ const attempts = await Promise.allSettled([publish(f), publish(f)]);
320
+ for (const result of attempts)
321
+ if (result.status === 'rejected')
322
+ expect(['P1008', 'P2028', 'P2034', 'P2002']).toContain(result.reason.code);
323
+ expect(attempts.some(result => result.status === 'fulfilled')).toBe(true);
324
+ const first = await publish(f);
325
+ const second = await publish(f);
326
+ expect(second).toEqual(first);
327
+ expect(await db.roomMessage.count({ where: { deliveryId: f.row.id } })).toBe(1);
328
+ expect(await db.workRequest.count({ where: { triggerMessageId: first.messageId } })).toBe(2);
329
+ expect(await db.deliveryReceipt.count({ where: { deliveryId: f.row.id } })).toBe(1);
330
+ }
331
+ finally {
332
+ await db.$queryRawUnsafe('PRAGMA busy_timeout = 5000');
333
+ }
334
+ }, 15_000);
335
+ it.each([1, 2])('holds newer original input U until all %i queued consumers of pending T are cancelled', async (consumers) => {
336
+ const f = await fixture({ mode: 'AUTO', state: 'PENDING' });
337
+ await db.messageDelivery.delete({ where: { id: f.row.id } });
338
+ const pendingT = f.snapshot;
339
+ const trigger = await db.roomMessage.create({ data: { teamRunId: f.team.id, senderType: 'user', kind: 'work_request', visibility: 'PUBLIC', content: buildTaskUserText(f.task.title, f.task.description), mentions: '[]', workRequestIds: '[]' } });
340
+ const earlier = [];
341
+ for (const member of f.members.slice(0, consumers))
342
+ earlier.push(await db.workRequest.create({ data: { teamRunId: f.team.id, requesterType: 'user', targetMemberId: member.id, triggerMessageId: trigger.id, instruction: 'Earlier input T waits for a decision', status: 'QUEUED', taskInputSnapshotId: pendingT.id, createdAt: new Date(Date.now() - 1000) } }));
343
+ await db.roomMessage.update({ where: { id: trigger.id }, data: { workRequestIds: JSON.stringify(earlier.map(w => w.id)) } });
344
+ f.task = await db.task.update({ where: { id: f.task.id }, data: { title: 'Newer original task U', description: 'U must wait while a consumer still requires pending T' } });
345
+ const taskSourceHash = taskInputHash(f.task.title, f.task.description);
346
+ f.snapshot = await db.teamTaskInputSnapshot.create({ data: { teamRunId: f.team.id, taskId: f.task.id, taskSourceHash, originalTitle: f.task.title, originalDescription: f.task.description, state: 'PENDING' } });
347
+ f.intent.taskSourceHash = taskSourceHash;
348
+ const { id: _id, createdAt: _createdAt, updatedAt: _updatedAt, ...deliveryData } = f.row;
349
+ f.row = await db.messageDelivery.create({ data: { ...deliveryData, clientSubmissionId: randomUUID(), intent: JSON.stringify(f.intent) } });
350
+ f.envelope = { deliveryId: f.row.id, expectedRevision: f.row.revision, enableEpoch: f.row.enableEpoch, acceptedText: f.row.acceptedText, taskInput: { snapshotId: f.snapshot.id, revision: f.snapshot.revision, taskSourceHash, effectiveUserText: buildTaskUserText(f.task.title, f.task.description), sentMode: 'ORIGINAL', acceptedAttemptId: null } };
351
+ expect((await settings.getPublic()).enabled).toBe(false);
352
+ const assertBlocked = async () => {
353
+ await expect(publish(f)).rejects.toMatchObject({ code: 'TASK_INPUT_PREDECESSOR' });
354
+ expect(await db.roomMessage.count({ where: { deliveryId: f.row.id } })).toBe(0);
355
+ expect(await db.deliveryReceipt.count({ where: { deliveryId: f.row.id } })).toBe(0);
356
+ expect(await db.messageDelivery.findUniqueOrThrow({ where: { id: f.row.id } })).toEqual(f.row);
357
+ expect(await db.teamTaskInputSnapshot.findUniqueOrThrow({ where: { id: f.snapshot.id } })).toEqual(f.snapshot);
358
+ expect(await db.teamTaskInputSnapshot.findUniqueOrThrow({ where: { id: pendingT.id } })).toEqual(pendingT);
359
+ expect((await db.teamRun.findUniqueOrThrow({ where: { id: f.team.id } })).currentTaskInputSnapshotId).toBeNull();
360
+ expect(await db.workRequest.count({ where: { teamRunId: f.team.id } })).toBe(consumers);
361
+ };
362
+ await assertBlocked();
363
+ for (const [index, request] of earlier.entries()) {
364
+ await db.workRequest.update({ where: { id: request.id }, data: { status: 'CANCELLED' } });
365
+ if (index < earlier.length - 1)
366
+ await assertBlocked();
367
+ }
368
+ const result = await publish(f);
369
+ expect(await db.roomMessage.count({ where: { deliveryId: f.row.id } })).toBe(1);
370
+ expect(result.workRequestIds).toHaveLength(2);
371
+ expect(await db.teamTaskInputSnapshot.findUniqueOrThrow({ where: { id: f.snapshot.id } })).toMatchObject({ state: 'ACCEPTED', sentMode: 'ORIGINAL', effectiveUserText: f.envelope.taskInput.effectiveUserText });
372
+ expect(await db.teamTaskInputSnapshot.findUniqueOrThrow({ where: { id: pendingT.id } })).toEqual(pendingT);
373
+ expect((await db.teamRun.findUniqueOrThrow({ where: { id: f.team.id } })).currentTaskInputSnapshotId).toBe(f.snapshot.id);
374
+ });
375
+ it.each(['TEAM_INITIAL'])('malformed legacy %s intent cannot enter the enabled creation adapter', async (origin) => {
376
+ const f = await fixture();
377
+ await db.messageDelivery.update({ where: { id: f.row.id }, data: { origin, state: 'WAITING_TRANSLATION', requiresTranslation: true } });
378
+ const before = await db.messageDelivery.findUniqueOrThrow({ where: { id: f.row.id } });
379
+ await delivery.start();
380
+ await delivery.pump();
381
+ await expect(delivery.get(f.row.id)).rejects.toMatchObject({ code: 'ROOM_DELIVERY_INTEGRITY' });
382
+ await expect(delivery.action(f.row.id, 'send_original', before.revision, randomUUID())).rejects.toMatchObject({ code: 'ROOM_DELIVERY_INTEGRITY' });
383
+ await expect(delivery.claim(f.row.id, before.revision)).rejects.toMatchObject({ code: 'DELIVERY_DESTINATION_DISABLED' });
384
+ await expect(translation.registerSource({ kind: 'USER_DELIVERY', deliveryId: f.row.id })).rejects.toMatchObject({ code: 'ROOM_DELIVERY_INTEGRITY' });
385
+ expect(await db.messageDelivery.findUniqueOrThrow({ where: { id: f.row.id } })).toEqual(before);
386
+ expect(await db.translationSource.count()).toBe(0);
387
+ expect(await db.deliveryAction.count()).toBe(0);
388
+ expect(await db.roomMessage.count()).toBe(0);
389
+ expect(await db.workRequest.count()).toBe(0);
390
+ expect(runtime.start).not.toHaveBeenCalled();
391
+ expect(runtime.sendMessage).not.toHaveBeenCalled();
392
+ });
393
+ it.each(['TASK_INPUT', 'unknown-adapter'])('rejects runtime adapter injection for %s before looking up a valid room message', async (kind) => {
394
+ const f = await fixture();
395
+ const message = await db.roomMessage.create({ data: { teamRunId: f.team.id, senderType: 'agent', senderId: f.members[0].id, kind: 'chat', visibility: 'PUBLIC', content: 'Eligible PUBLIC agent message', translationEligibilityEpoch: 0, mentions: '[]' } });
396
+ const injected = { kind, messageId: message.id };
397
+ await expect(translation.registerSource(injected)).rejects.toMatchObject({ code: 'INELIGIBLE_SOURCE' });
398
+ expect(await db.translationSource.count()).toBe(0);
399
+ });
400
+ // Original independent review counterexamples, retained in the evidence directory.
401
+ async function startupBarrier() {
402
+ const { synchronizeDatabaseSchema } = await import('../../utils/database-schema.js');
403
+ await synchronizeDatabaseSchema({ dataDir: dir, dbPath: path.join(dir, 'test.db'), schemaPath: path.join(root, 'prisma/schema.prisma'), entryDir: path.join(root, 'src') });
404
+ }
405
+ it('review: rejects reuse of an ACCEPTED snapshot tied to another team source', async () => {
406
+ const f = await fixture({ state: 'PENDING' });
407
+ const other = await fixture({ state: 'PENDING' });
408
+ const { source, attempt } = await translatedTaskInput(other);
409
+ await db.teamTaskInputSnapshot.update({ where: { id: other.snapshot.id }, data: { translationSourceId: null } });
410
+ await db.teamTaskInputSnapshot.update({ where: { id: f.snapshot.id }, data: { state: 'ACCEPTED', translationSourceId: source.id, acceptedAttemptId: attempt.id, sentMode: 'TRANSLATED', effectiveUserText: attempt.resultText, acceptedAt: new Date() } });
411
+ f.envelope.taskInput = { ...f.envelope.taskInput, sentMode: 'TRANSLATED', effectiveUserText: attempt.resultText, acceptedAttemptId: attempt.id };
412
+ f.snapshot = await db.teamTaskInputSnapshot.findUniqueOrThrow({ where: { id: f.snapshot.id } });
413
+ // A valid hash/text does not make another team's translation this snapshot's provenance.
414
+ const result = await publish(f).then(value => ({ published: value }), error => ({ code: error.code }));
415
+ const startup = await startupBarrier().then(() => ({ ok: true }), error => ({ error: error.message }));
416
+ console.log('FOREIGN_ACCEPTED_SOURCE', JSON.stringify({ result, startup }));
417
+ expect(result).toEqual({ code: 'TASK_INPUT_CHANGED' });
418
+ await unchanged(f);
419
+ expect(startup).toHaveProperty('error');
420
+ });
421
+ it('review: startup rejects a RoomMessage linked to a SESSION delivery', async () => {
422
+ const f = await fixture();
423
+ await db.messageDelivery.delete({ where: { id: f.row.id } });
424
+ const workspace = await db.workspace.create({ data: { taskId: f.task.id, branchName: 'fixture', worktreePath: dir } });
425
+ const session = await db.session.create({ data: { workspaceId: workspace.id, agentType: 'CODEX', prompt: 'fixture', status: 'COMPLETED' } });
426
+ const d = await db.messageDelivery.create({ data: { clientSubmissionId: randomUUID(), requestFingerprint: 'frozen', sessionId: session.id, channelKey: `session:${session.id}`, sequence: 1, origin: 'TASK_FOLLOWUP', originalText: 'Original', sourceHash: hash('Original'), enableEpoch: 0, requiresTranslation: false, state: 'READY', sentMode: 'ORIGINAL', acceptedText: 'Original' } });
427
+ await db.roomMessage.create({ data: { deliveryId: d.id, teamRunId: f.team.id, senderType: 'agent', senderId: f.members[0].id, kind: 'chat', visibility: 'PUBLIC', content: 'Unrelated agent room message', mentions: '[]' } });
428
+ await expect(startupBarrier()).rejects.toThrow();
429
+ });
430
+ it('review: startup rejects invalid ACCEPTED ORIGINAL snapshot outside delivery history', async () => {
431
+ const f = await fixture();
432
+ await db.messageDelivery.delete({ where: { id: f.row.id } });
433
+ await db.teamTaskInputSnapshot.update({ where: { id: f.snapshot.id }, data: { effectiveUserText: 'Not the original task input' } });
434
+ await db.teamRun.update({ where: { id: f.team.id }, data: { currentTaskInputSnapshotId: f.snapshot.id } });
435
+ await expect(startupBarrier()).rejects.toThrow();
436
+ });
437
+ it('review: receipt recovery tolerates valid WR input rebinding before claim', async () => {
438
+ const f = await fixture({ mode: 'AUTO' });
439
+ const result = await publish(f);
440
+ const title = 'New task title before queued work is claimed';
441
+ await db.task.update({ where: { id: f.task.id }, data: { title } });
442
+ const next = await db.teamTaskInputSnapshot.create({ data: { teamRunId: f.team.id, taskId: f.task.id, taskSourceHash: taskInputHash(title, f.task.description), originalTitle: title, originalDescription: f.task.description, state: 'ACCEPTED', sentMode: 'ORIGINAL', effectiveUserText: buildTaskUserText(title, f.task.description), acceptedAt: new Date() } });
443
+ await db.workRequest.update({ where: { id: result.workRequestIds[0] }, data: { taskInputSnapshotId: next.id } });
444
+ const recovery = await publish(f).then(value => ({ value }), error => ({ code: error.code }));
445
+ const startup = await startupBarrier().then(() => ({ ok: true }), error => ({ error: error.message }));
446
+ console.log('WR_REBINDING', JSON.stringify({ recovery, startup }));
447
+ expect(recovery).toEqual({ value: result });
448
+ expect(startup).toEqual({ ok: true });
449
+ });
450
+ it.each(['current', 'invocation', 'unreferenced'])('checks independent ORIGINAL acceptance referenced by %s before workers', async (reference) => {
451
+ const f = await fixture();
452
+ await db.messageDelivery.delete({ where: { id: f.row.id } });
453
+ if (reference === 'current')
454
+ await db.teamRun.update({ where: { id: f.team.id }, data: { currentTaskInputSnapshotId: f.snapshot.id } });
455
+ if (reference === 'invocation')
456
+ await db.agentInvocation.create({ data: { teamRunId: f.team.id, memberId: f.members[0].id, workRequestId: 'historical-reference', taskInputSnapshotId: f.snapshot.id, status: 'COMPLETED' } });
457
+ await db.teamTaskInputSnapshot.update({ where: { id: f.snapshot.id }, data: { effectiveUserText: 'Corrupted original acceptance' } });
458
+ await expect(startupBarrier()).rejects.toThrow('Team task input facts are invalid');
459
+ expect(runtime.start).not.toHaveBeenCalled();
460
+ });
461
+ it.each(['source-scope-type', 'source-scope', 'source-kind', 'source-key', 'source-lane', 'source-direction', 'source-anchor', 'source-original', 'source-hash', 'source-open', 'attempt-source', 'attempt-hash', 'attempt-status', 'attempt-result', 'snapshot-hash', 'snapshot-mode'])('publishing and startup reject immutable ACCEPTED evidence corruption: %s', async (corrupt) => {
462
+ const f = await fixture({ state: 'PENDING' });
463
+ const { source, attempt } = await translatedTaskInput(f);
464
+ await db.teamTaskInputSnapshot.update({ where: { id: f.snapshot.id }, data: { state: 'ACCEPTED', effectiveUserText: attempt.resultText, acceptedAttemptId: attempt.id, sentMode: 'TRANSLATED', acceptedAt: new Date() } });
465
+ if (corrupt === 'source-scope-type')
466
+ await db.translationSource.update({ where: { id: source.id }, data: { scopeType: 'SESSION' } });
467
+ else if (corrupt === 'source-scope')
468
+ await db.translationSource.update({ where: { id: source.id }, data: { scopeId: 'other-team' } });
469
+ else if (corrupt === 'source-kind')
470
+ await db.translationSource.update({ where: { id: source.id }, data: { sourceKind: 'ROOM_MESSAGE' } });
471
+ else if (corrupt === 'source-key')
472
+ await db.translationSource.update({ where: { id: source.id }, data: { sourceKey: JSON.stringify([f.team.id, 'other-version']) } });
473
+ else if (corrupt === 'source-lane')
474
+ await db.translationSource.update({ where: { id: source.id }, data: { laneKey: 'team:other-team' } });
475
+ else if (corrupt === 'source-direction')
476
+ await db.translationSource.update({ where: { id: source.id }, data: { direction: 'DISPLAY' } });
477
+ else if (corrupt === 'source-anchor')
478
+ await db.translationSource.update({ where: { id: source.id }, data: { messageId: 'another-anchor' } });
479
+ else if (corrupt === 'source-original')
480
+ await db.translationSource.update({ where: { id: source.id }, data: { originalText: 'Other original' } });
481
+ else if (corrupt === 'source-hash')
482
+ await db.translationSource.update({ where: { id: source.id }, data: { sourceHash: hash('Other original') } });
483
+ else if (corrupt === 'source-open')
484
+ await db.translationSource.update({ where: { id: source.id }, data: { blockState: 'OPEN' } });
485
+ else if (corrupt === 'attempt-source') {
486
+ const other = await fixture({ state: 'PENDING' });
487
+ const foreign = await translatedTaskInput(other);
488
+ await db.teamTaskInputSnapshot.update({ where: { id: f.snapshot.id }, data: { acceptedAttemptId: foreign.attempt.id } });
489
+ }
490
+ else if (corrupt === 'attempt-hash')
491
+ await db.translationAttempt.update({ where: { id: attempt.id }, data: { sourceHash: hash('Other original') } });
492
+ else if (corrupt === 'attempt-status')
493
+ await db.translationAttempt.update({ where: { id: attempt.id }, data: { status: 'FAILED' } });
494
+ else if (corrupt === 'attempt-result')
495
+ await db.translationAttempt.update({ where: { id: attempt.id }, data: { resultText: 'Other result' } });
496
+ else if (corrupt === 'snapshot-hash')
497
+ await db.teamTaskInputSnapshot.update({ where: { id: f.snapshot.id }, data: { taskSourceHash: 'wrong-hash' } });
498
+ else
499
+ await db.teamTaskInputSnapshot.update({ where: { id: f.snapshot.id }, data: { sentMode: 'ORIGINAL' } });
500
+ f.snapshot = await db.teamTaskInputSnapshot.findUniqueOrThrow({ where: { id: f.snapshot.id } });
501
+ await expect(publish(f)).rejects.toMatchObject({ code: 'TASK_INPUT_CHANGED' });
502
+ await unchanged(f);
503
+ await expect(startupBarrier()).rejects.toThrow('Team task input facts are invalid');
504
+ });
505
+ it('preserves accepted history across config, source revision/generation and a newer successful attempt', async () => {
506
+ const f = await fixture({ state: 'PENDING' });
507
+ const { source, attempt } = await translatedTaskInput(f);
508
+ const result = await publish(f);
509
+ const later = await db.translationAttempt.create({ data: { sourceId: source.id, sourceRevision: 9, sourceHash: source.sourceHash, attemptVersion: 2, configRevision: 10, credentialRevision: 10, targetLanguage: 'French', sanitizedConfigSnapshot: '{}', protectionVersion: 'fixture', status: 'SUCCESS', resultText: 'A different later translation', generation: 9, enableEpoch: 8 } });
510
+ await db.translationSettings.update({ where: { id: 'singleton' }, data: { enabled: false, revision: 10, credentialRevision: 10, enableEpoch: 8, sendTargetLanguage: 'French' } });
511
+ await db.translationSource.update({ where: { id: source.id }, data: { generation: 9, sourceRevision: 9, currentAttemptVersion: 2, effectiveAttemptId: later.id, enableEpoch: 8, deletedAt: new Date(), jobState: 'SUPERSEDED' } });
512
+ await db.task.update({ where: { id: f.task.id }, data: { title: 'Later task version' } });
513
+ expect(await publish(f)).toEqual(result);
514
+ await startupBarrier();
515
+ expect(await db.teamTaskInputSnapshot.findUniqueOrThrow({ where: { id: f.snapshot.id } })).toMatchObject({ acceptedAttemptId: attempt.id, effectiveUserText: attempt.resultText });
516
+ });
517
+ it.each([['PENDING', 'AUTO'], ['ACCEPTED', 'AUTO'], ['PENDING', 'CONFIRM'], ['ACCEPTED', 'CONFIRM']])('recovers original publication identity while WR follows a new %s input in %s', async (state, mode) => {
518
+ const f = await fixture({ mode });
519
+ const result = await publish(f);
520
+ const title = 'Next actual task version';
521
+ await db.task.update({ where: { id: f.task.id }, data: { title, description: null } });
522
+ const next = await db.teamTaskInputSnapshot.create({ data: { teamRunId: f.team.id, taskId: f.task.id, taskSourceHash: taskInputHash(title, null), originalTitle: title, state, ...(state === 'ACCEPTED' ? { sentMode: 'ORIGINAL', effectiveUserText: title, acceptedAt: new Date() } : {}) } });
523
+ await db.workRequest.update({ where: { id: result.workRequestIds[0] }, data: { taskInputSnapshotId: next.id } });
524
+ if (state === 'ACCEPTED' && mode === 'AUTO') {
525
+ const claimed = await db.workRequest.update({ where: { id: result.workRequestIds[0] }, data: { status: 'STARTED' } });
526
+ await db.agentInvocation.create({ data: { teamRunId: f.team.id, workRequestId: claimed.id, memberId: claimed.targetMemberId, taskInputSnapshotId: next.id, status: 'RUNNING' } });
527
+ }
528
+ expect(await publish(f)).toEqual(result);
529
+ await startupBarrier();
530
+ await delivery.start();
531
+ expect((await db.messageDelivery.findUniqueOrThrow({ where: { id: f.row.id } })).taskInputSnapshotId).toBe(f.snapshot.id);
532
+ expect((await db.workRequest.findUniqueOrThrow({ where: { id: result.workRequestIds[0] } })).taskInputSnapshotId).toBe(next.id);
533
+ expect(runtime.start).not.toHaveBeenCalled();
534
+ });
535
+ it.each(['PENDING', 'ACCEPTED'])('keeps a failed invocation on its old input while the requeued WR awaits %s', async (state) => {
536
+ const f = await fixture({ mode: 'AUTO' });
537
+ const result = await publish(f);
538
+ const request = await db.workRequest.findUniqueOrThrow({ where: { id: result.workRequestIds[0] } });
539
+ const failed = await db.agentInvocation.create({ data: { teamRunId: f.team.id, memberId: request.targetMemberId, workRequestId: request.id, taskInputSnapshotId: f.snapshot.id, status: 'FAILED' } });
540
+ await db.task.update({ where: { id: f.task.id }, data: { title: 'Retry after task edit', description: null } });
541
+ const next = await db.teamTaskInputSnapshot.create({ data: { teamRunId: f.team.id, taskId: f.task.id, taskSourceHash: taskInputHash('Retry after task edit', null), originalTitle: 'Retry after task edit', state, ...(state === 'ACCEPTED' ? { sentMode: 'ORIGINAL', effectiveUserText: 'Retry after task edit', acceptedAt: new Date() } : {}) } });
542
+ await db.workRequest.update({ where: { id: request.id }, data: { taskInputSnapshotId: next.id, startAttemptCount: 1 } });
543
+ expect(await publish(f)).toEqual(result);
544
+ await startupBarrier();
545
+ expect((await db.agentInvocation.findUniqueOrThrow({ where: { id: failed.id } })).taskInputSnapshotId).toBe(f.snapshot.id);
546
+ });
547
+ it.each(['null', 'foreign', 'invalid-accepted', 'claimed-pending', 'invocation-mismatch'])('rejects an invalid WR input dependency after publication: %s', async (corrupt) => {
548
+ const f = await fixture({ mode: 'AUTO' });
549
+ const result = await publish(f);
550
+ const requestId = result.workRequestIds[0];
551
+ let dependencyId = f.snapshot.id;
552
+ if (corrupt === 'null')
553
+ await db.workRequest.update({ where: { id: requestId }, data: { taskInputSnapshotId: null } });
554
+ else if (corrupt === 'foreign') {
555
+ const other = await fixture();
556
+ await db.workRequest.update({ where: { id: requestId }, data: { taskInputSnapshotId: other.snapshot.id } });
557
+ }
558
+ else if (corrupt === 'invalid-accepted')
559
+ await db.teamTaskInputSnapshot.update({ where: { id: f.snapshot.id }, data: { effectiveUserText: 'Corrupted acceptance' } });
560
+ else {
561
+ const next = await db.teamTaskInputSnapshot.create({ data: { teamRunId: f.team.id, taskId: f.task.id, taskSourceHash: taskInputHash('Next version', null), originalTitle: 'Next version', ...(corrupt === 'invocation-mismatch' ? { state: 'ACCEPTED', sentMode: 'ORIGINAL', effectiveUserText: 'Next version', acceptedAt: new Date() } : {}) } });
562
+ dependencyId = next.id;
563
+ if (corrupt === 'claimed-pending')
564
+ await db.workRequest.update({ where: { id: requestId }, data: { status: 'STARTED', taskInputSnapshotId: next.id } });
565
+ await db.agentInvocation.create({ data: { teamRunId: f.team.id, workRequestId: requestId, memberId: (await db.workRequest.findUniqueOrThrow({ where: { id: requestId } })).targetMemberId, taskInputSnapshotId: dependencyId, status: 'RUNNING' } });
566
+ }
567
+ const counts = [await db.roomMessage.count(), await db.workRequest.count(), await db.deliveryReceipt.count()];
568
+ await expect(publish(f)).rejects.toMatchObject({ code: 'ROOM_DELIVERY_INTEGRITY' });
569
+ await expect(startupBarrier()).rejects.toThrow();
570
+ expect([await db.roomMessage.count(), await db.workRequest.count(), await db.deliveryReceipt.count()]).toEqual(counts);
571
+ });
572
+ const activeStatuses = ['QUEUED', 'RUNNING', 'SESSION_ENDED', 'WAITING_ROOM_REPLY'];
573
+ const claimFaults = ['null', 'foreign-team-null', 'wrong-member-null', 'foreign-team', 'wrong-member', 'other-wr', 'foreign-input', 'pending-input', 'invalid-accepted', 'version-mismatch', 'deleted-input', 'revoked-null', 'second-null'];
574
+ async function nextInput(f, state) {
575
+ const title = 'Next input after publication';
576
+ return db.teamTaskInputSnapshot.create({ data: {
577
+ teamRunId: f.team.id, taskId: f.task.id, taskSourceHash: taskInputHash(title, null), originalTitle: title, state,
578
+ ...(state === 'ACCEPTED' ? { sentMode: 'ORIGINAL', effectiveUserText: title, acceptedAt: new Date() } : {}),
579
+ } });
580
+ }
581
+ const savedFacts = () => Promise.all([
582
+ db.messageDelivery.findMany(), db.roomMessage.findMany(), db.workRequest.findMany(), db.agentInvocation.findMany(),
583
+ db.deliveryReceipt.findMany(), db.teamTaskInputSnapshot.findMany(), db.teamRun.findMany(),
584
+ ]);
585
+ it.each(activeStatuses.flatMap(status => claimFaults.map(corrupt => [status, corrupt])))('rejects every active claim at recovery and startup: %s / %s', async (status, corrupt) => {
586
+ const f = await fixture({ mode: 'AUTO' });
587
+ const result = await publish(f);
588
+ const request = await db.workRequest.findUniqueOrThrow({ where: { id: result.workRequestIds[0] } });
589
+ const next = await nextInput(f, corrupt === 'pending-input' ? 'PENDING' : 'ACCEPTED');
590
+ await db.workRequest.update({ where: { id: request.id }, data: { status: corrupt === 'pending-input' ? 'QUEUED' : 'STARTED', taskInputSnapshotId: next.id } });
591
+ const foreign = corrupt.startsWith('foreign-') ? await fixture() : f;
592
+ const claim = await db.agentInvocation.create({ data: {
593
+ teamRunId: corrupt.startsWith('foreign-team') ? foreign.team.id : f.team.id,
594
+ memberId: corrupt.startsWith('foreign-team') ? foreign.members[0].id : corrupt.startsWith('wrong-member') ? f.members[1].id : request.targetMemberId,
595
+ workRequestId: corrupt === 'other-wr' ? result.workRequestIds[1] : request.id,
596
+ taskInputSnapshotId: corrupt.endsWith('null') && corrupt !== 'second-null' ? null : corrupt === 'foreign-input' ? foreign.snapshot.id : corrupt === 'version-mismatch' ? f.snapshot.id : next.id,
597
+ status, ...(corrupt === 'revoked-null' ? { dispatchRevokedAt: new Date() } : {}),
598
+ } });
599
+ if (corrupt === 'invalid-accepted')
600
+ await db.teamTaskInputSnapshot.update({ where: { id: next.id }, data: { effectiveUserText: 'Corrupted accepted evidence' } });
601
+ if (corrupt === 'deleted-input') {
602
+ // Delete a separate claim input: FK SET NULL must not hide lost evidence.
603
+ const removed = await db.teamTaskInputSnapshot.create({ data: { ...next, id: randomUUID(), taskSourceHash: taskInputHash('Removed input', null), originalTitle: 'Removed input', effectiveUserText: 'Removed input' } });
604
+ await db.agentInvocation.update({ where: { id: claim.id }, data: { taskInputSnapshotId: removed.id } });
605
+ await db.teamTaskInputSnapshot.delete({ where: { id: removed.id } });
606
+ }
607
+ if (corrupt === 'second-null')
608
+ await db.agentInvocation.create({ data: { teamRunId: f.team.id, memberId: request.targetMemberId, workRequestId: request.id, taskInputSnapshotId: null, status } });
609
+ const before = await savedFacts();
610
+ await expect(publish(f)).rejects.toMatchObject({ code: 'ROOM_DELIVERY_INTEGRITY' });
611
+ await expect(startupBarrier()).rejects.toThrow();
612
+ expect(await savedFacts()).toEqual(before);
613
+ expect(runtime.start).not.toHaveBeenCalled();
614
+ });
615
+ it.each(activeStatuses)('allows a complete active claim on the WR new ACCEPTED version: %s', async (status) => {
616
+ const f = await fixture({ mode: 'AUTO' });
617
+ const result = await publish(f);
618
+ const next = await nextInput(f, 'ACCEPTED');
619
+ const request = await db.workRequest.update({ where: { id: result.workRequestIds[0] }, data: { status: 'STARTED', taskInputSnapshotId: next.id } });
620
+ await db.agentInvocation.create({ data: { teamRunId: f.team.id, memberId: request.targetMemberId, workRequestId: request.id, taskInputSnapshotId: next.id, status } });
621
+ const before = await savedFacts();
622
+ expect(await publish(f)).toEqual(result);
623
+ await startupBarrier();
624
+ expect(await savedFacts()).toEqual(before);
625
+ expect(runtime.start).not.toHaveBeenCalled();
626
+ });
627
+ it.each(['COMPLETED', 'FAILED', 'CANCELLED'].flatMap(status => ['PENDING', 'ACCEPTED'].map(state => [status, state])))('preserves terminal input and NULL history while requeued WR uses a new input: %s / %s', async (status, state) => {
628
+ const f = await fixture({ mode: 'AUTO' });
629
+ const result = await publish(f);
630
+ const next = await nextInput(f, state);
631
+ const request = await db.workRequest.update({ where: { id: result.workRequestIds[0] }, data: { taskInputSnapshotId: next.id } });
632
+ for (const taskInputSnapshotId of [f.snapshot.id, null])
633
+ await db.agentInvocation.create({ data: { teamRunId: f.team.id, memberId: request.targetMemberId, workRequestId: request.id, taskInputSnapshotId, status } });
634
+ const before = await savedFacts();
635
+ expect(await publish(f)).toEqual(result);
636
+ await startupBarrier();
637
+ expect(await savedFacts()).toEqual(before);
638
+ });
639
+ it.each([...activeStatuses, 'COMPLETED', 'FAILED', 'CANCELLED'])('preserves unlinked legacy message/WR/Invocation NULL history: %s', async (status) => {
640
+ const legacy = await fixture();
641
+ const message = await db.roomMessage.create({ data: { teamRunId: legacy.team.id, senderType: 'user', kind: 'chat', content: 'Legacy original', mentions: '[]' } });
642
+ const request = await db.workRequest.create({ data: { teamRunId: legacy.team.id, requesterType: 'user', targetMemberId: legacy.members[0].id, triggerMessageId: message.id, instruction: 'Legacy original', status: 'STARTED' } });
643
+ await db.agentInvocation.create({ data: { teamRunId: legacy.team.id, memberId: request.targetMemberId, workRequestId: request.id, status } });
644
+ const f = await fixture();
645
+ const result = await publish(f);
646
+ const before = await savedFacts();
647
+ expect(await publish(f)).toEqual(result);
648
+ await startupBarrier();
649
+ expect(await savedFacts()).toEqual(before);
650
+ });
651
+ it.each(['agent', 'private', 'foreign-team', 'missing-receipt', 'wrong-origin'])('startup checks every reverse room publication claim: %s', async (corrupt) => {
652
+ const f = await fixture();
653
+ const result = await publish(f);
654
+ if (corrupt === 'agent')
655
+ await db.roomMessage.update({ where: { id: result.messageId }, data: { senderType: 'agent', senderId: f.members[0].id } });
656
+ else if (corrupt === 'private')
657
+ await db.roomMessage.update({ where: { id: result.messageId }, data: { visibility: 'PRIVATE' } });
658
+ else if (corrupt === 'foreign-team') {
659
+ const other = await fixture();
660
+ await db.roomMessage.update({ where: { id: result.messageId }, data: { teamRunId: other.team.id } });
661
+ }
662
+ else if (corrupt === 'missing-receipt')
663
+ await db.deliveryReceipt.delete({ where: { deliveryId: f.row.id } });
664
+ else
665
+ await db.messageDelivery.update({ where: { id: f.row.id }, data: { origin: 'TASK_FOLLOWUP' } });
666
+ await expect(startupBarrier()).rejects.toThrow();
667
+ expect(runtime.start).not.toHaveBeenCalled();
668
+ });
669
+ });
670
+ //# sourceMappingURL=team-room-delivery.test.js.map