agent-tower 0.5.1-beta.2 → 0.5.2-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) 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/routes/__tests__/tasks.test.js +37 -0
  15. package/dist/routes/__tests__/tasks.test.js.map +1 -1
  16. package/dist/routes/sessions.d.ts.map +1 -1
  17. package/dist/routes/sessions.js +12 -0
  18. package/dist/routes/sessions.js.map +1 -1
  19. package/dist/routes/tasks.js +2 -2
  20. package/dist/routes/tasks.js.map +1 -1
  21. package/dist/services/__tests__/project.service.test.d.ts +2 -0
  22. package/dist/services/__tests__/project.service.test.d.ts.map +1 -0
  23. package/dist/services/__tests__/project.service.test.js +62 -0
  24. package/dist/services/__tests__/project.service.test.js.map +1 -0
  25. package/dist/services/__tests__/session-manager.team-run.test.js +98 -0
  26. package/dist/services/__tests__/session-manager.team-run.test.js.map +1 -1
  27. package/dist/services/__tests__/task.service.test.js +352 -38
  28. package/dist/services/__tests__/task.service.test.js.map +1 -1
  29. package/dist/services/__tests__/team-reconciler.service.test.js +68 -0
  30. package/dist/services/__tests__/team-reconciler.service.test.js.map +1 -1
  31. package/dist/services/__tests__/team-run.service.test.js +47 -1
  32. package/dist/services/__tests__/team-run.service.test.js.map +1 -1
  33. package/dist/services/__tests__/team-scheduler.service.test.js +49 -0
  34. package/dist/services/__tests__/team-scheduler.service.test.js.map +1 -1
  35. package/dist/services/__tests__/workspace.service.test.js +102 -0
  36. package/dist/services/__tests__/workspace.service.test.js.map +1 -1
  37. package/dist/services/deleted-task-guard.d.ts +11 -0
  38. package/dist/services/deleted-task-guard.d.ts.map +1 -0
  39. package/dist/services/deleted-task-guard.js +10 -0
  40. package/dist/services/deleted-task-guard.js.map +1 -0
  41. package/dist/services/project.service.d.ts +1 -0
  42. package/dist/services/project.service.d.ts.map +1 -1
  43. package/dist/services/project.service.js +1 -1
  44. package/dist/services/project.service.js.map +1 -1
  45. package/dist/services/session-manager.d.ts +47 -1
  46. package/dist/services/session-manager.d.ts.map +1 -1
  47. package/dist/services/session-manager.js +124 -35
  48. package/dist/services/session-manager.js.map +1 -1
  49. package/dist/services/task-cleanup.service.d.ts +46 -0
  50. package/dist/services/task-cleanup.service.d.ts.map +1 -0
  51. package/dist/services/task-cleanup.service.js +202 -0
  52. package/dist/services/task-cleanup.service.js.map +1 -0
  53. package/dist/services/task.service.d.ts +15 -7
  54. package/dist/services/task.service.d.ts.map +1 -1
  55. package/dist/services/task.service.js +136 -63
  56. package/dist/services/task.service.js.map +1 -1
  57. package/dist/services/team-reconciler.service.d.ts.map +1 -1
  58. package/dist/services/team-reconciler.service.js +21 -1
  59. package/dist/services/team-reconciler.service.js.map +1 -1
  60. package/dist/services/team-run.service.d.ts.map +1 -1
  61. package/dist/services/team-run.service.js +15 -1
  62. package/dist/services/team-run.service.js.map +1 -1
  63. package/dist/services/team-scheduler.service.d.ts +3 -1
  64. package/dist/services/team-scheduler.service.d.ts.map +1 -1
  65. package/dist/services/team-scheduler.service.js +16 -0
  66. package/dist/services/team-scheduler.service.js.map +1 -1
  67. package/dist/services/workspace.service.d.ts +8 -0
  68. package/dist/services/workspace.service.d.ts.map +1 -1
  69. package/dist/services/workspace.service.js +70 -12
  70. package/dist/services/workspace.service.js.map +1 -1
  71. package/dist/web/assets/AgentDemoPage-CWcu7-P7.js +1 -0
  72. package/dist/web/assets/{DemoPage-DVt_gGQE.js → DemoPage-6hMli_sP.js} +3 -3
  73. package/dist/web/assets/GeneralSettingsPage-DUaFSgCY.js +1 -0
  74. package/dist/web/assets/MemberAvatar-S-eJAdWH.js +1 -0
  75. package/dist/web/assets/NotificationSettingsPage-Cl5x8F4G.js +1 -0
  76. package/dist/web/assets/ProfileSettingsPage-QFN9385l.js +3 -0
  77. package/dist/web/assets/ProjectKanbanPage-Dye8K5K1.js +89 -0
  78. package/dist/web/assets/ProjectSettingsPage-Do3Q1_r4.js +2 -0
  79. package/dist/web/assets/ProviderSettingsPage-DYOb33XQ.js +54 -0
  80. package/dist/web/assets/SettingsSection-D0Zzbbhp.js +1 -0
  81. package/dist/web/assets/TeamSettingsPage-Dq_DU1S8.js +1 -0
  82. package/dist/web/assets/arrow-left-D0xGRsQG.js +1 -0
  83. package/dist/web/assets/button-C-IDw2d8.js +1 -0
  84. package/dist/web/assets/check-KjbNKANU.js +1 -0
  85. package/dist/web/assets/chevron-down-C735lPj9.js +1 -0
  86. package/dist/web/assets/chevron-right-DfFiWY3R.js +1 -0
  87. package/dist/web/assets/{chevron-up-BkPAyFXE.js → chevron-up-DqGc0SsZ.js} +1 -1
  88. package/dist/web/assets/{circle-check-BmWaxH2e.js → circle-check-CSlvn06a.js} +1 -1
  89. package/dist/web/assets/{code-block-OCS4YCEC-U8rjjKMO.js → code-block-OCS4YCEC-BRT1Nrpy.js} +1 -1
  90. package/dist/web/assets/confirm-dialog-BtsxywSG.js +1 -0
  91. package/dist/web/assets/folder-picker-BuvfyaHB.js +1 -0
  92. package/dist/web/assets/index-BAz6UxmL.js +13 -0
  93. package/dist/web/assets/index-r8en4dlI.css +1 -0
  94. package/dist/web/assets/loader-circle-BLO3w-ze.js +1 -0
  95. package/dist/web/assets/{mermaid-NOHMQCX5-CfVlHQvQ.js → mermaid-NOHMQCX5-BLfGHrOE.js} +53 -53
  96. package/dist/web/assets/message-square-CIyucdyo.js +1 -0
  97. package/dist/web/assets/modal-D_VShm6D.js +1 -0
  98. package/dist/web/assets/{pencil-DvjVNKIw.js → pencil-CdJXJig-.js} +1 -1
  99. package/dist/web/assets/rotate-ccw-B42Jf1RW.js +1 -0
  100. package/dist/web/assets/{select-Xt2WadPY.js → select-kVmZCXo9.js} +1 -1
  101. package/dist/web/assets/{use-profiles-oZ63gfvO.js → use-profiles-Bvj_EW-7.js} +1 -1
  102. package/dist/web/assets/{use-providers-BVrApYJA.js → use-providers-CcFzAUyf.js} +1 -1
  103. package/dist/web/index.html +2 -2
  104. package/node_modules/@prisma/client/.prisma/client/edge.js +22 -5
  105. package/node_modules/@prisma/client/.prisma/client/index-browser.js +17 -0
  106. package/node_modules/@prisma/client/.prisma/client/index.d.ts +1534 -142
  107. package/node_modules/@prisma/client/.prisma/client/index.js +22 -5
  108. package/node_modules/@prisma/client/.prisma/client/package.json +1 -1
  109. package/node_modules/@prisma/client/.prisma/client/schema.prisma +23 -0
  110. package/node_modules/@prisma/client/.prisma/client/wasm.js +17 -0
  111. package/package.json +1 -1
  112. package/prisma/migrations/20260603000000_add_task_cleanup_jobs/migration.sql +21 -0
  113. package/prisma/schema.prisma +23 -0
  114. package/dist/web/assets/AgentDemoPage-CEWSskKz.js +0 -1
  115. package/dist/web/assets/GeneralSettingsPage-B7DbUzT6.js +0 -1
  116. package/dist/web/assets/MemberAvatar--HnaCo5f.js +0 -1
  117. package/dist/web/assets/NotificationSettingsPage-BklUEhB2.js +0 -1
  118. package/dist/web/assets/ProfileSettingsPage-Be7yYwbc.js +0 -3
  119. package/dist/web/assets/ProjectKanbanPage-BZR3HX3_.js +0 -90
  120. package/dist/web/assets/ProjectSettingsPage-Cj1h7G2-.js +0 -2
  121. package/dist/web/assets/ProviderSettingsPage-l244dDeI.js +0 -54
  122. package/dist/web/assets/TeamSettingsPage-CXp-7kqb.js +0 -1
  123. package/dist/web/assets/button-Cc_0rVdG.js +0 -1
  124. package/dist/web/assets/chevron-down-DAto5uxR.js +0 -1
  125. package/dist/web/assets/chevron-right-fcp8yUcf.js +0 -1
  126. package/dist/web/assets/confirm-dialog-D0R59Lv2.js +0 -1
  127. package/dist/web/assets/folder-picker-COVJdb7m.js +0 -1
  128. package/dist/web/assets/index-BCz-PlSG.js +0 -13
  129. package/dist/web/assets/index-BjIvVVfi.css +0 -1
  130. package/dist/web/assets/loader-circle-B0dxeUg4.js +0 -1
  131. package/dist/web/assets/modal-kzH1vcBa.js +0 -1
  132. package/dist/web/assets/upload-dtpXWuCN.js +0 -1
  133. package/dist/web/assets/utils-CkSf8FUe.js +0 -1
@@ -0,0 +1,46 @@
1
+ import type { SessionManager } from './session-manager.js';
2
+ export declare const TaskCleanupJobStatus: {
3
+ readonly PENDING: "PENDING";
4
+ readonly RUNNING: "RUNNING";
5
+ readonly COMPLETED: "COMPLETED";
6
+ readonly FAILED: "FAILED";
7
+ };
8
+ interface TaskCleanupSessionSnapshot {
9
+ id: string;
10
+ }
11
+ interface TaskCleanupWorkspaceSnapshot {
12
+ id: string;
13
+ worktreePath: string;
14
+ branchName: string;
15
+ baseBranch: string | null;
16
+ sessions: TaskCleanupSessionSnapshot[];
17
+ }
18
+ export interface TaskCleanupSnapshot {
19
+ taskId: string;
20
+ projectId: string;
21
+ project: {
22
+ repoPath: string;
23
+ mainBranch: string;
24
+ };
25
+ workspaces: TaskCleanupWorkspaceSnapshot[];
26
+ }
27
+ export declare function getTaskCleanupRetryDelayMs(attempts: number): number;
28
+ /**
29
+ * Persistent worker for resources left behind by fast task deletion.
30
+ */
31
+ export declare class TaskCleanupService {
32
+ private readonly sessionManager;
33
+ private running;
34
+ private timer;
35
+ constructor(sessionManager: SessionManager);
36
+ start(intervalMs?: number): void;
37
+ stop(): void;
38
+ trigger(): void;
39
+ processDueJobs(limit?: number): Promise<number>;
40
+ processJob(jobId: string): Promise<void>;
41
+ private claimNextJob;
42
+ private cleanupSnapshot;
43
+ private recordFailure;
44
+ }
45
+ export {};
46
+ //# sourceMappingURL=task-cleanup.service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"task-cleanup.service.d.ts","sourceRoot":"","sources":["../../src/services/task-cleanup.service.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAE3D,eAAO,MAAM,oBAAoB;;;;;CAKvB,CAAC;AAEX,UAAU,0BAA0B;IAClC,EAAE,EAAE,MAAM,CAAC;CACZ;AAED,UAAU,4BAA4B;IACpC,EAAE,EAAE,MAAM,CAAC;IACX,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,QAAQ,EAAE,0BAA0B,EAAE,CAAC;CACxC;AAED,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE;QACP,QAAQ,EAAE,MAAM,CAAC;QACjB,UAAU,EAAE,MAAM,CAAC;KACpB,CAAC;IACF,UAAU,EAAE,4BAA4B,EAAE,CAAC;CAC5C;AAmBD,wBAAgB,0BAA0B,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAEnE;AAeD;;GAEG;AACH,qBAAa,kBAAkB;IAIjB,OAAO,CAAC,QAAQ,CAAC,cAAc;IAH3C,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,KAAK,CAA+C;gBAE/B,cAAc,EAAE,cAAc;IAE3D,KAAK,CAAC,UAAU,SAAS,GAAG,IAAI;IAYhC,IAAI,IAAI,IAAI;IAMZ,OAAO,IAAI,IAAI;IAMT,cAAc,CAAC,KAAK,SAAI,GAAG,OAAO,CAAC,MAAM,CAAC;IAiB1C,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;YAyBhC,YAAY;YA6CZ,eAAe;YAuCf,aAAa;CAW5B"}
@@ -0,0 +1,202 @@
1
+ import { prisma } from '../utils/index.js';
2
+ import { WorktreeManager } from '../git/worktree.manager.js';
3
+ export const TaskCleanupJobStatus = {
4
+ PENDING: 'PENDING',
5
+ RUNNING: 'RUNNING',
6
+ COMPLETED: 'COMPLETED',
7
+ FAILED: 'FAILED',
8
+ };
9
+ const MAX_JOB_ATTEMPTS = 5;
10
+ const RETRY_DELAYS_MS = [
11
+ 30_000,
12
+ 2 * 60_000,
13
+ 10 * 60_000,
14
+ 30 * 60_000,
15
+ 60 * 60_000,
16
+ ];
17
+ function parseSnapshot(payload) {
18
+ const parsed = JSON.parse(payload);
19
+ if (!parsed.taskId || !parsed.projectId || !parsed.project?.repoPath || !Array.isArray(parsed.workspaces)) {
20
+ throw new Error('Invalid task cleanup snapshot');
21
+ }
22
+ return parsed;
23
+ }
24
+ export function getTaskCleanupRetryDelayMs(attempts) {
25
+ return RETRY_DELAYS_MS[Math.min(Math.max(attempts - 1, 0), RETRY_DELAYS_MS.length - 1)];
26
+ }
27
+ function toErrorMessage(error) {
28
+ return error instanceof Error ? error.message : String(error);
29
+ }
30
+ function isMissingCleanupTableError(error) {
31
+ return Boolean(error &&
32
+ typeof error === 'object' &&
33
+ 'code' in error &&
34
+ error.code === 'P2021');
35
+ }
36
+ /**
37
+ * Persistent worker for resources left behind by fast task deletion.
38
+ */
39
+ export class TaskCleanupService {
40
+ sessionManager;
41
+ running = false;
42
+ timer = null;
43
+ constructor(sessionManager) {
44
+ this.sessionManager = sessionManager;
45
+ }
46
+ start(intervalMs = 30_000) {
47
+ if (this.timer)
48
+ return;
49
+ this.timer = setInterval(() => {
50
+ this.processDueJobs().catch((error) => {
51
+ console.error('[TaskCleanupService] processDueJobs failed:', error);
52
+ });
53
+ }, intervalMs);
54
+ this.processDueJobs().catch((error) => {
55
+ console.error('[TaskCleanupService] startup cleanup failed:', error);
56
+ });
57
+ }
58
+ stop() {
59
+ if (!this.timer)
60
+ return;
61
+ clearInterval(this.timer);
62
+ this.timer = null;
63
+ }
64
+ trigger() {
65
+ this.processDueJobs().catch((error) => {
66
+ console.error('[TaskCleanupService] triggered cleanup failed:', error);
67
+ });
68
+ }
69
+ async processDueJobs(limit = 5) {
70
+ if (this.running)
71
+ return 0;
72
+ this.running = true;
73
+ try {
74
+ let processed = 0;
75
+ while (processed < limit) {
76
+ const job = await this.claimNextJob();
77
+ if (!job)
78
+ break;
79
+ await this.processJob(job.id);
80
+ processed++;
81
+ }
82
+ return processed;
83
+ }
84
+ finally {
85
+ this.running = false;
86
+ }
87
+ }
88
+ async processJob(jobId) {
89
+ const job = await prisma.taskCleanupJob.findUnique({ where: { id: jobId } });
90
+ if (!job)
91
+ return;
92
+ try {
93
+ const snapshot = parseSnapshot(job.payload);
94
+ await this.cleanupSnapshot(snapshot);
95
+ await prisma.$transaction([
96
+ prisma.task.deleteMany({ where: { id: snapshot.taskId } }),
97
+ prisma.taskCleanupJob.update({
98
+ where: { id: jobId },
99
+ data: {
100
+ status: TaskCleanupJobStatus.COMPLETED,
101
+ lastError: null,
102
+ nextRetryAt: null,
103
+ completedAt: new Date(),
104
+ },
105
+ }),
106
+ ]);
107
+ }
108
+ catch (error) {
109
+ await this.recordFailure(jobId, job.attempts, toErrorMessage(error));
110
+ }
111
+ }
112
+ async claimNextJob() {
113
+ const now = new Date();
114
+ let job;
115
+ try {
116
+ job = await prisma.taskCleanupJob.findFirst({
117
+ where: {
118
+ OR: [
119
+ { status: TaskCleanupJobStatus.PENDING },
120
+ {
121
+ status: TaskCleanupJobStatus.RUNNING,
122
+ attempts: { lt: MAX_JOB_ATTEMPTS },
123
+ },
124
+ {
125
+ status: TaskCleanupJobStatus.FAILED,
126
+ attempts: { lt: MAX_JOB_ATTEMPTS },
127
+ OR: [{ nextRetryAt: null }, { nextRetryAt: { lte: now } }],
128
+ },
129
+ ],
130
+ },
131
+ orderBy: [{ createdAt: 'asc' }],
132
+ });
133
+ }
134
+ catch (error) {
135
+ if (isMissingCleanupTableError(error)) {
136
+ return null;
137
+ }
138
+ throw error;
139
+ }
140
+ if (!job)
141
+ return null;
142
+ try {
143
+ return await prisma.taskCleanupJob.update({
144
+ where: { id: job.id },
145
+ data: {
146
+ status: TaskCleanupJobStatus.RUNNING,
147
+ attempts: { increment: 1 },
148
+ startedAt: now,
149
+ lastError: null,
150
+ },
151
+ });
152
+ }
153
+ catch {
154
+ return null;
155
+ }
156
+ }
157
+ async cleanupSnapshot(snapshot) {
158
+ const worktreeManager = new WorktreeManager(snapshot.project.repoPath);
159
+ for (const workspace of snapshot.workspaces) {
160
+ for (const session of workspace.sessions) {
161
+ try {
162
+ await this.sessionManager.stop(session.id, { skipTeamRunReconcile: true });
163
+ }
164
+ catch (error) {
165
+ console.warn(`[TaskCleanupService] failed to stop session ${session.id}:`, toErrorMessage(error));
166
+ }
167
+ }
168
+ }
169
+ for (const workspace of snapshot.workspaces) {
170
+ if (!workspace.worktreePath)
171
+ continue;
172
+ const result = await worktreeManager.remove(workspace.worktreePath);
173
+ if (result.status === 'unregistered') {
174
+ throw new Error(`Workspace ${workspace.id} path is unregistered or unsafe to remove: ${result.path}`);
175
+ }
176
+ }
177
+ for (const workspace of snapshot.workspaces) {
178
+ const result = await worktreeManager.deleteBranchIfSafe(workspace.branchName, {
179
+ protectedBranches: [snapshot.project.mainBranch, workspace.baseBranch],
180
+ });
181
+ if (result.status === 'failed') {
182
+ throw new Error(`Failed to delete branch ${result.branchName}: ${result.reason ?? 'unknown error'}`);
183
+ }
184
+ if (result.status === 'checked_out') {
185
+ throw new Error(`Branch ${result.branchName} is checked out: ${result.reason ?? 'unknown location'}`);
186
+ }
187
+ }
188
+ await worktreeManager.prune();
189
+ }
190
+ async recordFailure(jobId, attempts, message) {
191
+ const retryable = attempts < MAX_JOB_ATTEMPTS;
192
+ await prisma.taskCleanupJob.update({
193
+ where: { id: jobId },
194
+ data: {
195
+ status: TaskCleanupJobStatus.FAILED,
196
+ lastError: message,
197
+ nextRetryAt: retryable ? new Date(Date.now() + getTaskCleanupRetryDelayMs(attempts)) : null,
198
+ },
199
+ });
200
+ }
201
+ }
202
+ //# 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
  */
@@ -60,6 +62,7 @@ export declare class TaskService {
60
62
  description: string | null;
61
63
  createdAt: Date;
62
64
  updatedAt: Date;
65
+ deletedAt: Date | null;
63
66
  title: string;
64
67
  status: string;
65
68
  priority: number;
@@ -110,6 +113,7 @@ export declare class TaskService {
110
113
  description: string | null;
111
114
  createdAt: Date;
112
115
  updatedAt: Date;
116
+ deletedAt: Date | null;
113
117
  title: string;
114
118
  status: string;
115
119
  priority: number;
@@ -126,6 +130,7 @@ export declare class TaskService {
126
130
  description: string | null;
127
131
  createdAt: Date;
128
132
  updatedAt: Date;
133
+ deletedAt: Date | null;
129
134
  title: string;
130
135
  status: string;
131
136
  priority: number;
@@ -140,6 +145,7 @@ export declare class TaskService {
140
145
  description: string | null;
141
146
  createdAt: Date;
142
147
  updatedAt: Date;
148
+ deletedAt: Date | null;
143
149
  title: string;
144
150
  status: string;
145
151
  priority: number;
@@ -155,6 +161,7 @@ export declare class TaskService {
155
161
  description: string | null;
156
162
  createdAt: Date;
157
163
  updatedAt: Date;
164
+ deletedAt: Date | null;
158
165
  title: string;
159
166
  status: string;
160
167
  priority: number;
@@ -170,6 +177,7 @@ export declare class TaskService {
170
177
  description: string | null;
171
178
  createdAt: Date;
172
179
  updatedAt: Date;
180
+ deletedAt: Date | null;
173
181
  title: string;
174
182
  status: string;
175
183
  priority: number;
@@ -177,13 +185,12 @@ export declare class TaskService {
177
185
  projectId: string;
178
186
  }>;
179
187
  /**
180
- * 删除任务(增强版)
188
+ * 快速删除任务
181
189
  *
182
- * 1. 停止所有 RUNNING/PENDING 状态的 Session
183
- * 2. 清理所有关联 Workspace 的 git worktree
184
- * 3. 清理所有关联 Workspace 的本地任务分支
185
- * 4. 级联删除数据库记录
186
- * 5. 通过 EventBus 通知前端实时删除
190
+ * 1. 原子标记 Task deletedAt,使普通列表立即隐藏
191
+ * 2. 保存后台清理快照
192
+ * 3. 通过 EventBus 通知前端实时删除
193
+ * 4. 后台 worker 再停止 Session、删除 worktree/branch,并最终硬删除 Task
187
194
  */
188
195
  delete(id: string): Promise<boolean>;
189
196
  /**
@@ -210,6 +217,7 @@ export declare class TaskService {
210
217
  description: string | null;
211
218
  createdAt: Date;
212
219
  updatedAt: Date;
220
+ deletedAt: Date | null;
213
221
  title: string;
214
222
  status: string;
215
223
  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;AAM/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;IAwIvB;;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,8 @@
1
1
  import { prisma } from '../utils/index.js';
2
2
  import { TaskStatus, SessionStatus, SessionPurpose, WorkspaceStatus } from '../types/index.js';
3
3
  import { NotFoundError, ValidationError, InvalidStateTransitionError, } from '../errors.js';
4
- import { WorktreeManager } from '../git/worktree.manager.js';
5
4
  import { ensureProjectIsMutable } from './project-guards.js';
5
+ import { defaultTeamLockService } from './team-lock.service.js';
6
6
  /**
7
7
  * 合法的状态流转规则
8
8
  * 看板拖拽场景下允许任意状态互转,状态变更无危险副作用
@@ -14,6 +14,8 @@ const VALID_TRANSITIONS = {
14
14
  [TaskStatus.DONE]: [TaskStatus.TODO, TaskStatus.IN_PROGRESS, TaskStatus.IN_REVIEW, TaskStatus.CANCELLED],
15
15
  [TaskStatus.CANCELLED]: [TaskStatus.TODO, TaskStatus.IN_PROGRESS, TaskStatus.IN_REVIEW, TaskStatus.DONE],
16
16
  };
17
+ const CANCELLABLE_DELETE_WORK_REQUEST_STATUSES = ['PENDING_APPROVAL', 'QUEUED', 'STARTED'];
18
+ const CANCELLABLE_DELETE_INVOCATION_STATUSES = ['QUEUED', 'RUNNING', 'SESSION_ENDED', 'WAITING_ROOM_REPLY'];
17
19
  function normalizeTaskTitle(title) {
18
20
  const normalized = title.trim();
19
21
  if (normalized.length === 0) {
@@ -24,9 +26,11 @@ function normalizeTaskTitle(title) {
24
26
  export class TaskService {
25
27
  eventBus;
26
28
  sessionManager;
27
- constructor(eventBus, sessionManager) {
29
+ cleanupService;
30
+ constructor(eventBus, sessionManager, cleanupService) {
28
31
  this.eventBus = eventBus;
29
32
  this.sessionManager = sessionManager;
33
+ this.cleanupService = cleanupService;
30
34
  }
31
35
  /**
32
36
  * 获取项目的任务列表(支持按状态过滤和分页)
@@ -42,7 +46,7 @@ export class TaskService {
42
46
  const page = Math.max(1, params.page || 1);
43
47
  const limit = Math.min(1000, Math.max(1, params.limit || 200));
44
48
  const skip = (page - 1) * limit;
45
- const where = { projectId };
49
+ const where = { projectId, deletedAt: null };
46
50
  if (params.status) {
47
51
  where.status = params.status;
48
52
  }
@@ -84,8 +88,8 @@ export class TaskService {
84
88
  * 获取任务详情
85
89
  */
86
90
  async findById(id) {
87
- const task = await prisma.task.findUnique({
88
- where: { id },
91
+ const task = await prisma.task.findFirst({
92
+ where: { id, deletedAt: null },
89
93
  include: { workspaces: { include: { sessions: { where: { purpose: { not: SessionPurpose.COMMIT_MSG } } } } } },
90
94
  });
91
95
  if (!task) {
@@ -110,7 +114,7 @@ export class TaskService {
110
114
  ensureProjectIsMutable(project, 'create tasks');
111
115
  // 自动计算 position
112
116
  const maxPosition = await prisma.task.aggregate({
113
- where: { projectId, status: TaskStatus.TODO },
117
+ where: { projectId, status: TaskStatus.TODO, deletedAt: null },
114
118
  _max: { position: true },
115
119
  });
116
120
  return prisma.task.create({
@@ -131,8 +135,8 @@ export class TaskService {
131
135
  ...input,
132
136
  ...(input.title !== undefined ? { title: normalizeTaskTitle(input.title) } : {}),
133
137
  };
134
- const task = await prisma.task.findUnique({
135
- where: { id },
138
+ const task = await prisma.task.findFirst({
139
+ where: { id, deletedAt: null },
136
140
  include: { project: true },
137
141
  });
138
142
  if (!task) {
@@ -149,8 +153,8 @@ export class TaskService {
149
153
  * 更新后通过 EventBus 发射 task:updated 事件,通知前端实时更新
150
154
  */
151
155
  async updateStatus(id, status) {
152
- const task = await prisma.task.findUnique({
153
- where: { id },
156
+ const task = await prisma.task.findFirst({
157
+ where: { id, deletedAt: null },
154
158
  include: { project: true },
155
159
  });
156
160
  if (!task) {
@@ -169,7 +173,7 @@ export class TaskService {
169
173
  }
170
174
  // 切换状态时自动计算新列的 position
171
175
  const maxPosition = await prisma.task.aggregate({
172
- where: { projectId: task.projectId, status },
176
+ where: { projectId: task.projectId, status, deletedAt: null },
173
177
  _max: { position: true },
174
178
  });
175
179
  const updated = await prisma.task.update({
@@ -188,8 +192,8 @@ export class TaskService {
188
192
  * 如果同时传了 status,会进行状态流转并通知前端
189
193
  */
190
194
  async updatePosition(id, position, status) {
191
- const task = await prisma.task.findUnique({
192
- where: { id },
195
+ const task = await prisma.task.findFirst({
196
+ where: { id, deletedAt: null },
193
197
  include: { project: true },
194
198
  });
195
199
  if (!task) {
@@ -215,69 +219,138 @@ export class TaskService {
215
219
  return updated;
216
220
  }
217
221
  /**
218
- * 删除任务(增强版)
222
+ * 快速删除任务
219
223
  *
220
- * 1. 停止所有 RUNNING/PENDING 状态的 Session
221
- * 2. 清理所有关联 Workspace 的 git worktree
222
- * 3. 清理所有关联 Workspace 的本地任务分支
223
- * 4. 级联删除数据库记录
224
- * 5. 通过 EventBus 通知前端实时删除
224
+ * 1. 原子标记 Task deletedAt,使普通列表立即隐藏
225
+ * 2. 保存后台清理快照
226
+ * 3. 通过 EventBus 通知前端实时删除
227
+ * 4. 后台 worker 再停止 Session、删除 worktree/branch,并最终硬删除 Task
225
228
  */
226
229
  async delete(id) {
227
- const task = await prisma.task.findUnique({
230
+ const deletedAt = new Date();
231
+ const taskForGuard = await prisma.task.findUnique({
228
232
  where: { id },
229
- include: {
230
- project: true,
231
- workspaces: {
232
- include: { sessions: true },
233
- },
234
- },
233
+ include: { project: true },
235
234
  });
236
- if (!task) {
235
+ if (!taskForGuard || taskForGuard.deletedAt) {
237
236
  throw new NotFoundError('Task', id);
238
237
  }
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);
238
+ ensureProjectIsMutable(taskForGuard.project, 'delete tasks');
239
+ const marked = await prisma.task.updateMany({
240
+ where: {
241
+ id,
242
+ deletedAt: null,
243
+ project: { archivedAt: null },
244
+ },
245
+ data: { deletedAt },
246
+ });
247
+ if (marked.count === 0) {
248
+ const current = await prisma.task.findUnique({
249
+ where: { id },
250
+ include: { project: true },
251
+ });
252
+ if (!current || current.deletedAt) {
253
+ throw new NotFoundError('Task', id);
260
254
  }
255
+ ensureProjectIsMutable(current.project, 'delete tasks');
256
+ throw new NotFoundError('Task', id);
261
257
  }
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],
258
+ let deleteResult;
259
+ let cleanupJobCreated = false;
260
+ try {
261
+ const taskForCleanup = await prisma.task.findUnique({
262
+ where: { id },
263
+ include: {
264
+ project: true,
265
+ teamRun: {
266
+ include: {
267
+ invocations: {
268
+ where: { status: { in: CANCELLABLE_DELETE_INVOCATION_STATUSES } },
269
+ select: { id: true },
270
+ },
271
+ },
272
+ },
273
+ workspaces: {
274
+ include: { sessions: true },
275
+ },
276
+ },
266
277
  });
267
- if (result.status === 'failed') {
268
- console.warn(`[TaskService] Failed to delete branch ${result.branchName} for workspace ${workspace.id} during task delete:`, result.reason);
278
+ if (!taskForCleanup) {
279
+ throw new NotFoundError('Task', id);
269
280
  }
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);
281
+ ensureProjectIsMutable(taskForCleanup.project, 'delete tasks');
282
+ const snapshot = {
283
+ taskId: taskForCleanup.id,
284
+ projectId: taskForCleanup.projectId,
285
+ project: {
286
+ repoPath: taskForCleanup.project.repoPath,
287
+ mainBranch: taskForCleanup.project.mainBranch,
288
+ },
289
+ workspaces: taskForCleanup.workspaces.map((workspace) => ({
290
+ id: workspace.id,
291
+ worktreePath: workspace.worktreePath,
292
+ branchName: workspace.branchName,
293
+ baseBranch: workspace.baseBranch,
294
+ sessions: workspace.sessions
295
+ .filter((session) => session.status === SessionStatus.PENDING || session.status === SessionStatus.RUNNING)
296
+ .map((session) => ({ id: session.id })),
297
+ })),
298
+ };
299
+ const teamRunId = taskForCleanup.teamRun?.id;
300
+ const cancelledInvocationIds = taskForCleanup.teamRun?.invocations.map((invocation) => invocation.id) ?? [];
301
+ await prisma.$transaction(async (tx) => {
302
+ await tx.taskCleanupJob.create({
303
+ data: {
304
+ taskId: taskForCleanup.id,
305
+ projectId: taskForCleanup.projectId,
306
+ payload: JSON.stringify(snapshot),
307
+ },
308
+ });
309
+ if (teamRunId) {
310
+ await tx.workRequest.updateMany({
311
+ where: {
312
+ teamRunId,
313
+ status: { in: CANCELLABLE_DELETE_WORK_REQUEST_STATUSES },
314
+ },
315
+ data: { status: 'CANCELLED' },
316
+ });
317
+ await tx.agentInvocation.updateMany({
318
+ where: {
319
+ teamRunId,
320
+ status: { in: CANCELLABLE_DELETE_INVOCATION_STATUSES },
321
+ },
322
+ data: {
323
+ status: 'CANCELLED',
324
+ nextRoomReplyReminderAt: null,
325
+ },
326
+ });
327
+ }
328
+ });
329
+ cleanupJobCreated = true;
330
+ deleteResult = {
331
+ projectId: taskForCleanup.projectId,
332
+ cancelledInvocationIds,
333
+ };
334
+ }
335
+ catch (error) {
336
+ if (!cleanupJobCreated) {
337
+ await prisma.task.updateMany({
338
+ where: { id, deletedAt },
339
+ data: { deletedAt: null },
340
+ }).catch(() => {
341
+ // If rollback fails, leave the task hidden rather than masking the original error.
342
+ });
272
343
  }
344
+ throw error;
345
+ }
346
+ for (const invocationId of deleteResult.cancelledInvocationIds) {
347
+ defaultTeamLockService.releaseByOwner(invocationId);
273
348
  }
274
- // 4. 级联删除数据库记录
275
- await prisma.task.delete({ where: { id } });
276
- // 5. 通知前端
277
349
  this.eventBus.emit('task:deleted', {
278
350
  taskId: id,
279
- projectId: task.projectId,
351
+ projectId: deleteResult.projectId,
280
352
  });
353
+ this.cleanupService?.trigger();
281
354
  return true;
282
355
  }
283
356
  /**
@@ -292,7 +365,7 @@ export class TaskService {
292
365
  }
293
366
  const counts = await prisma.task.groupBy({
294
367
  by: ['status'],
295
- where: { projectId },
368
+ where: { projectId, deletedAt: null },
296
369
  _count: { id: true },
297
370
  });
298
371
  const stats = {
@@ -335,8 +408,8 @@ export class TaskService {
335
408
  * 4. 通知前端 — 用户可重新派发 Agent(会创建新 Worktree)
336
409
  */
337
410
  async retry(id) {
338
- const task = await prisma.task.findUnique({
339
- where: { id },
411
+ const task = await prisma.task.findFirst({
412
+ where: { id, deletedAt: null },
340
413
  include: {
341
414
  project: true,
342
415
  workspaces: {