@overlordai/server 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/database/migrations/001-init-schema.sql +226 -0
- package/database/migrations/002-add-indexes.sql +17 -0
- package/database/migrations/003-add-settings-table.sql +4 -0
- package/database/migrations/004-add-developer-id-index.sql +5 -0
- package/dist/adapters/adapter.interface.d.ts +41 -0
- package/dist/adapters/adapter.interface.d.ts.map +1 -0
- package/dist/adapters/adapter.interface.js +6 -0
- package/dist/adapters/adapter.interface.js.map +1 -0
- package/dist/adapters/adapter.module.d.ts +3 -0
- package/dist/adapters/adapter.module.d.ts.map +1 -0
- package/dist/adapters/adapter.module.js +54 -0
- package/dist/adapters/adapter.module.js.map +1 -0
- package/dist/adapters/adapter.registry.d.ts +19 -0
- package/dist/adapters/adapter.registry.d.ts.map +1 -0
- package/dist/adapters/adapter.registry.js +51 -0
- package/dist/adapters/adapter.registry.js.map +1 -0
- package/dist/adapters/lark/lark-card.builder.d.ts +48 -0
- package/dist/adapters/lark/lark-card.builder.d.ts.map +1 -0
- package/dist/adapters/lark/lark-card.builder.js +259 -0
- package/dist/adapters/lark/lark-card.builder.js.map +1 -0
- package/dist/adapters/lark/lark-message.parser.d.ts +51 -0
- package/dist/adapters/lark/lark-message.parser.d.ts.map +1 -0
- package/dist/adapters/lark/lark-message.parser.js +189 -0
- package/dist/adapters/lark/lark-message.parser.js.map +1 -0
- package/dist/adapters/lark/lark-signature.d.ts +13 -0
- package/dist/adapters/lark/lark-signature.d.ts.map +1 -0
- package/dist/adapters/lark/lark-signature.js +58 -0
- package/dist/adapters/lark/lark-signature.js.map +1 -0
- package/dist/adapters/lark/lark.adapter.d.ts +65 -0
- package/dist/adapters/lark/lark.adapter.d.ts.map +1 -0
- package/dist/adapters/lark/lark.adapter.js +565 -0
- package/dist/adapters/lark/lark.adapter.js.map +1 -0
- package/dist/adapters/lark/lark.controller.d.ts +21 -0
- package/dist/adapters/lark/lark.controller.d.ts.map +1 -0
- package/dist/adapters/lark/lark.controller.js +120 -0
- package/dist/adapters/lark/lark.controller.js.map +1 -0
- package/dist/adapters/slack/slack.adapter.d.ts +19 -0
- package/dist/adapters/slack/slack.adapter.d.ts.map +1 -0
- package/dist/adapters/slack/slack.adapter.js +42 -0
- package/dist/adapters/slack/slack.adapter.js.map +1 -0
- package/dist/app.module.d.ts +5 -0
- package/dist/app.module.d.ts.map +1 -0
- package/dist/app.module.js +48 -0
- package/dist/app.module.js.map +1 -0
- package/dist/auth/auth.controller.d.ts +15 -0
- package/dist/auth/auth.controller.d.ts.map +1 -0
- package/dist/auth/auth.controller.js +67 -0
- package/dist/auth/auth.controller.js.map +1 -0
- package/dist/auth/auth.module.d.ts +3 -0
- package/dist/auth/auth.module.d.ts.map +1 -0
- package/dist/auth/auth.module.js +46 -0
- package/dist/auth/auth.module.js.map +1 -0
- package/dist/auth/auth.service.d.ts +62 -0
- package/dist/auth/auth.service.d.ts.map +1 -0
- package/dist/auth/auth.service.js +307 -0
- package/dist/auth/auth.service.js.map +1 -0
- package/dist/auth/decorators/allow-totp-setup.decorator.d.ts +3 -0
- package/dist/auth/decorators/allow-totp-setup.decorator.d.ts.map +1 -0
- package/dist/auth/decorators/allow-totp-setup.decorator.js +8 -0
- package/dist/auth/decorators/allow-totp-setup.decorator.js.map +1 -0
- package/dist/auth/decorators/project-roles.decorator.d.ts +4 -0
- package/dist/auth/decorators/project-roles.decorator.d.ts.map +1 -0
- package/dist/auth/decorators/project-roles.decorator.js +8 -0
- package/dist/auth/decorators/project-roles.decorator.js.map +1 -0
- package/dist/auth/decorators/roles.decorator.d.ts +4 -0
- package/dist/auth/decorators/roles.decorator.d.ts.map +1 -0
- package/dist/auth/decorators/roles.decorator.js +8 -0
- package/dist/auth/decorators/roles.decorator.js.map +1 -0
- package/dist/auth/extract-user.middleware.d.ts +21 -0
- package/dist/auth/extract-user.middleware.d.ts.map +1 -0
- package/dist/auth/extract-user.middleware.js +57 -0
- package/dist/auth/extract-user.middleware.js.map +1 -0
- package/dist/auth/guards/jwt-auth.guard.d.ts +14 -0
- package/dist/auth/guards/jwt-auth.guard.d.ts.map +1 -0
- package/dist/auth/guards/jwt-auth.guard.js +139 -0
- package/dist/auth/guards/jwt-auth.guard.js.map +1 -0
- package/dist/auth/guards/project-role.guard.d.ts +10 -0
- package/dist/auth/guards/project-role.guard.d.ts.map +1 -0
- package/dist/auth/guards/project-role.guard.js +72 -0
- package/dist/auth/guards/project-role.guard.js.map +1 -0
- package/dist/auth/guards/roles.guard.d.ts +8 -0
- package/dist/auth/guards/roles.guard.d.ts.map +1 -0
- package/dist/auth/guards/roles.guard.js +56 -0
- package/dist/auth/guards/roles.guard.js.map +1 -0
- package/dist/auth/jwt.strategy.d.ts +23 -0
- package/dist/auth/jwt.strategy.d.ts.map +1 -0
- package/dist/auth/jwt.strategy.js +49 -0
- package/dist/auth/jwt.strategy.js.map +1 -0
- package/dist/common/crypto.service.d.ts +31 -0
- package/dist/common/crypto.service.d.ts.map +1 -0
- package/dist/common/crypto.service.js +120 -0
- package/dist/common/crypto.service.js.map +1 -0
- package/dist/common/error-filter.d.ts +6 -0
- package/dist/common/error-filter.d.ts.map +1 -0
- package/dist/common/error-filter.js +78 -0
- package/dist/common/error-filter.js.map +1 -0
- package/dist/common/health.controller.d.ts +13 -0
- package/dist/common/health.controller.d.ts.map +1 -0
- package/dist/common/health.controller.js +75 -0
- package/dist/common/health.controller.js.map +1 -0
- package/dist/common/logger.service.d.ts +11 -0
- package/dist/common/logger.service.d.ts.map +1 -0
- package/dist/common/logger.service.js +48 -0
- package/dist/common/logger.service.js.map +1 -0
- package/dist/common/pagination.d.ts +18 -0
- package/dist/common/pagination.d.ts.map +1 -0
- package/dist/common/pagination.js +39 -0
- package/dist/common/pagination.js.map +1 -0
- package/dist/common/rate-limit.guard.d.ts +48 -0
- package/dist/common/rate-limit.guard.d.ts.map +1 -0
- package/dist/common/rate-limit.guard.js +129 -0
- package/dist/common/rate-limit.guard.js.map +1 -0
- package/dist/common/sensitive-filter.d.ts +7 -0
- package/dist/common/sensitive-filter.d.ts.map +1 -0
- package/dist/common/sensitive-filter.js +20 -0
- package/dist/common/sensitive-filter.js.map +1 -0
- package/dist/database/database.module.d.ts +3 -0
- package/dist/database/database.module.d.ts.map +1 -0
- package/dist/database/database.module.js +22 -0
- package/dist/database/database.module.js.map +1 -0
- package/dist/database/database.service.d.ts +13 -0
- package/dist/database/database.service.d.ts.map +1 -0
- package/dist/database/database.service.js +107 -0
- package/dist/database/database.service.js.map +1 -0
- package/dist/database/migration-runner.d.ts +5 -0
- package/dist/database/migration-runner.d.ts.map +1 -0
- package/dist/database/migration-runner.js +86 -0
- package/dist/database/migration-runner.js.map +1 -0
- package/dist/database/repositories/audit-log.repository.d.ts +29 -0
- package/dist/database/repositories/audit-log.repository.d.ts.map +1 -0
- package/dist/database/repositories/audit-log.repository.js +80 -0
- package/dist/database/repositories/audit-log.repository.js.map +1 -0
- package/dist/database/repositories/bot.repository.d.ts +67 -0
- package/dist/database/repositories/bot.repository.d.ts.map +1 -0
- package/dist/database/repositories/bot.repository.js +133 -0
- package/dist/database/repositories/bot.repository.js.map +1 -0
- package/dist/database/repositories/developer-token.repository.d.ts +40 -0
- package/dist/database/repositories/developer-token.repository.d.ts.map +1 -0
- package/dist/database/repositories/developer-token.repository.js +84 -0
- package/dist/database/repositories/developer-token.repository.js.map +1 -0
- package/dist/database/repositories/developer.repository.d.ts +25 -0
- package/dist/database/repositories/developer.repository.d.ts.map +1 -0
- package/dist/database/repositories/developer.repository.js +139 -0
- package/dist/database/repositories/developer.repository.js.map +1 -0
- package/dist/database/repositories/machine.repository.d.ts +39 -0
- package/dist/database/repositories/machine.repository.d.ts.map +1 -0
- package/dist/database/repositories/machine.repository.js +176 -0
- package/dist/database/repositories/machine.repository.js.map +1 -0
- package/dist/database/repositories/notification.repository.d.ts +19 -0
- package/dist/database/repositories/notification.repository.d.ts.map +1 -0
- package/dist/database/repositories/notification.repository.js +94 -0
- package/dist/database/repositories/notification.repository.js.map +1 -0
- package/dist/database/repositories/project-member.repository.d.ts +30 -0
- package/dist/database/repositories/project-member.repository.d.ts.map +1 -0
- package/dist/database/repositories/project-member.repository.js +75 -0
- package/dist/database/repositories/project-member.repository.js.map +1 -0
- package/dist/database/repositories/project.repository.d.ts +24 -0
- package/dist/database/repositories/project.repository.d.ts.map +1 -0
- package/dist/database/repositories/project.repository.js +154 -0
- package/dist/database/repositories/project.repository.js.map +1 -0
- package/dist/database/repositories/session.repository.d.ts +19 -0
- package/dist/database/repositories/session.repository.d.ts.map +1 -0
- package/dist/database/repositories/session.repository.js +117 -0
- package/dist/database/repositories/session.repository.js.map +1 -0
- package/dist/database/repositories/task.repository.d.ts +37 -0
- package/dist/database/repositories/task.repository.d.ts.map +1 -0
- package/dist/database/repositories/task.repository.js +229 -0
- package/dist/database/repositories/task.repository.js.map +1 -0
- package/dist/database/repositories/worker-token.repository.d.ts +20 -0
- package/dist/database/repositories/worker-token.repository.d.ts.map +1 -0
- package/dist/database/repositories/worker-token.repository.js +94 -0
- package/dist/database/repositories/worker-token.repository.js.map +1 -0
- package/dist/database/repositories/workspace.repository.d.ts +19 -0
- package/dist/database/repositories/workspace.repository.d.ts.map +1 -0
- package/dist/database/repositories/workspace.repository.js +82 -0
- package/dist/database/repositories/workspace.repository.js.map +1 -0
- package/dist/dispatcher/capability.service.d.ts +50 -0
- package/dist/dispatcher/capability.service.d.ts.map +1 -0
- package/dist/dispatcher/capability.service.js +159 -0
- package/dist/dispatcher/capability.service.js.map +1 -0
- package/dist/dispatcher/cleanup.service.d.ts +23 -0
- package/dist/dispatcher/cleanup.service.d.ts.map +1 -0
- package/dist/dispatcher/cleanup.service.js +107 -0
- package/dist/dispatcher/cleanup.service.js.map +1 -0
- package/dist/dispatcher/dedup.service.d.ts +48 -0
- package/dist/dispatcher/dedup.service.d.ts.map +1 -0
- package/dist/dispatcher/dedup.service.js +189 -0
- package/dist/dispatcher/dedup.service.js.map +1 -0
- package/dist/dispatcher/dispatcher.module.d.ts +3 -0
- package/dist/dispatcher/dispatcher.module.d.ts.map +1 -0
- package/dist/dispatcher/dispatcher.module.js +76 -0
- package/dist/dispatcher/dispatcher.module.js.map +1 -0
- package/dist/dispatcher/dispatcher.service.d.ts +134 -0
- package/dist/dispatcher/dispatcher.service.d.ts.map +1 -0
- package/dist/dispatcher/dispatcher.service.js +1034 -0
- package/dist/dispatcher/dispatcher.service.js.map +1 -0
- package/dist/dispatcher/heartbeat.service.d.ts +50 -0
- package/dist/dispatcher/heartbeat.service.d.ts.map +1 -0
- package/dist/dispatcher/heartbeat.service.js +154 -0
- package/dist/dispatcher/heartbeat.service.js.map +1 -0
- package/dist/dispatcher/machine-selector.d.ts +18 -0
- package/dist/dispatcher/machine-selector.d.ts.map +1 -0
- package/dist/dispatcher/machine-selector.js +144 -0
- package/dist/dispatcher/machine-selector.js.map +1 -0
- package/dist/dispatcher/pty-relay.service.d.ts +75 -0
- package/dist/dispatcher/pty-relay.service.d.ts.map +1 -0
- package/dist/dispatcher/pty-relay.service.js +404 -0
- package/dist/dispatcher/pty-relay.service.js.map +1 -0
- package/dist/dispatcher/reconciler.d.ts +39 -0
- package/dist/dispatcher/reconciler.d.ts.map +1 -0
- package/dist/dispatcher/reconciler.js +556 -0
- package/dist/dispatcher/reconciler.js.map +1 -0
- package/dist/dispatcher/scheduler.service.d.ts +50 -0
- package/dist/dispatcher/scheduler.service.d.ts.map +1 -0
- package/dist/dispatcher/scheduler.service.js +287 -0
- package/dist/dispatcher/scheduler.service.js.map +1 -0
- package/dist/dispatcher/state-machine.d.ts +16 -0
- package/dist/dispatcher/state-machine.d.ts.map +1 -0
- package/dist/dispatcher/state-machine.js +77 -0
- package/dist/dispatcher/state-machine.js.map +1 -0
- package/dist/dispatcher/task-log-batcher.d.ts +50 -0
- package/dist/dispatcher/task-log-batcher.d.ts.map +1 -0
- package/dist/dispatcher/task-log-batcher.js +184 -0
- package/dist/dispatcher/task-log-batcher.js.map +1 -0
- package/dist/dispatcher/worker-connection.manager.d.ts +49 -0
- package/dist/dispatcher/worker-connection.manager.d.ts.map +1 -0
- package/dist/dispatcher/worker-connection.manager.js +128 -0
- package/dist/dispatcher/worker-connection.manager.js.map +1 -0
- package/dist/main.d.ts +2 -0
- package/dist/main.d.ts.map +1 -0
- package/dist/main.js +85 -0
- package/dist/main.js.map +1 -0
- package/dist/notifier/debouncer.d.ts +39 -0
- package/dist/notifier/debouncer.d.ts.map +1 -0
- package/dist/notifier/debouncer.js +123 -0
- package/dist/notifier/debouncer.js.map +1 -0
- package/dist/notifier/notification-consumer.d.ts +88 -0
- package/dist/notifier/notification-consumer.d.ts.map +1 -0
- package/dist/notifier/notification-consumer.js +186 -0
- package/dist/notifier/notification-consumer.js.map +1 -0
- package/dist/notifier/notifier.module.d.ts +9 -0
- package/dist/notifier/notifier.module.d.ts.map +1 -0
- package/dist/notifier/notifier.module.js +58 -0
- package/dist/notifier/notifier.module.js.map +1 -0
- package/dist/notifier/notifier.service.d.ts +40 -0
- package/dist/notifier/notifier.service.d.ts.map +1 -0
- package/dist/notifier/notifier.service.js +191 -0
- package/dist/notifier/notifier.service.js.map +1 -0
- package/dist/notifier/template.service.d.ts +42 -0
- package/dist/notifier/template.service.d.ts.map +1 -0
- package/dist/notifier/template.service.js +201 -0
- package/dist/notifier/template.service.js.map +1 -0
- package/dist/redis/redis.module.d.ts +3 -0
- package/dist/redis/redis.module.d.ts.map +1 -0
- package/dist/redis/redis.module.js +22 -0
- package/dist/redis/redis.module.js.map +1 -0
- package/dist/redis/redis.service.d.ts +19 -0
- package/dist/redis/redis.service.d.ts.map +1 -0
- package/dist/redis/redis.service.js +69 -0
- package/dist/redis/redis.service.js.map +1 -0
- package/dist/web/admin/admin-audit.controller.d.ts +7 -0
- package/dist/web/admin/admin-audit.controller.d.ts.map +1 -0
- package/dist/web/admin/admin-audit.controller.js +53 -0
- package/dist/web/admin/admin-audit.controller.js.map +1 -0
- package/dist/web/admin/admin-bot.controller.d.ts +79 -0
- package/dist/web/admin/admin-bot.controller.d.ts.map +1 -0
- package/dist/web/admin/admin-bot.controller.js +193 -0
- package/dist/web/admin/admin-bot.controller.js.map +1 -0
- package/dist/web/admin/admin-developer.controller.d.ts +52 -0
- package/dist/web/admin/admin-developer.controller.d.ts.map +1 -0
- package/dist/web/admin/admin-developer.controller.js +160 -0
- package/dist/web/admin/admin-developer.controller.js.map +1 -0
- package/dist/web/admin/admin-machine.controller.d.ts +64 -0
- package/dist/web/admin/admin-machine.controller.d.ts.map +1 -0
- package/dist/web/admin/admin-machine.controller.js +111 -0
- package/dist/web/admin/admin-machine.controller.js.map +1 -0
- package/dist/web/admin/admin-project.controller.d.ts +45 -0
- package/dist/web/admin/admin-project.controller.d.ts.map +1 -0
- package/dist/web/admin/admin-project.controller.js +207 -0
- package/dist/web/admin/admin-project.controller.js.map +1 -0
- package/dist/web/admin/admin-settings.controller.d.ts +18 -0
- package/dist/web/admin/admin-settings.controller.d.ts.map +1 -0
- package/dist/web/admin/admin-settings.controller.js +93 -0
- package/dist/web/admin/admin-settings.controller.js.map +1 -0
- package/dist/web/admin/admin-token.controller.d.ts +45 -0
- package/dist/web/admin/admin-token.controller.d.ts.map +1 -0
- package/dist/web/admin/admin-token.controller.js +182 -0
- package/dist/web/admin/admin-token.controller.js.map +1 -0
- package/dist/web/dashboard.controller.d.ts +16 -0
- package/dist/web/dashboard.controller.d.ts.map +1 -0
- package/dist/web/dashboard.controller.js +78 -0
- package/dist/web/dashboard.controller.js.map +1 -0
- package/dist/web/dashboard.service.d.ts +39 -0
- package/dist/web/dashboard.service.d.ts.map +1 -0
- package/dist/web/dashboard.service.js +234 -0
- package/dist/web/dashboard.service.js.map +1 -0
- package/dist/web/interaction.service.d.ts +42 -0
- package/dist/web/interaction.service.d.ts.map +1 -0
- package/dist/web/interaction.service.js +102 -0
- package/dist/web/interaction.service.js.map +1 -0
- package/dist/web/machine.controller.d.ts +102 -0
- package/dist/web/machine.controller.d.ts.map +1 -0
- package/dist/web/machine.controller.js +121 -0
- package/dist/web/machine.controller.js.map +1 -0
- package/dist/web/notification.controller.d.ts +22 -0
- package/dist/web/notification.controller.d.ts.map +1 -0
- package/dist/web/notification.controller.js +70 -0
- package/dist/web/notification.controller.js.map +1 -0
- package/dist/web/profile.controller.d.ts +70 -0
- package/dist/web/profile.controller.d.ts.map +1 -0
- package/dist/web/profile.controller.js +262 -0
- package/dist/web/profile.controller.js.map +1 -0
- package/dist/web/project.controller.d.ts +8 -0
- package/dist/web/project.controller.d.ts.map +1 -0
- package/dist/web/project.controller.js +54 -0
- package/dist/web/project.controller.js.map +1 -0
- package/dist/web/pty.gateway.d.ts +32 -0
- package/dist/web/pty.gateway.d.ts.map +1 -0
- package/dist/web/pty.gateway.js +358 -0
- package/dist/web/pty.gateway.js.map +1 -0
- package/dist/web/search.service.d.ts +34 -0
- package/dist/web/search.service.d.ts.map +1 -0
- package/dist/web/search.service.js +106 -0
- package/dist/web/search.service.js.map +1 -0
- package/dist/web/task.controller.d.ts +54 -0
- package/dist/web/task.controller.d.ts.map +1 -0
- package/dist/web/task.controller.js +266 -0
- package/dist/web/task.controller.js.map +1 -0
- package/dist/web/web.module.d.ts +3 -0
- package/dist/web/web.module.d.ts.map +1 -0
- package/dist/web/web.module.js +97 -0
- package/dist/web/web.module.js.map +1 -0
- package/dist/web/worker-channel.gateway.d.ts +45 -0
- package/dist/web/worker-channel.gateway.d.ts.map +1 -0
- package/dist/web/worker-channel.gateway.js +283 -0
- package/dist/web/worker-channel.gateway.js.map +1 -0
- package/dist/web/worker.controller.d.ts +14 -0
- package/dist/web/worker.controller.d.ts.map +1 -0
- package/dist/web/worker.controller.js +73 -0
- package/dist/web/worker.controller.js.map +1 -0
- package/dist/web/workspace.controller.d.ts +109 -0
- package/dist/web/workspace.controller.d.ts.map +1 -0
- package/dist/web/workspace.controller.js +386 -0
- package/dist/web/workspace.controller.js.map +1 -0
- package/package.json +61 -0
|
@@ -0,0 +1,1034 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
19
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
20
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
21
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
22
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
23
|
+
};
|
|
24
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
25
|
+
var ownKeys = function(o) {
|
|
26
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
27
|
+
var ar = [];
|
|
28
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
29
|
+
return ar;
|
|
30
|
+
};
|
|
31
|
+
return ownKeys(o);
|
|
32
|
+
};
|
|
33
|
+
return function (mod) {
|
|
34
|
+
if (mod && mod.__esModule) return mod;
|
|
35
|
+
var result = {};
|
|
36
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
37
|
+
__setModuleDefault(result, mod);
|
|
38
|
+
return result;
|
|
39
|
+
};
|
|
40
|
+
})();
|
|
41
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
42
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
43
|
+
};
|
|
44
|
+
var DispatcherService_1;
|
|
45
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
46
|
+
exports.DispatcherService = void 0;
|
|
47
|
+
const common_1 = require("@nestjs/common");
|
|
48
|
+
const crypto = __importStar(require("node:crypto"));
|
|
49
|
+
const protocol_1 = require("@overlordai/protocol");
|
|
50
|
+
const task_repository_1 = require("../database/repositories/task.repository");
|
|
51
|
+
const machine_repository_1 = require("../database/repositories/machine.repository");
|
|
52
|
+
const project_repository_1 = require("../database/repositories/project.repository");
|
|
53
|
+
const project_member_repository_1 = require("../database/repositories/project-member.repository");
|
|
54
|
+
const worker_token_repository_1 = require("../database/repositories/worker-token.repository");
|
|
55
|
+
const audit_log_repository_1 = require("../database/repositories/audit-log.repository");
|
|
56
|
+
const developer_repository_1 = require("../database/repositories/developer.repository");
|
|
57
|
+
const database_service_1 = require("../database/database.service");
|
|
58
|
+
const auth_service_1 = require("../auth/auth.service");
|
|
59
|
+
const crypto_service_1 = require("../common/crypto.service");
|
|
60
|
+
const redis_service_1 = require("../redis/redis.service");
|
|
61
|
+
const state_machine_1 = require("./state-machine");
|
|
62
|
+
const dedup_service_1 = require("./dedup.service");
|
|
63
|
+
const scheduler_service_1 = require("./scheduler.service");
|
|
64
|
+
const worker_connection_manager_1 = require("./worker-connection.manager");
|
|
65
|
+
const cleanup_service_1 = require("./cleanup.service");
|
|
66
|
+
const workspace_repository_1 = require("../database/repositories/workspace.repository");
|
|
67
|
+
let DispatcherService = DispatcherService_1 = class DispatcherService {
|
|
68
|
+
taskRepo;
|
|
69
|
+
machineRepo;
|
|
70
|
+
projectRepo;
|
|
71
|
+
projectMemberRepo;
|
|
72
|
+
workerTokenRepo;
|
|
73
|
+
auditLogRepo;
|
|
74
|
+
developerRepo;
|
|
75
|
+
databaseService;
|
|
76
|
+
authService;
|
|
77
|
+
cryptoService;
|
|
78
|
+
redis;
|
|
79
|
+
dedupService;
|
|
80
|
+
schedulerService;
|
|
81
|
+
workerConnectionManager;
|
|
82
|
+
cleanupService;
|
|
83
|
+
workspaceRepo;
|
|
84
|
+
logger = new common_1.Logger(DispatcherService_1.name);
|
|
85
|
+
constructor(taskRepo, machineRepo, projectRepo, projectMemberRepo, workerTokenRepo, auditLogRepo, developerRepo, databaseService, authService, cryptoService, redis, dedupService, schedulerService, workerConnectionManager, cleanupService, workspaceRepo) {
|
|
86
|
+
this.taskRepo = taskRepo;
|
|
87
|
+
this.machineRepo = machineRepo;
|
|
88
|
+
this.projectRepo = projectRepo;
|
|
89
|
+
this.projectMemberRepo = projectMemberRepo;
|
|
90
|
+
this.workerTokenRepo = workerTokenRepo;
|
|
91
|
+
this.auditLogRepo = auditLogRepo;
|
|
92
|
+
this.developerRepo = developerRepo;
|
|
93
|
+
this.databaseService = databaseService;
|
|
94
|
+
this.authService = authService;
|
|
95
|
+
this.cryptoService = cryptoService;
|
|
96
|
+
this.redis = redis;
|
|
97
|
+
this.dedupService = dedupService;
|
|
98
|
+
this.schedulerService = schedulerService;
|
|
99
|
+
this.workerConnectionManager = workerConnectionManager;
|
|
100
|
+
this.cleanupService = cleanupService;
|
|
101
|
+
this.workspaceRepo = workspaceRepo;
|
|
102
|
+
}
|
|
103
|
+
// ---------------------------------------------------------------------------
|
|
104
|
+
// handleCommand — unified entry point from adapters
|
|
105
|
+
// ---------------------------------------------------------------------------
|
|
106
|
+
async handleCommand(command) {
|
|
107
|
+
// Event-level dedup: checkEventDedup returns true if this is the first
|
|
108
|
+
// occurrence (Redis SET NX succeeded). False means the key already existed.
|
|
109
|
+
const isFirstOccurrence = await this.dedupService.checkEventDedup(command.eventId);
|
|
110
|
+
if (!isFirstOccurrence) {
|
|
111
|
+
this.logger.debug(`Duplicate event ${command.eventId}, discarding`);
|
|
112
|
+
return { success: false, message: 'Duplicate event' };
|
|
113
|
+
}
|
|
114
|
+
switch (command.type) {
|
|
115
|
+
case protocol_1.CommandType.DEVELOP:
|
|
116
|
+
return this.handleDevelopCommand(command);
|
|
117
|
+
case protocol_1.CommandType.CANCEL:
|
|
118
|
+
return this.handleCancelCommand(command);
|
|
119
|
+
case protocol_1.CommandType.RETRY:
|
|
120
|
+
return this.handleRetryCommand(command);
|
|
121
|
+
case protocol_1.CommandType.CONFIRM:
|
|
122
|
+
return this.handleConfirmCommand(command);
|
|
123
|
+
case protocol_1.CommandType.LIST:
|
|
124
|
+
return this.handleListCommand(command);
|
|
125
|
+
case protocol_1.CommandType.PROGRESS: {
|
|
126
|
+
const taskId = command.args.taskId;
|
|
127
|
+
if (!taskId) {
|
|
128
|
+
return { success: false, message: 'taskId is required' };
|
|
129
|
+
}
|
|
130
|
+
const task = this.taskRepo.findById(taskId);
|
|
131
|
+
if (!task) {
|
|
132
|
+
return { success: false, message: `Task #${taskId} not found` };
|
|
133
|
+
}
|
|
134
|
+
return { success: true, task };
|
|
135
|
+
}
|
|
136
|
+
case protocol_1.CommandType.LOGS: {
|
|
137
|
+
const taskId = command.args.taskId;
|
|
138
|
+
if (!taskId) {
|
|
139
|
+
return { success: false, message: 'taskId is required' };
|
|
140
|
+
}
|
|
141
|
+
const task = this.taskRepo.findById(taskId);
|
|
142
|
+
if (!task) {
|
|
143
|
+
return { success: false, message: `Task #${taskId} not found` };
|
|
144
|
+
}
|
|
145
|
+
return { success: true, task };
|
|
146
|
+
}
|
|
147
|
+
case protocol_1.CommandType.DRAIN:
|
|
148
|
+
return this.handleDrainCommand(command, true);
|
|
149
|
+
case protocol_1.CommandType.UNDRAIN:
|
|
150
|
+
return this.handleDrainCommand(command, false);
|
|
151
|
+
case protocol_1.CommandType.WORKSPACE: {
|
|
152
|
+
const taskId = command.args.taskId;
|
|
153
|
+
if (!taskId) {
|
|
154
|
+
return { success: false, message: 'taskId is required' };
|
|
155
|
+
}
|
|
156
|
+
const task = this.taskRepo.findById(taskId);
|
|
157
|
+
if (!task) {
|
|
158
|
+
return { success: false, message: `Task #${taskId} not found` };
|
|
159
|
+
}
|
|
160
|
+
return { success: true, task };
|
|
161
|
+
}
|
|
162
|
+
default:
|
|
163
|
+
return { success: false, message: `Unknown command type: ${command.type}` };
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
// ---------------------------------------------------------------------------
|
|
167
|
+
// Command handlers
|
|
168
|
+
// ---------------------------------------------------------------------------
|
|
169
|
+
async handleDevelopCommand(command) {
|
|
170
|
+
const { description, project: projectKeyOrAlias } = command.args;
|
|
171
|
+
if (!description) {
|
|
172
|
+
return { success: false, message: 'Description is required' };
|
|
173
|
+
}
|
|
174
|
+
const request = {
|
|
175
|
+
description,
|
|
176
|
+
projectKey: projectKeyOrAlias ?? '',
|
|
177
|
+
machineId: command.args.machine,
|
|
178
|
+
developerId: command.user.id,
|
|
179
|
+
};
|
|
180
|
+
try {
|
|
181
|
+
const task = await this.createTask(request, command.user.id, {
|
|
182
|
+
platform: command.platform,
|
|
183
|
+
msgId: command.source.msgId,
|
|
184
|
+
chatId: command.source.chatId,
|
|
185
|
+
appId: command.source.appId,
|
|
186
|
+
});
|
|
187
|
+
return { success: true, task };
|
|
188
|
+
}
|
|
189
|
+
catch (err) {
|
|
190
|
+
if (err instanceof common_1.ConflictException) {
|
|
191
|
+
// Fingerprint dedup — check if there is a pending confirm
|
|
192
|
+
const body = err.getResponse();
|
|
193
|
+
if (body && typeof body === 'object' && body['confirmRequired']) {
|
|
194
|
+
return {
|
|
195
|
+
success: false,
|
|
196
|
+
confirmRequired: body['confirmRequired'],
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
throw err;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
async handleCancelCommand(command) {
|
|
204
|
+
const taskId = command.args.taskId;
|
|
205
|
+
if (!taskId) {
|
|
206
|
+
return { success: false, message: 'taskId is required' };
|
|
207
|
+
}
|
|
208
|
+
await this.cancelTask(taskId, command.user.id);
|
|
209
|
+
const task = this.taskRepo.findById(taskId);
|
|
210
|
+
return { success: true, task, message: `Task #${taskId} cancelled` };
|
|
211
|
+
}
|
|
212
|
+
async handleRetryCommand(command) {
|
|
213
|
+
const taskId = command.args.taskId;
|
|
214
|
+
if (!taskId) {
|
|
215
|
+
return { success: false, message: 'taskId is required' };
|
|
216
|
+
}
|
|
217
|
+
try {
|
|
218
|
+
const task = await this.retryTask(taskId, command.user.id);
|
|
219
|
+
return { success: true, task, message: `Task #${taskId} retried` };
|
|
220
|
+
}
|
|
221
|
+
catch (err) {
|
|
222
|
+
if (err instanceof common_1.ConflictException) {
|
|
223
|
+
const body = err.getResponse();
|
|
224
|
+
if (body && typeof body === 'object' && body['confirmRequired']) {
|
|
225
|
+
return {
|
|
226
|
+
success: false,
|
|
227
|
+
confirmRequired: body['confirmRequired'],
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
throw err;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
async handleConfirmCommand(command) {
|
|
235
|
+
const taskId = command.args.taskId;
|
|
236
|
+
if (!taskId) {
|
|
237
|
+
return { success: false, message: 'taskId is required' };
|
|
238
|
+
}
|
|
239
|
+
// Resolve pending confirm from Redis.
|
|
240
|
+
// Priority: reply-to msgId association, fallback to #taskId lookup.
|
|
241
|
+
const pendingData = await this.resolvePendingConfirm(taskId, command.user.id, command.source.msgId);
|
|
242
|
+
if (!pendingData) {
|
|
243
|
+
return { success: false, message: 'No pending confirmation found or expired' };
|
|
244
|
+
}
|
|
245
|
+
if (pendingData.action === 'create_task' && pendingData.request) {
|
|
246
|
+
// Confirmed dedup override — create the task anyway
|
|
247
|
+
const task = await this.createTaskInternal(pendingData.request, pendingData.createdBy, pendingData.source, true);
|
|
248
|
+
return { success: true, task, message: 'Task created (dedup override)' };
|
|
249
|
+
}
|
|
250
|
+
if (pendingData.action === 'retry' && pendingData.taskId) {
|
|
251
|
+
// Confirmed retry with existing MR
|
|
252
|
+
const task = await this.executeRetry(pendingData.taskId, pendingData.createdBy);
|
|
253
|
+
return { success: true, task, message: `Task #${pendingData.taskId} retried (MR override)` };
|
|
254
|
+
}
|
|
255
|
+
return { success: false, message: 'Unknown confirm action' };
|
|
256
|
+
}
|
|
257
|
+
handleListCommand(command) {
|
|
258
|
+
const projectKey = command.args.project;
|
|
259
|
+
const tasks = this.taskRepo.findByStatus([
|
|
260
|
+
protocol_1.TaskStatus.QUEUED,
|
|
261
|
+
protocol_1.TaskStatus.ASSIGNED,
|
|
262
|
+
protocol_1.TaskStatus.RUNNING,
|
|
263
|
+
protocol_1.TaskStatus.SUSPENDED,
|
|
264
|
+
]);
|
|
265
|
+
const filtered = projectKey
|
|
266
|
+
? tasks.filter((t) => t.projectKey === projectKey)
|
|
267
|
+
: tasks;
|
|
268
|
+
return { success: true, tasks: filtered };
|
|
269
|
+
}
|
|
270
|
+
handleDrainCommand(command, drain) {
|
|
271
|
+
const machineName = command.args.machine;
|
|
272
|
+
if (!machineName) {
|
|
273
|
+
return { success: false, message: 'Machine name is required' };
|
|
274
|
+
}
|
|
275
|
+
const machine = this.machineRepo.findByName(machineName);
|
|
276
|
+
if (!machine) {
|
|
277
|
+
return { success: false, message: `Machine '${machineName}' not found` };
|
|
278
|
+
}
|
|
279
|
+
const developer = this.developerRepo.findById(command.user.id);
|
|
280
|
+
if (!developer || developer.role !== protocol_1.DeveloperRole.ADMIN) {
|
|
281
|
+
return { success: false, message: 'Only admins can drain/undrain machines' };
|
|
282
|
+
}
|
|
283
|
+
const newStatus = drain ? protocol_1.MachineStatus.DRAINING : protocol_1.MachineStatus.ONLINE;
|
|
284
|
+
this.machineRepo.updateStatus(machine.id, newStatus);
|
|
285
|
+
this.auditLogRepo.create({
|
|
286
|
+
userId: command.user.id,
|
|
287
|
+
action: drain ? 'machine_drain' : 'machine_undrain',
|
|
288
|
+
resource: `machine:${machine.id}`,
|
|
289
|
+
detail: `Machine '${machine.name}' ${drain ? 'drained' : 'undrained'}`,
|
|
290
|
+
});
|
|
291
|
+
return { success: true, message: `Machine '${machine.name}' ${drain ? 'drained' : 'undrained'}` };
|
|
292
|
+
}
|
|
293
|
+
// ---------------------------------------------------------------------------
|
|
294
|
+
// createTask
|
|
295
|
+
// ---------------------------------------------------------------------------
|
|
296
|
+
async createTask(request, createdBy, source) {
|
|
297
|
+
return this.createTaskInternal(request, createdBy, source, false);
|
|
298
|
+
}
|
|
299
|
+
async createTaskInternal(request, createdBy, source, skipDedup = false) {
|
|
300
|
+
// 1. Find project by key or alias
|
|
301
|
+
const project = this.resolveProject(request.projectKey);
|
|
302
|
+
if (!project) {
|
|
303
|
+
throw new common_1.NotFoundException(`Project '${request.projectKey}' not found`);
|
|
304
|
+
}
|
|
305
|
+
// 2. Authorization: check developer is a project member (or admin bypass)
|
|
306
|
+
const developer = this.developerRepo.findById(createdBy);
|
|
307
|
+
if (!developer) {
|
|
308
|
+
throw new common_1.NotFoundException(`Developer #${createdBy} not found`);
|
|
309
|
+
}
|
|
310
|
+
if (developer.role !== protocol_1.DeveloperRole.ADMIN) {
|
|
311
|
+
const membership = this.projectMemberRepo.findByProjectAndDeveloper(project.key, createdBy);
|
|
312
|
+
if (!membership) {
|
|
313
|
+
throw new common_1.ForbiddenException(`Developer '${developer.name}' is not a member of project '${project.key}'`);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
// 3. Compute fingerprint and check dedup
|
|
317
|
+
const fingerprint = this.dedupService.computeFingerprint(project.key, request.description);
|
|
318
|
+
if (!skipDedup) {
|
|
319
|
+
const existingTask = this.taskRepo.findActiveByFingerprint(fingerprint);
|
|
320
|
+
if (existingTask) {
|
|
321
|
+
// Store pending confirm in Redis
|
|
322
|
+
const confirmKey = `pending_confirm:dedup:${crypto.randomUUID()}`;
|
|
323
|
+
const pendingData = JSON.stringify({
|
|
324
|
+
action: 'create_task',
|
|
325
|
+
request: { ...request, projectKey: project.key },
|
|
326
|
+
createdBy,
|
|
327
|
+
source,
|
|
328
|
+
existingTaskId: existingTask.id,
|
|
329
|
+
});
|
|
330
|
+
await this.redis.setex(confirmKey, protocol_1.CONFIRM_PENDING_TTL_SEC, pendingData);
|
|
331
|
+
// Store index from taskId to confirmKey for lookup during confirm
|
|
332
|
+
await this.redis.setex(`pending_confirm_idx:task:${existingTask.id}`, protocol_1.CONFIRM_PENDING_TTL_SEC, confirmKey);
|
|
333
|
+
throw new common_1.ConflictException({
|
|
334
|
+
message: `A similar task already exists: #${existingTask.id} (${existingTask.status})`,
|
|
335
|
+
confirmRequired: {
|
|
336
|
+
reason: 'duplicate_fingerprint',
|
|
337
|
+
message: `Task #${existingTask.id} with the same description is already ${existingTask.status}. Create anyway?`,
|
|
338
|
+
pendingAction: 'create_task',
|
|
339
|
+
taskId: existingTask.id,
|
|
340
|
+
confirmKey,
|
|
341
|
+
},
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
// 4. Snapshot config (14 fields from project)
|
|
346
|
+
const configSnapshot = {
|
|
347
|
+
repoUrl: project.repoUrl,
|
|
348
|
+
gitPlatform: project.gitPlatform,
|
|
349
|
+
defaultBranch: project.defaultBranch,
|
|
350
|
+
workspaceRoot: project.workspaceRoot,
|
|
351
|
+
setupCommands: project.setupCommands,
|
|
352
|
+
testCommand: project.testCommand,
|
|
353
|
+
agentType: project.agentType,
|
|
354
|
+
agentCommand: project.agentCommand,
|
|
355
|
+
agentEnv: project.agentEnv,
|
|
356
|
+
allowedTools: project.allowedTools,
|
|
357
|
+
maxTurns: project.maxTurns,
|
|
358
|
+
skillsPath: project.skillsPath,
|
|
359
|
+
pipeline: project.pipeline,
|
|
360
|
+
ptyOutputFilter: project.ptyOutputFilter,
|
|
361
|
+
};
|
|
362
|
+
// 5. Insert task (QUEUED)
|
|
363
|
+
let task;
|
|
364
|
+
try {
|
|
365
|
+
task = this.taskRepo.create({
|
|
366
|
+
description: request.description,
|
|
367
|
+
fingerprint,
|
|
368
|
+
projectKey: project.key,
|
|
369
|
+
machineId: request.machineId,
|
|
370
|
+
developerId: request.developerId ?? createdBy,
|
|
371
|
+
configSnapshot: JSON.stringify(configSnapshot),
|
|
372
|
+
sourcePlatform: source?.platform,
|
|
373
|
+
sourceMsgId: source?.msgId,
|
|
374
|
+
sourceChatId: source?.chatId,
|
|
375
|
+
sourceAppId: source?.appId,
|
|
376
|
+
createdBy,
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
catch (err) {
|
|
380
|
+
// Catch UNIQUE constraint violation on fingerprint (concurrent create)
|
|
381
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
382
|
+
if (errMsg.includes('UNIQUE') || errMsg.includes('unique')) {
|
|
383
|
+
throw new common_1.ConflictException('A task with the same fingerprint was just created concurrently');
|
|
384
|
+
}
|
|
385
|
+
throw err;
|
|
386
|
+
}
|
|
387
|
+
// 6. Enqueue to BullMQ
|
|
388
|
+
await this.schedulerService.enqueueTask(task.id);
|
|
389
|
+
this.logger.log(`Task #${task.id} created for project '${project.key}' by developer #${createdBy}`);
|
|
390
|
+
this.auditLogRepo.create({
|
|
391
|
+
userId: createdBy,
|
|
392
|
+
action: 'task_create',
|
|
393
|
+
resource: `task:${task.id}`,
|
|
394
|
+
detail: `Created task for project '${project.key}': ${request.description.slice(0, 100)}`,
|
|
395
|
+
});
|
|
396
|
+
return task;
|
|
397
|
+
}
|
|
398
|
+
// ---------------------------------------------------------------------------
|
|
399
|
+
// cancelTask
|
|
400
|
+
// ---------------------------------------------------------------------------
|
|
401
|
+
async cancelTask(taskId, cancelledBy) {
|
|
402
|
+
const task = this.taskRepo.findById(taskId);
|
|
403
|
+
if (!task) {
|
|
404
|
+
throw new common_1.NotFoundException(`Task #${taskId} not found`);
|
|
405
|
+
}
|
|
406
|
+
// Validate transition
|
|
407
|
+
state_machine_1.TaskStateMachine.assertTransition(task.status, protocol_1.TaskStatus.CANCELLED);
|
|
408
|
+
switch (task.status) {
|
|
409
|
+
case protocol_1.TaskStatus.QUEUED: {
|
|
410
|
+
// Remove from BullMQ — task stays QUEUED until we set CANCELLED below
|
|
411
|
+
this.updateTaskStatusWithRetry(taskId, protocol_1.TaskStatus.CANCELLED, task.revision);
|
|
412
|
+
break;
|
|
413
|
+
}
|
|
414
|
+
case protocol_1.TaskStatus.RUNNING: {
|
|
415
|
+
// Send cancel to Worker via WS, wait ack with timeout
|
|
416
|
+
if (task.machineId) {
|
|
417
|
+
const acked = await this.sendCancelToWorkerWithAck(task.machineId, taskId);
|
|
418
|
+
if (!acked) {
|
|
419
|
+
this.logger.warn(`Cancel ack timeout for task #${taskId} on machine ${task.machineId}, forcing cancel`);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
// Reload task to get latest revision (may have changed during ack wait)
|
|
423
|
+
const freshTask = this.taskRepo.findById(taskId);
|
|
424
|
+
if (freshTask && freshTask.status !== protocol_1.TaskStatus.CANCELLED) {
|
|
425
|
+
this.updateTaskStatusWithRetry(taskId, protocol_1.TaskStatus.CANCELLED, freshTask.revision, { completedAt: new Date().toISOString() });
|
|
426
|
+
}
|
|
427
|
+
break;
|
|
428
|
+
}
|
|
429
|
+
case protocol_1.TaskStatus.ASSIGNED: {
|
|
430
|
+
// Re-read to catch ASSIGNED->RUNNING race between initial read and CAS
|
|
431
|
+
const freshAssigned = this.taskRepo.findById(taskId);
|
|
432
|
+
if (!freshAssigned || freshAssigned.status === protocol_1.TaskStatus.CANCELLED) {
|
|
433
|
+
break;
|
|
434
|
+
}
|
|
435
|
+
state_machine_1.TaskStateMachine.assertTransition(freshAssigned.status, protocol_1.TaskStatus.CANCELLED);
|
|
436
|
+
// Send cancel to Worker if assigned, set CANCELLED
|
|
437
|
+
if (freshAssigned.machineId) {
|
|
438
|
+
this.sendCancelToWorker(freshAssigned.machineId, taskId);
|
|
439
|
+
}
|
|
440
|
+
this.updateTaskStatusWithRetry(taskId, protocol_1.TaskStatus.CANCELLED, freshAssigned.revision, { completedAt: new Date().toISOString() });
|
|
441
|
+
break;
|
|
442
|
+
}
|
|
443
|
+
case protocol_1.TaskStatus.SUSPENDED: {
|
|
444
|
+
// Direct set CANCELLED — no Worker ack needed
|
|
445
|
+
this.updateTaskStatusWithRetry(taskId, protocol_1.TaskStatus.CANCELLED, task.revision, { completedAt: new Date().toISOString() });
|
|
446
|
+
break;
|
|
447
|
+
}
|
|
448
|
+
default:
|
|
449
|
+
throw new common_1.BadRequestException(`Cannot cancel task #${taskId} in status ${task.status}`);
|
|
450
|
+
}
|
|
451
|
+
this.auditLogRepo.create({
|
|
452
|
+
userId: cancelledBy,
|
|
453
|
+
action: 'task_cancel',
|
|
454
|
+
resource: `task:${taskId}`,
|
|
455
|
+
detail: `Cancelled task #${taskId} from status ${task.status}`,
|
|
456
|
+
});
|
|
457
|
+
// Schedule workspace cleanup for the cancelled task
|
|
458
|
+
if (task.machineId) {
|
|
459
|
+
const workspace = this.workspaceRepo.findByTaskId(taskId);
|
|
460
|
+
if (workspace) {
|
|
461
|
+
this.cleanupService.scheduleCleanup(taskId, task.machineId, workspace.path).catch((err) => {
|
|
462
|
+
this.logger.error(`Failed to schedule cleanup for cancelled task #${taskId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
this.logger.log(`Task #${taskId} cancelled by developer #${cancelledBy}`);
|
|
467
|
+
}
|
|
468
|
+
// ---------------------------------------------------------------------------
|
|
469
|
+
// retryTask
|
|
470
|
+
// ---------------------------------------------------------------------------
|
|
471
|
+
async retryTask(taskId, retriedBy) {
|
|
472
|
+
const task = this.taskRepo.findById(taskId);
|
|
473
|
+
if (!task) {
|
|
474
|
+
throw new common_1.NotFoundException(`Task #${taskId} not found`);
|
|
475
|
+
}
|
|
476
|
+
if (task.status !== protocol_1.TaskStatus.FAILED) {
|
|
477
|
+
throw new common_1.BadRequestException(`Cannot retry task #${taskId}: status is ${task.status}, expected FAILED`);
|
|
478
|
+
}
|
|
479
|
+
// Validate transition
|
|
480
|
+
state_machine_1.TaskStateMachine.assertTransition(task.status, protocol_1.TaskStatus.QUEUED);
|
|
481
|
+
// Check fingerprint conflict — another active task with same fingerprint
|
|
482
|
+
if (task.fingerprint) {
|
|
483
|
+
const conflicting = this.taskRepo.findActiveByFingerprint(task.fingerprint);
|
|
484
|
+
if (conflicting && conflicting.id !== taskId) {
|
|
485
|
+
throw new common_1.ConflictException(`Cannot retry: active task #${conflicting.id} has the same fingerprint`);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
// Check for existing MR/PR on the branch (if branch exists)
|
|
489
|
+
if (task.branch && task.configSnapshot) {
|
|
490
|
+
const mrCheckResult = await this.checkExistingMergeRequest(task);
|
|
491
|
+
if (mrCheckResult) {
|
|
492
|
+
// Store pending confirm for retry with existing MR
|
|
493
|
+
const confirmKey = `pending_confirm:retry:${crypto.randomUUID()}`;
|
|
494
|
+
const pendingData = JSON.stringify({
|
|
495
|
+
action: 'retry',
|
|
496
|
+
taskId,
|
|
497
|
+
createdBy: retriedBy,
|
|
498
|
+
});
|
|
499
|
+
await this.redis.setex(confirmKey, protocol_1.CONFIRM_PENDING_TTL_SEC, pendingData);
|
|
500
|
+
// Store index from taskId to confirmKey for lookup during confirm
|
|
501
|
+
await this.redis.setex(`pending_confirm_idx:task:${taskId}`, protocol_1.CONFIRM_PENDING_TTL_SEC, confirmKey);
|
|
502
|
+
throw new common_1.ConflictException({
|
|
503
|
+
message: `Task #${taskId} branch '${task.branch}' has an open MR/PR`,
|
|
504
|
+
confirmRequired: {
|
|
505
|
+
reason: 'existing_mr',
|
|
506
|
+
message: `Task #${taskId} branch '${task.branch}' has an open MR/PR (${mrCheckResult}). Retry will force push to the same branch. Continue?`,
|
|
507
|
+
pendingAction: 'retry',
|
|
508
|
+
taskId,
|
|
509
|
+
mrUrl: mrCheckResult,
|
|
510
|
+
confirmKey,
|
|
511
|
+
},
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
return this.executeRetry(taskId, retriedBy);
|
|
516
|
+
}
|
|
517
|
+
async executeRetry(taskId, retriedBy) {
|
|
518
|
+
const task = this.taskRepo.findById(taskId);
|
|
519
|
+
if (!task) {
|
|
520
|
+
throw new common_1.NotFoundException(`Task #${taskId} not found`);
|
|
521
|
+
}
|
|
522
|
+
// CAS update: FAILED -> QUEUED
|
|
523
|
+
const updated = this.updateTaskStatusWithRetry(taskId, protocol_1.TaskStatus.QUEUED, task.revision, {
|
|
524
|
+
retryCount: task.retryCount + 1,
|
|
525
|
+
errorMessage: null,
|
|
526
|
+
machineId: null,
|
|
527
|
+
assignedAt: null,
|
|
528
|
+
startedAt: null,
|
|
529
|
+
completedAt: null,
|
|
530
|
+
currentStage: null,
|
|
531
|
+
currentSessionId: null,
|
|
532
|
+
});
|
|
533
|
+
if (!updated) {
|
|
534
|
+
throw new common_1.ConflictException(`Failed to retry task #${taskId}: concurrent modification`);
|
|
535
|
+
}
|
|
536
|
+
// Re-enqueue to BullMQ
|
|
537
|
+
await this.schedulerService.enqueueTask(taskId);
|
|
538
|
+
this.auditLogRepo.create({
|
|
539
|
+
userId: retriedBy,
|
|
540
|
+
action: 'task_retry',
|
|
541
|
+
resource: `task:${taskId}`,
|
|
542
|
+
detail: `Retried task #${taskId} (attempt ${task.retryCount + 1})`,
|
|
543
|
+
});
|
|
544
|
+
this.logger.log(`Task #${taskId} retried by developer #${retriedBy}`);
|
|
545
|
+
return this.taskRepo.findById(taskId);
|
|
546
|
+
}
|
|
547
|
+
// ---------------------------------------------------------------------------
|
|
548
|
+
// confirmStage — forward interactive stage confirm result to Worker
|
|
549
|
+
// ---------------------------------------------------------------------------
|
|
550
|
+
async confirmStage(taskId, stageIndex, result) {
|
|
551
|
+
const task = this.taskRepo.findById(taskId);
|
|
552
|
+
if (!task) {
|
|
553
|
+
throw new common_1.NotFoundException(`Task #${taskId} not found`);
|
|
554
|
+
}
|
|
555
|
+
if (task.status !== protocol_1.TaskStatus.RUNNING &&
|
|
556
|
+
task.status !== protocol_1.TaskStatus.SUSPENDED) {
|
|
557
|
+
throw new common_1.BadRequestException(`Task #${taskId} is not in a confirmable state (${task.status})`);
|
|
558
|
+
}
|
|
559
|
+
if (!task.machineId) {
|
|
560
|
+
throw new common_1.BadRequestException(`Task #${taskId} has no assigned machine`);
|
|
561
|
+
}
|
|
562
|
+
// Forward confirm response to Worker via WS
|
|
563
|
+
const frame = {
|
|
564
|
+
type: 'stage_confirm_response',
|
|
565
|
+
taskId,
|
|
566
|
+
stageIndex,
|
|
567
|
+
result,
|
|
568
|
+
};
|
|
569
|
+
const ws = this.workerConnectionManager.getConnection(task.machineId);
|
|
570
|
+
if (!ws || ws.readyState !== 1 /* WebSocket.OPEN */) {
|
|
571
|
+
throw new common_1.BadRequestException(`Worker for task #${taskId} is not connected`);
|
|
572
|
+
}
|
|
573
|
+
ws.send(JSON.stringify(frame));
|
|
574
|
+
// Set a short TTL on the stage confirm key instead of deleting immediately.
|
|
575
|
+
// This gives a 60-second window for retry if the worker doesn't process the message,
|
|
576
|
+
// while still ensuring eventual cleanup.
|
|
577
|
+
const stageConfirmKey = `confirm:stage:${taskId}:${stageIndex}`;
|
|
578
|
+
await this.redis.expire(stageConfirmKey, 60);
|
|
579
|
+
this.logger.log(`Stage confirm forwarded to Worker for task #${taskId}, stage ${stageIndex}`);
|
|
580
|
+
}
|
|
581
|
+
// ---------------------------------------------------------------------------
|
|
582
|
+
// getTask / listTasks
|
|
583
|
+
// ---------------------------------------------------------------------------
|
|
584
|
+
getTask(taskId) {
|
|
585
|
+
return this.taskRepo.findById(taskId);
|
|
586
|
+
}
|
|
587
|
+
listTasks(query) {
|
|
588
|
+
return this.taskRepo.listPaginated(query);
|
|
589
|
+
}
|
|
590
|
+
// ---------------------------------------------------------------------------
|
|
591
|
+
// registerWorker
|
|
592
|
+
// ---------------------------------------------------------------------------
|
|
593
|
+
async registerWorker(request) {
|
|
594
|
+
// 1. Parse token format: ovw_<id>_<secret>
|
|
595
|
+
const { tokenId, secret } = this.parseWorkerToken(request.token);
|
|
596
|
+
// 2. Find token by id, verify it's active and unused
|
|
597
|
+
const token = this.workerTokenRepo.findActiveUnused(tokenId);
|
|
598
|
+
if (!token) {
|
|
599
|
+
throw new common_1.UnauthorizedException('Invalid or already used worker token');
|
|
600
|
+
}
|
|
601
|
+
// 3. Verify bcrypt(secret, hash)
|
|
602
|
+
const secretValid = await this.cryptoService.comparePassword(secret, token.tokenHash);
|
|
603
|
+
if (!secretValid) {
|
|
604
|
+
throw new common_1.UnauthorizedException('Invalid worker token secret');
|
|
605
|
+
}
|
|
606
|
+
// 4. Validate protocol version (major must match)
|
|
607
|
+
this.validateProtocolVersion(request.protocolVersion);
|
|
608
|
+
// 5. Mark token as used
|
|
609
|
+
this.workerTokenRepo.markUsed(tokenId);
|
|
610
|
+
// 6. Create or update machine record
|
|
611
|
+
const machineId = request.machineId ?? crypto.randomUUID();
|
|
612
|
+
const isFirstRegistration = !request.machineId;
|
|
613
|
+
let machine = this.machineRepo.findById(machineId);
|
|
614
|
+
if (machine) {
|
|
615
|
+
// Update existing machine
|
|
616
|
+
this.machineRepo.updateStatus(machineId, protocol_1.MachineStatus.ONLINE);
|
|
617
|
+
this.machineRepo.updateTokenId(machineId, tokenId);
|
|
618
|
+
}
|
|
619
|
+
else {
|
|
620
|
+
// Create new machine
|
|
621
|
+
machine = this.machineRepo.create({
|
|
622
|
+
id: machineId,
|
|
623
|
+
name: request.machineName,
|
|
624
|
+
host: request.host,
|
|
625
|
+
port: request.port,
|
|
626
|
+
tokenId,
|
|
627
|
+
os: request.os,
|
|
628
|
+
cpuCores: request.cpuCores,
|
|
629
|
+
memoryGb: request.memoryGb,
|
|
630
|
+
capabilities: request.capabilities,
|
|
631
|
+
tags: request.tags,
|
|
632
|
+
protocolVersion: request.protocolVersion,
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
// 7. Generate recovery secret on first registration
|
|
636
|
+
let recoverySecret;
|
|
637
|
+
if (isFirstRegistration) {
|
|
638
|
+
recoverySecret = crypto.randomBytes(32).toString('hex');
|
|
639
|
+
const recoveryHash = await this.cryptoService.hashPassword(recoverySecret);
|
|
640
|
+
this.machineRepo.updateRecoverySecretHash(machineId, recoveryHash);
|
|
641
|
+
}
|
|
642
|
+
// 8. Sign Worker JWT
|
|
643
|
+
const jwt = this.authService.signWorkerJwt(machineId, tokenId);
|
|
644
|
+
this.auditLogRepo.create({
|
|
645
|
+
action: 'worker_register',
|
|
646
|
+
resource: `machine:${machineId}`,
|
|
647
|
+
detail: `Worker '${request.machineName}' registered (token #${tokenId}, first=${isFirstRegistration})`,
|
|
648
|
+
});
|
|
649
|
+
this.logger.log(`Worker '${request.machineName}' registered as ${machineId}`);
|
|
650
|
+
return {
|
|
651
|
+
jwt,
|
|
652
|
+
machineId,
|
|
653
|
+
recoverySecret,
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
// ---------------------------------------------------------------------------
|
|
657
|
+
// refreshWorkerToken
|
|
658
|
+
// ---------------------------------------------------------------------------
|
|
659
|
+
async refreshWorkerToken(request, currentJwt) {
|
|
660
|
+
const { machineId } = request;
|
|
661
|
+
// Verify tokenId matches the current JWT's tokenId
|
|
662
|
+
const machine = this.machineRepo.findById(machineId);
|
|
663
|
+
if (!machine) {
|
|
664
|
+
throw new common_1.NotFoundException(`Machine '${machineId}' not found`);
|
|
665
|
+
}
|
|
666
|
+
if (machine.tokenId !== currentJwt.tokenId) {
|
|
667
|
+
throw new common_1.UnauthorizedException('Token ID mismatch — token may have been revoked');
|
|
668
|
+
}
|
|
669
|
+
// Verify the token hasn't been revoked
|
|
670
|
+
const token = this.workerTokenRepo.findById(currentJwt.tokenId);
|
|
671
|
+
if (!token || token.status === 'revoked') {
|
|
672
|
+
throw new common_1.UnauthorizedException('Worker token has been revoked');
|
|
673
|
+
}
|
|
674
|
+
// Sign new Worker JWT
|
|
675
|
+
const jwt = this.authService.signWorkerJwt(machineId, currentJwt.tokenId);
|
|
676
|
+
this.logger.debug(`Worker JWT refreshed for machine ${machineId}`);
|
|
677
|
+
return { jwt };
|
|
678
|
+
}
|
|
679
|
+
// ---------------------------------------------------------------------------
|
|
680
|
+
// recoverWorker
|
|
681
|
+
// ---------------------------------------------------------------------------
|
|
682
|
+
async recoverWorker(request) {
|
|
683
|
+
const { machineId, machineName, recoverySecret } = request;
|
|
684
|
+
// 1. Find machine
|
|
685
|
+
const machine = this.machineRepo.findById(machineId);
|
|
686
|
+
if (!machine) {
|
|
687
|
+
throw new common_1.NotFoundException(`Machine '${machineId}' not found`);
|
|
688
|
+
}
|
|
689
|
+
// 2. Validate machine name matches
|
|
690
|
+
if (machine.name !== machineName) {
|
|
691
|
+
throw new common_1.UnauthorizedException('Machine name does not match the registered name');
|
|
692
|
+
}
|
|
693
|
+
// 3. Validate recovery secret hash exists
|
|
694
|
+
if (!machine.recoverySecretHash) {
|
|
695
|
+
throw new common_1.UnauthorizedException('No recovery secret configured for this machine');
|
|
696
|
+
}
|
|
697
|
+
// 4. Validate recovery secret
|
|
698
|
+
const secretValid = await this.cryptoService.comparePassword(recoverySecret, machine.recoverySecretHash);
|
|
699
|
+
if (!secretValid) {
|
|
700
|
+
throw new common_1.UnauthorizedException('Invalid recovery secret');
|
|
701
|
+
}
|
|
702
|
+
// 5. Atomically claim the recovery token via Redis GETDEL to prevent
|
|
703
|
+
// race conditions where two concurrent recovery requests consume
|
|
704
|
+
// the same token.
|
|
705
|
+
const recoveryClaimKey = `recovery_claim:${machineId}`;
|
|
706
|
+
// Set the claim key if not already present (first recovery wins)
|
|
707
|
+
const claimed = await this.redis.getClient().set(recoveryClaimKey, '1', 'EX', 30, 'NX');
|
|
708
|
+
if (claimed === null) {
|
|
709
|
+
throw new common_1.ConflictException('Recovery already in progress for this machine');
|
|
710
|
+
}
|
|
711
|
+
try {
|
|
712
|
+
// 6. Wrap SQLite updates in a transaction for atomicity
|
|
713
|
+
const db = this.databaseService.getDb();
|
|
714
|
+
const runRecoveryTransaction = db.transaction(() => {
|
|
715
|
+
// Update machine status to online
|
|
716
|
+
db.prepare('UPDATE machines SET status = ? WHERE id = ?')
|
|
717
|
+
.run(protocol_1.MachineStatus.ONLINE, machineId);
|
|
718
|
+
// Record audit log
|
|
719
|
+
db.prepare(`INSERT INTO audit_logs (user_id, action, resource, detail)
|
|
720
|
+
VALUES (?, ?, ?, ?)`).run(null, 'worker_recover', `machine:${machineId}`, `Worker '${machineName}' recovered via recovery secret`);
|
|
721
|
+
});
|
|
722
|
+
runRecoveryTransaction();
|
|
723
|
+
// 7. Issue new token — sign Worker JWT with existing tokenId
|
|
724
|
+
const jwt = this.authService.signWorkerJwt(machineId, machine.tokenId);
|
|
725
|
+
this.logger.log(`Worker '${machineName}' (${machineId}) recovered via recovery secret`);
|
|
726
|
+
return { jwt, machineId };
|
|
727
|
+
}
|
|
728
|
+
finally {
|
|
729
|
+
// Clean up the claim key
|
|
730
|
+
await this.redis.del(recoveryClaimKey);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
// ---------------------------------------------------------------------------
|
|
734
|
+
// issuePtyToken
|
|
735
|
+
// ---------------------------------------------------------------------------
|
|
736
|
+
issuePtyToken(taskId, aud) {
|
|
737
|
+
const task = this.taskRepo.findById(taskId);
|
|
738
|
+
if (!task) {
|
|
739
|
+
throw new common_1.NotFoundException(`Task #${taskId} not found`);
|
|
740
|
+
}
|
|
741
|
+
return this.authService.signChannelToken(taskId, aud);
|
|
742
|
+
}
|
|
743
|
+
// ---------------------------------------------------------------------------
|
|
744
|
+
// Helpers
|
|
745
|
+
// ---------------------------------------------------------------------------
|
|
746
|
+
resolveProject(keyOrAlias) {
|
|
747
|
+
if (!keyOrAlias) {
|
|
748
|
+
// Try default project
|
|
749
|
+
return this.projectRepo.findDefault();
|
|
750
|
+
}
|
|
751
|
+
// Try by key first
|
|
752
|
+
const byKey = this.projectRepo.findByKey(keyOrAlias);
|
|
753
|
+
if (byKey)
|
|
754
|
+
return byKey;
|
|
755
|
+
// Try by alias
|
|
756
|
+
return this.projectRepo.findByAlias(keyOrAlias);
|
|
757
|
+
}
|
|
758
|
+
parseWorkerToken(raw) {
|
|
759
|
+
// Format: ovw_<id>_<secret>
|
|
760
|
+
const match = /^ovw_(\d+)_(.+)$/.exec(raw);
|
|
761
|
+
if (!match) {
|
|
762
|
+
throw new common_1.UnauthorizedException('Invalid token format. Expected: ovw_<id>_<secret>');
|
|
763
|
+
}
|
|
764
|
+
return {
|
|
765
|
+
tokenId: parseInt(match[1], 10),
|
|
766
|
+
secret: match[2],
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
validateProtocolVersion(workerVersion) {
|
|
770
|
+
const [serverMajor] = protocol_1.PROTOCOL_VERSION.split('.');
|
|
771
|
+
const [workerMajor] = workerVersion.split('.');
|
|
772
|
+
if (serverMajor !== workerMajor) {
|
|
773
|
+
throw new common_1.BadRequestException(`Protocol version mismatch: server=${protocol_1.PROTOCOL_VERSION}, worker=${workerVersion}. Major version must match.`);
|
|
774
|
+
}
|
|
775
|
+
if (workerVersion !== protocol_1.PROTOCOL_VERSION) {
|
|
776
|
+
this.logger.warn(`Protocol minor version mismatch: server=${protocol_1.PROTOCOL_VERSION}, worker=${workerVersion}`);
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
/**
|
|
780
|
+
* Resolve a pending confirmation from Redis.
|
|
781
|
+
* Tries reply-to msgId lookup first, then taskId-based index.
|
|
782
|
+
* Validates that the confirming user matches the original creator.
|
|
783
|
+
* Consumes (deletes) the pending confirm on success.
|
|
784
|
+
*/
|
|
785
|
+
async resolvePendingConfirm(taskId, userId, _msgId) {
|
|
786
|
+
// Look up the confirm key via taskId index (non-destructive read)
|
|
787
|
+
const indexKey = `pending_confirm_idx:task:${taskId}`;
|
|
788
|
+
const confirmKey = await this.redis.get(indexKey);
|
|
789
|
+
if (!confirmKey)
|
|
790
|
+
return null;
|
|
791
|
+
// Atomically consume the confirm data (GETDEL prevents double-confirm race)
|
|
792
|
+
const raw = await this.redis.getdel(confirmKey);
|
|
793
|
+
if (!raw)
|
|
794
|
+
return null;
|
|
795
|
+
let data;
|
|
796
|
+
try {
|
|
797
|
+
data = JSON.parse(raw);
|
|
798
|
+
}
|
|
799
|
+
catch {
|
|
800
|
+
return null;
|
|
801
|
+
}
|
|
802
|
+
// Validate user matches original creator.
|
|
803
|
+
// If auth fails the confirm data is already consumed (intentional —
|
|
804
|
+
// prevents an unauthorized user from probing indefinitely, and the
|
|
805
|
+
// confirm would have expired via TTL anyway).
|
|
806
|
+
if (data.createdBy !== userId) {
|
|
807
|
+
this.logger.warn(`Confirm attempt by user #${userId} but pending confirm belongs to user #${data.createdBy}`);
|
|
808
|
+
return null;
|
|
809
|
+
}
|
|
810
|
+
// Clean up the index key
|
|
811
|
+
await this.redis.del(indexKey);
|
|
812
|
+
return data;
|
|
813
|
+
}
|
|
814
|
+
/**
|
|
815
|
+
* Update task status with CAS retry (up to CAS_MAX_RETRIES).
|
|
816
|
+
* Returns true if successful, throws ConflictException on exhaustion.
|
|
817
|
+
*/
|
|
818
|
+
updateTaskStatusWithRetry(taskId, status, initialRevision, extra) {
|
|
819
|
+
let revision = initialRevision;
|
|
820
|
+
for (let attempt = 0; attempt < protocol_1.CAS_MAX_RETRIES; attempt++) {
|
|
821
|
+
const success = this.taskRepo.updateStatus(taskId, status, revision, extra);
|
|
822
|
+
if (success)
|
|
823
|
+
return true;
|
|
824
|
+
// Reload task to get fresh revision
|
|
825
|
+
const freshTask = this.taskRepo.findById(taskId);
|
|
826
|
+
if (!freshTask) {
|
|
827
|
+
throw new common_1.NotFoundException(`Task #${taskId} not found during CAS retry`);
|
|
828
|
+
}
|
|
829
|
+
// If task already reached target status, that's fine
|
|
830
|
+
if (freshTask.status === status)
|
|
831
|
+
return true;
|
|
832
|
+
revision = freshTask.revision;
|
|
833
|
+
}
|
|
834
|
+
throw new common_1.ConflictException(`CAS retry exhausted for task #${taskId} after ${protocol_1.CAS_MAX_RETRIES} attempts`);
|
|
835
|
+
}
|
|
836
|
+
/**
|
|
837
|
+
* Send cancel frame to Worker and wait for ack within CANCEL_ACK_TIMEOUT_MS.
|
|
838
|
+
*/
|
|
839
|
+
async sendCancelToWorkerWithAck(machineId, taskId) {
|
|
840
|
+
const msgId = crypto.randomUUID();
|
|
841
|
+
try {
|
|
842
|
+
await this.workerConnectionManager
|
|
843
|
+
.sendWithAck(machineId, { type: 'cancel', msgId, taskId }, protocol_1.CANCEL_ACK_TIMEOUT_MS);
|
|
844
|
+
return true;
|
|
845
|
+
}
|
|
846
|
+
catch {
|
|
847
|
+
return false;
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
/**
|
|
851
|
+
* Send cancel frame to Worker (fire and forget).
|
|
852
|
+
*/
|
|
853
|
+
sendCancelToWorker(machineId, taskId) {
|
|
854
|
+
const msgId = crypto.randomUUID();
|
|
855
|
+
this.workerConnectionManager.send(machineId, {
|
|
856
|
+
type: 'cancel',
|
|
857
|
+
msgId,
|
|
858
|
+
taskId,
|
|
859
|
+
});
|
|
860
|
+
}
|
|
861
|
+
/**
|
|
862
|
+
* Check if the task's branch has an existing open MR/PR on the Git platform.
|
|
863
|
+
* Returns the MR/PR URL if found, null otherwise.
|
|
864
|
+
* On timeout or error, returns null (allows retry to proceed).
|
|
865
|
+
*
|
|
866
|
+
* Supports GitLab and GitHub APIs. Extracts the project path from the repo URL
|
|
867
|
+
* and queries the platform API for open merge/pull requests on the branch.
|
|
868
|
+
*/
|
|
869
|
+
async checkExistingMergeRequest(task) {
|
|
870
|
+
if (!task.branch || !task.configSnapshot)
|
|
871
|
+
return null;
|
|
872
|
+
try {
|
|
873
|
+
const config = JSON.parse(task.configSnapshot);
|
|
874
|
+
const { repoUrl, gitPlatform } = config;
|
|
875
|
+
if (!repoUrl)
|
|
876
|
+
return null;
|
|
877
|
+
// Extract the project path from the repo URL.
|
|
878
|
+
const projectPath = this.extractProjectPath(repoUrl);
|
|
879
|
+
if (!projectPath) {
|
|
880
|
+
this.logger.warn(`Cannot extract project path from repo URL '${repoUrl}' for task #${task.id}`);
|
|
881
|
+
return task.mrUrl ?? null;
|
|
882
|
+
}
|
|
883
|
+
// Extract the base URL for API calls
|
|
884
|
+
const baseUrl = this.extractGitBaseUrl(repoUrl);
|
|
885
|
+
if (!baseUrl) {
|
|
886
|
+
this.logger.warn(`Cannot extract base URL from repo URL '${repoUrl}' for task #${task.id}`);
|
|
887
|
+
return task.mrUrl ?? null;
|
|
888
|
+
}
|
|
889
|
+
// Look up the git token. Since the DB schema does not currently include
|
|
890
|
+
// git_token_encrypted, use the GIT_TOKEN environment variable as fallback.
|
|
891
|
+
const gitToken = process.env.GIT_TOKEN || '';
|
|
892
|
+
if (!gitToken) {
|
|
893
|
+
this.logger.debug(`No GIT_TOKEN configured; skipping MR/PR API check for task #${task.id}`);
|
|
894
|
+
return task.mrUrl ?? null;
|
|
895
|
+
}
|
|
896
|
+
const abortController = new AbortController();
|
|
897
|
+
const timeout = setTimeout(() => abortController.abort(), 10_000);
|
|
898
|
+
try {
|
|
899
|
+
let mrUrl = null;
|
|
900
|
+
if (gitPlatform === 'gitlab') {
|
|
901
|
+
mrUrl = await this.checkGitLabMergeRequest(baseUrl, projectPath, task.branch, gitToken, abortController.signal);
|
|
902
|
+
}
|
|
903
|
+
else if (gitPlatform === 'github') {
|
|
904
|
+
mrUrl = await this.checkGitHubPullRequest(baseUrl, projectPath, task.branch, gitToken, abortController.signal);
|
|
905
|
+
}
|
|
906
|
+
else {
|
|
907
|
+
this.logger.debug(`Unsupported git platform '${gitPlatform}' for MR/PR check on task #${task.id}`);
|
|
908
|
+
return task.mrUrl ?? null;
|
|
909
|
+
}
|
|
910
|
+
return mrUrl;
|
|
911
|
+
}
|
|
912
|
+
finally {
|
|
913
|
+
clearTimeout(timeout);
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
catch (err) {
|
|
917
|
+
this.logger.warn(`MR/PR check failed for task #${task.id}: ${err instanceof Error ? err.message : String(err)}`);
|
|
918
|
+
// On error, allow retry to proceed
|
|
919
|
+
return null;
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
/**
|
|
923
|
+
* Query GitLab API for open merge requests on a branch.
|
|
924
|
+
* GET /api/v4/projects/:id/merge_requests?source_branch=...&state=opened
|
|
925
|
+
*/
|
|
926
|
+
async checkGitLabMergeRequest(baseUrl, projectPath, branch, token, signal) {
|
|
927
|
+
const encodedPath = encodeURIComponent(projectPath);
|
|
928
|
+
const url = `${baseUrl}/api/v4/projects/${encodedPath}/merge_requests?source_branch=${encodeURIComponent(branch)}&state=opened&per_page=1`;
|
|
929
|
+
const response = await fetch(url, {
|
|
930
|
+
headers: {
|
|
931
|
+
'PRIVATE-TOKEN': token,
|
|
932
|
+
'Accept': 'application/json',
|
|
933
|
+
},
|
|
934
|
+
signal,
|
|
935
|
+
});
|
|
936
|
+
if (!response.ok) {
|
|
937
|
+
this.logger.warn(`GitLab MR check returned ${response.status} for project '${projectPath}', branch '${branch}'`);
|
|
938
|
+
return null;
|
|
939
|
+
}
|
|
940
|
+
const data = (await response.json());
|
|
941
|
+
if (Array.isArray(data) && data.length > 0 && data[0].web_url) {
|
|
942
|
+
this.logger.debug(`Found open GitLab MR for branch '${branch}': ${data[0].web_url}`);
|
|
943
|
+
return data[0].web_url;
|
|
944
|
+
}
|
|
945
|
+
return null;
|
|
946
|
+
}
|
|
947
|
+
/**
|
|
948
|
+
* Query GitHub API for open pull requests on a branch.
|
|
949
|
+
* GET /repos/:owner/:repo/pulls?head=owner:branch&state=open
|
|
950
|
+
*/
|
|
951
|
+
async checkGitHubPullRequest(baseUrl, projectPath, branch, token, signal) {
|
|
952
|
+
const owner = projectPath.split('/')[0];
|
|
953
|
+
const apiBase = baseUrl === 'https://github.com'
|
|
954
|
+
? 'https://api.github.com'
|
|
955
|
+
: `${baseUrl}/api/v3`;
|
|
956
|
+
const url = `${apiBase}/repos/${projectPath}/pulls?head=${encodeURIComponent(`${owner}:${branch}`)}&state=open&per_page=1`;
|
|
957
|
+
const response = await fetch(url, {
|
|
958
|
+
headers: {
|
|
959
|
+
'Authorization': `Bearer ${token}`,
|
|
960
|
+
'Accept': 'application/vnd.github+json',
|
|
961
|
+
'X-GitHub-Api-Version': '2022-11-28',
|
|
962
|
+
},
|
|
963
|
+
signal,
|
|
964
|
+
});
|
|
965
|
+
if (!response.ok) {
|
|
966
|
+
this.logger.warn(`GitHub PR check returned ${response.status} for project '${projectPath}', branch '${branch}'`);
|
|
967
|
+
return null;
|
|
968
|
+
}
|
|
969
|
+
const data = (await response.json());
|
|
970
|
+
if (Array.isArray(data) && data.length > 0 && data[0].html_url) {
|
|
971
|
+
this.logger.debug(`Found open GitHub PR for branch '${branch}': ${data[0].html_url}`);
|
|
972
|
+
return data[0].html_url;
|
|
973
|
+
}
|
|
974
|
+
return null;
|
|
975
|
+
}
|
|
976
|
+
/**
|
|
977
|
+
* Extract the project path (e.g. "group/project") from a git repo URL.
|
|
978
|
+
* Supports HTTPS and SSH formats.
|
|
979
|
+
*/
|
|
980
|
+
extractProjectPath(repoUrl) {
|
|
981
|
+
// SSH format: git@host:path.git
|
|
982
|
+
const sshMatch = /^git@[^:]+:(.+?)(?:\.git)?$/.exec(repoUrl);
|
|
983
|
+
if (sshMatch)
|
|
984
|
+
return sshMatch[1];
|
|
985
|
+
// HTTPS format: https://host/path.git or https://host/path
|
|
986
|
+
try {
|
|
987
|
+
const parsed = new URL(repoUrl);
|
|
988
|
+
let path = parsed.pathname;
|
|
989
|
+
path = path.replace(/^\//, '').replace(/\.git$/, '');
|
|
990
|
+
return path || null;
|
|
991
|
+
}
|
|
992
|
+
catch {
|
|
993
|
+
return null;
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
/**
|
|
997
|
+
* Extract the base URL (scheme + host) from a git repo URL.
|
|
998
|
+
*/
|
|
999
|
+
extractGitBaseUrl(repoUrl) {
|
|
1000
|
+
// SSH format: git@host:path -> https://host
|
|
1001
|
+
const sshMatch = /^git@([^:]+):/.exec(repoUrl);
|
|
1002
|
+
if (sshMatch)
|
|
1003
|
+
return `https://${sshMatch[1]}`;
|
|
1004
|
+
// HTTPS format
|
|
1005
|
+
try {
|
|
1006
|
+
const parsed = new URL(repoUrl);
|
|
1007
|
+
return `${parsed.protocol}//${parsed.host}`;
|
|
1008
|
+
}
|
|
1009
|
+
catch {
|
|
1010
|
+
return null;
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
};
|
|
1014
|
+
exports.DispatcherService = DispatcherService;
|
|
1015
|
+
exports.DispatcherService = DispatcherService = DispatcherService_1 = __decorate([
|
|
1016
|
+
(0, common_1.Injectable)(),
|
|
1017
|
+
__metadata("design:paramtypes", [task_repository_1.TaskRepository,
|
|
1018
|
+
machine_repository_1.MachineRepository,
|
|
1019
|
+
project_repository_1.ProjectRepository,
|
|
1020
|
+
project_member_repository_1.ProjectMemberRepository,
|
|
1021
|
+
worker_token_repository_1.WorkerTokenRepository,
|
|
1022
|
+
audit_log_repository_1.AuditLogRepository,
|
|
1023
|
+
developer_repository_1.DeveloperRepository,
|
|
1024
|
+
database_service_1.DatabaseService,
|
|
1025
|
+
auth_service_1.AuthService,
|
|
1026
|
+
crypto_service_1.CryptoService,
|
|
1027
|
+
redis_service_1.RedisService,
|
|
1028
|
+
dedup_service_1.DedupService,
|
|
1029
|
+
scheduler_service_1.SchedulerService,
|
|
1030
|
+
worker_connection_manager_1.WorkerConnectionManager,
|
|
1031
|
+
cleanup_service_1.CleanupService,
|
|
1032
|
+
workspace_repository_1.WorkspaceRepository])
|
|
1033
|
+
], DispatcherService);
|
|
1034
|
+
//# sourceMappingURL=dispatcher.service.js.map
|