agent-tower 0.4.15 → 0.4.16-beta.3

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 (279) hide show
  1. package/dist/app.test.js +2 -0
  2. package/dist/app.test.js.map +1 -1
  3. package/dist/core/event-bus.d.ts +2 -0
  4. package/dist/core/event-bus.d.ts.map +1 -1
  5. package/dist/core/event-bus.js.map +1 -1
  6. package/dist/executors/__tests__/codex.executor.test.d.ts +2 -0
  7. package/dist/executors/__tests__/codex.executor.test.d.ts.map +1 -0
  8. package/dist/executors/__tests__/codex.executor.test.js +28 -0
  9. package/dist/executors/__tests__/codex.executor.test.js.map +1 -0
  10. package/dist/executors/codex.executor.d.ts +1 -0
  11. package/dist/executors/codex.executor.d.ts.map +1 -1
  12. package/dist/executors/codex.executor.js +19 -1
  13. package/dist/executors/codex.executor.js.map +1 -1
  14. package/dist/git/git-cli.d.ts +18 -1
  15. package/dist/git/git-cli.d.ts.map +1 -1
  16. package/dist/git/git-cli.js +17 -1
  17. package/dist/git/git-cli.js.map +1 -1
  18. package/dist/git/worktree.manager.d.ts +29 -2
  19. package/dist/git/worktree.manager.d.ts.map +1 -1
  20. package/dist/git/worktree.manager.js +137 -16
  21. package/dist/git/worktree.manager.js.map +1 -1
  22. package/dist/git/worktree.manager.test.d.ts +2 -0
  23. package/dist/git/worktree.manager.test.d.ts.map +1 -0
  24. package/dist/git/worktree.manager.test.js +104 -0
  25. package/dist/git/worktree.manager.test.js.map +1 -0
  26. package/dist/mcp/context.d.ts +3 -0
  27. package/dist/mcp/context.d.ts.map +1 -1
  28. package/dist/mcp/context.js +10 -1
  29. package/dist/mcp/context.js.map +1 -1
  30. package/dist/mcp/http-client.d.ts +24 -1
  31. package/dist/mcp/http-client.d.ts.map +1 -1
  32. package/dist/mcp/http-client.js +37 -3
  33. package/dist/mcp/http-client.js.map +1 -1
  34. package/dist/mcp/server.d.ts.map +1 -1
  35. package/dist/mcp/server.js +190 -0
  36. package/dist/mcp/server.js.map +1 -1
  37. package/dist/middleware/tunnel-auth.d.ts.map +1 -1
  38. package/dist/middleware/tunnel-auth.js +2 -0
  39. package/dist/middleware/tunnel-auth.js.map +1 -1
  40. package/dist/output/__tests__/codex-parser.test.d.ts +2 -0
  41. package/dist/output/__tests__/codex-parser.test.d.ts.map +1 -0
  42. package/dist/output/__tests__/codex-parser.test.js +148 -0
  43. package/dist/output/__tests__/codex-parser.test.js.map +1 -0
  44. package/dist/output/codex-parser.d.ts +12 -0
  45. package/dist/output/codex-parser.d.ts.map +1 -1
  46. package/dist/output/codex-parser.js +129 -12
  47. package/dist/output/codex-parser.js.map +1 -1
  48. package/dist/routes/__tests__/attachments.test.d.ts +2 -0
  49. package/dist/routes/__tests__/attachments.test.d.ts.map +1 -0
  50. package/dist/routes/__tests__/attachments.test.js +86 -0
  51. package/dist/routes/__tests__/attachments.test.js.map +1 -0
  52. package/dist/routes/__tests__/filesystem.test.d.ts +2 -0
  53. package/dist/routes/__tests__/filesystem.test.d.ts.map +1 -0
  54. package/dist/routes/__tests__/filesystem.test.js +80 -0
  55. package/dist/routes/__tests__/filesystem.test.js.map +1 -0
  56. package/dist/routes/__tests__/previews.test.d.ts +2 -0
  57. package/dist/routes/__tests__/previews.test.d.ts.map +1 -0
  58. package/dist/routes/__tests__/previews.test.js +89 -0
  59. package/dist/routes/__tests__/previews.test.js.map +1 -0
  60. package/dist/routes/__tests__/tasks.test.d.ts +2 -0
  61. package/dist/routes/__tests__/tasks.test.d.ts.map +1 -0
  62. package/dist/routes/__tests__/tasks.test.js +72 -0
  63. package/dist/routes/__tests__/tasks.test.js.map +1 -0
  64. package/dist/routes/attachments.d.ts.map +1 -1
  65. package/dist/routes/attachments.js +36 -16
  66. package/dist/routes/attachments.js.map +1 -1
  67. package/dist/routes/filesystem.d.ts.map +1 -1
  68. package/dist/routes/filesystem.js +24 -3
  69. package/dist/routes/filesystem.js.map +1 -1
  70. package/dist/routes/index.d.ts.map +1 -1
  71. package/dist/routes/index.js +6 -0
  72. package/dist/routes/index.js.map +1 -1
  73. package/dist/routes/previews.d.ts +6 -0
  74. package/dist/routes/previews.d.ts.map +1 -0
  75. package/dist/routes/previews.js +413 -0
  76. package/dist/routes/previews.js.map +1 -0
  77. package/dist/routes/projects.d.ts.map +1 -1
  78. package/dist/routes/projects.js +1 -0
  79. package/dist/routes/projects.js.map +1 -1
  80. package/dist/routes/system.d.ts.map +1 -1
  81. package/dist/routes/system.js +35 -1
  82. package/dist/routes/system.js.map +1 -1
  83. package/dist/routes/tasks.js +2 -2
  84. package/dist/routes/tasks.js.map +1 -1
  85. package/dist/routes/team-runs.d.ts +11 -0
  86. package/dist/routes/team-runs.d.ts.map +1 -0
  87. package/dist/routes/team-runs.js +309 -0
  88. package/dist/routes/team-runs.js.map +1 -0
  89. package/dist/routes/tunnel.d.ts.map +1 -1
  90. package/dist/routes/tunnel.js +20 -0
  91. package/dist/routes/tunnel.js.map +1 -1
  92. package/dist/routes/workspaces.d.ts.map +1 -1
  93. package/dist/routes/workspaces.js +15 -1
  94. package/dist/routes/workspaces.js.map +1 -1
  95. package/dist/services/__tests__/preview.service.test.d.ts +2 -0
  96. package/dist/services/__tests__/preview.service.test.d.ts.map +1 -0
  97. package/dist/services/__tests__/preview.service.test.js +29 -0
  98. package/dist/services/__tests__/preview.service.test.js.map +1 -0
  99. package/dist/services/__tests__/session-manager.team-run.test.d.ts +2 -0
  100. package/dist/services/__tests__/session-manager.team-run.test.d.ts.map +1 -0
  101. package/dist/services/__tests__/session-manager.team-run.test.js +286 -0
  102. package/dist/services/__tests__/session-manager.team-run.test.js.map +1 -0
  103. package/dist/services/__tests__/task.service.test.d.ts +2 -0
  104. package/dist/services/__tests__/task.service.test.d.ts.map +1 -0
  105. package/dist/services/__tests__/task.service.test.js +65 -0
  106. package/dist/services/__tests__/task.service.test.js.map +1 -0
  107. package/dist/services/__tests__/team-lock.service.test.d.ts +2 -0
  108. package/dist/services/__tests__/team-lock.service.test.d.ts.map +1 -0
  109. package/dist/services/__tests__/team-lock.service.test.js +81 -0
  110. package/dist/services/__tests__/team-lock.service.test.js.map +1 -0
  111. package/dist/services/__tests__/team-reconciler.service.test.d.ts +2 -0
  112. package/dist/services/__tests__/team-reconciler.service.test.d.ts.map +1 -0
  113. package/dist/services/__tests__/team-reconciler.service.test.js +1536 -0
  114. package/dist/services/__tests__/team-reconciler.service.test.js.map +1 -0
  115. package/dist/services/__tests__/team-run.service.test.d.ts +2 -0
  116. package/dist/services/__tests__/team-run.service.test.d.ts.map +1 -0
  117. package/dist/services/__tests__/team-run.service.test.js +699 -0
  118. package/dist/services/__tests__/team-run.service.test.js.map +1 -0
  119. package/dist/services/__tests__/team-scheduler.service.test.d.ts +2 -0
  120. package/dist/services/__tests__/team-scheduler.service.test.d.ts.map +1 -0
  121. package/dist/services/__tests__/team-scheduler.service.test.js +1688 -0
  122. package/dist/services/__tests__/team-scheduler.service.test.js.map +1 -0
  123. package/dist/services/__tests__/tunnel.service.test.d.ts +2 -0
  124. package/dist/services/__tests__/tunnel.service.test.d.ts.map +1 -0
  125. package/dist/services/__tests__/tunnel.service.test.js +138 -0
  126. package/dist/services/__tests__/tunnel.service.test.js.map +1 -0
  127. package/dist/services/__tests__/workspace.service.test.d.ts +2 -0
  128. package/dist/services/__tests__/workspace.service.test.d.ts.map +1 -0
  129. package/dist/services/__tests__/workspace.service.test.js +695 -0
  130. package/dist/services/__tests__/workspace.service.test.js.map +1 -0
  131. package/dist/services/attachment-context.d.ts +3 -0
  132. package/dist/services/attachment-context.d.ts.map +1 -0
  133. package/dist/services/attachment-context.js +34 -0
  134. package/dist/services/attachment-context.js.map +1 -0
  135. package/dist/services/preview.service.d.ts +19 -0
  136. package/dist/services/preview.service.d.ts.map +1 -0
  137. package/dist/services/preview.service.js +147 -0
  138. package/dist/services/preview.service.js.map +1 -0
  139. package/dist/services/project.service.d.ts +2 -0
  140. package/dist/services/project.service.d.ts.map +1 -1
  141. package/dist/services/project.service.js +87 -18
  142. package/dist/services/project.service.js.map +1 -1
  143. package/dist/services/session-manager.d.ts +43 -1
  144. package/dist/services/session-manager.d.ts.map +1 -1
  145. package/dist/services/session-manager.js +110 -2
  146. package/dist/services/session-manager.js.map +1 -1
  147. package/dist/services/task.service.d.ts +6 -0
  148. package/dist/services/task.service.d.ts.map +1 -1
  149. package/dist/services/task.service.js +15 -3
  150. package/dist/services/task.service.js.map +1 -1
  151. package/dist/services/team-lock.service.d.ts +25 -0
  152. package/dist/services/team-lock.service.d.ts.map +1 -0
  153. package/dist/services/team-lock.service.js +56 -0
  154. package/dist/services/team-lock.service.js.map +1 -0
  155. package/dist/services/team-reconciler.service.d.ts +44 -0
  156. package/dist/services/team-reconciler.service.d.ts.map +1 -0
  157. package/dist/services/team-reconciler.service.js +286 -0
  158. package/dist/services/team-reconciler.service.js.map +1 -0
  159. package/dist/services/team-run-events.d.ts +13 -0
  160. package/dist/services/team-run-events.d.ts.map +1 -0
  161. package/dist/services/team-run-events.js +27 -0
  162. package/dist/services/team-run-events.js.map +1 -0
  163. package/dist/services/team-run.service.d.ts +92 -0
  164. package/dist/services/team-run.service.d.ts.map +1 -0
  165. package/dist/services/team-run.service.js +835 -0
  166. package/dist/services/team-run.service.js.map +1 -0
  167. package/dist/services/team-scheduler.service.d.ts +104 -0
  168. package/dist/services/team-scheduler.service.d.ts.map +1 -0
  169. package/dist/services/team-scheduler.service.js +843 -0
  170. package/dist/services/team-scheduler.service.js.map +1 -0
  171. package/dist/services/tunnel.service.d.ts +31 -5
  172. package/dist/services/tunnel.service.d.ts.map +1 -1
  173. package/dist/services/tunnel.service.js +293 -32
  174. package/dist/services/tunnel.service.js.map +1 -1
  175. package/dist/services/workspace.service.d.ts +161 -7
  176. package/dist/services/workspace.service.d.ts.map +1 -1
  177. package/dist/services/workspace.service.js +396 -51
  178. package/dist/services/workspace.service.js.map +1 -1
  179. package/dist/socket/events.d.ts +1 -1
  180. package/dist/socket/events.d.ts.map +1 -1
  181. package/dist/socket/events.js.map +1 -1
  182. package/dist/socket/socket-gateway.d.ts.map +1 -1
  183. package/dist/socket/socket-gateway.js +5 -1
  184. package/dist/socket/socket-gateway.js.map +1 -1
  185. package/dist/web/assets/AgentDemoPage-Bf6labVB.js +1 -0
  186. package/dist/web/assets/{DemoPage-XwuS8vNB.js → DemoPage-DlfG47rV.js} +3 -3
  187. package/dist/web/assets/{GeneralSettingsPage-CliIgpwf.js → GeneralSettingsPage-DefqwzVn.js} +1 -1
  188. package/dist/web/assets/MemberAvatar-DVw_TedB.js +1 -0
  189. package/dist/web/assets/NotificationSettingsPage-C9h1U1Za.js +1 -0
  190. package/dist/web/assets/{ProfileSettingsPage-CkU_kZKG.js → ProfileSettingsPage-BkZE2yVP.js} +1 -1
  191. package/dist/web/assets/ProjectKanbanPage-B1Ckl1uY.js +89 -0
  192. package/dist/web/assets/ProjectSettingsPage-ByZ13awb.js +2 -0
  193. package/dist/web/assets/{ProviderSettingsPage-CfvdeoEU.js → ProviderSettingsPage-DSQYe8B6.js} +12 -12
  194. package/dist/web/assets/TeamSettingsPage-DUukJ_Ih.js +1 -0
  195. package/dist/web/assets/agent-tower-logo-COx9gy77.png +0 -0
  196. package/dist/web/assets/{button-BWFTEdOr.js → button-Bpm98eOV.js} +1 -1
  197. package/dist/web/assets/{chevron-down-CuPdBAx-.js → chevron-down-DSKKXCi8.js} +1 -1
  198. package/dist/web/assets/{chevron-right-Cs8vYTMn.js → chevron-right-CZdDV9GU.js} +1 -1
  199. package/dist/web/assets/chevron-up-gnnlwvYe.js +1 -0
  200. package/dist/web/assets/{circle-check-BXZTzqw0.js → circle-check-DeD_VuLK.js} +1 -1
  201. package/dist/web/assets/{code-block-OCS4YCEC-BxUpvXK_.js → code-block-OCS4YCEC-BrGjkdjS.js} +1 -1
  202. package/dist/web/assets/{confirm-dialog-CDLHRthd.js → confirm-dialog-CEVVvAcE.js} +1 -1
  203. package/dist/web/assets/folder-picker-ZBQlFEWL.js +1 -0
  204. package/dist/web/assets/index-B5g4V0NU.js +13 -0
  205. package/dist/web/assets/index-ltjI8o6A.css +1 -0
  206. package/dist/web/assets/loader-circle-GMfBClX0.js +1 -0
  207. package/dist/web/assets/{log-adapter-CeKrvZcz.js → log-adapter-DKKM3sxS.js} +1 -1
  208. package/dist/web/assets/{mermaid-NOHMQCX5-BOSwJqP0.js → mermaid-NOHMQCX5-D5USvUiZ.js} +44 -44
  209. package/dist/web/assets/modal-JMpuh-LG.js +1 -0
  210. package/dist/web/assets/{pencil-BMxBxIhw.js → pencil-QrCW47nn.js} +1 -1
  211. package/dist/web/assets/{select-BUmRG0LY.js → select-CINRzLiE.js} +1 -1
  212. package/dist/web/assets/upload-vFxZxKHo.js +1 -0
  213. package/dist/web/assets/{use-profiles-C1vlPE-2.js → use-profiles-SrVWPYv0.js} +1 -1
  214. package/dist/web/assets/{use-providers-Cdxr4Jbz.js → use-providers-BihMydl0.js} +1 -1
  215. package/dist/web/avatars/presets/avatar-preset-01-developer.png +0 -0
  216. package/dist/web/avatars/presets/avatar-preset-02-architect.png +0 -0
  217. package/dist/web/avatars/presets/avatar-preset-03-tester.png +0 -0
  218. package/dist/web/avatars/presets/avatar-preset-04-devops.png +0 -0
  219. package/dist/web/avatars/presets/avatar-preset-05-data-scientist.png +0 -0
  220. package/dist/web/avatars/presets/avatar-preset-06-frontend.png +0 -0
  221. package/dist/web/avatars/presets/avatar-preset-07-backend.png +0 -0
  222. package/dist/web/avatars/presets/avatar-preset-08-security.png +0 -0
  223. package/dist/web/avatars/presets/avatar-preset-09-project-manager.png +0 -0
  224. package/dist/web/avatars/presets/avatar-preset-10-product-manager.png +0 -0
  225. package/dist/web/avatars/presets/avatar-preset-11-scrum-master.png +0 -0
  226. package/dist/web/avatars/presets/avatar-preset-12-tech-lead.png +0 -0
  227. package/dist/web/avatars/presets/avatar-preset-13-coordinator.png +0 -0
  228. package/dist/web/avatars/presets/avatar-preset-14-mentor.png +0 -0
  229. package/dist/web/avatars/presets/avatar-preset-15-reviewer.png +0 -0
  230. package/dist/web/avatars/presets/avatar-preset-16-ui-designer.png +0 -0
  231. package/dist/web/avatars/presets/avatar-preset-17-ux-researcher.png +0 -0
  232. package/dist/web/avatars/presets/avatar-preset-18-documenter.png +0 -0
  233. package/dist/web/avatars/presets/avatar-preset-19-translator.png +0 -0
  234. package/dist/web/avatars/presets/avatar-preset-20-analyst.png +0 -0
  235. package/dist/web/avatars/presets/avatar-preset-21-consultant.png +0 -0
  236. package/dist/web/avatars/presets/avatar-preset-22-creative-director.png +0 -0
  237. package/dist/web/avatars/presets/avatar-preset-23-support.png +0 -0
  238. package/dist/web/avatars/presets/avatar-preset-24-assistant.png +0 -0
  239. package/dist/web/avatars/presets/avatar-preset-25-robot.png +0 -0
  240. package/dist/web/avatars/presets/avatar-preset-grid.png +0 -0
  241. package/dist/web/index.html +2 -2
  242. package/node_modules/@agent-tower/shared/dist/socket/events.d.ts +10 -0
  243. package/node_modules/@agent-tower/shared/dist/socket/events.d.ts.map +1 -1
  244. package/node_modules/@agent-tower/shared/dist/socket/events.js +1 -0
  245. package/node_modules/@agent-tower/shared/dist/socket/events.js.map +1 -1
  246. package/node_modules/@agent-tower/shared/dist/types.d.ts +161 -0
  247. package/node_modules/@agent-tower/shared/dist/types.d.ts.map +1 -1
  248. package/node_modules/@agent-tower/shared/dist/types.js.map +1 -1
  249. package/node_modules/@prisma/client/.prisma/client/default.d.ts +1 -0
  250. package/node_modules/@prisma/client/.prisma/client/default.js +1 -0
  251. package/node_modules/@prisma/client/.prisma/client/edge.d.ts +1 -0
  252. package/node_modules/@prisma/client/.prisma/client/edge.js +396 -0
  253. package/node_modules/@prisma/client/.prisma/client/index-browser.js +385 -0
  254. package/node_modules/@prisma/client/.prisma/client/index.d.ts +26996 -0
  255. package/node_modules/@prisma/client/.prisma/client/index.js +421 -0
  256. package/node_modules/@prisma/client/.prisma/client/libquery_engine-darwin-arm64.dylib.node +0 -0
  257. package/node_modules/@prisma/client/.prisma/client/package.json +97 -0
  258. package/node_modules/@prisma/client/.prisma/client/query_engine-windows.dll.node +0 -0
  259. package/node_modules/@prisma/client/.prisma/client/schema.prisma +296 -0
  260. package/node_modules/@prisma/client/.prisma/client/wasm.d.ts +1 -0
  261. package/node_modules/@prisma/client/.prisma/client/wasm.js +385 -0
  262. package/node_modules/@prisma/client/package.json +3 -2
  263. package/package.json +2 -1
  264. package/prisma/migrations/20260515000000_add_workspace_preview_target/migration.sql +2 -0
  265. package/prisma/migrations/20260518000000_add_team_run_collaboration/migration.sql +150 -0
  266. package/prisma/migrations/20260522000000_add_team_member_session_policy/migration.sql +2 -0
  267. package/prisma/migrations/20260526000000_add_team_run_main_and_dedicated_workspaces/migration.sql +21 -0
  268. package/prisma/schema.prisma +147 -1
  269. package/dist/web/assets/AgentDemoPage-ClnGPAV9.js +0 -1
  270. package/dist/web/assets/NotificationSettingsPage-y3vhVgPv.js +0 -1
  271. package/dist/web/assets/ProjectKanbanPage-BddzfZRV.js +0 -87
  272. package/dist/web/assets/ProjectSettingsPage-B6xhbziO.js +0 -2
  273. package/dist/web/assets/circle-alert-EUyZcWhp.js +0 -1
  274. package/dist/web/assets/folder-picker-CUbhsnhi.js +0 -1
  275. package/dist/web/assets/index-BGvfX18x.css +0 -1
  276. package/dist/web/assets/index-CHN8jahE.js +0 -13
  277. package/dist/web/assets/loader-circle-BHzDVpxt.js +0 -1
  278. package/dist/web/assets/modal-D_AU4URz.js +0 -1
  279. package/dist/web/assets/use-projects-Bcd5hIOY.js +0 -1
@@ -1,21 +1,75 @@
1
1
  import { prisma } from '../utils/index.js';
2
2
  import { WorkspaceStatus, TaskStatus, SessionStatus, SessionPurpose } from '../types/index.js';
3
3
  import { WorktreeManager } from '../git/worktree.manager.js';
4
- import { execGit } from '../git/git-cli.js';
4
+ import { execGit, MergeConflictError } from '../git/git-cli.js';
5
5
  import { NotFoundError, ServiceError } from '../errors.js';
6
6
  import { getSessionManager, getEventBus } from '../core/container.js';
7
7
  import { copyProjectFiles } from './copy-files.service.js';
8
+ import { defaultTeamLockService } from './team-lock.service.js';
8
9
  import { exec } from 'node:child_process';
9
10
  import { promisify } from 'node:util';
11
+ import { randomUUID } from 'node:crypto';
10
12
  import fs from 'node:fs/promises';
13
+ import path from 'node:path';
11
14
  import { ensureProjectIsMutable } from './project-guards.js';
12
15
  const DEFAULT_IDLE_THRESHOLD_HOURS = 24;
16
+ const WORKSPACE_READY_RETRY_COUNT = 20;
17
+ const WORKSPACE_READY_RETRY_DELAY_MS = 50;
13
18
  const execAsync = promisify(exec);
14
19
  /** 过滤条件:只返回用户可见的 CHAT session */
15
20
  const visibleSessionsFilter = { where: { purpose: { not: SessionPurpose.COMMIT_MSG } } };
21
+ const activeSessionStatuses = [SessionStatus.PENDING, SessionStatus.RUNNING];
22
+ const finalChildWorkspaceStatuses = [WorkspaceStatus.MERGED, WorkspaceStatus.ABANDONED];
23
+ function normalizeCreateOptions(input) {
24
+ if (typeof input === 'string') {
25
+ return {
26
+ branchName: input,
27
+ branchNamePrefix: '',
28
+ startPoint: null,
29
+ parentWorkspaceId: null,
30
+ ownerMemberId: null,
31
+ reuseInactive: true,
32
+ };
33
+ }
34
+ return {
35
+ branchName: input?.branchName ?? '',
36
+ branchNamePrefix: input?.branchNamePrefix ?? '',
37
+ startPoint: input?.startPoint ?? null,
38
+ parentWorkspaceId: input?.parentWorkspaceId ?? null,
39
+ ownerMemberId: input?.ownerMemberId ?? null,
40
+ reuseInactive: input?.reuseInactive ?? true,
41
+ };
42
+ }
43
+ function isUniqueConstraintError(error) {
44
+ return typeof error === 'object'
45
+ && error !== null
46
+ && 'code' in error
47
+ && error.code === 'P2002';
48
+ }
49
+ function branchFromOptions(workspaceId, options) {
50
+ if (options.branchName) {
51
+ return options.branchName;
52
+ }
53
+ if (options.branchNamePrefix) {
54
+ return `${options.branchNamePrefix}/${workspaceId.slice(0, 8)}`;
55
+ }
56
+ return `at/${workspaceId.slice(0, 8)}`;
57
+ }
58
+ function teamRunBranchPrefix(teamRunId) {
59
+ return `at/team/${teamRunId.slice(0, 8)}`;
60
+ }
61
+ function sleep(ms) {
62
+ return new Promise((resolve) => setTimeout(resolve, ms));
63
+ }
16
64
  export class WorkspaceService {
65
+ lockService;
66
+ static mainWorkspaceClaims = new Map();
67
+ static dedicatedWorkspaceClaims = new Map();
17
68
  sessionService = getSessionManager();
18
69
  eventBus = getEventBus();
70
+ constructor(lockService = defaultTeamLockService) {
71
+ this.lockService = lockService;
72
+ }
19
73
  getBaseBranch(workspace) {
20
74
  return workspace.baseBranch || workspace.task.project.mainBranch;
21
75
  }
@@ -50,7 +104,8 @@ export class WorkspaceService {
50
104
  * - 创建后自动将关联 Task 状态改为 IN_PROGRESS
51
105
  * - 失败时回滚已创建的数据库记录
52
106
  */
53
- async create(taskId, branchName) {
107
+ async create(taskId, branchNameOrOptions) {
108
+ const options = normalizeCreateOptions(branchNameOrOptions);
54
109
  const task = await prisma.task.findUnique({
55
110
  where: { id: taskId },
56
111
  include: { project: true },
@@ -61,34 +116,29 @@ export class WorkspaceService {
61
116
  ensureProjectIsMutable(task.project, 'create workspaces');
62
117
  const worktreeManager = new WorktreeManager(task.project.repoPath);
63
118
  // 查找可复用的 MERGED 或 HIBERNATED workspace
64
- if (!branchName) {
119
+ if (!options.branchName && options.reuseInactive) {
65
120
  const reusableWorkspace = await prisma.workspace.findFirst({
66
121
  where: {
67
122
  taskId,
123
+ parentWorkspaceId: options.parentWorkspaceId,
124
+ ownerMemberId: options.ownerMemberId,
68
125
  status: { in: [WorkspaceStatus.MERGED, WorkspaceStatus.HIBERNATED] },
69
126
  },
70
127
  orderBy: { updatedAt: 'desc' },
71
128
  });
72
129
  if (reusableWorkspace) {
73
- const worktreePath = await worktreeManager.ensureWorktreeExists(reusableWorkspace.branchName);
74
- this.runCopyFiles(task.project.repoPath, worktreePath, task.project.copyFiles);
75
- this.fireSetupScript(reusableWorkspace.id, taskId, worktreePath, task.project.setupScript);
76
- const updated = await prisma.workspace.update({
77
- where: { id: reusableWorkspace.id },
78
- data: {
79
- status: WorkspaceStatus.ACTIVE,
80
- worktreePath,
81
- hibernatedAt: null,
82
- },
83
- include: { sessions: true, task: { include: { project: true } } },
130
+ return this.restoreInactiveWorkspace({
131
+ ...reusableWorkspace,
132
+ task: { ...task, project: task.project },
84
133
  });
85
- return updated;
86
134
  }
87
135
  }
88
136
  // 先在数据库创建记录以获取 ID(用于生成默认分支名)
89
137
  const workspace = await prisma.workspace.create({
90
138
  data: {
91
139
  taskId,
140
+ parentWorkspaceId: options.parentWorkspaceId,
141
+ ownerMemberId: options.ownerMemberId,
92
142
  branchName: '', // 占位,稍后更新
93
143
  worktreePath: '', // 占位,稍后更新
94
144
  status: WorkspaceStatus.ACTIVE,
@@ -96,18 +146,21 @@ export class WorkspaceService {
96
146
  });
97
147
  try {
98
148
  // 生成分支名:用户指定 or 自动生成 at/{shortId}
99
- const branch = branchName || `at/${workspace.id.slice(0, 8)}`;
149
+ const branch = branchFromOptions(workspace.id, options);
100
150
  // 空仓库(无任何 commit)无法创建 worktree,提前报错
101
- let baseBranch = null;
102
- try {
103
- const currentBranch = (await execGit(task.project.repoPath, ['rev-parse', '--abbrev-ref', 'HEAD'])).trim();
104
- baseBranch = currentBranch && currentBranch !== 'HEAD' ? currentBranch : null;
105
- }
106
- catch {
107
- throw new Error('仓库尚无任何提交记录,无法创建 Workspace。请重新编辑项目以触发自动初始化,或手动执行 git commit。');
151
+ const startPoint = options.startPoint || null;
152
+ let baseBranch = startPoint;
153
+ if (!startPoint) {
154
+ try {
155
+ const currentBranch = (await execGit(task.project.repoPath, ['rev-parse', '--abbrev-ref', 'HEAD'])).trim();
156
+ baseBranch = currentBranch && currentBranch !== 'HEAD' ? currentBranch : null;
157
+ }
158
+ catch {
159
+ throw new Error('仓库尚无任何提交记录,无法创建 Workspace。请重新编辑项目以触发自动初始化,或手动执行 git commit。');
160
+ }
108
161
  }
109
162
  // WorktreeManager.create 内部已做分支名合法性校验和重复检查
110
- const worktreePath = await worktreeManager.create(branch);
163
+ const worktreePath = await worktreeManager.create(branch, startPoint ?? undefined);
111
164
  // worktree 创建后:复制文件 + 异步执行 setup 脚本(fire-and-forget)
112
165
  this.runCopyFiles(task.project.repoPath, worktreePath, task.project.copyFiles);
113
166
  this.fireSetupScript(workspace.id, taskId, worktreePath, task.project.setupScript);
@@ -127,6 +180,201 @@ export class WorkspaceService {
127
180
  throw err;
128
181
  }
129
182
  }
183
+ async getOrCreateMainWorkspace(teamRunId) {
184
+ const existingClaim = WorkspaceService.mainWorkspaceClaims.get(teamRunId);
185
+ if (existingClaim) {
186
+ return existingClaim;
187
+ }
188
+ const claim = this.findOrCreateMainWorkspace(teamRunId);
189
+ WorkspaceService.mainWorkspaceClaims.set(teamRunId, claim);
190
+ try {
191
+ return await claim;
192
+ }
193
+ finally {
194
+ if (WorkspaceService.mainWorkspaceClaims.get(teamRunId) === claim) {
195
+ WorkspaceService.mainWorkspaceClaims.delete(teamRunId);
196
+ }
197
+ }
198
+ }
199
+ async getOrCreateDedicatedWorkspace(teamRunId, memberId) {
200
+ const mainWorkspace = await this.getOrCreateMainWorkspace(teamRunId);
201
+ const claimKey = `${mainWorkspace.id}:${memberId}`;
202
+ const existingClaim = WorkspaceService.dedicatedWorkspaceClaims.get(claimKey);
203
+ if (existingClaim) {
204
+ return existingClaim;
205
+ }
206
+ const claim = this.findOrCreateDedicatedWorkspace(teamRunId, memberId, mainWorkspace);
207
+ WorkspaceService.dedicatedWorkspaceClaims.set(claimKey, claim);
208
+ try {
209
+ return await claim;
210
+ }
211
+ finally {
212
+ if (WorkspaceService.dedicatedWorkspaceClaims.get(claimKey) === claim) {
213
+ WorkspaceService.dedicatedWorkspaceClaims.delete(claimKey);
214
+ }
215
+ }
216
+ }
217
+ async findOrCreateMainWorkspace(teamRunId) {
218
+ const teamRun = await prisma.teamRun.findUnique({
219
+ where: { id: teamRunId },
220
+ include: {
221
+ task: { include: { project: true } },
222
+ mainWorkspace: { include: { task: { include: { project: true } } } },
223
+ },
224
+ });
225
+ if (!teamRun) {
226
+ throw new NotFoundError('TeamRun', teamRunId);
227
+ }
228
+ ensureProjectIsMutable(teamRun.task.project, 'create workspaces');
229
+ if (teamRun.mainWorkspace
230
+ && teamRun.mainWorkspace.taskId === teamRun.taskId
231
+ && teamRun.mainWorkspace.parentWorkspaceId == null
232
+ && teamRun.mainWorkspace.ownerMemberId == null
233
+ && teamRun.mainWorkspace.status === WorkspaceStatus.ACTIVE) {
234
+ return this.ensureActiveWorkspaceWorktree(teamRun.mainWorkspace);
235
+ }
236
+ const activeRoot = await prisma.workspace.findFirst({
237
+ where: {
238
+ taskId: teamRun.taskId,
239
+ parentWorkspaceId: null,
240
+ ownerMemberId: null,
241
+ status: WorkspaceStatus.ACTIVE,
242
+ },
243
+ orderBy: [{ createdAt: 'asc' }, { id: 'asc' }],
244
+ include: { task: { include: { project: true } } },
245
+ });
246
+ if (activeRoot) {
247
+ await prisma.teamRun.update({
248
+ where: { id: teamRun.id },
249
+ data: { mainWorkspaceId: activeRoot.id },
250
+ });
251
+ return this.ensureActiveWorkspaceWorktree(activeRoot);
252
+ }
253
+ const workspace = await this.create(teamRun.taskId, {
254
+ branchNamePrefix: `${teamRunBranchPrefix(teamRun.id)}/main`,
255
+ parentWorkspaceId: null,
256
+ ownerMemberId: null,
257
+ reuseInactive: false,
258
+ });
259
+ await prisma.teamRun.update({
260
+ where: { id: teamRun.id },
261
+ data: { mainWorkspaceId: workspace.id },
262
+ });
263
+ return workspace;
264
+ }
265
+ async findOrCreateDedicatedWorkspace(teamRunId, memberId, mainWorkspace) {
266
+ const teamRun = await prisma.teamRun.findUnique({
267
+ where: { id: teamRunId },
268
+ include: { task: { include: { project: true } } },
269
+ });
270
+ if (!teamRun) {
271
+ throw new NotFoundError('TeamRun', teamRunId);
272
+ }
273
+ const member = await prisma.teamMember.findFirst({
274
+ where: { id: memberId, teamRunId },
275
+ select: { id: true },
276
+ });
277
+ if (!member) {
278
+ throw new NotFoundError('TeamMember', memberId);
279
+ }
280
+ ensureProjectIsMutable(teamRun.task.project, 'create workspaces');
281
+ const existing = await this.findDedicatedWorkspace(mainWorkspace.id, memberId);
282
+ if (existing) {
283
+ return this.activateDedicatedWorkspace(existing);
284
+ }
285
+ try {
286
+ return await this.create(teamRun.taskId, {
287
+ branchNamePrefix: `${teamRunBranchPrefix(teamRun.id)}/member-${memberId.slice(0, 8)}`,
288
+ startPoint: mainWorkspace.branchName,
289
+ parentWorkspaceId: mainWorkspace.id,
290
+ ownerMemberId: memberId,
291
+ reuseInactive: false,
292
+ });
293
+ }
294
+ catch (error) {
295
+ if (!isUniqueConstraintError(error)) {
296
+ throw error;
297
+ }
298
+ const raced = await this.findDedicatedWorkspace(mainWorkspace.id, memberId);
299
+ if (!raced) {
300
+ throw error;
301
+ }
302
+ return this.activateDedicatedWorkspace(await this.waitForWorkspaceReady(raced));
303
+ }
304
+ }
305
+ async findDedicatedWorkspace(mainWorkspaceId, memberId) {
306
+ return prisma.workspace.findFirst({
307
+ where: {
308
+ parentWorkspaceId: mainWorkspaceId,
309
+ ownerMemberId: memberId,
310
+ },
311
+ include: { task: { include: { project: true } } },
312
+ });
313
+ }
314
+ async activateDedicatedWorkspace(workspace) {
315
+ if (!workspace.branchName) {
316
+ workspace = await this.waitForWorkspaceReady(workspace);
317
+ }
318
+ if (workspace.status === WorkspaceStatus.ACTIVE) {
319
+ return this.ensureActiveWorkspaceWorktree(workspace);
320
+ }
321
+ if (workspace.status === WorkspaceStatus.HIBERNATED) {
322
+ return this.restoreInactiveWorkspace(workspace);
323
+ }
324
+ throw new ServiceError(`Cannot reuse dedicated workspace in ${workspace.status} status`, 'DEDICATED_WORKSPACE_UNAVAILABLE', 409);
325
+ }
326
+ async waitForWorkspaceReady(workspace) {
327
+ if (workspace.branchName) {
328
+ return workspace;
329
+ }
330
+ for (let attempt = 0; attempt < WORKSPACE_READY_RETRY_COUNT; attempt++) {
331
+ await sleep(WORKSPACE_READY_RETRY_DELAY_MS);
332
+ const reloaded = await prisma.workspace.findUnique({
333
+ where: { id: workspace.id },
334
+ include: { task: { include: { project: true } } },
335
+ });
336
+ if (!reloaded) {
337
+ break;
338
+ }
339
+ if (reloaded.branchName) {
340
+ return reloaded;
341
+ }
342
+ }
343
+ throw new ServiceError(`Workspace ${workspace.id} is still initializing`, 'WORKSPACE_INITIALIZING', 409);
344
+ }
345
+ async ensureActiveWorkspaceWorktree(workspace) {
346
+ if (!workspace.branchName) {
347
+ workspace = await this.waitForWorkspaceReady(workspace);
348
+ }
349
+ if (workspace.worktreePath) {
350
+ const gitFileExists = await fs
351
+ .access(path.join(workspace.worktreePath, '.git'))
352
+ .then(() => true)
353
+ .catch(() => false);
354
+ if (gitFileExists) {
355
+ return prisma.workspace.findUniqueOrThrow({
356
+ where: { id: workspace.id },
357
+ include: { sessions: visibleSessionsFilter, task: { include: { project: true } } },
358
+ });
359
+ }
360
+ }
361
+ return this.restoreInactiveWorkspace(workspace);
362
+ }
363
+ async restoreInactiveWorkspace(workspace) {
364
+ const worktreeManager = new WorktreeManager(workspace.task.project.repoPath);
365
+ const worktreePath = await worktreeManager.ensureWorktreeExists(workspace.branchName);
366
+ this.runCopyFiles(workspace.task.project.repoPath, worktreePath, workspace.task.project.copyFiles);
367
+ this.fireSetupScript(workspace.id, workspace.taskId, worktreePath, workspace.task.project.setupScript);
368
+ return prisma.workspace.update({
369
+ where: { id: workspace.id },
370
+ data: {
371
+ status: WorkspaceStatus.ACTIVE,
372
+ worktreePath,
373
+ hibernatedAt: null,
374
+ },
375
+ include: { sessions: visibleSessionsFilter, task: { include: { project: true } } },
376
+ });
377
+ }
130
378
  // ── Delete ───────────────────────────────────────────────────────────────────
131
379
  /**
132
380
  * 删除 Workspace
@@ -229,57 +477,150 @@ export class WorkspaceService {
229
477
  const worktreeManager = new WorktreeManager(workspace.task.project.repoPath);
230
478
  await worktreeManager.abortOperation(workspace.worktreePath);
231
479
  }
232
- // ── Merge (squash) ──────────────────────────────────────────────────────────
233
- /**
234
- * 合并 Workspace 到主分支(squash merge)
235
- *
236
- * @param commitMessage - 可选的自定义 commit message
237
- * @returns squash commit 的 SHA
238
- */
239
- async merge(id, commitMessage) {
240
- const totalStart = performance.now();
241
- const step = (label, start) => console.log(`[WorkspaceService.merge] ${label}: ${(performance.now() - start).toFixed(0)}ms`);
242
- let t = performance.now();
480
+ async merge(id, commitMessageOrOptions) {
481
+ const options = typeof commitMessageOrOptions === 'string'
482
+ ? { commitMessage: commitMessageOrOptions }
483
+ : (commitMessageOrOptions ?? {});
243
484
  const workspace = await prisma.workspace.findUnique({
244
485
  where: { id },
245
- include: { task: { include: { project: true } } },
486
+ include: { task: { include: { project: true, teamRun: true } } },
246
487
  });
247
- step('db findUnique', t);
248
488
  if (!workspace) {
249
489
  throw new NotFoundError('Workspace', id);
250
490
  }
251
491
  ensureProjectIsMutable(workspace.task.project, 'merge workspaces');
252
- // 优先使用传入的 commitMessage,其次使用 AI 生成的缓存
253
- const message = commitMessage || workspace.commitMessage || undefined;
254
- t = performance.now();
492
+ return this.withProjectMergeLock(workspace.task.projectId, options.lockOwnerId, () => this.mergeWithLock(workspace, options.commitMessage));
493
+ }
494
+ async mergeWithLock(workspace, commitMessage) {
495
+ if (workspace.status !== WorkspaceStatus.ACTIVE) {
496
+ throw new ServiceError(`Cannot merge workspace in ${workspace.status} status`, 'INVALID_WORKSPACE_STATE', 400);
497
+ }
498
+ if (workspace.parentWorkspaceId) {
499
+ return this.mergeChildIntoParent(workspace, commitMessage);
500
+ }
501
+ return this.mergeRootWorkspaceToMain(workspace, commitMessage);
502
+ }
503
+ async mergeChildIntoParent(workspace, commitMessage) {
504
+ const parentWorkspace = await prisma.workspace.findUnique({
505
+ where: { id: workspace.parentWorkspaceId ?? '' },
506
+ include: { task: { include: { project: true } } },
507
+ });
508
+ if (!parentWorkspace) {
509
+ throw new NotFoundError('Workspace', workspace.parentWorkspaceId ?? '');
510
+ }
511
+ if (parentWorkspace.taskId !== workspace.taskId || parentWorkspace.ownerMemberId != null) {
512
+ throw new ServiceError('Dedicated child workspace parent is not a valid TeamRun main workspace', 'INVALID_PARENT_WORKSPACE', 400);
513
+ }
514
+ if (parentWorkspace.status !== WorkspaceStatus.ACTIVE) {
515
+ throw new ServiceError(`Cannot merge into parent workspace in ${parentWorkspace.status} status`, 'INVALID_PARENT_WORKSPACE_STATE', 409);
516
+ }
517
+ await this.assertNoActiveWriteSessions(parentWorkspace.id);
518
+ const worktreeManager = new WorktreeManager(workspace.task.project.repoPath);
519
+ let sha;
520
+ try {
521
+ ({ sha } = await worktreeManager.mergeIntoWorktree(workspace.worktreePath, parentWorkspace.worktreePath, { commitMessage: commitMessage || workspace.commitMessage || undefined }));
522
+ }
523
+ catch (error) {
524
+ if (error instanceof MergeConflictError) {
525
+ error.sourceWorkspaceId = workspace.id;
526
+ error.targetWorkspaceId = parentWorkspace.id;
527
+ error.sourceWorktreePath ??= workspace.worktreePath;
528
+ error.targetWorktreePath ??= parentWorkspace.worktreePath;
529
+ error.sourceBranch ??= workspace.branchName;
530
+ error.targetBranch ??= parentWorkspace.branchName;
531
+ }
532
+ throw error;
533
+ }
534
+ await prisma.workspace.update({
535
+ where: { id: workspace.id },
536
+ data: { status: WorkspaceStatus.MERGED },
537
+ });
538
+ return sha;
539
+ }
540
+ async mergeRootWorkspaceToMain(workspace, commitMessage) {
541
+ await this.assertTeamRunFinalMergeAllowed(workspace);
255
542
  const worktreeManager = new WorktreeManager(workspace.task.project.repoPath);
256
- const { sha } = await worktreeManager.merge(workspace.worktreePath, this.getBaseBranch(workspace), message ? { commitMessage: message } : undefined);
257
- step('worktreeManager.merge', t);
258
- // 更新 workspace:标记 MERGED,保留 worktreePath 供 cleanup() 后续清理物理目录
259
- t = performance.now();
543
+ let sha;
544
+ const targetBranch = this.getBaseBranch(workspace);
545
+ try {
546
+ ({ sha } = await worktreeManager.merge(workspace.worktreePath, targetBranch, { commitMessage: commitMessage || workspace.commitMessage || undefined }));
547
+ }
548
+ catch (error) {
549
+ if (error instanceof MergeConflictError) {
550
+ error.sourceWorkspaceId = workspace.id;
551
+ error.sourceWorktreePath ??= workspace.worktreePath;
552
+ error.sourceBranch ??= workspace.branchName;
553
+ error.targetBranch ??= targetBranch;
554
+ }
555
+ throw error;
556
+ }
260
557
  await prisma.workspace.update({
261
- where: { id },
558
+ where: { id: workspace.id },
262
559
  data: { status: WorkspaceStatus.MERGED },
263
560
  });
264
- step('db update workspace', t);
265
- // Task 推进到 DONE
266
561
  const advanceableStatuses = [TaskStatus.IN_PROGRESS, TaskStatus.IN_REVIEW];
267
562
  if (advanceableStatuses.includes(workspace.task.status)) {
268
- t = performance.now();
269
563
  await prisma.task.update({
270
564
  where: { id: workspace.task.id },
271
565
  data: { status: TaskStatus.DONE },
272
566
  });
273
- step('db update task', t);
274
567
  this.eventBus.emit('task:updated', {
275
568
  taskId: workspace.task.id,
276
569
  projectId: workspace.task.projectId,
277
570
  status: TaskStatus.DONE,
278
571
  });
279
572
  }
280
- console.log(`[WorkspaceService.merge] TOTAL: ${(performance.now() - totalStart).toFixed(0)}ms`);
281
573
  return sha;
282
574
  }
575
+ async assertTeamRunFinalMergeAllowed(workspace) {
576
+ const teamRun = workspace.task.teamRun;
577
+ if (!teamRun) {
578
+ return;
579
+ }
580
+ if (teamRun.mainWorkspaceId !== workspace.id) {
581
+ throw new ServiceError('Only the bound TeamRun main workspace can be merged into the project main branch', 'TEAM_RUN_NON_MAIN_WORKSPACE_FINAL_MERGE_FORBIDDEN', 409);
582
+ }
583
+ const blockingChildren = await prisma.workspace.findMany({
584
+ where: {
585
+ parentWorkspaceId: workspace.id,
586
+ status: { notIn: finalChildWorkspaceStatuses },
587
+ },
588
+ select: { id: true, status: true },
589
+ orderBy: [{ createdAt: 'asc' }, { id: 'asc' }],
590
+ });
591
+ if (blockingChildren.length > 0) {
592
+ throw new ServiceError('Cannot merge TeamRun main workspace before all dedicated child workspaces are merged or abandoned', 'TEAM_RUN_CHILD_WORKSPACES_NOT_FINAL', 409);
593
+ }
594
+ }
595
+ async assertNoActiveWriteSessions(workspaceId) {
596
+ const activeSession = await prisma.session.findFirst({
597
+ where: {
598
+ workspaceId,
599
+ status: { in: activeSessionStatuses },
600
+ purpose: SessionPurpose.CHAT,
601
+ },
602
+ select: { id: true },
603
+ });
604
+ if (activeSession) {
605
+ throw new ServiceError('Cannot merge into parent workspace while it has an active write session', 'PARENT_WORKSPACE_HAS_ACTIVE_SESSION', 409);
606
+ }
607
+ }
608
+ async withProjectMergeLock(projectId, requestedOwnerId, fn) {
609
+ const lockKey = `project:${projectId}:merge`;
610
+ const ownerId = requestedOwnerId ?? `workspace-merge:${randomUUID()}`;
611
+ const alreadyHeldByOwner = this.lockService.isHeldBy(ownerId, lockKey);
612
+ if (!alreadyHeldByOwner && !this.lockService.acquire(ownerId, [lockKey])) {
613
+ throw new ServiceError('Another workspace merge is already running for this project', 'PROJECT_MERGE_LOCKED', 409);
614
+ }
615
+ try {
616
+ return await fn();
617
+ }
618
+ finally {
619
+ if (!alreadyHeldByOwner) {
620
+ this.lockService.release(ownerId, [lockKey]);
621
+ }
622
+ }
623
+ }
283
624
  // ── Archive ──────────────────────────────────────────────────────────────────
284
625
  /**
285
626
  * 归档 Workspace(标记状态为 ABANDONED)
@@ -475,7 +816,11 @@ export class WorkspaceService {
475
816
  const worktreeManager = new WorktreeManager(workspace.task.project.repoPath);
476
817
  // 清理残留 worktree(如果还存在)
477
818
  if (workspace.worktreePath) {
478
- await worktreeManager.remove(workspace.worktreePath);
819
+ const removeResult = await worktreeManager.remove(workspace.worktreePath);
820
+ if (removeResult.status === 'unregistered') {
821
+ console.warn(`[WorkspaceService] cleanup: workspace ${workspace.id} path is unregistered or unsafe to remove: ${removeResult.path}`);
822
+ continue;
823
+ }
479
824
  }
480
825
  // Task 已 DONE,branch 不再需要,删除
481
826
  if (workspace.branchName) {