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.
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +4 -0
- package/dist/app.js.map +1 -1
- package/dist/cli.js +39 -19
- package/dist/cli.js.map +1 -1
- package/dist/executors/__tests__/base.executor.test.js +133 -1
- package/dist/executors/__tests__/base.executor.test.js.map +1 -1
- package/dist/executors/__tests__/stdin-prompt.executors.test.js +18 -0
- package/dist/executors/__tests__/stdin-prompt.executors.test.js.map +1 -1
- package/dist/executors/base.executor.d.ts +8 -0
- package/dist/executors/base.executor.d.ts.map +1 -1
- package/dist/executors/base.executor.js +336 -56
- package/dist/executors/base.executor.js.map +1 -1
- package/dist/executors/index.d.ts +2 -1
- package/dist/executors/index.d.ts.map +1 -1
- package/dist/executors/index.js +1 -1
- package/dist/executors/index.js.map +1 -1
- package/dist/executors/start-error.d.ts +17 -0
- package/dist/executors/start-error.d.ts.map +1 -1
- package/dist/executors/start-error.js +36 -1
- package/dist/executors/start-error.js.map +1 -1
- package/dist/index.js +39 -29
- package/dist/index.js.map +1 -1
- package/dist/mcp/http-client.test.js +20 -0
- package/dist/mcp/http-client.test.js.map +1 -1
- package/dist/mcp/tools/__tests__/sessions.test.d.ts +2 -0
- package/dist/mcp/tools/__tests__/sessions.test.d.ts.map +1 -0
- package/dist/mcp/tools/__tests__/sessions.test.js +33 -0
- package/dist/mcp/tools/__tests__/sessions.test.js.map +1 -0
- package/dist/routes/__tests__/sessions-runtime.test.js +75 -3
- package/dist/routes/__tests__/sessions-runtime.test.js.map +1 -1
- package/dist/routes/conversations.d.ts.map +1 -1
- package/dist/routes/conversations.js +5 -1
- package/dist/routes/conversations.js.map +1 -1
- package/dist/routes/sessions.d.ts.map +1 -1
- package/dist/routes/sessions.js +13 -2
- package/dist/routes/sessions.js.map +1 -1
- package/dist/routes/team-runs.d.ts.map +1 -1
- package/dist/routes/team-runs.js +13 -1
- package/dist/routes/team-runs.js.map +1 -1
- package/dist/runtime/__tests__/acp-driver-lifecycle.test.js +86 -0
- package/dist/runtime/__tests__/acp-driver-lifecycle.test.js.map +1 -1
- package/dist/runtime/__tests__/acp-process-manager.test.js +393 -6
- package/dist/runtime/__tests__/acp-process-manager.test.js.map +1 -1
- package/dist/runtime/__tests__/cli-driver-lifecycle.test.d.ts +2 -0
- package/dist/runtime/__tests__/cli-driver-lifecycle.test.d.ts.map +1 -0
- package/dist/runtime/__tests__/cli-driver-lifecycle.test.js +244 -0
- package/dist/runtime/__tests__/cli-driver-lifecycle.test.js.map +1 -0
- package/dist/runtime/__tests__/launch-cleanup-registry.test.d.ts +2 -0
- package/dist/runtime/__tests__/launch-cleanup-registry.test.d.ts.map +1 -0
- package/dist/runtime/__tests__/launch-cleanup-registry.test.js +154 -0
- package/dist/runtime/__tests__/launch-cleanup-registry.test.js.map +1 -0
- package/dist/runtime/__tests__/runtime-coordinator.test.js +117 -1
- package/dist/runtime/__tests__/runtime-coordinator.test.js.map +1 -1
- package/dist/runtime/__tests__/server-entry-shutdown.test.d.ts +2 -0
- package/dist/runtime/__tests__/server-entry-shutdown.test.d.ts.map +1 -0
- package/dist/runtime/__tests__/server-entry-shutdown.test.js +38 -0
- package/dist/runtime/__tests__/server-entry-shutdown.test.js.map +1 -0
- package/dist/runtime/__tests__/shutdown-coordinator.test.d.ts +2 -0
- package/dist/runtime/__tests__/shutdown-coordinator.test.d.ts.map +1 -0
- package/dist/runtime/__tests__/shutdown-coordinator.test.js +28 -0
- package/dist/runtime/__tests__/shutdown-coordinator.test.js.map +1 -0
- package/dist/runtime/acp/acp-driver.d.ts.map +1 -1
- package/dist/runtime/acp/acp-driver.js +108 -35
- package/dist/runtime/acp/acp-driver.js.map +1 -1
- package/dist/runtime/acp/agents/managed-directory.d.ts.map +1 -1
- package/dist/runtime/acp/agents/managed-directory.js +3 -1
- package/dist/runtime/acp/agents/managed-directory.js.map +1 -1
- package/dist/runtime/acp/agents/managed-directory.test.d.ts +2 -0
- package/dist/runtime/acp/agents/managed-directory.test.d.ts.map +1 -0
- package/dist/runtime/acp/agents/managed-directory.test.js +25 -0
- package/dist/runtime/acp/agents/managed-directory.test.js.map +1 -0
- package/dist/runtime/acp/launch-cleanup-registry.d.ts +32 -0
- package/dist/runtime/acp/launch-cleanup-registry.d.ts.map +1 -0
- package/dist/runtime/acp/launch-cleanup-registry.js +176 -0
- package/dist/runtime/acp/launch-cleanup-registry.js.map +1 -0
- package/dist/runtime/acp/process-manager.d.ts +58 -0
- package/dist/runtime/acp/process-manager.d.ts.map +1 -1
- package/dist/runtime/acp/process-manager.js +489 -33
- package/dist/runtime/acp/process-manager.js.map +1 -1
- package/dist/runtime/cli-driver.d.ts.map +1 -1
- package/dist/runtime/cli-driver.js +230 -46
- package/dist/runtime/cli-driver.js.map +1 -1
- package/dist/runtime/contracts.d.ts +21 -2
- package/dist/runtime/contracts.d.ts.map +1 -1
- package/dist/runtime/index.d.ts +1 -0
- package/dist/runtime/index.d.ts.map +1 -1
- package/dist/runtime/index.js +1 -0
- package/dist/runtime/index.js.map +1 -1
- package/dist/runtime/runtime-coordinator.d.ts +8 -1
- package/dist/runtime/runtime-coordinator.d.ts.map +1 -1
- package/dist/runtime/runtime-coordinator.js +118 -18
- package/dist/runtime/runtime-coordinator.js.map +1 -1
- package/dist/runtime/server-entry-shutdown.d.ts +12 -0
- package/dist/runtime/server-entry-shutdown.d.ts.map +1 -0
- package/dist/runtime/server-entry-shutdown.js +23 -0
- package/dist/runtime/server-entry-shutdown.js.map +1 -0
- package/dist/runtime/shutdown-coordinator.d.ts +17 -0
- package/dist/runtime/shutdown-coordinator.d.ts.map +1 -0
- package/dist/runtime/shutdown-coordinator.js +67 -0
- package/dist/runtime/shutdown-coordinator.js.map +1 -0
- package/dist/services/__tests__/conversation.service.test.js +160 -0
- package/dist/services/__tests__/conversation.service.test.js.map +1 -1
- package/dist/services/__tests__/database-maintenance.service.test.js +97 -0
- package/dist/services/__tests__/database-maintenance.service.test.js.map +1 -1
- package/dist/services/__tests__/member-heartbeat-scheduler.test.js +6 -0
- package/dist/services/__tests__/member-heartbeat-scheduler.test.js.map +1 -1
- package/dist/services/__tests__/member-heartbeat.test.js +7 -11
- package/dist/services/__tests__/member-heartbeat.test.js.map +1 -1
- package/dist/services/__tests__/session-manager.lifecycle.test.js +402 -12
- package/dist/services/__tests__/session-manager.lifecycle.test.js.map +1 -1
- package/dist/services/__tests__/session-manager.team-run.test.js +422 -29
- package/dist/services/__tests__/session-manager.team-run.test.js.map +1 -1
- package/dist/services/__tests__/team-queue-recovery.integration.test.js +1 -0
- package/dist/services/__tests__/team-queue-recovery.integration.test.js.map +1 -1
- package/dist/services/__tests__/team-reconciler.service.test.js +1282 -4
- package/dist/services/__tests__/team-reconciler.service.test.js.map +1 -1
- package/dist/services/__tests__/team-run.service.test.js +161 -1
- package/dist/services/__tests__/team-run.service.test.js.map +1 -1
- package/dist/services/__tests__/team-scheduler.service.test.js +334 -6
- package/dist/services/__tests__/team-scheduler.service.test.js.map +1 -1
- package/dist/services/conversation.service.d.ts.map +1 -1
- package/dist/services/conversation.service.js +21 -2
- package/dist/services/conversation.service.js.map +1 -1
- package/dist/services/database-maintenance.service.d.ts.map +1 -1
- package/dist/services/database-maintenance.service.js +68 -17
- package/dist/services/database-maintenance.service.js.map +1 -1
- package/dist/services/member-heartbeat-scheduler.d.ts +6 -5
- package/dist/services/member-heartbeat-scheduler.d.ts.map +1 -1
- package/dist/services/member-heartbeat-scheduler.js +7 -5
- package/dist/services/member-heartbeat-scheduler.js.map +1 -1
- package/dist/services/session-manager.d.ts +124 -14
- package/dist/services/session-manager.d.ts.map +1 -1
- package/dist/services/session-manager.js +1035 -116
- package/dist/services/session-manager.js.map +1 -1
- package/dist/services/session-runtime-cleanup-gate.d.ts +25 -0
- package/dist/services/session-runtime-cleanup-gate.d.ts.map +1 -0
- package/dist/services/session-runtime-cleanup-gate.js +284 -0
- package/dist/services/session-runtime-cleanup-gate.js.map +1 -0
- package/dist/services/task.service.d.ts +2 -2
- package/dist/services/team-member-admission-barrier.d.ts +3 -0
- package/dist/services/team-member-admission-barrier.d.ts.map +1 -0
- package/dist/services/team-member-admission-barrier.js +26 -0
- package/dist/services/team-member-admission-barrier.js.map +1 -0
- package/dist/services/team-reconciler.service.d.ts +30 -7
- package/dist/services/team-reconciler.service.d.ts.map +1 -1
- package/dist/services/team-reconciler.service.js +528 -111
- package/dist/services/team-reconciler.service.js.map +1 -1
- package/dist/services/team-run.service.d.ts.map +1 -1
- package/dist/services/team-run.service.js +37 -13
- package/dist/services/team-run.service.js.map +1 -1
- package/dist/services/team-scheduler.service.d.ts +8 -6
- package/dist/services/team-scheduler.service.d.ts.map +1 -1
- package/dist/services/team-scheduler.service.js +323 -120
- package/dist/services/team-scheduler.service.js.map +1 -1
- package/dist/services/workspace-background-process-manager.d.ts.map +1 -1
- package/dist/services/workspace-background-process-manager.js +2 -5
- package/dist/services/workspace-background-process-manager.js.map +1 -1
- package/dist/services/workspace.service.d.ts +15 -8
- package/dist/services/workspace.service.d.ts.map +1 -1
- package/dist/socket/index.d.ts.map +1 -1
- package/dist/socket/index.js +27 -14
- package/dist/socket/index.js.map +1 -1
- package/dist/utils/__tests__/unix-process-identity.test.d.ts +2 -0
- package/dist/utils/__tests__/unix-process-identity.test.d.ts.map +1 -0
- package/dist/utils/__tests__/unix-process-identity.test.js +73 -0
- package/dist/utils/__tests__/unix-process-identity.test.js.map +1 -0
- package/dist/utils/error-log.d.ts +2 -0
- package/dist/utils/error-log.d.ts.map +1 -1
- package/dist/utils/error-log.js +40 -3
- package/dist/utils/error-log.js.map +1 -1
- package/dist/utils/error-log.test.js +14 -2
- package/dist/utils/error-log.test.js.map +1 -1
- package/dist/utils/process-launch.d.ts +2 -2
- package/dist/utils/process-launch.d.ts.map +1 -1
- package/dist/utils/process-launch.js +339 -79
- package/dist/utils/process-launch.js.map +1 -1
- package/dist/utils/process-launch.test.js +168 -10
- package/dist/utils/process-launch.test.js.map +1 -1
- package/dist/utils/spawned-process-identity.d.ts +7 -0
- package/dist/utils/spawned-process-identity.d.ts.map +1 -0
- package/dist/utils/spawned-process-identity.js +36 -0
- package/dist/utils/spawned-process-identity.js.map +1 -0
- package/dist/utils/tree-cleanup-channel.d.ts +18 -0
- package/dist/utils/tree-cleanup-channel.d.ts.map +1 -0
- package/dist/utils/tree-cleanup-channel.js +16 -0
- package/dist/utils/tree-cleanup-channel.js.map +1 -0
- package/dist/utils/tree-cleanup-channel.test.d.ts +2 -0
- package/dist/utils/tree-cleanup-channel.test.d.ts.map +1 -0
- package/dist/utils/tree-cleanup-channel.test.js +15 -0
- package/dist/utils/tree-cleanup-channel.test.js.map +1 -0
- package/dist/utils/unix-process-identity.d.ts +13 -1
- package/dist/utils/unix-process-identity.d.ts.map +1 -1
- package/dist/utils/unix-process-identity.js +146 -25
- package/dist/utils/unix-process-identity.js.map +1 -1
- package/node_modules/@agent-tower/shared/dist/types.d.ts +1 -0
- package/node_modules/@agent-tower/shared/dist/types.d.ts.map +1 -1
- package/node_modules/@agent-tower/shared/dist/types.js.map +1 -1
- package/package.json +1 -1
- package/prisma/migrations/20260824000000_add_execution_process_ownership/migration.sql +10 -0
- package/prisma/migrations/20260824010000_add_runtime_cleanup_recovery/migration.sql +10 -0
- package/prisma/migrations/20260824020000_add_runtime_launch_admission/migration.sql +72 -0
- package/prisma/migrations/20260826000000_add_conversation_turn_queue/migration.sql +19 -0
- 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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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],
|