agent-tower 0.6.0-beta.11 → 0.6.0-beta.13

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 (204) 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/cli.js +39 -19
  5. package/dist/cli.js.map +1 -1
  6. package/dist/executors/__tests__/base.executor.test.js +133 -1
  7. package/dist/executors/__tests__/base.executor.test.js.map +1 -1
  8. package/dist/executors/__tests__/stdin-prompt.executors.test.js +18 -0
  9. package/dist/executors/__tests__/stdin-prompt.executors.test.js.map +1 -1
  10. package/dist/executors/base.executor.d.ts +8 -0
  11. package/dist/executors/base.executor.d.ts.map +1 -1
  12. package/dist/executors/base.executor.js +336 -56
  13. package/dist/executors/base.executor.js.map +1 -1
  14. package/dist/executors/index.d.ts +2 -1
  15. package/dist/executors/index.d.ts.map +1 -1
  16. package/dist/executors/index.js +1 -1
  17. package/dist/executors/index.js.map +1 -1
  18. package/dist/executors/start-error.d.ts +17 -0
  19. package/dist/executors/start-error.d.ts.map +1 -1
  20. package/dist/executors/start-error.js +36 -1
  21. package/dist/executors/start-error.js.map +1 -1
  22. package/dist/index.js +39 -29
  23. package/dist/index.js.map +1 -1
  24. package/dist/mcp/http-client.test.js +20 -0
  25. package/dist/mcp/http-client.test.js.map +1 -1
  26. package/dist/mcp/tools/__tests__/sessions.test.d.ts +2 -0
  27. package/dist/mcp/tools/__tests__/sessions.test.d.ts.map +1 -0
  28. package/dist/mcp/tools/__tests__/sessions.test.js +33 -0
  29. package/dist/mcp/tools/__tests__/sessions.test.js.map +1 -0
  30. package/dist/routes/__tests__/sessions-runtime.test.js +75 -3
  31. package/dist/routes/__tests__/sessions-runtime.test.js.map +1 -1
  32. package/dist/routes/conversations.d.ts.map +1 -1
  33. package/dist/routes/conversations.js +5 -1
  34. package/dist/routes/conversations.js.map +1 -1
  35. package/dist/routes/sessions.d.ts.map +1 -1
  36. package/dist/routes/sessions.js +13 -2
  37. package/dist/routes/sessions.js.map +1 -1
  38. package/dist/routes/team-runs.d.ts.map +1 -1
  39. package/dist/routes/team-runs.js +13 -1
  40. package/dist/routes/team-runs.js.map +1 -1
  41. package/dist/runtime/__tests__/acp-driver-lifecycle.test.js +86 -0
  42. package/dist/runtime/__tests__/acp-driver-lifecycle.test.js.map +1 -1
  43. package/dist/runtime/__tests__/acp-process-manager.test.js +393 -6
  44. package/dist/runtime/__tests__/acp-process-manager.test.js.map +1 -1
  45. package/dist/runtime/__tests__/cli-driver-lifecycle.test.d.ts +2 -0
  46. package/dist/runtime/__tests__/cli-driver-lifecycle.test.d.ts.map +1 -0
  47. package/dist/runtime/__tests__/cli-driver-lifecycle.test.js +244 -0
  48. package/dist/runtime/__tests__/cli-driver-lifecycle.test.js.map +1 -0
  49. package/dist/runtime/__tests__/launch-cleanup-registry.test.d.ts +2 -0
  50. package/dist/runtime/__tests__/launch-cleanup-registry.test.d.ts.map +1 -0
  51. package/dist/runtime/__tests__/launch-cleanup-registry.test.js +154 -0
  52. package/dist/runtime/__tests__/launch-cleanup-registry.test.js.map +1 -0
  53. package/dist/runtime/__tests__/runtime-coordinator.test.js +117 -1
  54. package/dist/runtime/__tests__/runtime-coordinator.test.js.map +1 -1
  55. package/dist/runtime/__tests__/server-entry-shutdown.test.d.ts +2 -0
  56. package/dist/runtime/__tests__/server-entry-shutdown.test.d.ts.map +1 -0
  57. package/dist/runtime/__tests__/server-entry-shutdown.test.js +38 -0
  58. package/dist/runtime/__tests__/server-entry-shutdown.test.js.map +1 -0
  59. package/dist/runtime/__tests__/shutdown-coordinator.test.d.ts +2 -0
  60. package/dist/runtime/__tests__/shutdown-coordinator.test.d.ts.map +1 -0
  61. package/dist/runtime/__tests__/shutdown-coordinator.test.js +28 -0
  62. package/dist/runtime/__tests__/shutdown-coordinator.test.js.map +1 -0
  63. package/dist/runtime/acp/acp-driver.d.ts.map +1 -1
  64. package/dist/runtime/acp/acp-driver.js +108 -35
  65. package/dist/runtime/acp/acp-driver.js.map +1 -1
  66. package/dist/runtime/acp/agents/managed-directory.d.ts.map +1 -1
  67. package/dist/runtime/acp/agents/managed-directory.js +3 -1
  68. package/dist/runtime/acp/agents/managed-directory.js.map +1 -1
  69. package/dist/runtime/acp/agents/managed-directory.test.d.ts +2 -0
  70. package/dist/runtime/acp/agents/managed-directory.test.d.ts.map +1 -0
  71. package/dist/runtime/acp/agents/managed-directory.test.js +25 -0
  72. package/dist/runtime/acp/agents/managed-directory.test.js.map +1 -0
  73. package/dist/runtime/acp/launch-cleanup-registry.d.ts +32 -0
  74. package/dist/runtime/acp/launch-cleanup-registry.d.ts.map +1 -0
  75. package/dist/runtime/acp/launch-cleanup-registry.js +176 -0
  76. package/dist/runtime/acp/launch-cleanup-registry.js.map +1 -0
  77. package/dist/runtime/acp/process-manager.d.ts +58 -0
  78. package/dist/runtime/acp/process-manager.d.ts.map +1 -1
  79. package/dist/runtime/acp/process-manager.js +489 -33
  80. package/dist/runtime/acp/process-manager.js.map +1 -1
  81. package/dist/runtime/cli-driver.d.ts.map +1 -1
  82. package/dist/runtime/cli-driver.js +230 -46
  83. package/dist/runtime/cli-driver.js.map +1 -1
  84. package/dist/runtime/contracts.d.ts +21 -2
  85. package/dist/runtime/contracts.d.ts.map +1 -1
  86. package/dist/runtime/index.d.ts +1 -0
  87. package/dist/runtime/index.d.ts.map +1 -1
  88. package/dist/runtime/index.js +1 -0
  89. package/dist/runtime/index.js.map +1 -1
  90. package/dist/runtime/runtime-coordinator.d.ts +8 -1
  91. package/dist/runtime/runtime-coordinator.d.ts.map +1 -1
  92. package/dist/runtime/runtime-coordinator.js +118 -18
  93. package/dist/runtime/runtime-coordinator.js.map +1 -1
  94. package/dist/runtime/server-entry-shutdown.d.ts +12 -0
  95. package/dist/runtime/server-entry-shutdown.d.ts.map +1 -0
  96. package/dist/runtime/server-entry-shutdown.js +23 -0
  97. package/dist/runtime/server-entry-shutdown.js.map +1 -0
  98. package/dist/runtime/shutdown-coordinator.d.ts +17 -0
  99. package/dist/runtime/shutdown-coordinator.d.ts.map +1 -0
  100. package/dist/runtime/shutdown-coordinator.js +67 -0
  101. package/dist/runtime/shutdown-coordinator.js.map +1 -0
  102. package/dist/services/__tests__/conversation.service.test.js +160 -0
  103. package/dist/services/__tests__/conversation.service.test.js.map +1 -1
  104. package/dist/services/__tests__/database-maintenance.service.test.js +97 -0
  105. package/dist/services/__tests__/database-maintenance.service.test.js.map +1 -1
  106. package/dist/services/__tests__/member-heartbeat-scheduler.test.js +6 -0
  107. package/dist/services/__tests__/member-heartbeat-scheduler.test.js.map +1 -1
  108. package/dist/services/__tests__/member-heartbeat.test.js +7 -11
  109. package/dist/services/__tests__/member-heartbeat.test.js.map +1 -1
  110. package/dist/services/__tests__/session-manager.lifecycle.test.js +402 -12
  111. package/dist/services/__tests__/session-manager.lifecycle.test.js.map +1 -1
  112. package/dist/services/__tests__/session-manager.team-run.test.js +422 -29
  113. package/dist/services/__tests__/session-manager.team-run.test.js.map +1 -1
  114. package/dist/services/__tests__/team-queue-recovery.integration.test.js +1 -0
  115. package/dist/services/__tests__/team-queue-recovery.integration.test.js.map +1 -1
  116. package/dist/services/__tests__/team-reconciler.service.test.js +1282 -4
  117. package/dist/services/__tests__/team-reconciler.service.test.js.map +1 -1
  118. package/dist/services/__tests__/team-run.service.test.js +161 -1
  119. package/dist/services/__tests__/team-run.service.test.js.map +1 -1
  120. package/dist/services/__tests__/team-scheduler.service.test.js +334 -6
  121. package/dist/services/__tests__/team-scheduler.service.test.js.map +1 -1
  122. package/dist/services/conversation.service.d.ts.map +1 -1
  123. package/dist/services/conversation.service.js +21 -2
  124. package/dist/services/conversation.service.js.map +1 -1
  125. package/dist/services/database-maintenance.service.d.ts.map +1 -1
  126. package/dist/services/database-maintenance.service.js +68 -17
  127. package/dist/services/database-maintenance.service.js.map +1 -1
  128. package/dist/services/member-heartbeat-scheduler.d.ts +6 -5
  129. package/dist/services/member-heartbeat-scheduler.d.ts.map +1 -1
  130. package/dist/services/member-heartbeat-scheduler.js +7 -5
  131. package/dist/services/member-heartbeat-scheduler.js.map +1 -1
  132. package/dist/services/session-manager.d.ts +124 -14
  133. package/dist/services/session-manager.d.ts.map +1 -1
  134. package/dist/services/session-manager.js +1035 -116
  135. package/dist/services/session-manager.js.map +1 -1
  136. package/dist/services/session-runtime-cleanup-gate.d.ts +25 -0
  137. package/dist/services/session-runtime-cleanup-gate.d.ts.map +1 -0
  138. package/dist/services/session-runtime-cleanup-gate.js +284 -0
  139. package/dist/services/session-runtime-cleanup-gate.js.map +1 -0
  140. package/dist/services/task.service.d.ts +2 -2
  141. package/dist/services/team-member-admission-barrier.d.ts +3 -0
  142. package/dist/services/team-member-admission-barrier.d.ts.map +1 -0
  143. package/dist/services/team-member-admission-barrier.js +26 -0
  144. package/dist/services/team-member-admission-barrier.js.map +1 -0
  145. package/dist/services/team-reconciler.service.d.ts +30 -7
  146. package/dist/services/team-reconciler.service.d.ts.map +1 -1
  147. package/dist/services/team-reconciler.service.js +528 -111
  148. package/dist/services/team-reconciler.service.js.map +1 -1
  149. package/dist/services/team-run.service.d.ts.map +1 -1
  150. package/dist/services/team-run.service.js +37 -13
  151. package/dist/services/team-run.service.js.map +1 -1
  152. package/dist/services/team-scheduler.service.d.ts +8 -6
  153. package/dist/services/team-scheduler.service.d.ts.map +1 -1
  154. package/dist/services/team-scheduler.service.js +323 -120
  155. package/dist/services/team-scheduler.service.js.map +1 -1
  156. package/dist/services/workspace-background-process-manager.d.ts.map +1 -1
  157. package/dist/services/workspace-background-process-manager.js +2 -5
  158. package/dist/services/workspace-background-process-manager.js.map +1 -1
  159. package/dist/services/workspace.service.d.ts +15 -8
  160. package/dist/services/workspace.service.d.ts.map +1 -1
  161. package/dist/socket/index.d.ts.map +1 -1
  162. package/dist/socket/index.js +27 -14
  163. package/dist/socket/index.js.map +1 -1
  164. package/dist/utils/__tests__/unix-process-identity.test.d.ts +2 -0
  165. package/dist/utils/__tests__/unix-process-identity.test.d.ts.map +1 -0
  166. package/dist/utils/__tests__/unix-process-identity.test.js +73 -0
  167. package/dist/utils/__tests__/unix-process-identity.test.js.map +1 -0
  168. package/dist/utils/error-log.d.ts +2 -0
  169. package/dist/utils/error-log.d.ts.map +1 -1
  170. package/dist/utils/error-log.js +40 -3
  171. package/dist/utils/error-log.js.map +1 -1
  172. package/dist/utils/error-log.test.js +14 -2
  173. package/dist/utils/error-log.test.js.map +1 -1
  174. package/dist/utils/process-launch.d.ts +2 -2
  175. package/dist/utils/process-launch.d.ts.map +1 -1
  176. package/dist/utils/process-launch.js +339 -79
  177. package/dist/utils/process-launch.js.map +1 -1
  178. package/dist/utils/process-launch.test.js +168 -10
  179. package/dist/utils/process-launch.test.js.map +1 -1
  180. package/dist/utils/spawned-process-identity.d.ts +7 -0
  181. package/dist/utils/spawned-process-identity.d.ts.map +1 -0
  182. package/dist/utils/spawned-process-identity.js +36 -0
  183. package/dist/utils/spawned-process-identity.js.map +1 -0
  184. package/dist/utils/tree-cleanup-channel.d.ts +18 -0
  185. package/dist/utils/tree-cleanup-channel.d.ts.map +1 -0
  186. package/dist/utils/tree-cleanup-channel.js +16 -0
  187. package/dist/utils/tree-cleanup-channel.js.map +1 -0
  188. package/dist/utils/tree-cleanup-channel.test.d.ts +2 -0
  189. package/dist/utils/tree-cleanup-channel.test.d.ts.map +1 -0
  190. package/dist/utils/tree-cleanup-channel.test.js +15 -0
  191. package/dist/utils/tree-cleanup-channel.test.js.map +1 -0
  192. package/dist/utils/unix-process-identity.d.ts +13 -1
  193. package/dist/utils/unix-process-identity.d.ts.map +1 -1
  194. package/dist/utils/unix-process-identity.js +146 -25
  195. package/dist/utils/unix-process-identity.js.map +1 -1
  196. package/node_modules/@agent-tower/shared/dist/types.d.ts +1 -0
  197. package/node_modules/@agent-tower/shared/dist/types.d.ts.map +1 -1
  198. package/node_modules/@agent-tower/shared/dist/types.js.map +1 -1
  199. package/package.json +1 -1
  200. package/prisma/migrations/20260824000000_add_execution_process_ownership/migration.sql +10 -0
  201. package/prisma/migrations/20260824010000_add_runtime_cleanup_recovery/migration.sql +10 -0
  202. package/prisma/migrations/20260824020000_add_runtime_launch_admission/migration.sql +72 -0
  203. package/prisma/migrations/20260826000000_add_conversation_turn_queue/migration.sql +19 -0
  204. package/prisma/schema.prisma +50 -7
@@ -16,6 +16,7 @@ const __dirname = path.dirname(__filename);
16
16
  const serverRoot = path.resolve(__dirname, '../../..');
17
17
  const schemaPath = path.join(serverRoot, 'prisma/schema.prisma');
18
18
  let TeamSchedulerService;
19
+ let TeamRunService;
19
20
  let prisma;
20
21
  let CommandBuildError;
21
22
  let ExecutorNotFoundError;
@@ -206,7 +207,7 @@ function createDeferred() {
206
207
  }
207
208
  async function waitForCondition(predicate, timeoutMs = 1000) {
208
209
  const startedAt = Date.now();
209
- while (!predicate()) {
210
+ while (!(await predicate())) {
210
211
  if (Date.now() - startedAt > timeoutMs) {
211
212
  throw new Error('Timed out waiting for condition');
212
213
  }
@@ -223,10 +224,12 @@ describe('TeamSchedulerService', () => {
223
224
  stdio: 'pipe',
224
225
  });
225
226
  const serviceModule = await import('../team-scheduler.service.js');
227
+ const teamRunServiceModule = await import('../team-run.service.js');
226
228
  const utilsModule = await import('../../utils/index.js');
227
229
  const commandBuilderModule = await import('../../executors/command-builder.js');
228
230
  const startErrorModule = await import('../../executors/start-error.js');
229
231
  TeamSchedulerService = serviceModule.TeamSchedulerService;
232
+ TeamRunService = teamRunServiceModule.TeamRunService;
230
233
  prisma = utilsModule.prisma;
231
234
  CommandBuildError = commandBuilderModule.CommandBuildError;
232
235
  ExecutorNotFoundError = startErrorModule.ExecutorNotFoundError;
@@ -729,6 +732,147 @@ describe('TeamSchedulerService', () => {
729
732
  status: 'QUEUED',
730
733
  });
731
734
  });
735
+ it('does not admit queued work while a terminal invocation still has failed process cleanup', async () => {
736
+ const { teamRun, workspace, members } = await createTeamRunFixture();
737
+ const previousRequest = await createWorkRequest({
738
+ teamRunId: teamRun.id,
739
+ targetMemberId: members[0].id,
740
+ status: 'COMPLETED',
741
+ });
742
+ const previousSession = await prisma.session.create({
743
+ data: {
744
+ workspaceId: workspace.id,
745
+ agentType: AgentType.CODEX,
746
+ providerId: members[0].providerId,
747
+ prompt: 'previous',
748
+ status: 'COMPLETED',
749
+ },
750
+ });
751
+ await prisma.agentInvocation.create({
752
+ data: {
753
+ teamRunId: teamRun.id,
754
+ workRequestId: previousRequest.id,
755
+ memberId: members[0].id,
756
+ sessionId: previousSession.id,
757
+ status: 'COMPLETED',
758
+ },
759
+ });
760
+ await prisma.executionProcess.create({
761
+ data: {
762
+ sessionId: previousSession.id,
763
+ runtimeInstanceId: 'runtime-failed-cleanup',
764
+ cleanupState: 'FAILED',
765
+ },
766
+ });
767
+ const request = await createWorkRequest({
768
+ teamRunId: teamRun.id,
769
+ targetMemberId: members[0].id,
770
+ });
771
+ const sessionManager = createSessionManagerMock();
772
+ service = new TeamSchedulerService(lockService, {
773
+ sessionManager,
774
+ getProviderById: createProviderLookup(),
775
+ });
776
+ await expect(service.startNextSessions(teamRun.id)).resolves.toEqual([]);
777
+ expect(sessionManager.create).not.toHaveBeenCalled();
778
+ await expect(prisma.workRequest.findUnique({ where: { id: request.id } })).resolves.toMatchObject({
779
+ status: 'QUEUED',
780
+ });
781
+ });
782
+ it('quarantines a claimed launch with no process row and keeps queue admission blocked', async () => {
783
+ const { teamRun, workspace, members } = await createTeamRunFixture();
784
+ const previousRequest = await createWorkRequest({
785
+ teamRunId: teamRun.id,
786
+ targetMemberId: members[0].id,
787
+ status: 'COMPLETED',
788
+ });
789
+ const previousSession = await prisma.session.create({
790
+ data: {
791
+ workspaceId: workspace.id,
792
+ agentType: AgentType.CODEX,
793
+ providerId: members[0].providerId,
794
+ prompt: 'claimed before spawn',
795
+ status: 'RUNNING',
796
+ runtimeLaunchState: 'CLAIMED',
797
+ runtimeLaunchClaimCount: 1,
798
+ runtimeLaunchResolvedCount: 0,
799
+ },
800
+ });
801
+ await prisma.agentInvocation.create({
802
+ data: {
803
+ teamRunId: teamRun.id,
804
+ workRequestId: previousRequest.id,
805
+ memberId: members[0].id,
806
+ sessionId: previousSession.id,
807
+ status: 'COMPLETED',
808
+ },
809
+ });
810
+ const request = await createWorkRequest({
811
+ teamRunId: teamRun.id,
812
+ targetMemberId: members[0].id,
813
+ });
814
+ const sessionManager = createSessionManagerMock();
815
+ service = new TeamSchedulerService(lockService, {
816
+ sessionManager,
817
+ getProviderById: createProviderLookup(),
818
+ });
819
+ await expect(service.startNextSessions(teamRun.id)).resolves.toEqual([]);
820
+ await expect(prisma.workRequest.findUnique({ where: { id: request.id } })).resolves.toMatchObject({
821
+ status: 'QUEUED',
822
+ });
823
+ await expect(prisma.session.findUnique({ where: { id: previousSession.id } })).resolves.toMatchObject({
824
+ runtimeLaunchState: 'QUARANTINED',
825
+ runtimeLaunchClaimCount: 1,
826
+ runtimeLaunchResolvedCount: 0,
827
+ runtimeLaunchDiagnostic: expect.stringContaining('no durable resolution'),
828
+ });
829
+ expect(sessionManager.create).not.toHaveBeenCalled();
830
+ });
831
+ it('admits queued work after a terminal invocation with a proven pre-child launch failure', async () => {
832
+ const { teamRun, workspace, members } = await createTeamRunFixture();
833
+ const previousRequest = await createWorkRequest({
834
+ teamRunId: teamRun.id,
835
+ targetMemberId: members[0].id,
836
+ status: 'FAILED',
837
+ });
838
+ const previousSession = await prisma.session.create({
839
+ data: {
840
+ workspaceId: workspace.id,
841
+ agentType: AgentType.CODEX,
842
+ providerId: members[0].providerId,
843
+ prompt: 'known pre-child failure',
844
+ status: 'CANCELLED',
845
+ runtimeLaunchState: 'SAFE_PRE_CHILD_FAILURE',
846
+ runtimeLaunchClaimCount: 1,
847
+ runtimeLaunchResolvedCount: 1,
848
+ },
849
+ });
850
+ await prisma.agentInvocation.create({
851
+ data: {
852
+ teamRunId: teamRun.id,
853
+ workRequestId: previousRequest.id,
854
+ memberId: members[0].id,
855
+ sessionId: previousSession.id,
856
+ status: 'FAILED',
857
+ },
858
+ });
859
+ const request = await createWorkRequest({
860
+ teamRunId: teamRun.id,
861
+ targetMemberId: members[0].id,
862
+ });
863
+ const sessionManager = createSessionManagerMock();
864
+ service = new TeamSchedulerService(lockService, {
865
+ workspaceService: createWorkspaceServiceMock(),
866
+ sessionManager,
867
+ getProviderById: createProviderLookup(),
868
+ });
869
+ const started = await service.startNextSessions(teamRun.id);
870
+ await expect(prisma.workRequest.findUnique({ where: { id: request.id } })).resolves.toMatchObject({
871
+ status: 'STARTED',
872
+ });
873
+ expect(started).toHaveLength(1);
874
+ expect(started[0]).toMatchObject({ workRequestId: request.id, status: 'RUNNING' });
875
+ });
732
876
  it('starts only one queued WorkRequest for the same member in a single batch', async () => {
733
877
  const { teamRun, members } = await createTeamRunFixture();
734
878
  const first = await createWorkRequest({
@@ -1590,7 +1734,7 @@ describe('TeamSchedulerService', () => {
1590
1734
  const previousRequest = await createWorkRequest({
1591
1735
  teamRunId: teamRun.id,
1592
1736
  targetMemberId: members[0].id,
1593
- status: 'STARTED',
1737
+ status: 'COMPLETED',
1594
1738
  instruction: 'Previous work',
1595
1739
  });
1596
1740
  const previousSession = await prisma.session.create({
@@ -1654,7 +1798,7 @@ describe('TeamSchedulerService', () => {
1654
1798
  const previousRequest = await createWorkRequest({
1655
1799
  teamRunId: teamRun.id,
1656
1800
  targetMemberId: members[0].id,
1657
- status: 'STARTED',
1801
+ status: 'COMPLETED',
1658
1802
  target: {
1659
1803
  targetKind: 'WORKSPACE_COMMIT',
1660
1804
  targetPurpose: 'REVIEW',
@@ -1734,7 +1878,7 @@ describe('TeamSchedulerService', () => {
1734
1878
  const previousRequest = await createWorkRequest({
1735
1879
  teamRunId: teamRun.id,
1736
1880
  targetMemberId: members[0].id,
1737
- status: 'STARTED',
1881
+ status: 'COMPLETED',
1738
1882
  instruction: 'Previous work',
1739
1883
  });
1740
1884
  const previousSession = await prisma.session.create({
@@ -1848,7 +1992,7 @@ describe('TeamSchedulerService', () => {
1848
1992
  const previousRequest = await createWorkRequest({
1849
1993
  teamRunId: teamRun.id,
1850
1994
  targetMemberId: members[0].id,
1851
- status: 'STARTED',
1995
+ status: 'COMPLETED',
1852
1996
  instruction: 'Previous work in another workspace',
1853
1997
  });
1854
1998
  const otherWorkspaceSession = await prisma.session.create({
@@ -1906,6 +2050,10 @@ describe('TeamSchedulerService', () => {
1906
2050
  where: { id: firstRun[0].id },
1907
2051
  data: { status: 'COMPLETED' },
1908
2052
  });
2053
+ await prisma.workRequest.update({
2054
+ where: { id: firstRun[0].workRequestId },
2055
+ data: { status: 'COMPLETED' },
2056
+ });
1909
2057
  const followUpRequest = await createWorkRequest({
1910
2058
  teamRunId: teamRun.id,
1911
2059
  targetMemberId: members[0].id,
@@ -2072,7 +2220,7 @@ describe('TeamSchedulerService', () => {
2072
2220
  getProviderById: createProviderLookup(),
2073
2221
  });
2074
2222
  const result = await service.stopMemberWork(teamRun.id, members[0].id);
2075
- expect(sessionManager.stop).toHaveBeenCalledWith(session.id);
2223
+ expect(sessionManager.stop).toHaveBeenCalledWith(session.id, { skipTeamRunReconcile: true });
2076
2224
  expect(result.stoppedSessionIds).toEqual([session.id]);
2077
2225
  expect(result.cancelledInvocationIds).toEqual([]);
2078
2226
  expect(result.cancelledWorkRequestIds).toEqual([]);
@@ -2089,10 +2237,190 @@ describe('TeamSchedulerService', () => {
2089
2237
  status: 'CANCELLED',
2090
2238
  nextRoomReplyReminderAt: null,
2091
2239
  });
2240
+ await expect(prisma.workRequest.findUnique({ where: { id: activeRequest.id } })).resolves.toMatchObject({
2241
+ status: 'CANCELLED',
2242
+ });
2092
2243
  expect(lockService.listLocks()).toEqual([
2093
2244
  { key: `workspace:task:${task.id}:write`, ownerId: result.startedInvocations[0].id },
2094
2245
  ]);
2095
2246
  });
2247
+ it('revokes public and private dispatch while stop waits for process-tree cleanup', async () => {
2248
+ const { workspace, teamRun, members } = await createTeamRunFixture({
2249
+ memberCapabilities: [readOnlyCapabilities, readOnlyCapabilities],
2250
+ });
2251
+ const activeRequest = await createWorkRequest({
2252
+ teamRunId: teamRun.id,
2253
+ targetMemberId: members[0].id,
2254
+ status: 'STARTED',
2255
+ });
2256
+ const session = await prisma.session.create({
2257
+ data: {
2258
+ workspaceId: workspace.id,
2259
+ agentType: AgentType.CODEX,
2260
+ providerId: members[0].providerId,
2261
+ prompt: 'Active work',
2262
+ status: 'RUNNING',
2263
+ },
2264
+ });
2265
+ const invocation = await prisma.agentInvocation.create({
2266
+ data: {
2267
+ teamRunId: teamRun.id,
2268
+ workRequestId: activeRequest.id,
2269
+ memberId: members[0].id,
2270
+ workspaceId: workspace.id,
2271
+ sessionId: session.id,
2272
+ status: 'RUNNING',
2273
+ },
2274
+ });
2275
+ const stopEntered = createDeferred();
2276
+ const cleanup = createDeferred();
2277
+ const sessionManager = createSessionManagerMock();
2278
+ sessionManager.stop.mockImplementation(async () => {
2279
+ stopEntered.resolve();
2280
+ await cleanup.promise;
2281
+ return prisma.session.update({ where: { id: session.id }, data: { status: 'CANCELLED' } });
2282
+ });
2283
+ service = new TeamSchedulerService(lockService, {
2284
+ workspaceService: createWorkspaceServiceMock(),
2285
+ sessionManager,
2286
+ getProviderById: createProviderLookup(),
2287
+ });
2288
+ const stopping = service.stopMemberWork(teamRun.id, members[0].id, { cancelQueued: true });
2289
+ await stopEntered.promise;
2290
+ await expect(prisma.agentInvocation.findUnique({ where: { id: invocation.id } })).resolves.toMatchObject({
2291
+ status: 'RUNNING',
2292
+ dispatchRevokedAt: expect.any(Date),
2293
+ });
2294
+ const roomService = new TeamRunService();
2295
+ const publicMessage = await roomService.createRoomMessage(teamRun.id, {
2296
+ content: 'Late public handoff',
2297
+ senderType: 'agent',
2298
+ senderId: members[0].id,
2299
+ senderInvocationId: invocation.id,
2300
+ mentions: [{ memberId: members[1].id }],
2301
+ });
2302
+ const privateMessage = await roomService.createPrivateRoomMessage(teamRun.id, {
2303
+ content: 'Late private handoff',
2304
+ senderType: 'agent',
2305
+ senderId: members[0].id,
2306
+ senderInvocationId: invocation.id,
2307
+ recipientMemberIds: [members[1].id],
2308
+ });
2309
+ expect(publicMessage.workRequestIds).toEqual([]);
2310
+ expect(privateMessage.workRequestIds).toEqual([]);
2311
+ await expect(prisma.workRequest.count({ where: { teamRunId: teamRun.id } })).resolves.toBe(1);
2312
+ cleanup.resolve();
2313
+ await stopping;
2314
+ await expect(prisma.workRequest.count({ where: { teamRunId: teamRun.id } })).resolves.toBe(1);
2315
+ });
2316
+ it('serializes direct TeamRun session stop against concurrent queue admission', async () => {
2317
+ const { workspace, teamRun, members } = await createTeamRunFixture({
2318
+ memberCapabilities: [readOnlyCapabilities],
2319
+ });
2320
+ const activeRequest = await createWorkRequest({
2321
+ teamRunId: teamRun.id,
2322
+ targetMemberId: members[0].id,
2323
+ status: 'STARTED',
2324
+ });
2325
+ const activeSession = await prisma.session.create({
2326
+ data: {
2327
+ workspaceId: workspace.id,
2328
+ agentType: AgentType.CODEX,
2329
+ providerId: members[0].providerId,
2330
+ prompt: 'Active work',
2331
+ status: 'RUNNING',
2332
+ },
2333
+ });
2334
+ const activeInvocation = await prisma.agentInvocation.create({
2335
+ data: {
2336
+ teamRunId: teamRun.id,
2337
+ workRequestId: activeRequest.id,
2338
+ memberId: members[0].id,
2339
+ workspaceId: workspace.id,
2340
+ sessionId: activeSession.id,
2341
+ status: 'RUNNING',
2342
+ },
2343
+ });
2344
+ const nextRequest = await createWorkRequest({
2345
+ teamRunId: teamRun.id,
2346
+ targetMemberId: members[0].id,
2347
+ status: 'QUEUED',
2348
+ });
2349
+ const stopEntered = createDeferred();
2350
+ const allowStop = createDeferred();
2351
+ const sessionManager = createSessionManagerMock();
2352
+ sessionManager.stop.mockImplementation(async () => {
2353
+ stopEntered.resolve();
2354
+ await allowStop.promise;
2355
+ return prisma.session.update({ where: { id: activeSession.id }, data: { status: 'CANCELLED' } });
2356
+ });
2357
+ service = new TeamSchedulerService(lockService, {
2358
+ workspaceService: createWorkspaceServiceMock(),
2359
+ sessionManager,
2360
+ getProviderById: createProviderLookup(),
2361
+ });
2362
+ const directStop = service.stopSession(activeSession.id);
2363
+ await stopEntered.promise;
2364
+ const concurrentPump = service.startNextSessions(teamRun.id);
2365
+ expect(sessionManager.start).not.toHaveBeenCalled();
2366
+ allowStop.resolve();
2367
+ await Promise.all([directStop, concurrentPump]);
2368
+ expect(sessionManager.start).toHaveBeenCalledTimes(1);
2369
+ await expect(prisma.agentInvocation.findUnique({ where: { id: activeInvocation.id } })).resolves.toMatchObject({
2370
+ status: 'CANCELLED',
2371
+ dispatchRevokedAt: expect.any(Date),
2372
+ });
2373
+ await expect(prisma.workRequest.findUnique({ where: { id: nextRequest.id } })).resolves.toMatchObject({
2374
+ status: 'STARTED',
2375
+ });
2376
+ });
2377
+ it('does not spawn when direct stop wins after Session creation but before Invocation creation', async () => {
2378
+ const { workspace, teamRun, members } = await createTeamRunFixture({
2379
+ memberCapabilities: [readOnlyCapabilities],
2380
+ });
2381
+ const request = await createWorkRequest({
2382
+ teamRunId: teamRun.id,
2383
+ targetMemberId: members[0].id,
2384
+ status: 'QUEUED',
2385
+ });
2386
+ const sessionCreated = createDeferred();
2387
+ const allowInvocation = createDeferred();
2388
+ const sessionManager = createSessionManagerMock();
2389
+ sessionManager.create.mockImplementation(async () => {
2390
+ const session = await prisma.session.create({
2391
+ data: {
2392
+ workspaceId: workspace.id,
2393
+ agentType: AgentType.CODEX,
2394
+ providerId: members[0].providerId,
2395
+ prompt: 'queued work',
2396
+ status: 'PENDING',
2397
+ },
2398
+ });
2399
+ sessionCreated.resolve(session);
2400
+ await allowInvocation.promise;
2401
+ return session;
2402
+ });
2403
+ service = new TeamSchedulerService(lockService, {
2404
+ workspaceService: createWorkspaceServiceMock(),
2405
+ sessionManager,
2406
+ getProviderById: createProviderLookup(),
2407
+ });
2408
+ const starting = service.startNextSessions(teamRun.id);
2409
+ const session = await sessionCreated.promise;
2410
+ const stopping = service.stopSession(session.id);
2411
+ await waitForCondition(() => sessionManager.stop.mock.calls.length === 1);
2412
+ await waitForCondition(async () => ((await prisma.session.findUnique({ where: { id: session.id } }))?.status === 'CANCELLED'));
2413
+ await expect(prisma.session.findUnique({ where: { id: session.id } })).resolves.toMatchObject({
2414
+ status: 'CANCELLED',
2415
+ });
2416
+ allowInvocation.resolve();
2417
+ await Promise.all([starting, stopping]);
2418
+ expect(sessionManager.start).not.toHaveBeenCalled();
2419
+ await expect(prisma.agentInvocation.count({ where: { sessionId: session.id } })).resolves.toBe(0);
2420
+ await expect(prisma.workRequest.findUnique({ where: { id: request.id } })).resolves.toMatchObject({
2421
+ status: 'CANCELLED',
2422
+ });
2423
+ });
2096
2424
  it('does not start queued work when stopping a member with no active invocation and no queue cancellation', async () => {
2097
2425
  const { teamRun, members } = await createTeamRunFixture({
2098
2426
  memberCapabilities: [readOnlyCapabilities, readOnlyCapabilities],