@yancyyu/openhermit 1.6.28 → 1.6.30
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-renderer/assets/ProjectEditorOverlay-DsQt4FHy.js +52 -0
- package/dist-renderer/assets/{TeamGraphOverlay-Ba5njic5.js → TeamGraphOverlay-BjZC53xf.js} +1 -1
- package/dist-renderer/assets/{_basePickBy-BvnK-OC1.js → _basePickBy-CrWocIjq.js} +1 -1
- package/dist-renderer/assets/{_baseUniq-DmFYXx9G.js → _baseUniq-B6d8ysWi.js} +1 -1
- package/dist-renderer/assets/{arc-DX4ZQFY4.js → arc-DAIYCFP8.js} +1 -1
- package/dist-renderer/assets/{architectureDiagram-VXUJARFQ-DfYr3vEN.js → architectureDiagram-VXUJARFQ-B3UudXJh.js} +1 -1
- package/dist-renderer/assets/{blockDiagram-VD42YOAC-DuXdVeWn.js → blockDiagram-VD42YOAC-DbptKQ4W.js} +1 -1
- package/dist-renderer/assets/{c4Diagram-YG6GDRKO-Bw2nixXe.js → c4Diagram-YG6GDRKO-C4WQuZpV.js} +1 -1
- package/dist-renderer/assets/channel-DbjZvWii.js +1 -0
- package/dist-renderer/assets/{chunk-4BX2VUAB-DLiNGQoE.js → chunk-4BX2VUAB-Dp7fVpI_.js} +1 -1
- package/dist-renderer/assets/{chunk-55IACEB6-B1L_8VIF.js → chunk-55IACEB6-B8KGfbAy.js} +1 -1
- package/dist-renderer/assets/{chunk-B4BG7PRW-DaZMWKGk.js → chunk-B4BG7PRW-BG1oJrjA.js} +1 -1
- package/dist-renderer/assets/{chunk-DI55MBZ5-ku-dflJG.js → chunk-DI55MBZ5-DRmxNjht.js} +1 -1
- package/dist-renderer/assets/{chunk-FMBD7UC4-DV-mF1dP.js → chunk-FMBD7UC4-D6VLvy16.js} +1 -1
- package/dist-renderer/assets/{chunk-QN33PNHL-ByGcDFQ0.js → chunk-QN33PNHL-DZou1667.js} +1 -1
- package/dist-renderer/assets/{chunk-QZHKN3VN-7dv-Min8.js → chunk-QZHKN3VN-CghmasSh.js} +1 -1
- package/dist-renderer/assets/{chunk-TZMSLE5B-WdXL5fTu.js → chunk-TZMSLE5B-B7apcMPK.js} +1 -1
- package/dist-renderer/assets/classDiagram-2ON5EDUG-D_FGxxsl.js +1 -0
- package/dist-renderer/assets/classDiagram-v2-WZHVMYZB-D_FGxxsl.js +1 -0
- package/dist-renderer/assets/clone-CJ1kxO2J.js +1 -0
- package/dist-renderer/assets/{cose-bilkent-S5V4N54A-CNcsvqPl.js → cose-bilkent-S5V4N54A-05e5uQDp.js} +1 -1
- package/dist-renderer/assets/{dagre-6UL2VRFP-DBNx4qqx.js → dagre-6UL2VRFP-B06bRykF.js} +1 -1
- package/dist-renderer/assets/{diagram-PSM6KHXK-BfVlT6sT.js → diagram-PSM6KHXK-CY7VYQ7c.js} +1 -1
- package/dist-renderer/assets/{diagram-QEK2KX5R-HvVjs0K6.js → diagram-QEK2KX5R-BjKEH7dD.js} +1 -1
- package/dist-renderer/assets/{diagram-S2PKOQOG-DYb_KnWS.js → diagram-S2PKOQOG-Bf4ELS1_.js} +1 -1
- package/dist-renderer/assets/{erDiagram-Q2GNP2WA-Ba-IgI5G.js → erDiagram-Q2GNP2WA-DJ753_L9.js} +1 -1
- package/dist-renderer/assets/{flowDiagram-NV44I4VS-2iDN8Kpj.js → flowDiagram-NV44I4VS-B71S-lC-.js} +1 -1
- package/dist-renderer/assets/{ganttDiagram-JELNMOA3-Byjf8Fa3.js → ganttDiagram-JELNMOA3-C_U42mSZ.js} +1 -1
- package/dist-renderer/assets/{gitGraphDiagram-V2S2FVAM-DbKvfZ_j.js → gitGraphDiagram-V2S2FVAM-DKUJU4Ns.js} +1 -1
- package/dist-renderer/assets/{graph-Enirf-f8.js → graph-DY3qbzqj.js} +1 -1
- package/dist-renderer/assets/{index-DY1zqsb6.js → index-BlOrAXp3.js} +551 -537
- package/dist-renderer/assets/{index-AjxP_rE_.js → index-Bs27J5gB.js} +1 -1
- package/dist-renderer/assets/{index-CtlzGepK.js → index-C8B_nKOF.js} +1 -1
- package/dist-renderer/assets/index-CmZPUEhS.css +1 -0
- package/dist-renderer/assets/{index-COZPUWJW.js → index-DLKyDr4T.js} +1 -1
- package/dist-renderer/assets/{index-DdhqolqE.js → index-Dhsk3_DD.js} +1 -1
- package/dist-renderer/assets/{index-ChR1D6ZF.js → index-GpUvV2xs.js} +1 -1
- package/dist-renderer/assets/{infoDiagram-HS3SLOUP-D6uicwz1.js → infoDiagram-HS3SLOUP-BNs0y3IG.js} +1 -1
- package/dist-renderer/assets/{journeyDiagram-XKPGCS4Q-DqwZsXlQ.js → journeyDiagram-XKPGCS4Q-CqPnw4UV.js} +1 -1
- package/dist-renderer/assets/{kanban-definition-3W4ZIXB7-fCDVhVUm.js → kanban-definition-3W4ZIXB7-SLlzcUJ2.js} +1 -1
- package/dist-renderer/assets/{layout-CPFgj98r.js → layout-BZLlNmbr.js} +1 -1
- package/dist-renderer/assets/{linear-CYiQ7Y3M.js → linear-qz6v45xy.js} +1 -1
- package/dist-renderer/assets/{mindmap-definition-VGOIOE7T-D31dS2KE.js → mindmap-definition-VGOIOE7T-B1-kmEWV.js} +1 -1
- package/dist-renderer/assets/{pieDiagram-ADFJNKIX-BOsCJfds.js → pieDiagram-ADFJNKIX-B8a02iNx.js} +1 -1
- package/dist-renderer/assets/{quadrantDiagram-AYHSOK5B-CYTVQCfr.js → quadrantDiagram-AYHSOK5B-BKv1Xfou.js} +1 -1
- package/dist-renderer/assets/{requirementDiagram-UZGBJVZJ-CODCFpkt.js → requirementDiagram-UZGBJVZJ-B3DUpZi2.js} +1 -1
- package/dist-renderer/assets/{sankeyDiagram-TZEHDZUN-Z4ce9ZtZ.js → sankeyDiagram-TZEHDZUN-DmPzuTsy.js} +1 -1
- package/dist-renderer/assets/{sequenceDiagram-WL72ISMW-CmS9TxhW.js → sequenceDiagram-WL72ISMW-Bo7RelRb.js} +1 -1
- package/dist-renderer/assets/{stateDiagram-FKZM4ZOC-o9k-ns3q.js → stateDiagram-FKZM4ZOC-1epX98gV.js} +1 -1
- package/dist-renderer/assets/{stateDiagram-v2-4FDKWEC3-CxHMyEt1.js → stateDiagram-v2-4FDKWEC3-03Ym9PTr.js} +1 -1
- package/dist-renderer/assets/{timeline-definition-IT6M3QCI-B6T3zrde.js → timeline-definition-IT6M3QCI-r6isC62H.js} +1 -1
- package/dist-renderer/assets/{treemap-GDKQZRPO-CVd5GNDw.js → treemap-GDKQZRPO-CGKpOUF2.js} +1 -1
- package/dist-renderer/assets/{xychartDiagram-PRI3JC2R-CleBrdqc.js → xychartDiagram-PRI3JC2R-t4-rwdAw.js} +1 -1
- package/dist-renderer/index.html +2 -2
- package/package.json +4 -1
- package/src/main/ipc/extensions.ts +353 -0
- package/src/main/server.ts +907 -184
- package/src/main/services/extensions/ExtensionFacadeService.ts +135 -0
- package/src/main/services/extensions/catalog/GlamaMcpEnrichmentService.ts +190 -0
- package/src/main/services/extensions/catalog/McpCatalogAggregator.ts +150 -0
- package/src/main/services/extensions/catalog/OfficialMcpRegistryService.ts +381 -0
- package/src/main/services/extensions/catalog/PluginCatalogService.ts +392 -0
- package/src/main/services/extensions/credentials/CredentialService.ts +343 -0
- package/src/main/services/extensions/install/McpInstallService.ts +407 -0
- package/src/main/services/extensions/install/PluginInstallService.ts +198 -0
- package/src/main/services/extensions/runtime/ClaudeCodeAdapter.ts +199 -0
- package/src/main/services/extensions/runtime/CodexAdapter.ts +100 -0
- package/src/main/services/extensions/runtime/CursorAdapter.ts +154 -0
- package/src/main/services/extensions/runtime/ExtensionsRuntimeAdapter.ts +172 -0
- package/src/main/services/extensions/runtime/GeminiAdapter.ts +91 -0
- package/src/main/services/extensions/runtime/HarnessInstallAdapter.ts +49 -0
- package/src/main/services/extensions/runtime/McpConfigStateReader.ts +209 -0
- package/src/main/services/extensions/runtime/OpenCodeAdapter.ts +91 -0
- package/src/main/services/extensions/runtime/adapterRegistry.ts +54 -0
- package/src/main/services/extensions/runtime/mcpDiagnosticsParser.ts +214 -0
- package/src/main/services/extensions/runtime/mcpRuntimeJson.ts +45 -0
- package/src/main/services/extensions/skills/SkillImportService.ts +155 -0
- package/src/main/services/extensions/skills/SkillMetadataParser.ts +323 -0
- package/src/main/services/extensions/skills/SkillPlanService.ts +411 -0
- package/src/main/services/extensions/skills/SkillReviewService.ts +73 -0
- package/src/main/services/extensions/skills/SkillRootsResolver.ts +49 -0
- package/src/main/services/extensions/skills/SkillScaffoldService.ts +89 -0
- package/src/main/services/extensions/skills/SkillScanner.ts +117 -0
- package/src/main/services/extensions/skills/SkillValidator.ts +69 -0
- package/src/main/services/extensions/skills/SkillsCatalogService.ts +92 -0
- package/src/main/services/extensions/skills/SkillsMutationService.ts +146 -0
- package/src/main/services/extensions/skills/SkillsWatcherService.ts +134 -0
- package/src/main/services/extensions/state/McpInstallationStateService.ts +42 -0
- package/src/main/services/extensions/state/PluginInstallationStateService.ts +281 -0
- package/src/main/services/identity/AgentTeamsIdentityStore.ts +218 -0
- package/src/main/services/runtime/providerAwareCliEnv.ts +60 -0
- package/src/main/services/session-intelligence/UsageTelemetryService.ts +33 -18
- package/src/main/services/team/ClaudeBinaryResolver.ts +469 -0
- package/src/main/services/team/ClaudeDoctorProbe.ts +0 -0
- package/src/main/services/team/cliFlavor.ts +54 -0
- package/src/main/services/teams-mvp/CollaborationBoardService.ts +310 -0
- package/src/main/services/teams-mvp/TaskDispatchService.ts +883 -95
- package/src/main/services/teams-mvp/TeamProvisioningService.ts +58 -19
- package/src/main/services/teams-mvp/TeamWorkspaceService.ts +25 -2
- package/src/main/services/teams-mvp/index.ts +3 -0
- package/src/main/utils/atomicWrite.ts +72 -0
- package/src/main/utils/childProcess.ts +554 -0
- package/src/main/utils/cliEnv.ts +54 -0
- package/src/main/utils/cliPathMerge.ts +97 -0
- package/src/main/utils/pathDecoder.ts +664 -0
- package/src/main/utils/pathValidation.ts +432 -0
- package/src/main/utils/shellEnv.ts +331 -0
- package/src/renderer/App.tsx +5 -0
- package/src/renderer/api/httpClient.ts +128 -0
- package/src/renderer/components/extensions/ExtensionStoreView.tsx +59 -34
- package/src/renderer/components/extensions/ExtensionsSubTabTrigger.tsx +1 -1
- package/src/renderer/components/extensions/common/ExtensionToast.tsx +141 -0
- package/src/renderer/components/extensions/common/HarnessSelector.tsx +71 -0
- package/src/renderer/components/extensions/env/EnvVarPanel.tsx +335 -0
- package/src/renderer/components/extensions/env/ProjectEnvPanel.tsx +239 -0
- package/src/renderer/components/extensions/mcp/CustomMcpServerDialog.tsx +14 -223
- package/src/renderer/components/extensions/mcp/McpServerDetailDialog.tsx +11 -0
- package/src/renderer/components/extensions/mcp/McpServersPanel.tsx +51 -1
- package/src/renderer/components/extensions/skills/SkillsPanel.tsx +1 -126
- package/src/renderer/components/layout/PaneContent.tsx +2 -0
- package/src/renderer/components/layout/SortableTab.tsx +1 -0
- package/src/renderer/components/layout/TabBarActions.tsx +12 -12
- package/src/renderer/components/schedules/SchedulesView.tsx +54 -22
- package/src/renderer/components/settings/sections/AdvancedSection.tsx +1 -1
- package/src/renderer/components/settings/sections/HarnessSection.tsx +2 -6
- package/src/renderer/components/settings/sections/TaskBusSection.tsx +144 -84
- package/src/renderer/components/sidebar/SidebarSessions.tsx +23 -0
- package/src/renderer/components/sidebar/WorkspaceBrowser.tsx +1 -7
- package/src/renderer/components/tasks/TasksView.tsx +343 -0
- package/src/renderer/components/team/HarnessSelect.tsx +71 -0
- package/src/renderer/components/team/TeamDetailView.tsx +55 -98
- package/src/renderer/components/team/dialogs/CreateTeamDialog.tsx +21 -12
- package/src/renderer/components/team/dialogs/EditTeamDialog.tsx +8 -13
- package/src/renderer/components/team/dialogs/LaunchTeamDialog.tsx +1 -1
- package/src/renderer/components/team/editor/EditorContextMenu.tsx +8 -23
- package/src/renderer/components/team/editor/EditorFileTree.tsx +0 -4
- package/src/renderer/components/team/editor/EditorSelectionMenu.tsx +1 -8
- package/src/renderer/components/team/editor/ProjectEditorOverlay.tsx +0 -10
- package/src/renderer/components/team/kanban/KanbanBoard.tsx +31 -65
- package/src/renderer/components/team/members/MemberDetailDialog.tsx +8 -33
- package/src/renderer/components/team/messages/MessageComposer.tsx +39 -3
- package/src/renderer/components/team/messages/MessagesPanel.tsx +100 -26
- package/src/renderer/components/team/messages/StatusBlock.tsx +2 -24
- package/src/renderer/components/team/schedule/ScheduleEmptyState.tsx +1 -1
- package/src/renderer/components/terminal/TerminalPanel.tsx +156 -0
- package/src/renderer/components/ui/MentionableTextarea.tsx +0 -1
- package/src/renderer/hooks/useExtensionsTabState.ts +2 -2
- package/src/renderer/store/slices/extensionsSlice.ts +42 -107
- package/src/renderer/store/slices/scheduleSlice.ts +21 -0
- package/src/renderer/store/slices/teamSlice.ts +67 -25
- package/src/renderer/types/tabs.ts +1 -0
- package/src/shared/types/api.ts +58 -0
- package/src/shared/types/extensions/index.ts +1 -0
- package/src/shared/types/extensions/mcp.ts +2 -0
- package/src/shared/types/extensions/plugin.ts +2 -1
- package/src/shared/types/extensions/skill.ts +7 -0
- package/src/shared/types/team.ts +104 -1
- package/src/shared/utils/providerExtensionCapabilities.ts +1 -1
- package/dist-renderer/assets/ProjectEditorOverlay-A4DZTvSy.js +0 -57
- package/dist-renderer/assets/channel-Pre42N5O.js +0 -1
- package/dist-renderer/assets/classDiagram-2ON5EDUG-CdJsTJsj.js +0 -1
- package/dist-renderer/assets/classDiagram-v2-WZHVMYZB-CdJsTJsj.js +0 -1
- package/dist-renderer/assets/clone-BjQBiNfj.js +0 -1
- package/dist-renderer/assets/index-BIOJremZ.css +0 -1
- package/src/features/recent-projects/main/adapters/input/http/registerRecentProjectsHttp.ts +0 -30
- package/src/features/recent-projects/main/adapters/output/presenters/DashboardRecentProjectsPresenter.ts +0 -27
- package/src/features/recent-projects/main/adapters/output/sources/ClaudeRecentProjectsSourceAdapter.ts +0 -91
- package/src/features/recent-projects/main/adapters/output/sources/CodexRecentProjectsSourceAdapter.ts +0 -326
- package/src/features/recent-projects/main/composition/createRecentProjectsFeature.ts +0 -43
- package/src/features/recent-projects/main/index.ts +0 -3
- package/src/features/recent-projects/main/infrastructure/cache/InMemoryRecentProjectsCache.ts +0 -34
- package/src/features/recent-projects/main/infrastructure/codex/CodexAppServerClient.ts +0 -116
- package/src/features/recent-projects/main/infrastructure/identity/RecentProjectIdentityResolver.ts +0 -20
- package/src/features/recent-projects/main/infrastructure/identity/normalizeIdentityPath.ts +0 -10
- package/src/renderer/components/extensions/apikeys/ApiKeyCard.tsx +0 -143
- package/src/renderer/components/extensions/apikeys/ApiKeyFormDialog.tsx +0 -282
- package/src/renderer/components/extensions/apikeys/ApiKeysPanel.tsx +0 -280
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import type {
|
|
2
|
+
AgentCapability,
|
|
3
|
+
CollabTask,
|
|
2
4
|
DiscoverableTeam,
|
|
3
5
|
DispatchMeta,
|
|
4
6
|
TaskBusConfig,
|
|
5
7
|
TaskDispatchPayload,
|
|
8
|
+
TaskHandshakeResponse,
|
|
6
9
|
TaskStatusUpdate,
|
|
7
10
|
} from '@shared/types/team';
|
|
8
11
|
import type { TeamWorkspaceService, TeamManifest } from './TeamWorkspaceService';
|
|
12
|
+
import type { CollaborationBoardService } from './CollaborationBoardService';
|
|
9
13
|
import type Redis from 'ioredis';
|
|
10
14
|
|
|
11
15
|
const DISPATCH_RULES_DEFAULT = `When to dispatch a task to another team:
|
|
@@ -19,6 +23,14 @@ Do NOT dispatch:
|
|
|
19
23
|
- Task can be completed with available tools
|
|
20
24
|
- Task is a small change (< estimated 5 min)`;
|
|
21
25
|
|
|
26
|
+
interface PendingRequest {
|
|
27
|
+
payload: TaskDispatchPayload;
|
|
28
|
+
msgId: string;
|
|
29
|
+
groupName: string;
|
|
30
|
+
teamSlug: string;
|
|
31
|
+
localTaskId?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
22
34
|
export interface DispatchResult {
|
|
23
35
|
dispatchId: string;
|
|
24
36
|
status: DispatchMeta['status'];
|
|
@@ -28,15 +40,23 @@ export interface DispatchResult {
|
|
|
28
40
|
|
|
29
41
|
export class TaskDispatchService {
|
|
30
42
|
private workspace: TeamWorkspaceService;
|
|
43
|
+
private collabBoard: CollaborationBoardService;
|
|
31
44
|
private config: TaskBusConfig | null = null;
|
|
32
45
|
private redis: Redis | null = null;
|
|
33
46
|
private redisSub: Redis | null = null;
|
|
34
47
|
private heartbeatTimer: ReturnType<typeof setInterval> | null = null;
|
|
35
48
|
private consumerTimers: ReturnType<typeof setInterval>[] = [];
|
|
49
|
+
private responseConsumerTimers: ReturnType<typeof setInterval>[] = [];
|
|
50
|
+
private consumerTeamSlugs = new Set<string>();
|
|
51
|
+
private responseConsumerTeamSlugs = new Set<string>();
|
|
36
52
|
private disposed = false;
|
|
53
|
+
private pendingRequests: Map<string, PendingRequest> = new Map();
|
|
54
|
+
/** Callback fired when collab task state changes (for SSE broadcast). */
|
|
55
|
+
onCollabChange?: (dispatchId: string, status: string, fromTeam: string, toTeam: string) => void;
|
|
37
56
|
|
|
38
|
-
constructor(workspace: TeamWorkspaceService) {
|
|
57
|
+
constructor(workspace: TeamWorkspaceService, collabBoard: CollaborationBoardService) {
|
|
39
58
|
this.workspace = workspace;
|
|
59
|
+
this.collabBoard = collabBoard;
|
|
40
60
|
}
|
|
41
61
|
|
|
42
62
|
get dispatchRulesText(): string {
|
|
@@ -44,9 +64,13 @@ export class TaskDispatchService {
|
|
|
44
64
|
}
|
|
45
65
|
|
|
46
66
|
async start(config?: TaskBusConfig): Promise<void> {
|
|
67
|
+
this.disposed = false;
|
|
47
68
|
this.config = config ?? null;
|
|
48
69
|
if (config?.enabled && config.redis) {
|
|
49
70
|
await this.connectRedis();
|
|
71
|
+
if (!this.redis) {
|
|
72
|
+
throw new Error('Redis connection failed: PING did not succeed');
|
|
73
|
+
}
|
|
50
74
|
}
|
|
51
75
|
}
|
|
52
76
|
|
|
@@ -54,6 +78,7 @@ export class TaskDispatchService {
|
|
|
54
78
|
this.disposed = true;
|
|
55
79
|
this.stopHeartbeat();
|
|
56
80
|
this.stopConsumers();
|
|
81
|
+
this.stopResponseConsumers();
|
|
57
82
|
this.redis?.disconnect();
|
|
58
83
|
this.redisSub?.disconnect();
|
|
59
84
|
this.redis = null;
|
|
@@ -63,9 +88,12 @@ export class TaskDispatchService {
|
|
|
63
88
|
// ── Agent-facing ──────────────────────────────────────────────
|
|
64
89
|
|
|
65
90
|
async listTeams(): Promise<DiscoverableTeam[]> {
|
|
91
|
+
return this.discoverTeams();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async discoverTeams(): Promise<DiscoverableTeam[]> {
|
|
66
95
|
const teams: DiscoverableTeam[] = [];
|
|
67
96
|
|
|
68
|
-
// Local teams
|
|
69
97
|
const localTeams = await this.workspace.listTeams();
|
|
70
98
|
for (const team of localTeams) {
|
|
71
99
|
teams.push({
|
|
@@ -74,26 +102,44 @@ export class TaskDispatchService {
|
|
|
74
102
|
location: 'local',
|
|
75
103
|
status: 'online',
|
|
76
104
|
collaboration: team.collaboration !== false,
|
|
105
|
+
description: team.description,
|
|
106
|
+
harness: team.harness,
|
|
107
|
+
capabilities: this.inferCapabilities(team),
|
|
77
108
|
});
|
|
78
109
|
}
|
|
79
110
|
|
|
80
|
-
// Remote teams (via Redis)
|
|
81
111
|
if (this.redis) {
|
|
82
112
|
try {
|
|
83
113
|
const now = Date.now();
|
|
84
|
-
const staleThreshold = 90_000;
|
|
114
|
+
const staleThreshold = 90_000;
|
|
85
115
|
const entries = await this.redis.zrange('task:teams', 0, -1, 'WITHSCORES');
|
|
86
116
|
const localSlugs = new Set(teams.map((t) => t.slug));
|
|
87
117
|
for (let i = 0; i < entries.length; i += 2) {
|
|
88
118
|
const slug = entries[i] as string;
|
|
89
119
|
const ts = Number(entries[i + 1]);
|
|
90
120
|
if (localSlugs.has(slug)) continue;
|
|
121
|
+
const isOnline = now - ts < staleThreshold;
|
|
122
|
+
|
|
123
|
+
let info: Record<string, string> | null = null;
|
|
124
|
+
try {
|
|
125
|
+
info = (await this.redis!.hgetall(`task:team:info:${slug}`)) as Record<string, string>;
|
|
126
|
+
} catch {
|
|
127
|
+
/* degraded */
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const capabilities = info?.capabilities
|
|
131
|
+
? (JSON.parse(info.capabilities) as AgentCapability[])
|
|
132
|
+
: undefined;
|
|
133
|
+
|
|
91
134
|
teams.push({
|
|
92
135
|
slug,
|
|
93
|
-
displayName: slug,
|
|
136
|
+
displayName: info?.displayName ?? slug,
|
|
94
137
|
location: 'remote',
|
|
95
|
-
status:
|
|
96
|
-
collaboration:
|
|
138
|
+
status: isOnline ? 'online' : 'offline',
|
|
139
|
+
collaboration: info?.collaboration !== 'false',
|
|
140
|
+
description: info?.description || undefined,
|
|
141
|
+
harness: info?.harness || undefined,
|
|
142
|
+
capabilities,
|
|
97
143
|
});
|
|
98
144
|
}
|
|
99
145
|
} catch {
|
|
@@ -107,7 +153,8 @@ export class TaskDispatchService {
|
|
|
107
153
|
async dispatchTask(
|
|
108
154
|
fromTeam: string,
|
|
109
155
|
task: { subject: string; description?: string; prompt?: string },
|
|
110
|
-
targetTeam: string
|
|
156
|
+
targetTeam: string,
|
|
157
|
+
opts?: { deadlineMinutes?: number; needsHumanReview?: boolean }
|
|
111
158
|
): Promise<DispatchResult> {
|
|
112
159
|
if (fromTeam === targetTeam) {
|
|
113
160
|
return {
|
|
@@ -119,35 +166,476 @@ export class TaskDispatchService {
|
|
|
119
166
|
}
|
|
120
167
|
|
|
121
168
|
const dispatchId = crypto.randomUUID();
|
|
169
|
+
const now = new Date();
|
|
170
|
+
const deadline = opts?.deadlineMinutes
|
|
171
|
+
? new Date(now.getTime() + opts.deadlineMinutes * 60_000).toISOString()
|
|
172
|
+
: undefined;
|
|
173
|
+
|
|
122
174
|
const dispatchMeta: DispatchMeta = {
|
|
123
175
|
dispatchId,
|
|
124
176
|
originTeam: fromTeam,
|
|
125
177
|
targetTeam,
|
|
126
|
-
status: '
|
|
127
|
-
dispatchedAt:
|
|
178
|
+
status: 'pending_accept',
|
|
179
|
+
dispatchedAt: now.toISOString(),
|
|
180
|
+
deadline,
|
|
128
181
|
};
|
|
129
182
|
|
|
130
|
-
//
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
183
|
+
// Add to collaboration board before external delivery. Even failed dispatches
|
|
184
|
+
// must remain visible in the canonical task projection for diagnosis/retry.
|
|
185
|
+
const fromTeamManifest = await this.safeReadManifest(fromTeam);
|
|
186
|
+
const toTeamManifest = await this.safeReadManifest(targetTeam);
|
|
187
|
+
const collabTask: CollabTask = {
|
|
188
|
+
id: dispatchId,
|
|
189
|
+
dispatchId,
|
|
190
|
+
subject: task.subject,
|
|
191
|
+
description: task.description,
|
|
192
|
+
fromTeam,
|
|
193
|
+
fromTeamDisplay: fromTeamManifest?.displayName ?? fromTeam,
|
|
194
|
+
toTeam: targetTeam,
|
|
195
|
+
toTeamDisplay: toTeamManifest?.displayName ?? targetTeam,
|
|
196
|
+
status: 'pending_accept',
|
|
197
|
+
deadline,
|
|
198
|
+
needsHumanReview: opts?.needsHumanReview ?? false,
|
|
199
|
+
revisionCount: 0,
|
|
200
|
+
createdAt: now.toISOString(),
|
|
201
|
+
updatedAt: now.toISOString(),
|
|
202
|
+
};
|
|
203
|
+
this.collabBoard.addTask(collabTask);
|
|
204
|
+
|
|
205
|
+
// Route: all dispatches require Redis
|
|
206
|
+
if (!this.redis) {
|
|
207
|
+
const failedTask = this.collabBoard.transition({
|
|
208
|
+
dispatchId,
|
|
209
|
+
expected: 'pending_accept',
|
|
210
|
+
next: 'failed',
|
|
211
|
+
actor: { type: 'system', id: 'task-dispatch' },
|
|
212
|
+
eventType: 'task_failed',
|
|
213
|
+
payload: { reason: 'Redis not configured' },
|
|
214
|
+
extra: { reason: 'Redis not configured — cross-team dispatch requires task bus.' },
|
|
215
|
+
});
|
|
216
|
+
this.emitCollabChange(dispatchId, failedTask.status, fromTeam, targetTeam);
|
|
137
217
|
return {
|
|
138
218
|
dispatchId,
|
|
139
219
|
status: 'failed',
|
|
140
220
|
targetTeam,
|
|
141
|
-
message: 'Redis not configured —
|
|
221
|
+
message: 'Redis not configured — cross-team dispatch requires task bus.',
|
|
142
222
|
};
|
|
143
223
|
}
|
|
144
224
|
|
|
225
|
+
try {
|
|
226
|
+
await this.handleRedisDispatch(dispatchMeta, task, opts?.needsHumanReview);
|
|
227
|
+
} catch (err) {
|
|
228
|
+
this.pendingRequests.delete(dispatchId);
|
|
229
|
+
const reason = err instanceof Error ? err.message : 'Unknown Redis dispatch failure';
|
|
230
|
+
const failedTask = this.collabBoard.transition({
|
|
231
|
+
dispatchId,
|
|
232
|
+
expected: 'pending_accept',
|
|
233
|
+
next: 'failed',
|
|
234
|
+
actor: { type: 'system', id: 'task-dispatch' },
|
|
235
|
+
eventType: 'task_failed',
|
|
236
|
+
payload: { reason },
|
|
237
|
+
extra: { reason },
|
|
238
|
+
});
|
|
239
|
+
this.emitCollabChange(dispatchId, failedTask.status, fromTeam, targetTeam);
|
|
240
|
+
return {
|
|
241
|
+
dispatchId,
|
|
242
|
+
status: 'failed',
|
|
243
|
+
targetTeam,
|
|
244
|
+
message: `Task dispatch failed: ${reason}`,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
this.emitCollabChange(dispatchId, 'pending_accept', fromTeam, targetTeam);
|
|
249
|
+
|
|
145
250
|
return {
|
|
146
251
|
dispatchId,
|
|
147
|
-
status: '
|
|
252
|
+
status: 'pending_accept',
|
|
148
253
|
targetTeam,
|
|
149
|
-
message: `Task dispatched to ${targetTeam}
|
|
254
|
+
message: `Task dispatched to ${targetTeam}, awaiting acceptance.`,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
async acceptTask(teamSlug: string, dispatchId: string): Promise<{ taskId: string }> {
|
|
259
|
+
const pending = this.pendingRequests.get(dispatchId);
|
|
260
|
+
if (!pending) {
|
|
261
|
+
throw new Error(`No pending request found for dispatchId: ${dispatchId}`);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const { payload, msgId, groupName, localTaskId } = pending;
|
|
265
|
+
|
|
266
|
+
const remoteTaskId = localTaskId ?? payload.dispatchId;
|
|
267
|
+
|
|
268
|
+
// Send accept response
|
|
269
|
+
const response: TaskHandshakeResponse = {
|
|
270
|
+
dispatchId: payload.dispatchId,
|
|
271
|
+
type: 'task_accept',
|
|
272
|
+
fromTeam: teamSlug,
|
|
273
|
+
toTeam: payload.originTeam,
|
|
274
|
+
remoteTaskId,
|
|
275
|
+
acceptedAt: new Date().toISOString(),
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
const isLocalOrigin = await this.isLocalTeam(payload.originTeam);
|
|
279
|
+
if (isLocalOrigin) {
|
|
280
|
+
await this.handleLocalResponse(response);
|
|
281
|
+
} else if (this.redis) {
|
|
282
|
+
await this.redis
|
|
283
|
+
.xadd(`task:response:${payload.originTeam}`, '*', 'payload', JSON.stringify(response))
|
|
284
|
+
.catch((err: Error) => {
|
|
285
|
+
console.error('[TaskDispatchService] accept xadd failed:', err.message);
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (this.redis) {
|
|
290
|
+
await this.redis.xack(`task:dispatch:${teamSlug}`, groupName, msgId).catch(() => {});
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
this.pendingRequests.delete(dispatchId);
|
|
294
|
+
|
|
295
|
+
// Update collab board
|
|
296
|
+
const acceptedAt = new Date().toISOString();
|
|
297
|
+
this.collabBoard.transition({
|
|
298
|
+
dispatchId: payload.dispatchId,
|
|
299
|
+
expected: 'pending_accept',
|
|
300
|
+
next: 'accepted',
|
|
301
|
+
actor: { type: 'team', id: teamSlug },
|
|
302
|
+
eventType: 'task_accepted',
|
|
303
|
+
payload: { remoteTaskId },
|
|
304
|
+
extra: { acceptedAt },
|
|
305
|
+
});
|
|
306
|
+
this.emitCollabChange(payload.dispatchId, 'accepted', payload.originTeam, payload.targetTeam);
|
|
307
|
+
|
|
308
|
+
// Fixed flow: notify receiving agent to start executing
|
|
309
|
+
try {
|
|
310
|
+
await this.workspace.appendMessage(teamSlug, {
|
|
311
|
+
from: 'system',
|
|
312
|
+
to: 'team',
|
|
313
|
+
role: 'agent',
|
|
314
|
+
content: `[跨团队任务已确认] "${payload.task.subject}" — 来自 ${payload.originTeam} 的任务已被人工确认接单,请开始执行。任务描述:${payload.task.description ?? '无'}`,
|
|
315
|
+
meta: {
|
|
316
|
+
source: 'cross_team_accepted',
|
|
317
|
+
dispatchId: payload.dispatchId,
|
|
318
|
+
originTeam: payload.originTeam,
|
|
319
|
+
taskId: remoteTaskId,
|
|
320
|
+
},
|
|
321
|
+
});
|
|
322
|
+
} catch {}
|
|
323
|
+
|
|
324
|
+
// Fixed flow: notify originating agent that task was accepted
|
|
325
|
+
try {
|
|
326
|
+
await this.workspace.appendMessage(payload.originTeam, {
|
|
327
|
+
from: 'system',
|
|
328
|
+
to: 'team',
|
|
329
|
+
role: 'agent',
|
|
330
|
+
content: `[跨团队任务已接单] "${payload.task.subject}" — ${teamSlug} 已确认接单,正在执行中。`,
|
|
331
|
+
meta: {
|
|
332
|
+
source: 'cross_team_accepted_notify',
|
|
333
|
+
dispatchId: payload.dispatchId,
|
|
334
|
+
targetTeam: teamSlug,
|
|
335
|
+
},
|
|
336
|
+
});
|
|
337
|
+
} catch {}
|
|
338
|
+
|
|
339
|
+
// Feishu notification
|
|
340
|
+
this.sendFeishuNotification(
|
|
341
|
+
`跨团队任务已接单:${payload.originTeam} → ${teamSlug}\n${payload.task.subject}\n状态:执行中`
|
|
342
|
+
);
|
|
343
|
+
|
|
344
|
+
return { taskId: remoteTaskId };
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
async rejectTask(teamSlug: string, dispatchId: string, reason?: string): Promise<void> {
|
|
348
|
+
const pending = this.pendingRequests.get(dispatchId);
|
|
349
|
+
if (!pending) {
|
|
350
|
+
throw new Error(`No pending request found for dispatchId: ${dispatchId}`);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const { payload, msgId, groupName } = pending;
|
|
354
|
+
|
|
355
|
+
const response: TaskHandshakeResponse = {
|
|
356
|
+
dispatchId: payload.dispatchId,
|
|
357
|
+
type: 'task_reject',
|
|
358
|
+
fromTeam: teamSlug,
|
|
359
|
+
toTeam: payload.originTeam,
|
|
360
|
+
reason,
|
|
361
|
+
rejectedAt: new Date().toISOString(),
|
|
150
362
|
};
|
|
363
|
+
|
|
364
|
+
const isLocalOrigin = await this.isLocalTeam(payload.originTeam);
|
|
365
|
+
if (isLocalOrigin) {
|
|
366
|
+
await this.handleLocalResponse(response);
|
|
367
|
+
} else if (this.redis) {
|
|
368
|
+
await this.redis
|
|
369
|
+
.xadd(`task:response:${payload.originTeam}`, '*', 'payload', JSON.stringify(response))
|
|
370
|
+
.catch((err: Error) => {
|
|
371
|
+
console.error('[TaskDispatchService] reject xadd failed:', err.message);
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (this.redis) {
|
|
376
|
+
await this.redis.xack(`task:dispatch:${teamSlug}`, groupName, msgId).catch(() => {});
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
this.pendingRequests.delete(dispatchId);
|
|
380
|
+
|
|
381
|
+
// Update collab board
|
|
382
|
+
this.collabBoard.transition({
|
|
383
|
+
dispatchId: payload.dispatchId,
|
|
384
|
+
expected: 'pending_accept',
|
|
385
|
+
next: 'rejected',
|
|
386
|
+
actor: { type: 'team', id: teamSlug },
|
|
387
|
+
eventType: 'task_rejected',
|
|
388
|
+
payload: { reason },
|
|
389
|
+
extra: { reason, rejectedAt: response.rejectedAt },
|
|
390
|
+
});
|
|
391
|
+
this.emitCollabChange(payload.dispatchId, 'rejected', payload.originTeam, payload.targetTeam);
|
|
392
|
+
|
|
393
|
+
// Fixed flow: notify originating agent that task was rejected
|
|
394
|
+
try {
|
|
395
|
+
await this.workspace.appendMessage(payload.originTeam, {
|
|
396
|
+
from: 'system',
|
|
397
|
+
to: 'team',
|
|
398
|
+
role: 'agent',
|
|
399
|
+
content: `[跨团队任务被拒绝] "${payload.task.subject}" — ${teamSlug} 拒绝了此任务。原因:${reason ?? '未说明'}`,
|
|
400
|
+
meta: {
|
|
401
|
+
source: 'cross_team_rejected',
|
|
402
|
+
dispatchId: payload.dispatchId,
|
|
403
|
+
targetTeam: teamSlug,
|
|
404
|
+
rejectReason: reason,
|
|
405
|
+
},
|
|
406
|
+
});
|
|
407
|
+
} catch {}
|
|
408
|
+
|
|
409
|
+
// Feishu notification
|
|
410
|
+
this.sendFeishuNotification(
|
|
411
|
+
`跨团队任务被拒绝:${payload.originTeam} → ${teamSlug}\n${payload.task.subject}\n原因:${reason ?? '未说明'}`
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// ── Deliver / Approve / Revision ────────────────────────────────
|
|
416
|
+
|
|
417
|
+
async deliverTask(
|
|
418
|
+
teamSlug: string,
|
|
419
|
+
dispatchId: string,
|
|
420
|
+
result: string
|
|
421
|
+
): Promise<{ ok: boolean }> {
|
|
422
|
+
const collabTask = this.collabBoard.getTask(dispatchId);
|
|
423
|
+
if (!collabTask) {
|
|
424
|
+
throw new Error(`No collab task found for dispatchId: ${dispatchId}`);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const deliveredAt = new Date().toISOString();
|
|
428
|
+
|
|
429
|
+
// Send deliver response to origin team
|
|
430
|
+
const response: TaskHandshakeResponse = {
|
|
431
|
+
dispatchId,
|
|
432
|
+
type: 'task_deliver',
|
|
433
|
+
fromTeam: teamSlug,
|
|
434
|
+
toTeam: collabTask.fromTeam,
|
|
435
|
+
result,
|
|
436
|
+
deliveredAt,
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
const isLocalOrigin = await this.isLocalTeam(collabTask.fromTeam);
|
|
440
|
+
if (isLocalOrigin) {
|
|
441
|
+
await this.handleLocalResponse(response);
|
|
442
|
+
} else if (this.redis) {
|
|
443
|
+
await this.redis
|
|
444
|
+
.xadd(`task:response:${collabTask.fromTeam}`, '*', 'payload', JSON.stringify(response))
|
|
445
|
+
.catch((err: Error) => {
|
|
446
|
+
console.error('[TaskDispatchService] deliver xadd failed:', err.message);
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// Update local collab board
|
|
451
|
+
const deliveredTask = this.collabBoard.transition({
|
|
452
|
+
dispatchId,
|
|
453
|
+
expected: ['accepted', 'revision'],
|
|
454
|
+
next: 'delivered',
|
|
455
|
+
actor: { type: 'team', id: teamSlug },
|
|
456
|
+
eventType: 'task_delivered',
|
|
457
|
+
payload: { summary: result.slice(0, 1000) },
|
|
458
|
+
extra: { result, deliveredAt },
|
|
459
|
+
});
|
|
460
|
+
this.emitCollabChange(dispatchId, 'delivered', deliveredTask.fromTeam, deliveredTask.toTeam);
|
|
461
|
+
|
|
462
|
+
// Notify origin agent: task is ready for review
|
|
463
|
+
try {
|
|
464
|
+
await this.workspace.appendMessage(collabTask.fromTeam, {
|
|
465
|
+
from: 'system',
|
|
466
|
+
to: 'team',
|
|
467
|
+
role: 'agent',
|
|
468
|
+
content: `[跨团队任务待审核] "${collabTask.subject}" — ${teamSlug} 已完成任务并提交交付结果,请审核。结果:${result}`,
|
|
469
|
+
meta: {
|
|
470
|
+
source: 'cross_team_delivered',
|
|
471
|
+
dispatchId,
|
|
472
|
+
targetTeam: teamSlug,
|
|
473
|
+
result,
|
|
474
|
+
},
|
|
475
|
+
});
|
|
476
|
+
} catch {}
|
|
477
|
+
|
|
478
|
+
this.sendFeishuNotification(
|
|
479
|
+
`跨团队任务待审核:${collabTask.fromTeam} ← ${teamSlug}\n${collabTask.subject}\n状态:待审核`
|
|
480
|
+
);
|
|
481
|
+
|
|
482
|
+
return { ok: true };
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
async approveTask(teamSlug: string, dispatchId: string): Promise<{ ok: boolean }> {
|
|
486
|
+
const collabTask = this.collabBoard.getTask(dispatchId);
|
|
487
|
+
if (!collabTask) {
|
|
488
|
+
throw new Error(`No collab task found for dispatchId: ${dispatchId}`);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
const approvedAt = new Date().toISOString();
|
|
492
|
+
|
|
493
|
+
// Send approve response to target team
|
|
494
|
+
const response: TaskHandshakeResponse = {
|
|
495
|
+
dispatchId,
|
|
496
|
+
type: 'task_approve',
|
|
497
|
+
fromTeam: teamSlug,
|
|
498
|
+
toTeam: collabTask.toTeam,
|
|
499
|
+
approvedAt,
|
|
500
|
+
};
|
|
501
|
+
|
|
502
|
+
const isLocalTarget = await this.isLocalTeam(collabTask.toTeam);
|
|
503
|
+
if (isLocalTarget) {
|
|
504
|
+
await this.handleLocalResponse(response);
|
|
505
|
+
} else if (this.redis) {
|
|
506
|
+
await this.redis
|
|
507
|
+
.xadd(`task:response:${collabTask.toTeam}`, '*', 'payload', JSON.stringify(response))
|
|
508
|
+
.catch((err: Error) => {
|
|
509
|
+
console.error('[TaskDispatchService] approve xadd failed:', err.message);
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// Update collab board
|
|
514
|
+
const approvedTask = this.collabBoard.transition({
|
|
515
|
+
dispatchId,
|
|
516
|
+
expected: 'delivered',
|
|
517
|
+
next: 'approved',
|
|
518
|
+
actor: { type: 'team', id: teamSlug },
|
|
519
|
+
eventType: 'task_approved',
|
|
520
|
+
payload: { approvedAt },
|
|
521
|
+
extra: { approvedAt },
|
|
522
|
+
});
|
|
523
|
+
this.emitCollabChange(dispatchId, 'approved', approvedTask.fromTeam, approvedTask.toTeam);
|
|
524
|
+
|
|
525
|
+
// Notify origin agent: task is fully complete, you can continue
|
|
526
|
+
try {
|
|
527
|
+
await this.workspace.appendMessage(collabTask.fromTeam, {
|
|
528
|
+
from: 'system',
|
|
529
|
+
to: 'team',
|
|
530
|
+
role: 'agent',
|
|
531
|
+
content: `[跨团队任务已完成] "${collabTask.subject}" — ${collabTask.toTeam} 的交付已通过审核,此跨团队任务结束。`,
|
|
532
|
+
meta: {
|
|
533
|
+
source: 'cross_team_approved',
|
|
534
|
+
dispatchId,
|
|
535
|
+
targetTeam: collabTask.toTeam,
|
|
536
|
+
},
|
|
537
|
+
});
|
|
538
|
+
} catch {}
|
|
539
|
+
|
|
540
|
+
// Notify target agent: approved
|
|
541
|
+
try {
|
|
542
|
+
await this.workspace.appendMessage(collabTask.toTeam, {
|
|
543
|
+
from: 'system',
|
|
544
|
+
to: 'team',
|
|
545
|
+
role: 'agent',
|
|
546
|
+
content: `[跨团队任务审核通过] "${collabTask.subject}" — ${teamSlug} 已通过审核,任务完成。`,
|
|
547
|
+
meta: {
|
|
548
|
+
source: 'cross_team_approved_target',
|
|
549
|
+
dispatchId,
|
|
550
|
+
originTeam: teamSlug,
|
|
551
|
+
},
|
|
552
|
+
});
|
|
553
|
+
} catch {}
|
|
554
|
+
|
|
555
|
+
this.sendFeishuNotification(
|
|
556
|
+
`跨团队任务完成:${collabTask.fromTeam} ← ${collabTask.toTeam}\n${collabTask.subject}\n状态:已完成`
|
|
557
|
+
);
|
|
558
|
+
|
|
559
|
+
return { ok: true };
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
async rejectResult(
|
|
563
|
+
teamSlug: string,
|
|
564
|
+
dispatchId: string,
|
|
565
|
+
feedback: string
|
|
566
|
+
): Promise<{ ok: boolean }> {
|
|
567
|
+
const collabTask = this.collabBoard.getTask(dispatchId);
|
|
568
|
+
if (!collabTask) {
|
|
569
|
+
throw new Error(`No collab task found for dispatchId: ${dispatchId}`);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
const newRevisionCount = collabTask.revisionCount + 1;
|
|
573
|
+
|
|
574
|
+
// Send revision response to target team
|
|
575
|
+
const response: TaskHandshakeResponse = {
|
|
576
|
+
dispatchId,
|
|
577
|
+
type: 'task_revision',
|
|
578
|
+
fromTeam: teamSlug,
|
|
579
|
+
toTeam: collabTask.toTeam,
|
|
580
|
+
feedback,
|
|
581
|
+
};
|
|
582
|
+
|
|
583
|
+
const isLocalTarget = await this.isLocalTeam(collabTask.toTeam);
|
|
584
|
+
if (isLocalTarget) {
|
|
585
|
+
await this.handleLocalResponse(response);
|
|
586
|
+
} else if (this.redis) {
|
|
587
|
+
await this.redis
|
|
588
|
+
.xadd(`task:response:${collabTask.toTeam}`, '*', 'payload', JSON.stringify(response))
|
|
589
|
+
.catch((err: Error) => {
|
|
590
|
+
console.error('[TaskDispatchService] revision xadd failed:', err.message);
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// Update collab board
|
|
595
|
+
const revisionTask = this.collabBoard.transition({
|
|
596
|
+
dispatchId,
|
|
597
|
+
expected: 'delivered',
|
|
598
|
+
next: 'revision',
|
|
599
|
+
actor: { type: 'team', id: teamSlug },
|
|
600
|
+
eventType: 'revision_requested',
|
|
601
|
+
payload: {
|
|
602
|
+
feedback,
|
|
603
|
+
previousResult: collabTask.result,
|
|
604
|
+
revisionCount: newRevisionCount,
|
|
605
|
+
},
|
|
606
|
+
extra: {
|
|
607
|
+
feedback,
|
|
608
|
+
revisionCount: newRevisionCount,
|
|
609
|
+
},
|
|
610
|
+
});
|
|
611
|
+
this.emitCollabChange(dispatchId, 'revision', revisionTask.fromTeam, revisionTask.toTeam);
|
|
612
|
+
|
|
613
|
+
return { ok: true };
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
/** Get the collaboration board. */
|
|
617
|
+
getCollabBoard() {
|
|
618
|
+
return this.collabBoard.getBoard();
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
/** Get a single collab task. */
|
|
622
|
+
getCollabTask(dispatchId: string) {
|
|
623
|
+
return this.collabBoard.getTask(dispatchId);
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
/** Get event log for a collab task. */
|
|
627
|
+
getCollabTaskEvents(dispatchId: string) {
|
|
628
|
+
return this.collabBoard.getEvents(dispatchId);
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
listPendingRequests(teamSlug: string): TaskDispatchPayload[] {
|
|
632
|
+
const results: TaskDispatchPayload[] = [];
|
|
633
|
+
for (const [, req] of this.pendingRequests) {
|
|
634
|
+
if (req.teamSlug === teamSlug) {
|
|
635
|
+
results.push(req.payload);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
return results;
|
|
151
639
|
}
|
|
152
640
|
|
|
153
641
|
async onTaskCompleted(teamSlug: string, taskId: string): Promise<void> {
|
|
@@ -165,12 +653,10 @@ export class TaskDispatchService {
|
|
|
165
653
|
result: task.result ?? undefined,
|
|
166
654
|
};
|
|
167
655
|
|
|
168
|
-
// Update local dispatchMeta
|
|
169
656
|
await this.workspace.patchTask(teamSlug, taskId, {
|
|
170
657
|
dispatchMeta: { ...meta, status: 'completed', completedAt: update.timestamp },
|
|
171
658
|
} as any);
|
|
172
659
|
|
|
173
|
-
// Notify origin team
|
|
174
660
|
if (this.redis) {
|
|
175
661
|
const channel = `task:status:${meta.originTeam}`;
|
|
176
662
|
await this.redis.publish(channel, JSON.stringify(update)).catch((err: Error) => {
|
|
@@ -179,7 +665,7 @@ export class TaskDispatchService {
|
|
|
179
665
|
}
|
|
180
666
|
}
|
|
181
667
|
|
|
182
|
-
// ── Local
|
|
668
|
+
// ── Local team check ─────────────────────────────────────────
|
|
183
669
|
|
|
184
670
|
private async isLocalTeam(teamSlug: string): Promise<boolean> {
|
|
185
671
|
try {
|
|
@@ -190,21 +676,84 @@ export class TaskDispatchService {
|
|
|
190
676
|
}
|
|
191
677
|
}
|
|
192
678
|
|
|
193
|
-
|
|
679
|
+
// ── Unified Redis dispatch ───────────────────────────────────────
|
|
680
|
+
|
|
681
|
+
private async handleRedisDispatch(
|
|
194
682
|
dispatchMeta: DispatchMeta,
|
|
195
|
-
task: { subject: string; description?: string; prompt?: string }
|
|
683
|
+
task: { subject: string; description?: string; prompt?: string },
|
|
684
|
+
needsHumanReview?: boolean
|
|
196
685
|
): Promise<void> {
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
|
|
686
|
+
const payload: TaskDispatchPayload = {
|
|
687
|
+
dispatchId: dispatchMeta.dispatchId,
|
|
688
|
+
originTeam: dispatchMeta.originTeam,
|
|
689
|
+
targetTeam: dispatchMeta.targetTeam,
|
|
690
|
+
task: { subject: task.subject, description: task.description, prompt: task.prompt },
|
|
691
|
+
dispatchedAt: dispatchMeta.dispatchedAt,
|
|
692
|
+
deadline: dispatchMeta.deadline,
|
|
693
|
+
needsHumanReview,
|
|
694
|
+
};
|
|
695
|
+
|
|
696
|
+
// Store in memory for accept/reject
|
|
697
|
+
this.pendingRequests.set(dispatchMeta.dispatchId, {
|
|
698
|
+
payload,
|
|
699
|
+
msgId: `redis-${Date.now()}`,
|
|
700
|
+
groupName: 'dispatch-group',
|
|
701
|
+
teamSlug: dispatchMeta.targetTeam,
|
|
200
702
|
});
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
703
|
+
|
|
704
|
+
// Publish to Redis stream
|
|
705
|
+
const streamKey = `task:dispatch:${dispatchMeta.targetTeam}`;
|
|
706
|
+
await this.redis!.xadd(streamKey, '*', 'payload', JSON.stringify(payload));
|
|
707
|
+
|
|
708
|
+
// If local team, also write to inbox
|
|
709
|
+
const isLocal = await this.isLocalTeam(dispatchMeta.targetTeam);
|
|
710
|
+
if (isLocal) {
|
|
711
|
+
try {
|
|
712
|
+
await this.workspace.appendMessage(dispatchMeta.targetTeam, {
|
|
713
|
+
from: dispatchMeta.originTeam,
|
|
714
|
+
to: 'team',
|
|
715
|
+
role: 'agent',
|
|
716
|
+
content: `[跨团队任务] ${task.subject}${task.description ? '\n' + task.description : ''}`,
|
|
717
|
+
meta: {
|
|
718
|
+
source: 'cross_team_dispatch',
|
|
719
|
+
dispatchId: dispatchMeta.dispatchId,
|
|
720
|
+
originTeam: dispatchMeta.originTeam,
|
|
721
|
+
needsHumanReview,
|
|
722
|
+
},
|
|
723
|
+
});
|
|
724
|
+
} catch (err) {
|
|
725
|
+
console.error('[TaskDispatchService] inbox write failed:', (err as Error).message);
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
this.sendFeishuNotification(
|
|
730
|
+
`跨团队任务派发:${dispatchMeta.originTeam} → ${dispatchMeta.targetTeam}\n${task.subject}`
|
|
731
|
+
);
|
|
205
732
|
}
|
|
206
733
|
|
|
207
|
-
// ──
|
|
734
|
+
// ── Feishu notification helper ──────────────────────────────────
|
|
735
|
+
|
|
736
|
+
private sendFeishuNotification(text: string): void {
|
|
737
|
+
setTimeout(() => {
|
|
738
|
+
try {
|
|
739
|
+
const { execSync } = require('node:child_process');
|
|
740
|
+
execSync(
|
|
741
|
+
`feishu-cli msg send --receive-id-type chat_id --receive-id oc_e7d4204895f8f9d763d9f0e42ead1e5e --text ${JSON.stringify(text)}`,
|
|
742
|
+
{ timeout: 5000, stdio: 'pipe' }
|
|
743
|
+
);
|
|
744
|
+
} catch {
|
|
745
|
+
// best effort
|
|
746
|
+
}
|
|
747
|
+
}, 0);
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
// ── Local response (same machine) ─────────────────────────────
|
|
751
|
+
|
|
752
|
+
private async handleLocalResponse(response: TaskHandshakeResponse): Promise<void> {
|
|
753
|
+
await this.applyResponse(response);
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
// ── Redis connection ──────────────────────────────────────────
|
|
208
757
|
|
|
209
758
|
private async connectRedis(): Promise<void> {
|
|
210
759
|
if (!this.config?.redis) return;
|
|
@@ -222,8 +771,10 @@ export class TaskDispatchService {
|
|
|
222
771
|
|
|
223
772
|
await this.redis.ping();
|
|
224
773
|
|
|
774
|
+
this.collabBoard.setRedis(this.redis);
|
|
225
775
|
this.startHeartbeat();
|
|
226
776
|
this.startConsumers();
|
|
777
|
+
this.startResponseConsumers();
|
|
227
778
|
this.subscribeStatus();
|
|
228
779
|
} catch (err) {
|
|
229
780
|
console.error('[TaskDispatchService] Redis connect failed:', err);
|
|
@@ -232,27 +783,7 @@ export class TaskDispatchService {
|
|
|
232
783
|
}
|
|
233
784
|
}
|
|
234
785
|
|
|
235
|
-
|
|
236
|
-
dispatchMeta: DispatchMeta,
|
|
237
|
-
task: { subject: string; description?: string; prompt?: string }
|
|
238
|
-
): Promise<void> {
|
|
239
|
-
const payload: TaskDispatchPayload = {
|
|
240
|
-
dispatchId: dispatchMeta.dispatchId,
|
|
241
|
-
originTeam: dispatchMeta.originTeam,
|
|
242
|
-
targetTeam: dispatchMeta.targetTeam,
|
|
243
|
-
task: {
|
|
244
|
-
subject: task.subject,
|
|
245
|
-
description: task.description,
|
|
246
|
-
prompt: task.prompt,
|
|
247
|
-
},
|
|
248
|
-
dispatchedAt: dispatchMeta.dispatchedAt,
|
|
249
|
-
};
|
|
250
|
-
|
|
251
|
-
const streamKey = `task:dispatch:${dispatchMeta.targetTeam}`;
|
|
252
|
-
await this.redis!.xadd(streamKey, '*', 'payload', JSON.stringify(payload));
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
// ── Heartbeat ─────────────────────────────────────────────────
|
|
786
|
+
// ── Heartbeat + agent info ────────────────────────────────────
|
|
256
787
|
|
|
257
788
|
private startHeartbeat(): void {
|
|
258
789
|
this.stopHeartbeat();
|
|
@@ -262,7 +793,21 @@ export class TaskDispatchService {
|
|
|
262
793
|
const localTeams = await this.workspace.listTeams();
|
|
263
794
|
for (const team of localTeams) {
|
|
264
795
|
await this.redis.zadd('task:teams', now, team.slug).catch(() => {});
|
|
796
|
+
await this.redis
|
|
797
|
+
.hset(`task:team:info:${team.slug}`, {
|
|
798
|
+
slug: team.slug,
|
|
799
|
+
displayName: team.displayName ?? team.slug,
|
|
800
|
+
harness: team.harness,
|
|
801
|
+
description: team.description ?? '',
|
|
802
|
+
capabilities: JSON.stringify(this.inferCapabilities(team)),
|
|
803
|
+
collaboration: String(team.collaboration !== false),
|
|
804
|
+
updatedAt: new Date().toISOString(),
|
|
805
|
+
})
|
|
806
|
+
.catch(() => {});
|
|
265
807
|
}
|
|
808
|
+
|
|
809
|
+
// Check deadline timeouts
|
|
810
|
+
await this.checkDeadlines(localTeams);
|
|
266
811
|
};
|
|
267
812
|
beat();
|
|
268
813
|
this.heartbeatTimer = setInterval(beat, 30_000);
|
|
@@ -275,21 +820,47 @@ export class TaskDispatchService {
|
|
|
275
820
|
}
|
|
276
821
|
}
|
|
277
822
|
|
|
278
|
-
|
|
823
|
+
private async checkDeadlines(localTeams: TeamManifest[]): Promise<void> {
|
|
824
|
+
for (const team of localTeams) {
|
|
825
|
+
try {
|
|
826
|
+
const tasks = await this.workspace.readTasks(team.slug);
|
|
827
|
+
for (const task of tasks) {
|
|
828
|
+
if (
|
|
829
|
+
task.dispatchMeta?.status === 'pending_accept' &&
|
|
830
|
+
task.dispatchMeta.deadline &&
|
|
831
|
+
new Date(task.dispatchMeta.deadline).getTime() < Date.now()
|
|
832
|
+
) {
|
|
833
|
+
await this.workspace.patchTask(team.slug, task.id, {
|
|
834
|
+
dispatchMeta: {
|
|
835
|
+
...task.dispatchMeta,
|
|
836
|
+
status: 'failed',
|
|
837
|
+
rejectionReason: 'handshake timeout',
|
|
838
|
+
},
|
|
839
|
+
} as any);
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
} catch {
|
|
843
|
+
/* skip broken teams */
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
// ── Dispatch consumers (XREADGROUP) ───────────────────────────
|
|
279
849
|
|
|
280
850
|
private startConsumers(): void {
|
|
281
851
|
if (!this.redis || !this.redisSub) return;
|
|
282
852
|
|
|
283
853
|
const startForTeam = async (teamSlug: string) => {
|
|
854
|
+
if (this.consumerTeamSlugs.has(teamSlug)) return;
|
|
855
|
+
this.consumerTeamSlugs.add(teamSlug);
|
|
284
856
|
const streamKey = `task:dispatch:${teamSlug}`;
|
|
285
857
|
const groupName = `hermit-${teamSlug}`;
|
|
286
858
|
const consumerId = `consumer-${process.pid}`;
|
|
287
859
|
|
|
288
|
-
// Create consumer group (MKSTREAM creates stream if missing)
|
|
289
860
|
try {
|
|
290
861
|
await this.redis!.xgroup('CREATE', streamKey, groupName, '0', 'MKSTREAM');
|
|
291
862
|
} catch {
|
|
292
|
-
// Group already exists
|
|
863
|
+
// Group already exists
|
|
293
864
|
}
|
|
294
865
|
|
|
295
866
|
const poll = async () => {
|
|
@@ -322,18 +893,19 @@ export class TaskDispatchService {
|
|
|
322
893
|
}
|
|
323
894
|
};
|
|
324
895
|
|
|
325
|
-
// Initial poll, then interval
|
|
326
896
|
poll();
|
|
327
897
|
const timer = setInterval(poll, 5000);
|
|
328
898
|
this.consumerTimers.push(timer);
|
|
329
899
|
};
|
|
330
900
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
901
|
+
const syncConsumers = () =>
|
|
902
|
+
void this.workspace.listTeams().then((teams) => {
|
|
903
|
+
for (const team of teams) {
|
|
904
|
+
void startForTeam(team.slug);
|
|
905
|
+
}
|
|
906
|
+
});
|
|
907
|
+
syncConsumers();
|
|
908
|
+
this.consumerTimers.push(setInterval(syncConsumers, 10_000));
|
|
337
909
|
}
|
|
338
910
|
|
|
339
911
|
private async handleIncomingDispatch(
|
|
@@ -343,44 +915,87 @@ export class TaskDispatchService {
|
|
|
343
915
|
groupName: string
|
|
344
916
|
): Promise<void> {
|
|
345
917
|
try {
|
|
346
|
-
// fields is [key, value, key, value, ...]
|
|
347
918
|
const payloadStr = fields[1]?.toString();
|
|
348
919
|
if (!payloadStr) return;
|
|
349
920
|
|
|
350
921
|
const payload: TaskDispatchPayload = JSON.parse(payloadStr);
|
|
922
|
+
const alreadyPending = this.pendingRequests.has(payload.dispatchId);
|
|
351
923
|
|
|
352
|
-
|
|
353
|
-
const
|
|
354
|
-
|
|
924
|
+
const fromTeamManifest = await this.safeReadManifest(payload.originTeam);
|
|
925
|
+
const toTeamManifest = await this.safeReadManifest(teamSlug);
|
|
926
|
+
const createdAt = payload.dispatchedAt || new Date().toISOString();
|
|
927
|
+
this.collabBoard.addTask({
|
|
928
|
+
id: payload.dispatchId,
|
|
929
|
+
dispatchId: payload.dispatchId,
|
|
930
|
+
subject: payload.task.subject,
|
|
355
931
|
description: payload.task.description,
|
|
932
|
+
fromTeam: payload.originTeam,
|
|
933
|
+
fromTeamDisplay: fromTeamManifest?.displayName ?? payload.originTeam,
|
|
934
|
+
toTeam: teamSlug,
|
|
935
|
+
toTeamDisplay: toTeamManifest?.displayName ?? teamSlug,
|
|
936
|
+
status: 'pending_accept',
|
|
937
|
+
deadline: payload.deadline,
|
|
938
|
+
needsHumanReview: payload.needsHumanReview ?? false,
|
|
939
|
+
revisionCount: 0,
|
|
940
|
+
createdAt,
|
|
941
|
+
updatedAt: createdAt,
|
|
356
942
|
});
|
|
943
|
+
this.emitCollabChange(payload.dispatchId, 'pending_accept', payload.originTeam, teamSlug);
|
|
357
944
|
|
|
358
|
-
const
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
945
|
+
const existingTasks = await this.workspace.readTasks(teamSlug).catch(() => []);
|
|
946
|
+
const existingTask = existingTasks.find(
|
|
947
|
+
(task) => task.dispatchMeta?.dispatchId === payload.dispatchId
|
|
948
|
+
);
|
|
949
|
+
const localTask =
|
|
950
|
+
existingTask ??
|
|
951
|
+
(await this.workspace.createTask(teamSlug, {
|
|
952
|
+
title: payload.task.subject,
|
|
953
|
+
description: payload.task.description ?? payload.task.prompt ?? '',
|
|
954
|
+
status: 'todo',
|
|
955
|
+
dispatchMeta: {
|
|
956
|
+
dispatchId: payload.dispatchId,
|
|
957
|
+
originTeam: payload.originTeam,
|
|
958
|
+
targetTeam: teamSlug,
|
|
959
|
+
status: 'pending_accept',
|
|
960
|
+
dispatchedAt: payload.dispatchedAt,
|
|
961
|
+
receivedAt: new Date().toISOString(),
|
|
962
|
+
deadline: payload.deadline,
|
|
963
|
+
},
|
|
964
|
+
}));
|
|
367
965
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
966
|
+
// Store in pending requests — wait for a human to accept/reject the agent-created dispatch.
|
|
967
|
+
this.pendingRequests.set(payload.dispatchId, {
|
|
968
|
+
payload,
|
|
969
|
+
msgId,
|
|
970
|
+
groupName,
|
|
971
|
+
teamSlug,
|
|
972
|
+
localTaskId: localTask.id,
|
|
973
|
+
});
|
|
371
974
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
975
|
+
if (!alreadyPending) {
|
|
976
|
+
await this.workspace
|
|
977
|
+
.appendMessage(teamSlug, {
|
|
978
|
+
from: payload.originTeam,
|
|
979
|
+
to: 'team',
|
|
980
|
+
role: 'agent',
|
|
981
|
+
content: `[跨团队任务] ${payload.task.subject}${
|
|
982
|
+
payload.task.description ? '\n' + payload.task.description : ''
|
|
983
|
+
}`,
|
|
984
|
+
meta: {
|
|
985
|
+
source: 'cross_team_dispatch',
|
|
986
|
+
dispatchId: payload.dispatchId,
|
|
987
|
+
originTeam: payload.originTeam,
|
|
988
|
+
needsHumanReview: payload.needsHumanReview,
|
|
989
|
+
},
|
|
990
|
+
})
|
|
991
|
+
.catch((err: Error) => {
|
|
992
|
+
console.error('[TaskDispatchService] inbox write failed:', err.message);
|
|
993
|
+
});
|
|
383
994
|
}
|
|
995
|
+
|
|
996
|
+
console.log(
|
|
997
|
+
`[TaskDispatchService] received dispatch request: ${payload.dispatchId} from ${payload.originTeam} → ${teamSlug}`
|
|
998
|
+
);
|
|
384
999
|
} catch (err) {
|
|
385
1000
|
console.error('[TaskDispatchService] handleIncomingDispatch error:', err);
|
|
386
1001
|
}
|
|
@@ -389,9 +1004,155 @@ export class TaskDispatchService {
|
|
|
389
1004
|
private stopConsumers(): void {
|
|
390
1005
|
for (const t of this.consumerTimers) clearInterval(t);
|
|
391
1006
|
this.consumerTimers = [];
|
|
1007
|
+
this.consumerTeamSlugs.clear();
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
// ── Response consumers (XREADGROUP) ───────────────────────────
|
|
1011
|
+
|
|
1012
|
+
private startResponseConsumers(): void {
|
|
1013
|
+
if (!this.redis || !this.redisSub) return;
|
|
1014
|
+
|
|
1015
|
+
const startForTeam = async (teamSlug: string) => {
|
|
1016
|
+
if (this.responseConsumerTeamSlugs.has(teamSlug)) return;
|
|
1017
|
+
this.responseConsumerTeamSlugs.add(teamSlug);
|
|
1018
|
+
const streamKey = `task:response:${teamSlug}`;
|
|
1019
|
+
const groupName = `hermit-response-${teamSlug}`;
|
|
1020
|
+
const consumerId = `response-consumer-${process.pid}`;
|
|
1021
|
+
|
|
1022
|
+
try {
|
|
1023
|
+
await this.redis!.xgroup('CREATE', streamKey, groupName, '0', 'MKSTREAM');
|
|
1024
|
+
} catch {
|
|
1025
|
+
// Group already exists
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
const poll = async () => {
|
|
1029
|
+
if (this.disposed || !this.redisSub) return;
|
|
1030
|
+
try {
|
|
1031
|
+
const raw: unknown = await (this.redisSub as any).xreadgroup(
|
|
1032
|
+
'GROUP',
|
|
1033
|
+
groupName,
|
|
1034
|
+
consumerId,
|
|
1035
|
+
'BLOCK',
|
|
1036
|
+
5000,
|
|
1037
|
+
'COUNT',
|
|
1038
|
+
1,
|
|
1039
|
+
'STREAMS',
|
|
1040
|
+
streamKey,
|
|
1041
|
+
'>'
|
|
1042
|
+
);
|
|
1043
|
+
const results = raw as [string, [string, (string | Buffer)[]][]][] | null;
|
|
1044
|
+
|
|
1045
|
+
if (!results || results.length === 0) return;
|
|
1046
|
+
|
|
1047
|
+
for (const [, messages] of results) {
|
|
1048
|
+
if (!Array.isArray(messages)) continue;
|
|
1049
|
+
for (const [msgId, fields] of messages) {
|
|
1050
|
+
await this.handleIncomingResponse(teamSlug, msgId, fields, groupName);
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
} catch {
|
|
1054
|
+
// Read error — will retry next poll
|
|
1055
|
+
}
|
|
1056
|
+
};
|
|
1057
|
+
|
|
1058
|
+
poll();
|
|
1059
|
+
const timer = setInterval(poll, 5000);
|
|
1060
|
+
this.responseConsumerTimers.push(timer);
|
|
1061
|
+
};
|
|
1062
|
+
|
|
1063
|
+
const syncConsumers = () =>
|
|
1064
|
+
void this.workspace.listTeams().then((teams) => {
|
|
1065
|
+
for (const team of teams) {
|
|
1066
|
+
void startForTeam(team.slug);
|
|
1067
|
+
}
|
|
1068
|
+
});
|
|
1069
|
+
syncConsumers();
|
|
1070
|
+
this.responseConsumerTimers.push(setInterval(syncConsumers, 10_000));
|
|
392
1071
|
}
|
|
393
1072
|
|
|
394
|
-
|
|
1073
|
+
private async handleIncomingResponse(
|
|
1074
|
+
_teamSlug: string,
|
|
1075
|
+
msgId: string,
|
|
1076
|
+
fields: (string | Buffer)[],
|
|
1077
|
+
groupName: string
|
|
1078
|
+
): Promise<void> {
|
|
1079
|
+
try {
|
|
1080
|
+
const payloadStr = fields[1]?.toString();
|
|
1081
|
+
if (!payloadStr) return;
|
|
1082
|
+
|
|
1083
|
+
const response: TaskHandshakeResponse = JSON.parse(payloadStr);
|
|
1084
|
+
await this.applyResponse(response);
|
|
1085
|
+
|
|
1086
|
+
// ACK
|
|
1087
|
+
if (this.redis) {
|
|
1088
|
+
await this.redis.xack(`task:response:${_teamSlug}`, groupName, msgId).catch(() => {});
|
|
1089
|
+
}
|
|
1090
|
+
} catch (err) {
|
|
1091
|
+
console.error('[TaskDispatchService] handleIncomingResponse error:', err);
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
private async applyResponse(response: TaskHandshakeResponse): Promise<void> {
|
|
1096
|
+
const originTeam = response.toTeam;
|
|
1097
|
+
const tasks = await this.workspace.readTasks(originTeam);
|
|
1098
|
+
const shadowTask = tasks.find((t) => t.dispatchMeta?.dispatchId === response.dispatchId);
|
|
1099
|
+
if (!shadowTask) return;
|
|
1100
|
+
|
|
1101
|
+
const meta = { ...shadowTask.dispatchMeta! };
|
|
1102
|
+
|
|
1103
|
+
if (response.type === 'task_accept') {
|
|
1104
|
+
meta.status = 'accepted';
|
|
1105
|
+
meta.acceptedAt = response.acceptedAt;
|
|
1106
|
+
meta.remoteTaskId = response.remoteTaskId;
|
|
1107
|
+
// Collab board update already done in acceptTask
|
|
1108
|
+
} else if (response.type === 'task_reject') {
|
|
1109
|
+
meta.status = 'rejected';
|
|
1110
|
+
meta.rejectedAt = response.rejectedAt;
|
|
1111
|
+
meta.rejectionReason = response.reason;
|
|
1112
|
+
// Collab board update already done in rejectTask
|
|
1113
|
+
} else if (response.type === 'task_deliver') {
|
|
1114
|
+
meta.status = 'completed';
|
|
1115
|
+
meta.completedAt = response.deliveredAt;
|
|
1116
|
+
await this.workspace.patchTask(originTeam, shadowTask.id, {
|
|
1117
|
+
dispatchMeta: meta,
|
|
1118
|
+
} as any);
|
|
1119
|
+
|
|
1120
|
+
// Auto-approve if no human review needed
|
|
1121
|
+
const collabTask = this.collabBoard.getTask(response.dispatchId);
|
|
1122
|
+
if (collabTask && !collabTask.needsHumanReview && collabTask.status === 'delivered') {
|
|
1123
|
+
const approvedAt = new Date().toISOString();
|
|
1124
|
+
this.collabBoard.transition({
|
|
1125
|
+
dispatchId: response.dispatchId,
|
|
1126
|
+
expected: 'delivered',
|
|
1127
|
+
next: 'approved',
|
|
1128
|
+
actor: { type: 'system', id: 'auto-approve' },
|
|
1129
|
+
eventType: 'task_approved',
|
|
1130
|
+
payload: { auto: true },
|
|
1131
|
+
extra: { approvedAt },
|
|
1132
|
+
});
|
|
1133
|
+
this.emitCollabChange(response.dispatchId, 'approved', response.fromTeam, response.toTeam);
|
|
1134
|
+
}
|
|
1135
|
+
return;
|
|
1136
|
+
} else if (response.type === 'task_approve') {
|
|
1137
|
+
// Received by target team — already handled in approveTask
|
|
1138
|
+
return;
|
|
1139
|
+
} else if (response.type === 'task_revision') {
|
|
1140
|
+
// Received by target team — already handled in rejectResult
|
|
1141
|
+
return;
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
await this.workspace.patchTask(originTeam, shadowTask.id, {
|
|
1145
|
+
dispatchMeta: meta,
|
|
1146
|
+
} as any);
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
private stopResponseConsumers(): void {
|
|
1150
|
+
for (const t of this.responseConsumerTimers) clearInterval(t);
|
|
1151
|
+
this.responseConsumerTimers = [];
|
|
1152
|
+
this.responseConsumerTeamSlugs.clear();
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
// ── Status subscribe (completion notifications) ──────────────
|
|
395
1156
|
|
|
396
1157
|
private subscribeStatus(): void {
|
|
397
1158
|
if (!this.redisSub) return;
|
|
@@ -407,14 +1168,11 @@ export class TaskDispatchService {
|
|
|
407
1168
|
});
|
|
408
1169
|
|
|
409
1170
|
this.redisSub.on('message', (channel: string, message: string) => {
|
|
410
|
-
// channel format: task:status:{teamSlug}
|
|
411
1171
|
if (!channel.startsWith('task:status:')) return;
|
|
412
1172
|
|
|
413
1173
|
try {
|
|
414
1174
|
const update: TaskStatusUpdate = JSON.parse(message);
|
|
415
1175
|
const teamSlug = channel.replace('task:status:', '');
|
|
416
|
-
|
|
417
|
-
// Find shadow task (dispatched from this team) and update status
|
|
418
1176
|
this.handleStatusSync(teamSlug, update);
|
|
419
1177
|
} catch {
|
|
420
1178
|
// Ignore malformed messages
|
|
@@ -437,4 +1195,34 @@ export class TaskDispatchService {
|
|
|
437
1195
|
},
|
|
438
1196
|
} as any);
|
|
439
1197
|
}
|
|
1198
|
+
|
|
1199
|
+
// ── Capability inference ──────────────────────────────────────
|
|
1200
|
+
|
|
1201
|
+
private inferCapabilities(team: TeamManifest): AgentCapability[] {
|
|
1202
|
+
const caps: AgentCapability[] = [];
|
|
1203
|
+
if (team.harness) {
|
|
1204
|
+
caps.push({ skill: team.harness, description: `${team.harness} agent` });
|
|
1205
|
+
}
|
|
1206
|
+
if (team.description) {
|
|
1207
|
+
caps.push({ skill: 'general', description: team.description });
|
|
1208
|
+
}
|
|
1209
|
+
return caps;
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
private async safeReadManifest(teamSlug: string): Promise<TeamManifest | null> {
|
|
1213
|
+
try {
|
|
1214
|
+
return await this.workspace.readTeamManifest(teamSlug);
|
|
1215
|
+
} catch {
|
|
1216
|
+
return null;
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
private emitCollabChange(
|
|
1221
|
+
dispatchId: string,
|
|
1222
|
+
status: string,
|
|
1223
|
+
fromTeam: string,
|
|
1224
|
+
toTeam: string
|
|
1225
|
+
): void {
|
|
1226
|
+
this.onCollabChange?.(dispatchId, status, fromTeam, toTeam);
|
|
1227
|
+
}
|
|
440
1228
|
}
|