agent-tower 0.5.1 → 0.5.2-beta.1

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 (172) hide show
  1. package/dist/app.d.ts.map +1 -1
  2. package/dist/app.js +4 -0
  3. package/dist/app.js.map +1 -1
  4. package/dist/core/container.d.ts +2 -0
  5. package/dist/core/container.d.ts.map +1 -1
  6. package/dist/core/container.js +8 -0
  7. package/dist/core/container.js.map +1 -1
  8. package/dist/executors/__tests__/codex.executor.test.js +348 -7
  9. package/dist/executors/__tests__/codex.executor.test.js.map +1 -1
  10. package/dist/executors/codex.executor.d.ts +18 -1
  11. package/dist/executors/codex.executor.d.ts.map +1 -1
  12. package/dist/executors/codex.executor.js +75 -7
  13. package/dist/executors/codex.executor.js.map +1 -1
  14. package/dist/mcp/http-client.d.ts +4 -1
  15. package/dist/mcp/http-client.d.ts.map +1 -1
  16. package/dist/mcp/http-client.js +2 -2
  17. package/dist/mcp/http-client.js.map +1 -1
  18. package/dist/mcp/tools/workspaces.d.ts.map +1 -1
  19. package/dist/mcp/tools/workspaces.js +4 -1
  20. package/dist/mcp/tools/workspaces.js.map +1 -1
  21. package/dist/mcp/types.d.ts +3 -0
  22. package/dist/mcp/types.d.ts.map +1 -1
  23. package/dist/mcp/types.js +2 -0
  24. package/dist/mcp/types.js.map +1 -1
  25. package/dist/routes/__tests__/tasks.test.js +37 -0
  26. package/dist/routes/__tests__/tasks.test.js.map +1 -1
  27. package/dist/routes/git.d.ts.map +1 -1
  28. package/dist/routes/git.js +6 -1
  29. package/dist/routes/git.js.map +1 -1
  30. package/dist/routes/sessions.d.ts.map +1 -1
  31. package/dist/routes/sessions.js +12 -0
  32. package/dist/routes/sessions.js.map +1 -1
  33. package/dist/routes/system.d.ts.map +1 -1
  34. package/dist/routes/system.js +21 -5
  35. package/dist/routes/system.js.map +1 -1
  36. package/dist/routes/tasks.js +2 -2
  37. package/dist/routes/tasks.js.map +1 -1
  38. package/dist/routes/workspaces.d.ts.map +1 -1
  39. package/dist/routes/workspaces.js +11 -4
  40. package/dist/routes/workspaces.js.map +1 -1
  41. package/dist/services/__tests__/project.service.test.d.ts +2 -0
  42. package/dist/services/__tests__/project.service.test.d.ts.map +1 -0
  43. package/dist/services/__tests__/project.service.test.js +62 -0
  44. package/dist/services/__tests__/project.service.test.js.map +1 -0
  45. package/dist/services/__tests__/session-manager.team-run.test.js +98 -0
  46. package/dist/services/__tests__/session-manager.team-run.test.js.map +1 -1
  47. package/dist/services/__tests__/task.service.test.js +416 -38
  48. package/dist/services/__tests__/task.service.test.js.map +1 -1
  49. package/dist/services/__tests__/team-reconciler.service.test.js +68 -0
  50. package/dist/services/__tests__/team-reconciler.service.test.js.map +1 -1
  51. package/dist/services/__tests__/team-run.service.test.js +47 -1
  52. package/dist/services/__tests__/team-run.service.test.js.map +1 -1
  53. package/dist/services/__tests__/team-scheduler.service.test.js +49 -0
  54. package/dist/services/__tests__/team-scheduler.service.test.js.map +1 -1
  55. package/dist/services/__tests__/workspace.service.test.js +157 -0
  56. package/dist/services/__tests__/workspace.service.test.js.map +1 -1
  57. package/dist/services/commit-message.service.d.ts.map +1 -1
  58. package/dist/services/commit-message.service.js +3 -0
  59. package/dist/services/commit-message.service.js.map +1 -1
  60. package/dist/services/deleted-task-guard.d.ts +11 -0
  61. package/dist/services/deleted-task-guard.d.ts.map +1 -0
  62. package/dist/services/deleted-task-guard.js +10 -0
  63. package/dist/services/deleted-task-guard.js.map +1 -0
  64. package/dist/services/project.service.d.ts +1 -0
  65. package/dist/services/project.service.d.ts.map +1 -1
  66. package/dist/services/project.service.js +4 -3
  67. package/dist/services/project.service.js.map +1 -1
  68. package/dist/services/session-manager.d.ts +55 -1
  69. package/dist/services/session-manager.d.ts.map +1 -1
  70. package/dist/services/session-manager.js +133 -41
  71. package/dist/services/session-manager.js.map +1 -1
  72. package/dist/services/task-cleanup.service.d.ts +48 -0
  73. package/dist/services/task-cleanup.service.d.ts.map +1 -0
  74. package/dist/services/task-cleanup.service.js +209 -0
  75. package/dist/services/task-cleanup.service.js.map +1 -0
  76. package/dist/services/task.service.d.ts +19 -7
  77. package/dist/services/task.service.d.ts.map +1 -1
  78. package/dist/services/task.service.js +139 -63
  79. package/dist/services/task.service.js.map +1 -1
  80. package/dist/services/team-reconciler.service.d.ts.map +1 -1
  81. package/dist/services/team-reconciler.service.js +21 -1
  82. package/dist/services/team-reconciler.service.js.map +1 -1
  83. package/dist/services/team-run.service.d.ts.map +1 -1
  84. package/dist/services/team-run.service.js +15 -1
  85. package/dist/services/team-run.service.js.map +1 -1
  86. package/dist/services/team-scheduler.service.d.ts +3 -1
  87. package/dist/services/team-scheduler.service.d.ts.map +1 -1
  88. package/dist/services/team-scheduler.service.js +16 -0
  89. package/dist/services/team-scheduler.service.js.map +1 -1
  90. package/dist/services/workspace-kind.d.ts +13 -0
  91. package/dist/services/workspace-kind.d.ts.map +1 -0
  92. package/dist/services/workspace-kind.js +16 -0
  93. package/dist/services/workspace-kind.js.map +1 -0
  94. package/dist/services/workspace.service.d.ts +26 -0
  95. package/dist/services/workspace.service.d.ts.map +1 -1
  96. package/dist/services/workspace.service.js +173 -23
  97. package/dist/services/workspace.service.js.map +1 -1
  98. package/dist/types/index.d.ts +4 -0
  99. package/dist/types/index.d.ts.map +1 -1
  100. package/dist/types/index.js +6 -0
  101. package/dist/types/index.js.map +1 -1
  102. package/dist/web/assets/AgentDemoPage-CkhVcgZo.js +1 -0
  103. package/dist/web/assets/{DemoPage-DeXHpe0f.js → DemoPage-dkmp8Vvn.js} +3 -3
  104. package/dist/web/assets/GeneralSettingsPage-B4DH6g3D.js +1 -0
  105. package/dist/web/assets/MemberAvatar-pwugXGL4.js +1 -0
  106. package/dist/web/assets/NotificationSettingsPage-C-bPU_ZE.js +1 -0
  107. package/dist/web/assets/ProfileSettingsPage-Dlr6BmHb.js +3 -0
  108. package/dist/web/assets/ProjectKanbanPage-7oEfjjWV.js +89 -0
  109. package/dist/web/assets/ProjectSettingsPage-DZZksq5G.js +2 -0
  110. package/dist/web/assets/ProviderSettingsPage-BWN7CEG6.js +54 -0
  111. package/dist/web/assets/SettingsSection-C-sMhXpf.js +1 -0
  112. package/dist/web/assets/TeamSettingsPage-924xpocx.js +1 -0
  113. package/dist/web/assets/arrow-left-UHjQiY5K.js +1 -0
  114. package/dist/web/assets/button-CUTjpRqw.js +1 -0
  115. package/dist/web/assets/check-CPkZgPjx.js +1 -0
  116. package/dist/web/assets/chevron-down-Bby7sJEv.js +1 -0
  117. package/dist/web/assets/chevron-right-DCC0lyoB.js +1 -0
  118. package/dist/web/assets/{chevron-up-xy9sRgGr.js → chevron-up-BnCoaejn.js} +1 -1
  119. package/dist/web/assets/{circle-check-D53Ur01f.js → circle-check-BscClK07.js} +1 -1
  120. package/dist/web/assets/{code-block-OCS4YCEC-BHTzJkrx.js → code-block-OCS4YCEC-DkMlYSza.js} +1 -1
  121. package/dist/web/assets/confirm-dialog-wsb35VpE.js +1 -0
  122. package/dist/web/assets/folder-picker-B-X_nrS1.js +1 -0
  123. package/dist/web/assets/index-4hNWw0yi.css +1 -0
  124. package/dist/web/assets/index-CUJoWIuo.js +13 -0
  125. package/dist/web/assets/loader-circle-Cul4BuAa.js +1 -0
  126. package/dist/web/assets/log-adapter-CtvxzS4j.js +1 -0
  127. package/dist/web/assets/{mermaid-NOHMQCX5-BSPQ7bmi.js → mermaid-NOHMQCX5-DoPzf-UA.js} +53 -53
  128. package/dist/web/assets/message-square-C7Q71jFj.js +1 -0
  129. package/dist/web/assets/modal-25-qs8P5.js +1 -0
  130. package/dist/web/assets/{pencil-zfyDXHtM.js → pencil-CaJR6Dqm.js} +1 -1
  131. package/dist/web/assets/rotate-ccw-Bvz7590n.js +1 -0
  132. package/dist/web/assets/{select-CEtSClHi.js → select-CzeTYLO4.js} +1 -1
  133. package/dist/web/assets/upload-_2T21rVP.js +1 -0
  134. package/dist/web/assets/{use-profiles-BrCaq-M0.js → use-profiles-BaCwGP06.js} +1 -1
  135. package/dist/web/assets/{use-providers-BmGR6zlI.js → use-providers-Bwl7R5Ql.js} +1 -1
  136. package/dist/web/index.html +2 -2
  137. package/node_modules/@agent-tower/shared/dist/types.d.ts +10 -0
  138. package/node_modules/@agent-tower/shared/dist/types.d.ts.map +1 -1
  139. package/node_modules/@agent-tower/shared/dist/types.js +6 -0
  140. package/node_modules/@agent-tower/shared/dist/types.js.map +1 -1
  141. package/node_modules/@prisma/client/.prisma/client/edge.js +24 -5
  142. package/node_modules/@prisma/client/.prisma/client/index-browser.js +19 -0
  143. package/node_modules/@prisma/client/.prisma/client/index.d.ts +1652 -142
  144. package/node_modules/@prisma/client/.prisma/client/index.js +24 -5
  145. package/node_modules/@prisma/client/.prisma/client/package.json +1 -1
  146. package/node_modules/@prisma/client/.prisma/client/schema.prisma +29 -1
  147. package/node_modules/@prisma/client/.prisma/client/wasm.js +19 -0
  148. package/package.json +1 -1
  149. package/prisma/migrations/20260603000000_add_task_cleanup_jobs/migration.sql +21 -0
  150. package/prisma/migrations/20260609000000_add_workspace_kind_working_dir/migration.sql +7 -0
  151. package/prisma/schema.prisma +29 -1
  152. package/dist/web/assets/AgentDemoPage-C41Npg-J.js +0 -1
  153. package/dist/web/assets/GeneralSettingsPage-ROIAXf_I.js +0 -1
  154. package/dist/web/assets/MemberAvatar-Bgb9cNkt.js +0 -1
  155. package/dist/web/assets/NotificationSettingsPage-BSFFB_Xk.js +0 -1
  156. package/dist/web/assets/ProfileSettingsPage-n6J_nKUb.js +0 -3
  157. package/dist/web/assets/ProjectKanbanPage-BUwfUX3X.js +0 -90
  158. package/dist/web/assets/ProjectSettingsPage-Dv5Ab2MY.js +0 -2
  159. package/dist/web/assets/ProviderSettingsPage-Ckp_pVCi.js +0 -54
  160. package/dist/web/assets/TeamSettingsPage-DjlyHMct.js +0 -1
  161. package/dist/web/assets/button-_UA8cNod.js +0 -1
  162. package/dist/web/assets/chevron-down-qLfxqOnz.js +0 -1
  163. package/dist/web/assets/chevron-right-SY4_v7-h.js +0 -1
  164. package/dist/web/assets/confirm-dialog-D9WQU47x.js +0 -1
  165. package/dist/web/assets/folder-picker-DPW13OyX.js +0 -1
  166. package/dist/web/assets/index-BjIvVVfi.css +0 -1
  167. package/dist/web/assets/index-BsrxN6e4.js +0 -13
  168. package/dist/web/assets/loader-circle-CPz4Agyr.js +0 -1
  169. package/dist/web/assets/log-adapter-i7kNXXup.js +0 -1
  170. package/dist/web/assets/modal-BLPS1r7Q.js +0 -1
  171. package/dist/web/assets/upload-hdDyQIIu.js +0 -1
  172. package/dist/web/assets/utils-CkSf8FUe.js +0 -1
@@ -0,0 +1,209 @@
1
+ import { prisma } from '../utils/index.js';
2
+ import { WorktreeManager } from '../git/worktree.manager.js';
3
+ import { isWorktreeWorkspace } from './workspace-kind.js';
4
+ export const TaskCleanupJobStatus = {
5
+ PENDING: 'PENDING',
6
+ RUNNING: 'RUNNING',
7
+ COMPLETED: 'COMPLETED',
8
+ FAILED: 'FAILED',
9
+ };
10
+ const MAX_JOB_ATTEMPTS = 5;
11
+ const RETRY_DELAYS_MS = [
12
+ 30_000,
13
+ 2 * 60_000,
14
+ 10 * 60_000,
15
+ 30 * 60_000,
16
+ 60 * 60_000,
17
+ ];
18
+ function parseSnapshot(payload) {
19
+ const parsed = JSON.parse(payload);
20
+ if (!parsed.taskId || !parsed.projectId || !parsed.project?.repoPath || !Array.isArray(parsed.workspaces)) {
21
+ throw new Error('Invalid task cleanup snapshot');
22
+ }
23
+ return parsed;
24
+ }
25
+ export function getTaskCleanupRetryDelayMs(attempts) {
26
+ return RETRY_DELAYS_MS[Math.min(Math.max(attempts - 1, 0), RETRY_DELAYS_MS.length - 1)];
27
+ }
28
+ function toErrorMessage(error) {
29
+ return error instanceof Error ? error.message : String(error);
30
+ }
31
+ function isMissingCleanupTableError(error) {
32
+ return Boolean(error &&
33
+ typeof error === 'object' &&
34
+ 'code' in error &&
35
+ error.code === 'P2021');
36
+ }
37
+ /**
38
+ * Persistent worker for resources left behind by fast task deletion.
39
+ */
40
+ export class TaskCleanupService {
41
+ sessionManager;
42
+ running = false;
43
+ timer = null;
44
+ constructor(sessionManager) {
45
+ this.sessionManager = sessionManager;
46
+ }
47
+ start(intervalMs = 30_000) {
48
+ if (this.timer)
49
+ return;
50
+ this.timer = setInterval(() => {
51
+ this.processDueJobs().catch((error) => {
52
+ console.error('[TaskCleanupService] processDueJobs failed:', error);
53
+ });
54
+ }, intervalMs);
55
+ this.processDueJobs().catch((error) => {
56
+ console.error('[TaskCleanupService] startup cleanup failed:', error);
57
+ });
58
+ }
59
+ stop() {
60
+ if (!this.timer)
61
+ return;
62
+ clearInterval(this.timer);
63
+ this.timer = null;
64
+ }
65
+ trigger() {
66
+ this.processDueJobs().catch((error) => {
67
+ console.error('[TaskCleanupService] triggered cleanup failed:', error);
68
+ });
69
+ }
70
+ async processDueJobs(limit = 5) {
71
+ if (this.running)
72
+ return 0;
73
+ this.running = true;
74
+ try {
75
+ let processed = 0;
76
+ while (processed < limit) {
77
+ const job = await this.claimNextJob();
78
+ if (!job)
79
+ break;
80
+ await this.processJob(job.id);
81
+ processed++;
82
+ }
83
+ return processed;
84
+ }
85
+ finally {
86
+ this.running = false;
87
+ }
88
+ }
89
+ async processJob(jobId) {
90
+ const job = await prisma.taskCleanupJob.findUnique({ where: { id: jobId } });
91
+ if (!job)
92
+ return;
93
+ try {
94
+ const snapshot = parseSnapshot(job.payload);
95
+ await this.cleanupSnapshot(snapshot);
96
+ await prisma.$transaction([
97
+ prisma.task.deleteMany({ where: { id: snapshot.taskId } }),
98
+ prisma.taskCleanupJob.update({
99
+ where: { id: jobId },
100
+ data: {
101
+ status: TaskCleanupJobStatus.COMPLETED,
102
+ lastError: null,
103
+ nextRetryAt: null,
104
+ completedAt: new Date(),
105
+ },
106
+ }),
107
+ ]);
108
+ }
109
+ catch (error) {
110
+ await this.recordFailure(jobId, job.attempts, toErrorMessage(error));
111
+ }
112
+ }
113
+ async claimNextJob() {
114
+ const now = new Date();
115
+ let job;
116
+ try {
117
+ job = await prisma.taskCleanupJob.findFirst({
118
+ where: {
119
+ OR: [
120
+ { status: TaskCleanupJobStatus.PENDING },
121
+ {
122
+ status: TaskCleanupJobStatus.RUNNING,
123
+ attempts: { lt: MAX_JOB_ATTEMPTS },
124
+ },
125
+ {
126
+ status: TaskCleanupJobStatus.FAILED,
127
+ attempts: { lt: MAX_JOB_ATTEMPTS },
128
+ OR: [{ nextRetryAt: null }, { nextRetryAt: { lte: now } }],
129
+ },
130
+ ],
131
+ },
132
+ orderBy: [{ createdAt: 'asc' }],
133
+ });
134
+ }
135
+ catch (error) {
136
+ if (isMissingCleanupTableError(error)) {
137
+ return null;
138
+ }
139
+ throw error;
140
+ }
141
+ if (!job)
142
+ return null;
143
+ try {
144
+ return await prisma.taskCleanupJob.update({
145
+ where: { id: job.id },
146
+ data: {
147
+ status: TaskCleanupJobStatus.RUNNING,
148
+ attempts: { increment: 1 },
149
+ startedAt: now,
150
+ lastError: null,
151
+ },
152
+ });
153
+ }
154
+ catch {
155
+ return null;
156
+ }
157
+ }
158
+ async cleanupSnapshot(snapshot) {
159
+ const worktreeManager = new WorktreeManager(snapshot.project.repoPath);
160
+ for (const workspace of snapshot.workspaces) {
161
+ for (const session of workspace.sessions) {
162
+ try {
163
+ await this.sessionManager.stop(session.id, { skipTeamRunReconcile: true });
164
+ }
165
+ catch (error) {
166
+ console.warn(`[TaskCleanupService] failed to stop session ${session.id}:`, toErrorMessage(error));
167
+ }
168
+ }
169
+ }
170
+ for (const workspace of snapshot.workspaces) {
171
+ if (!isWorktreeWorkspace(workspace))
172
+ continue;
173
+ if (!workspace.worktreePath)
174
+ continue;
175
+ const result = await worktreeManager.remove(workspace.worktreePath);
176
+ if (result.status === 'unregistered') {
177
+ throw new Error(`Workspace ${workspace.id} path is unregistered or unsafe to remove: ${result.path}`);
178
+ }
179
+ }
180
+ for (const workspace of snapshot.workspaces) {
181
+ if (!isWorktreeWorkspace(workspace))
182
+ continue;
183
+ const result = await worktreeManager.deleteBranchIfSafe(workspace.branchName, {
184
+ protectedBranches: [snapshot.project.mainBranch, workspace.baseBranch],
185
+ });
186
+ if (result.status === 'failed') {
187
+ throw new Error(`Failed to delete branch ${result.branchName}: ${result.reason ?? 'unknown error'}`);
188
+ }
189
+ if (result.status === 'checked_out') {
190
+ throw new Error(`Branch ${result.branchName} is checked out: ${result.reason ?? 'unknown location'}`);
191
+ }
192
+ }
193
+ if (snapshot.workspaces.some((workspace) => isWorktreeWorkspace(workspace))) {
194
+ await worktreeManager.prune();
195
+ }
196
+ }
197
+ async recordFailure(jobId, attempts, message) {
198
+ const retryable = attempts < MAX_JOB_ATTEMPTS;
199
+ await prisma.taskCleanupJob.update({
200
+ where: { id: jobId },
201
+ data: {
202
+ status: TaskCleanupJobStatus.FAILED,
203
+ lastError: message,
204
+ nextRetryAt: retryable ? new Date(Date.now() + getTaskCleanupRetryDelayMs(attempts)) : null,
205
+ },
206
+ });
207
+ }
208
+ }
209
+ //# sourceMappingURL=task-cleanup.service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"task-cleanup.service.js","sourceRoot":"","sources":["../../src/services/task-cleanup.service.ts"],"names":[],"mappings":"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"}
@@ -1,6 +1,7 @@
1
1
  import { TaskStatus } from '../types/index.js';
2
2
  import type { EventBus } from '../core/event-bus.js';
3
3
  import type { SessionManager } from './session-manager.js';
4
+ import type { TaskCleanupService } from './task-cleanup.service.js';
4
5
  interface CreateTaskInput {
5
6
  title: string;
6
7
  description?: string;
@@ -19,7 +20,8 @@ interface FindTasksParams {
19
20
  export declare class TaskService {
20
21
  private readonly eventBus;
21
22
  private readonly sessionManager;
22
- constructor(eventBus: EventBus, sessionManager: SessionManager);
23
+ private readonly cleanupService?;
24
+ constructor(eventBus: EventBus, sessionManager: SessionManager, cleanupService?: Pick<TaskCleanupService, "trigger"> | undefined);
23
25
  /**
24
26
  * 获取项目的任务列表(支持按状态过滤和分页)
25
27
  */
@@ -51,6 +53,8 @@ export declare class TaskService {
51
53
  branchName: string;
52
54
  baseBranch: string | null;
53
55
  worktreePath: string;
56
+ workspaceKind: string;
57
+ workingDir: string;
54
58
  commitMessage: string | null;
55
59
  previewTarget: string | null;
56
60
  hibernatedAt: Date | null;
@@ -60,6 +64,7 @@ export declare class TaskService {
60
64
  description: string | null;
61
65
  createdAt: Date;
62
66
  updatedAt: Date;
67
+ deletedAt: Date | null;
63
68
  title: string;
64
69
  status: string;
65
70
  priority: number;
@@ -101,6 +106,8 @@ export declare class TaskService {
101
106
  branchName: string;
102
107
  baseBranch: string | null;
103
108
  worktreePath: string;
109
+ workspaceKind: string;
110
+ workingDir: string;
104
111
  commitMessage: string | null;
105
112
  previewTarget: string | null;
106
113
  hibernatedAt: Date | null;
@@ -110,6 +117,7 @@ export declare class TaskService {
110
117
  description: string | null;
111
118
  createdAt: Date;
112
119
  updatedAt: Date;
120
+ deletedAt: Date | null;
113
121
  title: string;
114
122
  status: string;
115
123
  priority: number;
@@ -126,6 +134,7 @@ export declare class TaskService {
126
134
  description: string | null;
127
135
  createdAt: Date;
128
136
  updatedAt: Date;
137
+ deletedAt: Date | null;
129
138
  title: string;
130
139
  status: string;
131
140
  priority: number;
@@ -140,6 +149,7 @@ export declare class TaskService {
140
149
  description: string | null;
141
150
  createdAt: Date;
142
151
  updatedAt: Date;
152
+ deletedAt: Date | null;
143
153
  title: string;
144
154
  status: string;
145
155
  priority: number;
@@ -155,6 +165,7 @@ export declare class TaskService {
155
165
  description: string | null;
156
166
  createdAt: Date;
157
167
  updatedAt: Date;
168
+ deletedAt: Date | null;
158
169
  title: string;
159
170
  status: string;
160
171
  priority: number;
@@ -170,6 +181,7 @@ export declare class TaskService {
170
181
  description: string | null;
171
182
  createdAt: Date;
172
183
  updatedAt: Date;
184
+ deletedAt: Date | null;
173
185
  title: string;
174
186
  status: string;
175
187
  priority: number;
@@ -177,13 +189,12 @@ export declare class TaskService {
177
189
  projectId: string;
178
190
  }>;
179
191
  /**
180
- * 删除任务(增强版)
192
+ * 快速删除任务
181
193
  *
182
- * 1. 停止所有 RUNNING/PENDING 状态的 Session
183
- * 2. 清理所有关联 Workspace 的 git worktree
184
- * 3. 清理所有关联 Workspace 的本地任务分支
185
- * 4. 级联删除数据库记录
186
- * 5. 通过 EventBus 通知前端实时删除
194
+ * 1. 原子标记 Task deletedAt,使普通列表立即隐藏
195
+ * 2. 保存后台清理快照
196
+ * 3. 通过 EventBus 通知前端实时删除
197
+ * 4. 后台 worker 再停止 Session、删除 worktree/branch,并最终硬删除 Task
187
198
  */
188
199
  delete(id: string): Promise<boolean>;
189
200
  /**
@@ -210,6 +221,7 @@ export declare class TaskService {
210
221
  description: string | null;
211
222
  createdAt: Date;
212
223
  updatedAt: Date;
224
+ deletedAt: Date | null;
213
225
  title: string;
214
226
  status: string;
215
227
  priority: number;
@@ -1 +1 @@
1
- {"version":3,"file":"task.service.d.ts","sourceRoot":"","sources":["../../src/services/task.service.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAkD,MAAM,mBAAmB,CAAC;AAM/F,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAI3D,UAAU,eAAe;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,UAAU,eAAe;IACvB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,UAAU,eAAe;IACvB,MAAM,CAAC,EAAE,UAAU,CAAC;IACpB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAsBD,qBAAa,WAAW;IAEpB,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB,OAAO,CAAC,QAAQ,CAAC,cAAc;gBADd,QAAQ,EAAE,QAAQ,EAClB,cAAc,EAAE,cAAc;IAGjD;;OAEG;IACG,eAAe,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,GAAE,eAAoB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IAuDrE;;OAEG;IACG,QAAQ,CAAC,EAAE,EAAE,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IAazB;;;;OAIG;IACG,MAAM,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,eAAe;;;;;;;;;;;IA6BtD;;OAEG;IACG,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,eAAe;;;;;;;;;;;IAoB/C;;;OAGG;IACG,YAAY,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU;;;;;;;;;;;IA2CjD;;;OAGG;IACG,cAAc,CAAC,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,UAAU;;;;;;;;;;;IAgCtE;;;;;;;;OAQG;IACG,MAAM,CAAC,EAAE,EAAE,MAAM;IAyEvB;;OAEG;IACG,mBAAmB,CAAC,SAAS,EAAE,MAAM;;;;;;;;IAgD3C;;;;;;;OAOG;IACG,KAAK,CAAC,EAAE,EAAE,MAAM;;;;;;;;;;;IA+CtB;;OAEG;IACH,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI;CAGzE"}
1
+ {"version":3,"file":"task.service.d.ts","sourceRoot":"","sources":["../../src/services/task.service.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAkD,MAAM,mBAAmB,CAAC;AAO/F,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAC3D,OAAO,KAAK,EAAE,kBAAkB,EAAuB,MAAM,2BAA2B,CAAC;AAIzF,UAAU,eAAe;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,UAAU,eAAe;IACvB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,UAAU,eAAe;IACvB,MAAM,CAAC,EAAE,UAAU,CAAC;IACpB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAwBD,qBAAa,WAAW;IAEpB,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB,OAAO,CAAC,QAAQ,CAAC,cAAc;IAC/B,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAC;gBAFf,QAAQ,EAAE,QAAQ,EAClB,cAAc,EAAE,cAAc,EAC9B,cAAc,CAAC,EAAE,IAAI,CAAC,kBAAkB,EAAE,SAAS,CAAC,YAAA;IAGvE;;OAEG;IACG,eAAe,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,GAAE,eAAoB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IAuDrE;;OAEG;IACG,QAAQ,CAAC,EAAE,EAAE,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IAazB;;;;OAIG;IACG,MAAM,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,eAAe;;;;;;;;;;;;IA6BtD;;OAEG;IACG,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,eAAe;;;;;;;;;;;;IAoB/C;;;OAGG;IACG,YAAY,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU;;;;;;;;;;;;IA2CjD;;;OAGG;IACG,cAAc,CAAC,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,UAAU;;;;;;;;;;;;IAgCtE;;;;;;;OAOG;IACG,MAAM,CAAC,EAAE,EAAE,MAAM;IA0IvB;;OAEG;IACG,mBAAmB,CAAC,SAAS,EAAE,MAAM;;;;;;;;IAgD3C;;;;;;;OAOG;IACG,KAAK,CAAC,EAAE,EAAE,MAAM;;;;;;;;;;;;IA+CtB;;OAEG;IACH,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI;CAGzE"}
@@ -1,8 +1,9 @@
1
1
  import { prisma } from '../utils/index.js';
2
2
  import { TaskStatus, SessionStatus, SessionPurpose, WorkspaceStatus } from '../types/index.js';
3
+ import { getWorkspaceWorkingDir } from './workspace-kind.js';
3
4
  import { NotFoundError, ValidationError, InvalidStateTransitionError, } from '../errors.js';
4
- import { WorktreeManager } from '../git/worktree.manager.js';
5
5
  import { ensureProjectIsMutable } from './project-guards.js';
6
+ import { defaultTeamLockService } from './team-lock.service.js';
6
7
  /**
7
8
  * 合法的状态流转规则
8
9
  * 看板拖拽场景下允许任意状态互转,状态变更无危险副作用
@@ -14,6 +15,8 @@ const VALID_TRANSITIONS = {
14
15
  [TaskStatus.DONE]: [TaskStatus.TODO, TaskStatus.IN_PROGRESS, TaskStatus.IN_REVIEW, TaskStatus.CANCELLED],
15
16
  [TaskStatus.CANCELLED]: [TaskStatus.TODO, TaskStatus.IN_PROGRESS, TaskStatus.IN_REVIEW, TaskStatus.DONE],
16
17
  };
18
+ const CANCELLABLE_DELETE_WORK_REQUEST_STATUSES = ['PENDING_APPROVAL', 'QUEUED', 'STARTED'];
19
+ const CANCELLABLE_DELETE_INVOCATION_STATUSES = ['QUEUED', 'RUNNING', 'SESSION_ENDED', 'WAITING_ROOM_REPLY'];
17
20
  function normalizeTaskTitle(title) {
18
21
  const normalized = title.trim();
19
22
  if (normalized.length === 0) {
@@ -24,9 +27,11 @@ function normalizeTaskTitle(title) {
24
27
  export class TaskService {
25
28
  eventBus;
26
29
  sessionManager;
27
- constructor(eventBus, sessionManager) {
30
+ cleanupService;
31
+ constructor(eventBus, sessionManager, cleanupService) {
28
32
  this.eventBus = eventBus;
29
33
  this.sessionManager = sessionManager;
34
+ this.cleanupService = cleanupService;
30
35
  }
31
36
  /**
32
37
  * 获取项目的任务列表(支持按状态过滤和分页)
@@ -42,7 +47,7 @@ export class TaskService {
42
47
  const page = Math.max(1, params.page || 1);
43
48
  const limit = Math.min(1000, Math.max(1, params.limit || 200));
44
49
  const skip = (page - 1) * limit;
45
- const where = { projectId };
50
+ const where = { projectId, deletedAt: null };
46
51
  if (params.status) {
47
52
  where.status = params.status;
48
53
  }
@@ -84,8 +89,8 @@ export class TaskService {
84
89
  * 获取任务详情
85
90
  */
86
91
  async findById(id) {
87
- const task = await prisma.task.findUnique({
88
- where: { id },
92
+ const task = await prisma.task.findFirst({
93
+ where: { id, deletedAt: null },
89
94
  include: { workspaces: { include: { sessions: { where: { purpose: { not: SessionPurpose.COMMIT_MSG } } } } } },
90
95
  });
91
96
  if (!task) {
@@ -110,7 +115,7 @@ export class TaskService {
110
115
  ensureProjectIsMutable(project, 'create tasks');
111
116
  // 自动计算 position
112
117
  const maxPosition = await prisma.task.aggregate({
113
- where: { projectId, status: TaskStatus.TODO },
118
+ where: { projectId, status: TaskStatus.TODO, deletedAt: null },
114
119
  _max: { position: true },
115
120
  });
116
121
  return prisma.task.create({
@@ -131,8 +136,8 @@ export class TaskService {
131
136
  ...input,
132
137
  ...(input.title !== undefined ? { title: normalizeTaskTitle(input.title) } : {}),
133
138
  };
134
- const task = await prisma.task.findUnique({
135
- where: { id },
139
+ const task = await prisma.task.findFirst({
140
+ where: { id, deletedAt: null },
136
141
  include: { project: true },
137
142
  });
138
143
  if (!task) {
@@ -149,8 +154,8 @@ export class TaskService {
149
154
  * 更新后通过 EventBus 发射 task:updated 事件,通知前端实时更新
150
155
  */
151
156
  async updateStatus(id, status) {
152
- const task = await prisma.task.findUnique({
153
- where: { id },
157
+ const task = await prisma.task.findFirst({
158
+ where: { id, deletedAt: null },
154
159
  include: { project: true },
155
160
  });
156
161
  if (!task) {
@@ -169,7 +174,7 @@ export class TaskService {
169
174
  }
170
175
  // 切换状态时自动计算新列的 position
171
176
  const maxPosition = await prisma.task.aggregate({
172
- where: { projectId: task.projectId, status },
177
+ where: { projectId: task.projectId, status, deletedAt: null },
173
178
  _max: { position: true },
174
179
  });
175
180
  const updated = await prisma.task.update({
@@ -188,8 +193,8 @@ export class TaskService {
188
193
  * 如果同时传了 status,会进行状态流转并通知前端
189
194
  */
190
195
  async updatePosition(id, position, status) {
191
- const task = await prisma.task.findUnique({
192
- where: { id },
196
+ const task = await prisma.task.findFirst({
197
+ where: { id, deletedAt: null },
193
198
  include: { project: true },
194
199
  });
195
200
  if (!task) {
@@ -215,69 +220,140 @@ export class TaskService {
215
220
  return updated;
216
221
  }
217
222
  /**
218
- * 删除任务(增强版)
223
+ * 快速删除任务
219
224
  *
220
- * 1. 停止所有 RUNNING/PENDING 状态的 Session
221
- * 2. 清理所有关联 Workspace 的 git worktree
222
- * 3. 清理所有关联 Workspace 的本地任务分支
223
- * 4. 级联删除数据库记录
224
- * 5. 通过 EventBus 通知前端实时删除
225
+ * 1. 原子标记 Task deletedAt,使普通列表立即隐藏
226
+ * 2. 保存后台清理快照
227
+ * 3. 通过 EventBus 通知前端实时删除
228
+ * 4. 后台 worker 再停止 Session、删除 worktree/branch,并最终硬删除 Task
225
229
  */
226
230
  async delete(id) {
227
- const task = await prisma.task.findUnique({
231
+ const deletedAt = new Date();
232
+ const taskForGuard = await prisma.task.findUnique({
228
233
  where: { id },
229
- include: {
230
- project: true,
231
- workspaces: {
232
- include: { sessions: true },
233
- },
234
- },
234
+ include: { project: true },
235
235
  });
236
- if (!task) {
236
+ if (!taskForGuard || taskForGuard.deletedAt) {
237
237
  throw new NotFoundError('Task', id);
238
238
  }
239
- ensureProjectIsMutable(task.project, 'delete tasks');
240
- // 1. 停止所有活跃 Session
241
- for (const workspace of task.workspaces) {
242
- const activeSessions = workspace.sessions.filter((s) => s.status === SessionStatus.PENDING || s.status === SessionStatus.RUNNING);
243
- for (const session of activeSessions) {
244
- try {
245
- await this.sessionManager.stop(session.id);
246
- }
247
- catch (err) {
248
- console.warn(`[TaskService] Failed to stop session ${session.id} during task delete:`, err);
249
- }
250
- }
251
- }
252
- // 2. 清理所有 Workspace 的 worktree
253
- const worktreeManager = new WorktreeManager(task.project.repoPath);
254
- for (const workspace of task.workspaces) {
255
- try {
256
- await worktreeManager.remove(workspace.worktreePath);
257
- }
258
- catch (err) {
259
- console.warn(`[TaskService] Failed to remove worktree for workspace ${workspace.id} during task delete:`, err instanceof Error ? err.message : err);
239
+ ensureProjectIsMutable(taskForGuard.project, 'delete tasks');
240
+ const marked = await prisma.task.updateMany({
241
+ where: {
242
+ id,
243
+ deletedAt: null,
244
+ project: { archivedAt: null },
245
+ },
246
+ data: { deletedAt },
247
+ });
248
+ if (marked.count === 0) {
249
+ const current = await prisma.task.findUnique({
250
+ where: { id },
251
+ include: { project: true },
252
+ });
253
+ if (!current || current.deletedAt) {
254
+ throw new NotFoundError('Task', id);
260
255
  }
256
+ ensureProjectIsMutable(current.project, 'delete tasks');
257
+ throw new NotFoundError('Task', id);
261
258
  }
262
- // 3. 清理所有 Workspace 的本地任务分支。分支清理失败不阻断任务删除。
263
- for (const workspace of task.workspaces) {
264
- const result = await worktreeManager.deleteBranchIfSafe(workspace.branchName, {
265
- protectedBranches: [task.project.mainBranch, workspace.baseBranch],
259
+ let deleteResult;
260
+ let cleanupJobCreated = false;
261
+ try {
262
+ const taskForCleanup = await prisma.task.findUnique({
263
+ where: { id },
264
+ include: {
265
+ project: true,
266
+ teamRun: {
267
+ include: {
268
+ invocations: {
269
+ where: { status: { in: CANCELLABLE_DELETE_INVOCATION_STATUSES } },
270
+ select: { id: true },
271
+ },
272
+ },
273
+ },
274
+ workspaces: {
275
+ include: { sessions: true },
276
+ },
277
+ },
266
278
  });
267
- if (result.status === 'failed') {
268
- console.warn(`[TaskService] Failed to delete branch ${result.branchName} for workspace ${workspace.id} during task delete:`, result.reason);
279
+ if (!taskForCleanup) {
280
+ throw new NotFoundError('Task', id);
269
281
  }
270
- else if (result.status === 'checked_out') {
271
- console.warn(`[TaskService] Skipped deleting checked-out branch ${result.branchName} for workspace ${workspace.id} during task delete:`, result.reason);
282
+ ensureProjectIsMutable(taskForCleanup.project, 'delete tasks');
283
+ const snapshot = {
284
+ taskId: taskForCleanup.id,
285
+ projectId: taskForCleanup.projectId,
286
+ project: {
287
+ repoPath: taskForCleanup.project.repoPath,
288
+ mainBranch: taskForCleanup.project.mainBranch,
289
+ },
290
+ workspaces: taskForCleanup.workspaces.map((workspace) => ({
291
+ id: workspace.id,
292
+ worktreePath: workspace.worktreePath,
293
+ workingDir: getWorkspaceWorkingDir(workspace),
294
+ workspaceKind: workspace.workspaceKind,
295
+ branchName: workspace.branchName,
296
+ baseBranch: workspace.baseBranch,
297
+ sessions: workspace.sessions
298
+ .filter((session) => session.status === SessionStatus.PENDING || session.status === SessionStatus.RUNNING)
299
+ .map((session) => ({ id: session.id })),
300
+ })),
301
+ };
302
+ const teamRunId = taskForCleanup.teamRun?.id;
303
+ const cancelledInvocationIds = taskForCleanup.teamRun?.invocations.map((invocation) => invocation.id) ?? [];
304
+ await prisma.$transaction(async (tx) => {
305
+ await tx.taskCleanupJob.create({
306
+ data: {
307
+ taskId: taskForCleanup.id,
308
+ projectId: taskForCleanup.projectId,
309
+ payload: JSON.stringify(snapshot),
310
+ },
311
+ });
312
+ if (teamRunId) {
313
+ await tx.workRequest.updateMany({
314
+ where: {
315
+ teamRunId,
316
+ status: { in: CANCELLABLE_DELETE_WORK_REQUEST_STATUSES },
317
+ },
318
+ data: { status: 'CANCELLED' },
319
+ });
320
+ await tx.agentInvocation.updateMany({
321
+ where: {
322
+ teamRunId,
323
+ status: { in: CANCELLABLE_DELETE_INVOCATION_STATUSES },
324
+ },
325
+ data: {
326
+ status: 'CANCELLED',
327
+ nextRoomReplyReminderAt: null,
328
+ },
329
+ });
330
+ }
331
+ });
332
+ cleanupJobCreated = true;
333
+ deleteResult = {
334
+ projectId: taskForCleanup.projectId,
335
+ cancelledInvocationIds,
336
+ };
337
+ }
338
+ catch (error) {
339
+ if (!cleanupJobCreated) {
340
+ await prisma.task.updateMany({
341
+ where: { id, deletedAt },
342
+ data: { deletedAt: null },
343
+ }).catch(() => {
344
+ // If rollback fails, leave the task hidden rather than masking the original error.
345
+ });
272
346
  }
347
+ throw error;
348
+ }
349
+ for (const invocationId of deleteResult.cancelledInvocationIds) {
350
+ defaultTeamLockService.releaseByOwner(invocationId);
273
351
  }
274
- // 4. 级联删除数据库记录
275
- await prisma.task.delete({ where: { id } });
276
- // 5. 通知前端
277
352
  this.eventBus.emit('task:deleted', {
278
353
  taskId: id,
279
- projectId: task.projectId,
354
+ projectId: deleteResult.projectId,
280
355
  });
356
+ this.cleanupService?.trigger();
281
357
  return true;
282
358
  }
283
359
  /**
@@ -292,7 +368,7 @@ export class TaskService {
292
368
  }
293
369
  const counts = await prisma.task.groupBy({
294
370
  by: ['status'],
295
- where: { projectId },
371
+ where: { projectId, deletedAt: null },
296
372
  _count: { id: true },
297
373
  });
298
374
  const stats = {
@@ -335,8 +411,8 @@ export class TaskService {
335
411
  * 4. 通知前端 — 用户可重新派发 Agent(会创建新 Worktree)
336
412
  */
337
413
  async retry(id) {
338
- const task = await prisma.task.findUnique({
339
- where: { id },
414
+ const task = await prisma.task.findFirst({
415
+ where: { id, deletedAt: null },
340
416
  include: {
341
417
  project: true,
342
418
  workspaces: {