@yancyyu/openhermit 1.6.27 → 1.6.29
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/README.md +7 -1
- package/bin/hermit.mjs +2 -2
- package/dist-renderer/assets/ProjectEditorOverlay-CQm6jUR1.js +52 -0
- package/dist-renderer/assets/{TeamGraphOverlay-DVq8rt6_.js → TeamGraphOverlay-h0WDfifv.js} +1 -1
- package/dist-renderer/assets/{_basePickBy-ZbF0pKvS.js → _basePickBy-CgG_tjgX.js} +1 -1
- package/dist-renderer/assets/{_baseUniq-BBLBOeXc.js → _baseUniq-DwPTU9lP.js} +1 -1
- package/dist-renderer/assets/{arc-wGaEgkCf.js → arc-7nIrGRzY.js} +1 -1
- package/dist-renderer/assets/{architectureDiagram-VXUJARFQ-BpMkdC35.js → architectureDiagram-VXUJARFQ-BYhA6Ev2.js} +1 -1
- package/dist-renderer/assets/{blockDiagram-VD42YOAC-C8Z1xhG4.js → blockDiagram-VD42YOAC-BVpZUGDg.js} +1 -1
- package/dist-renderer/assets/{c4Diagram-YG6GDRKO-CJmlw9LA.js → c4Diagram-YG6GDRKO-DsdreMQ9.js} +1 -1
- package/dist-renderer/assets/channel-C0SqeFU7.js +1 -0
- package/dist-renderer/assets/{chunk-4BX2VUAB-CHPHiRPP.js → chunk-4BX2VUAB-CcoAs7Jd.js} +1 -1
- package/dist-renderer/assets/{chunk-55IACEB6-DyVohOQb.js → chunk-55IACEB6-CGGAOoXd.js} +1 -1
- package/dist-renderer/assets/{chunk-B4BG7PRW-p5bffh_R.js → chunk-B4BG7PRW-FhpTEPvD.js} +1 -1
- package/dist-renderer/assets/{chunk-DI55MBZ5-BnfGPSUu.js → chunk-DI55MBZ5-DoYySbm1.js} +1 -1
- package/dist-renderer/assets/{chunk-FMBD7UC4-B6SCKseX.js → chunk-FMBD7UC4-e9l2tGHG.js} +1 -1
- package/dist-renderer/assets/{chunk-QN33PNHL-L12RvLBR.js → chunk-QN33PNHL-DeiXVTCy.js} +1 -1
- package/dist-renderer/assets/{chunk-QZHKN3VN-DeH1Kxge.js → chunk-QZHKN3VN-DC2UJLJM.js} +1 -1
- package/dist-renderer/assets/{chunk-TZMSLE5B-BWnjzSlI.js → chunk-TZMSLE5B-BHFD9eZI.js} +1 -1
- package/dist-renderer/assets/classDiagram-2ON5EDUG-DWew1HpM.js +1 -0
- package/dist-renderer/assets/classDiagram-v2-WZHVMYZB-DWew1HpM.js +1 -0
- package/dist-renderer/assets/clone-Dm-k63Yr.js +1 -0
- package/dist-renderer/assets/{cose-bilkent-S5V4N54A-BtzoT5fu.js → cose-bilkent-S5V4N54A-BdybQraU.js} +1 -1
- package/dist-renderer/assets/{dagre-6UL2VRFP-CBBvuoUD.js → dagre-6UL2VRFP-DdF3pwM3.js} +1 -1
- package/dist-renderer/assets/{diagram-PSM6KHXK-Be9BAKws.js → diagram-PSM6KHXK-B9Ldd3nh.js} +1 -1
- package/dist-renderer/assets/{diagram-QEK2KX5R-BDS4PI_i.js → diagram-QEK2KX5R-XEqkrbpu.js} +1 -1
- package/dist-renderer/assets/{diagram-S2PKOQOG-2Rameaq7.js → diagram-S2PKOQOG-CipwtY59.js} +1 -1
- package/dist-renderer/assets/{erDiagram-Q2GNP2WA-CSIzCEZD.js → erDiagram-Q2GNP2WA-BB-2ISGo.js} +1 -1
- package/dist-renderer/assets/{flowDiagram-NV44I4VS-ForEIVM5.js → flowDiagram-NV44I4VS-B8XmJ0u2.js} +1 -1
- package/dist-renderer/assets/{ganttDiagram-JELNMOA3-BJrli_xr.js → ganttDiagram-JELNMOA3-D-8XglBb.js} +1 -1
- package/dist-renderer/assets/{gitGraphDiagram-V2S2FVAM-C_4GuLno.js → gitGraphDiagram-V2S2FVAM-DL4ChakD.js} +1 -1
- package/dist-renderer/assets/{graph-B1EAT_gw.js → graph-BiFNoBjP.js} +1 -1
- package/dist-renderer/assets/{index-eKRmS5kI.js → index-6m1ZAymG.js} +1 -1
- package/dist-renderer/assets/index-BhellmRb.css +1 -0
- package/dist-renderer/assets/{index-DYdseEwc.js → index-BowUl0Jb.js} +518 -514
- package/dist-renderer/assets/{index-DR602dwJ.js → index-Dp3kJTEe.js} +1 -1
- package/dist-renderer/assets/{index-Dwr5wu5x.js → index-TOpt_T7A.js} +1 -1
- package/dist-renderer/assets/{index-DOA_jbYb.js → index-qNBNjW4K.js} +1 -1
- package/dist-renderer/assets/{index-k4tnOFC5.js → index-vAykq1H1.js} +1 -1
- package/dist-renderer/assets/{infoDiagram-HS3SLOUP-DjI0uaMz.js → infoDiagram-HS3SLOUP-DRIBfHDi.js} +1 -1
- package/dist-renderer/assets/{journeyDiagram-XKPGCS4Q-jQ6Thae-.js → journeyDiagram-XKPGCS4Q-BOMiigU4.js} +1 -1
- package/dist-renderer/assets/{kanban-definition-3W4ZIXB7-CKw6InbL.js → kanban-definition-3W4ZIXB7-DDxeyjod.js} +1 -1
- package/dist-renderer/assets/{layout-Dad20y3V.js → layout-DNANbrI4.js} +1 -1
- package/dist-renderer/assets/{linear-vMgo_2Cv.js → linear-DxEJi1yT.js} +1 -1
- package/dist-renderer/assets/{mindmap-definition-VGOIOE7T-DYp6YoHL.js → mindmap-definition-VGOIOE7T-nBfGriW8.js} +1 -1
- package/dist-renderer/assets/{pieDiagram-ADFJNKIX-BytBecG9.js → pieDiagram-ADFJNKIX-Din5j6sV.js} +1 -1
- package/dist-renderer/assets/{quadrantDiagram-AYHSOK5B-RUaspLsc.js → quadrantDiagram-AYHSOK5B-DMVK2BEQ.js} +1 -1
- package/dist-renderer/assets/{requirementDiagram-UZGBJVZJ-rR2B1Use.js → requirementDiagram-UZGBJVZJ-6SC94Gg_.js} +1 -1
- package/dist-renderer/assets/{sankeyDiagram-TZEHDZUN-BJi5qYhq.js → sankeyDiagram-TZEHDZUN-CD2gghhu.js} +1 -1
- package/dist-renderer/assets/{sequenceDiagram-WL72ISMW-BM-wggUb.js → sequenceDiagram-WL72ISMW-BnhkN7nZ.js} +1 -1
- package/dist-renderer/assets/{stateDiagram-FKZM4ZOC-BqmcVjnj.js → stateDiagram-FKZM4ZOC-Bn8XdYX-.js} +1 -1
- package/dist-renderer/assets/{stateDiagram-v2-4FDKWEC3-By3JDVbB.js → stateDiagram-v2-4FDKWEC3-1b6sI1_g.js} +1 -1
- package/dist-renderer/assets/{timeline-definition-IT6M3QCI-szH0GUyk.js → timeline-definition-IT6M3QCI-CNs3RPoa.js} +1 -1
- package/dist-renderer/assets/treemap-GDKQZRPO-DU_yr827.js +162 -0
- package/dist-renderer/assets/{xychartDiagram-PRI3JC2R-dwDpvw0w.js → xychartDiagram-PRI3JC2R-B8o5J2f3.js} +1 -1
- package/dist-renderer/index.html +2 -2
- package/package.json +1 -1
- package/src/main/server.ts +800 -163
- package/src/main/services/session-intelligence/SessionUsageParser.ts +446 -0
- package/src/main/services/session-intelligence/UsageTelemetryService.ts +252 -0
- package/src/main/services/teams-mvp/CollaborationBoardService.ts +310 -0
- package/src/main/services/teams-mvp/TaskDispatchService.ts +880 -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/renderer/App.tsx +5 -0
- package/src/renderer/api/httpClient.ts +67 -0
- package/src/renderer/components/dashboard/DashboardView.tsx +6 -105
- 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/SettingsTabs.tsx +2 -2
- package/src/renderer/components/settings/sections/AdvancedSection.tsx +1 -1
- package/src/renderer/components/settings/sections/TaskBusSection.tsx +511 -81
- package/src/renderer/components/tasks/TasksView.tsx +343 -0
- package/src/renderer/components/team/TeamDetailView.tsx +20 -98
- 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 +5 -1
- 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 +72 -2
- package/src/renderer/components/team/messages/StatusBlock.tsx +2 -24
- package/src/renderer/components/team/schedule/ScheduleEmptyState.tsx +1 -1
- package/src/renderer/components/ui/MentionableTextarea.tsx +0 -1
- package/src/renderer/store/slices/scheduleSlice.ts +21 -0
- package/src/renderer/store/slices/teamSlice.ts +59 -23
- package/src/renderer/types/tabs.ts +1 -0
- package/src/shared/types/api.ts +29 -0
- package/src/shared/types/team.ts +109 -1
- package/dist-renderer/assets/ProjectEditorOverlay-BBwYdXPv.js +0 -57
- package/dist-renderer/assets/channel-DJUrwVrK.js +0 -1
- package/dist-renderer/assets/classDiagram-2ON5EDUG-blc3DrH7.js +0 -1
- package/dist-renderer/assets/classDiagram-v2-WZHVMYZB-blc3DrH7.js +0 -1
- package/dist-renderer/assets/clone-BftjWakJ.js +0 -1
- package/dist-renderer/assets/index-CWpFqEvz.css +0 -1
- package/dist-renderer/assets/treemap-GDKQZRPO-BCMlh-Ex.js +0 -162
|
@@ -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,6 +64,7 @@ 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();
|
|
@@ -54,6 +75,7 @@ export class TaskDispatchService {
|
|
|
54
75
|
this.disposed = true;
|
|
55
76
|
this.stopHeartbeat();
|
|
56
77
|
this.stopConsumers();
|
|
78
|
+
this.stopResponseConsumers();
|
|
57
79
|
this.redis?.disconnect();
|
|
58
80
|
this.redisSub?.disconnect();
|
|
59
81
|
this.redis = null;
|
|
@@ -63,9 +85,12 @@ export class TaskDispatchService {
|
|
|
63
85
|
// ── Agent-facing ──────────────────────────────────────────────
|
|
64
86
|
|
|
65
87
|
async listTeams(): Promise<DiscoverableTeam[]> {
|
|
88
|
+
return this.discoverTeams();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async discoverTeams(): Promise<DiscoverableTeam[]> {
|
|
66
92
|
const teams: DiscoverableTeam[] = [];
|
|
67
93
|
|
|
68
|
-
// Local teams
|
|
69
94
|
const localTeams = await this.workspace.listTeams();
|
|
70
95
|
for (const team of localTeams) {
|
|
71
96
|
teams.push({
|
|
@@ -74,26 +99,44 @@ export class TaskDispatchService {
|
|
|
74
99
|
location: 'local',
|
|
75
100
|
status: 'online',
|
|
76
101
|
collaboration: team.collaboration !== false,
|
|
102
|
+
description: team.description,
|
|
103
|
+
harness: team.harness,
|
|
104
|
+
capabilities: this.inferCapabilities(team),
|
|
77
105
|
});
|
|
78
106
|
}
|
|
79
107
|
|
|
80
|
-
// Remote teams (via Redis)
|
|
81
108
|
if (this.redis) {
|
|
82
109
|
try {
|
|
83
110
|
const now = Date.now();
|
|
84
|
-
const staleThreshold = 90_000;
|
|
111
|
+
const staleThreshold = 90_000;
|
|
85
112
|
const entries = await this.redis.zrange('task:teams', 0, -1, 'WITHSCORES');
|
|
86
113
|
const localSlugs = new Set(teams.map((t) => t.slug));
|
|
87
114
|
for (let i = 0; i < entries.length; i += 2) {
|
|
88
115
|
const slug = entries[i] as string;
|
|
89
116
|
const ts = Number(entries[i + 1]);
|
|
90
117
|
if (localSlugs.has(slug)) continue;
|
|
118
|
+
const isOnline = now - ts < staleThreshold;
|
|
119
|
+
|
|
120
|
+
let info: Record<string, string> | null = null;
|
|
121
|
+
try {
|
|
122
|
+
info = (await this.redis!.hgetall(`task:team:info:${slug}`)) as Record<string, string>;
|
|
123
|
+
} catch {
|
|
124
|
+
/* degraded */
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const capabilities = info?.capabilities
|
|
128
|
+
? (JSON.parse(info.capabilities) as AgentCapability[])
|
|
129
|
+
: undefined;
|
|
130
|
+
|
|
91
131
|
teams.push({
|
|
92
132
|
slug,
|
|
93
|
-
displayName: slug,
|
|
133
|
+
displayName: info?.displayName ?? slug,
|
|
94
134
|
location: 'remote',
|
|
95
|
-
status:
|
|
96
|
-
collaboration:
|
|
135
|
+
status: isOnline ? 'online' : 'offline',
|
|
136
|
+
collaboration: info?.collaboration !== 'false',
|
|
137
|
+
description: info?.description || undefined,
|
|
138
|
+
harness: info?.harness || undefined,
|
|
139
|
+
capabilities,
|
|
97
140
|
});
|
|
98
141
|
}
|
|
99
142
|
} catch {
|
|
@@ -107,7 +150,8 @@ export class TaskDispatchService {
|
|
|
107
150
|
async dispatchTask(
|
|
108
151
|
fromTeam: string,
|
|
109
152
|
task: { subject: string; description?: string; prompt?: string },
|
|
110
|
-
targetTeam: string
|
|
153
|
+
targetTeam: string,
|
|
154
|
+
opts?: { deadlineMinutes?: number; needsHumanReview?: boolean }
|
|
111
155
|
): Promise<DispatchResult> {
|
|
112
156
|
if (fromTeam === targetTeam) {
|
|
113
157
|
return {
|
|
@@ -119,35 +163,476 @@ export class TaskDispatchService {
|
|
|
119
163
|
}
|
|
120
164
|
|
|
121
165
|
const dispatchId = crypto.randomUUID();
|
|
166
|
+
const now = new Date();
|
|
167
|
+
const deadline = opts?.deadlineMinutes
|
|
168
|
+
? new Date(now.getTime() + opts.deadlineMinutes * 60_000).toISOString()
|
|
169
|
+
: undefined;
|
|
170
|
+
|
|
122
171
|
const dispatchMeta: DispatchMeta = {
|
|
123
172
|
dispatchId,
|
|
124
173
|
originTeam: fromTeam,
|
|
125
174
|
targetTeam,
|
|
126
|
-
status: '
|
|
127
|
-
dispatchedAt:
|
|
175
|
+
status: 'pending_accept',
|
|
176
|
+
dispatchedAt: now.toISOString(),
|
|
177
|
+
deadline,
|
|
128
178
|
};
|
|
129
179
|
|
|
130
|
-
//
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
180
|
+
// Add to collaboration board before external delivery. Even failed dispatches
|
|
181
|
+
// must remain visible in the canonical task projection for diagnosis/retry.
|
|
182
|
+
const fromTeamManifest = await this.safeReadManifest(fromTeam);
|
|
183
|
+
const toTeamManifest = await this.safeReadManifest(targetTeam);
|
|
184
|
+
const collabTask: CollabTask = {
|
|
185
|
+
id: dispatchId,
|
|
186
|
+
dispatchId,
|
|
187
|
+
subject: task.subject,
|
|
188
|
+
description: task.description,
|
|
189
|
+
fromTeam,
|
|
190
|
+
fromTeamDisplay: fromTeamManifest?.displayName ?? fromTeam,
|
|
191
|
+
toTeam: targetTeam,
|
|
192
|
+
toTeamDisplay: toTeamManifest?.displayName ?? targetTeam,
|
|
193
|
+
status: 'pending_accept',
|
|
194
|
+
deadline,
|
|
195
|
+
needsHumanReview: opts?.needsHumanReview ?? false,
|
|
196
|
+
revisionCount: 0,
|
|
197
|
+
createdAt: now.toISOString(),
|
|
198
|
+
updatedAt: now.toISOString(),
|
|
199
|
+
};
|
|
200
|
+
this.collabBoard.addTask(collabTask);
|
|
201
|
+
|
|
202
|
+
// Route: all dispatches require Redis
|
|
203
|
+
if (!this.redis) {
|
|
204
|
+
const failedTask = this.collabBoard.transition({
|
|
205
|
+
dispatchId,
|
|
206
|
+
expected: 'pending_accept',
|
|
207
|
+
next: 'failed',
|
|
208
|
+
actor: { type: 'system', id: 'task-dispatch' },
|
|
209
|
+
eventType: 'task_failed',
|
|
210
|
+
payload: { reason: 'Redis not configured' },
|
|
211
|
+
extra: { reason: 'Redis not configured — cross-team dispatch requires task bus.' },
|
|
212
|
+
});
|
|
213
|
+
this.emitCollabChange(dispatchId, failedTask.status, fromTeam, targetTeam);
|
|
137
214
|
return {
|
|
138
215
|
dispatchId,
|
|
139
216
|
status: 'failed',
|
|
140
217
|
targetTeam,
|
|
141
|
-
message: 'Redis not configured —
|
|
218
|
+
message: 'Redis not configured — cross-team dispatch requires task bus.',
|
|
142
219
|
};
|
|
143
220
|
}
|
|
144
221
|
|
|
222
|
+
try {
|
|
223
|
+
await this.handleRedisDispatch(dispatchMeta, task, opts?.needsHumanReview);
|
|
224
|
+
} catch (err) {
|
|
225
|
+
this.pendingRequests.delete(dispatchId);
|
|
226
|
+
const reason = err instanceof Error ? err.message : 'Unknown Redis dispatch failure';
|
|
227
|
+
const failedTask = this.collabBoard.transition({
|
|
228
|
+
dispatchId,
|
|
229
|
+
expected: 'pending_accept',
|
|
230
|
+
next: 'failed',
|
|
231
|
+
actor: { type: 'system', id: 'task-dispatch' },
|
|
232
|
+
eventType: 'task_failed',
|
|
233
|
+
payload: { reason },
|
|
234
|
+
extra: { reason },
|
|
235
|
+
});
|
|
236
|
+
this.emitCollabChange(dispatchId, failedTask.status, fromTeam, targetTeam);
|
|
237
|
+
return {
|
|
238
|
+
dispatchId,
|
|
239
|
+
status: 'failed',
|
|
240
|
+
targetTeam,
|
|
241
|
+
message: `Task dispatch failed: ${reason}`,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
this.emitCollabChange(dispatchId, 'pending_accept', fromTeam, targetTeam);
|
|
246
|
+
|
|
145
247
|
return {
|
|
146
248
|
dispatchId,
|
|
147
|
-
status: '
|
|
249
|
+
status: 'pending_accept',
|
|
148
250
|
targetTeam,
|
|
149
|
-
message: `Task dispatched to ${targetTeam}
|
|
251
|
+
message: `Task dispatched to ${targetTeam}, awaiting acceptance.`,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
async acceptTask(teamSlug: string, dispatchId: string): Promise<{ taskId: string }> {
|
|
256
|
+
const pending = this.pendingRequests.get(dispatchId);
|
|
257
|
+
if (!pending) {
|
|
258
|
+
throw new Error(`No pending request found for dispatchId: ${dispatchId}`);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const { payload, msgId, groupName, localTaskId } = pending;
|
|
262
|
+
|
|
263
|
+
const remoteTaskId = localTaskId ?? payload.dispatchId;
|
|
264
|
+
|
|
265
|
+
// Send accept response
|
|
266
|
+
const response: TaskHandshakeResponse = {
|
|
267
|
+
dispatchId: payload.dispatchId,
|
|
268
|
+
type: 'task_accept',
|
|
269
|
+
fromTeam: teamSlug,
|
|
270
|
+
toTeam: payload.originTeam,
|
|
271
|
+
remoteTaskId,
|
|
272
|
+
acceptedAt: new Date().toISOString(),
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
const isLocalOrigin = await this.isLocalTeam(payload.originTeam);
|
|
276
|
+
if (isLocalOrigin) {
|
|
277
|
+
await this.handleLocalResponse(response);
|
|
278
|
+
} else if (this.redis) {
|
|
279
|
+
await this.redis
|
|
280
|
+
.xadd(`task:response:${payload.originTeam}`, '*', 'payload', JSON.stringify(response))
|
|
281
|
+
.catch((err: Error) => {
|
|
282
|
+
console.error('[TaskDispatchService] accept xadd failed:', err.message);
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (this.redis) {
|
|
287
|
+
await this.redis.xack(`task:dispatch:${teamSlug}`, groupName, msgId).catch(() => {});
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
this.pendingRequests.delete(dispatchId);
|
|
291
|
+
|
|
292
|
+
// Update collab board
|
|
293
|
+
const acceptedAt = new Date().toISOString();
|
|
294
|
+
this.collabBoard.transition({
|
|
295
|
+
dispatchId: payload.dispatchId,
|
|
296
|
+
expected: 'pending_accept',
|
|
297
|
+
next: 'accepted',
|
|
298
|
+
actor: { type: 'team', id: teamSlug },
|
|
299
|
+
eventType: 'task_accepted',
|
|
300
|
+
payload: { remoteTaskId },
|
|
301
|
+
extra: { acceptedAt },
|
|
302
|
+
});
|
|
303
|
+
this.emitCollabChange(payload.dispatchId, 'accepted', payload.originTeam, payload.targetTeam);
|
|
304
|
+
|
|
305
|
+
// Fixed flow: notify receiving agent to start executing
|
|
306
|
+
try {
|
|
307
|
+
await this.workspace.appendMessage(teamSlug, {
|
|
308
|
+
from: 'system',
|
|
309
|
+
to: 'team',
|
|
310
|
+
role: 'agent',
|
|
311
|
+
content: `[跨团队任务已确认] "${payload.task.subject}" — 来自 ${payload.originTeam} 的任务已被人工确认接单,请开始执行。任务描述:${payload.task.description ?? '无'}`,
|
|
312
|
+
meta: {
|
|
313
|
+
source: 'cross_team_accepted',
|
|
314
|
+
dispatchId: payload.dispatchId,
|
|
315
|
+
originTeam: payload.originTeam,
|
|
316
|
+
taskId: remoteTaskId,
|
|
317
|
+
},
|
|
318
|
+
});
|
|
319
|
+
} catch {}
|
|
320
|
+
|
|
321
|
+
// Fixed flow: notify originating agent that task was accepted
|
|
322
|
+
try {
|
|
323
|
+
await this.workspace.appendMessage(payload.originTeam, {
|
|
324
|
+
from: 'system',
|
|
325
|
+
to: 'team',
|
|
326
|
+
role: 'agent',
|
|
327
|
+
content: `[跨团队任务已接单] "${payload.task.subject}" — ${teamSlug} 已确认接单,正在执行中。`,
|
|
328
|
+
meta: {
|
|
329
|
+
source: 'cross_team_accepted_notify',
|
|
330
|
+
dispatchId: payload.dispatchId,
|
|
331
|
+
targetTeam: teamSlug,
|
|
332
|
+
},
|
|
333
|
+
});
|
|
334
|
+
} catch {}
|
|
335
|
+
|
|
336
|
+
// Feishu notification
|
|
337
|
+
this.sendFeishuNotification(
|
|
338
|
+
`跨团队任务已接单:${payload.originTeam} → ${teamSlug}\n${payload.task.subject}\n状态:执行中`
|
|
339
|
+
);
|
|
340
|
+
|
|
341
|
+
return { taskId: remoteTaskId };
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
async rejectTask(teamSlug: string, dispatchId: string, reason?: string): Promise<void> {
|
|
345
|
+
const pending = this.pendingRequests.get(dispatchId);
|
|
346
|
+
if (!pending) {
|
|
347
|
+
throw new Error(`No pending request found for dispatchId: ${dispatchId}`);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const { payload, msgId, groupName } = pending;
|
|
351
|
+
|
|
352
|
+
const response: TaskHandshakeResponse = {
|
|
353
|
+
dispatchId: payload.dispatchId,
|
|
354
|
+
type: 'task_reject',
|
|
355
|
+
fromTeam: teamSlug,
|
|
356
|
+
toTeam: payload.originTeam,
|
|
357
|
+
reason,
|
|
358
|
+
rejectedAt: new Date().toISOString(),
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
const isLocalOrigin = await this.isLocalTeam(payload.originTeam);
|
|
362
|
+
if (isLocalOrigin) {
|
|
363
|
+
await this.handleLocalResponse(response);
|
|
364
|
+
} else if (this.redis) {
|
|
365
|
+
await this.redis
|
|
366
|
+
.xadd(`task:response:${payload.originTeam}`, '*', 'payload', JSON.stringify(response))
|
|
367
|
+
.catch((err: Error) => {
|
|
368
|
+
console.error('[TaskDispatchService] reject xadd failed:', err.message);
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
if (this.redis) {
|
|
373
|
+
await this.redis.xack(`task:dispatch:${teamSlug}`, groupName, msgId).catch(() => {});
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
this.pendingRequests.delete(dispatchId);
|
|
377
|
+
|
|
378
|
+
// Update collab board
|
|
379
|
+
this.collabBoard.transition({
|
|
380
|
+
dispatchId: payload.dispatchId,
|
|
381
|
+
expected: 'pending_accept',
|
|
382
|
+
next: 'rejected',
|
|
383
|
+
actor: { type: 'team', id: teamSlug },
|
|
384
|
+
eventType: 'task_rejected',
|
|
385
|
+
payload: { reason },
|
|
386
|
+
extra: { reason, rejectedAt: response.rejectedAt },
|
|
387
|
+
});
|
|
388
|
+
this.emitCollabChange(payload.dispatchId, 'rejected', payload.originTeam, payload.targetTeam);
|
|
389
|
+
|
|
390
|
+
// Fixed flow: notify originating agent that task was rejected
|
|
391
|
+
try {
|
|
392
|
+
await this.workspace.appendMessage(payload.originTeam, {
|
|
393
|
+
from: 'system',
|
|
394
|
+
to: 'team',
|
|
395
|
+
role: 'agent',
|
|
396
|
+
content: `[跨团队任务被拒绝] "${payload.task.subject}" — ${teamSlug} 拒绝了此任务。原因:${reason ?? '未说明'}`,
|
|
397
|
+
meta: {
|
|
398
|
+
source: 'cross_team_rejected',
|
|
399
|
+
dispatchId: payload.dispatchId,
|
|
400
|
+
targetTeam: teamSlug,
|
|
401
|
+
rejectReason: reason,
|
|
402
|
+
},
|
|
403
|
+
});
|
|
404
|
+
} catch {}
|
|
405
|
+
|
|
406
|
+
// Feishu notification
|
|
407
|
+
this.sendFeishuNotification(
|
|
408
|
+
`跨团队任务被拒绝:${payload.originTeam} → ${teamSlug}\n${payload.task.subject}\n原因:${reason ?? '未说明'}`
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// ── Deliver / Approve / Revision ────────────────────────────────
|
|
413
|
+
|
|
414
|
+
async deliverTask(
|
|
415
|
+
teamSlug: string,
|
|
416
|
+
dispatchId: string,
|
|
417
|
+
result: string
|
|
418
|
+
): Promise<{ ok: boolean }> {
|
|
419
|
+
const collabTask = this.collabBoard.getTask(dispatchId);
|
|
420
|
+
if (!collabTask) {
|
|
421
|
+
throw new Error(`No collab task found for dispatchId: ${dispatchId}`);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const deliveredAt = new Date().toISOString();
|
|
425
|
+
|
|
426
|
+
// Send deliver response to origin team
|
|
427
|
+
const response: TaskHandshakeResponse = {
|
|
428
|
+
dispatchId,
|
|
429
|
+
type: 'task_deliver',
|
|
430
|
+
fromTeam: teamSlug,
|
|
431
|
+
toTeam: collabTask.fromTeam,
|
|
432
|
+
result,
|
|
433
|
+
deliveredAt,
|
|
150
434
|
};
|
|
435
|
+
|
|
436
|
+
const isLocalOrigin = await this.isLocalTeam(collabTask.fromTeam);
|
|
437
|
+
if (isLocalOrigin) {
|
|
438
|
+
await this.handleLocalResponse(response);
|
|
439
|
+
} else if (this.redis) {
|
|
440
|
+
await this.redis
|
|
441
|
+
.xadd(`task:response:${collabTask.fromTeam}`, '*', 'payload', JSON.stringify(response))
|
|
442
|
+
.catch((err: Error) => {
|
|
443
|
+
console.error('[TaskDispatchService] deliver xadd failed:', err.message);
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Update local collab board
|
|
448
|
+
const deliveredTask = this.collabBoard.transition({
|
|
449
|
+
dispatchId,
|
|
450
|
+
expected: ['accepted', 'revision'],
|
|
451
|
+
next: 'delivered',
|
|
452
|
+
actor: { type: 'team', id: teamSlug },
|
|
453
|
+
eventType: 'task_delivered',
|
|
454
|
+
payload: { summary: result.slice(0, 1000) },
|
|
455
|
+
extra: { result, deliveredAt },
|
|
456
|
+
});
|
|
457
|
+
this.emitCollabChange(dispatchId, 'delivered', deliveredTask.fromTeam, deliveredTask.toTeam);
|
|
458
|
+
|
|
459
|
+
// Notify origin agent: task is ready for review
|
|
460
|
+
try {
|
|
461
|
+
await this.workspace.appendMessage(collabTask.fromTeam, {
|
|
462
|
+
from: 'system',
|
|
463
|
+
to: 'team',
|
|
464
|
+
role: 'agent',
|
|
465
|
+
content: `[跨团队任务待审核] "${collabTask.subject}" — ${teamSlug} 已完成任务并提交交付结果,请审核。结果:${result}`,
|
|
466
|
+
meta: {
|
|
467
|
+
source: 'cross_team_delivered',
|
|
468
|
+
dispatchId,
|
|
469
|
+
targetTeam: teamSlug,
|
|
470
|
+
result,
|
|
471
|
+
},
|
|
472
|
+
});
|
|
473
|
+
} catch {}
|
|
474
|
+
|
|
475
|
+
this.sendFeishuNotification(
|
|
476
|
+
`跨团队任务待审核:${collabTask.fromTeam} ← ${teamSlug}\n${collabTask.subject}\n状态:待审核`
|
|
477
|
+
);
|
|
478
|
+
|
|
479
|
+
return { ok: true };
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
async approveTask(teamSlug: string, dispatchId: string): Promise<{ ok: boolean }> {
|
|
483
|
+
const collabTask = this.collabBoard.getTask(dispatchId);
|
|
484
|
+
if (!collabTask) {
|
|
485
|
+
throw new Error(`No collab task found for dispatchId: ${dispatchId}`);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
const approvedAt = new Date().toISOString();
|
|
489
|
+
|
|
490
|
+
// Send approve response to target team
|
|
491
|
+
const response: TaskHandshakeResponse = {
|
|
492
|
+
dispatchId,
|
|
493
|
+
type: 'task_approve',
|
|
494
|
+
fromTeam: teamSlug,
|
|
495
|
+
toTeam: collabTask.toTeam,
|
|
496
|
+
approvedAt,
|
|
497
|
+
};
|
|
498
|
+
|
|
499
|
+
const isLocalTarget = await this.isLocalTeam(collabTask.toTeam);
|
|
500
|
+
if (isLocalTarget) {
|
|
501
|
+
await this.handleLocalResponse(response);
|
|
502
|
+
} else if (this.redis) {
|
|
503
|
+
await this.redis
|
|
504
|
+
.xadd(`task:response:${collabTask.toTeam}`, '*', 'payload', JSON.stringify(response))
|
|
505
|
+
.catch((err: Error) => {
|
|
506
|
+
console.error('[TaskDispatchService] approve xadd failed:', err.message);
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// Update collab board
|
|
511
|
+
const approvedTask = this.collabBoard.transition({
|
|
512
|
+
dispatchId,
|
|
513
|
+
expected: 'delivered',
|
|
514
|
+
next: 'approved',
|
|
515
|
+
actor: { type: 'team', id: teamSlug },
|
|
516
|
+
eventType: 'task_approved',
|
|
517
|
+
payload: { approvedAt },
|
|
518
|
+
extra: { approvedAt },
|
|
519
|
+
});
|
|
520
|
+
this.emitCollabChange(dispatchId, 'approved', approvedTask.fromTeam, approvedTask.toTeam);
|
|
521
|
+
|
|
522
|
+
// Notify origin agent: task is fully complete, you can continue
|
|
523
|
+
try {
|
|
524
|
+
await this.workspace.appendMessage(collabTask.fromTeam, {
|
|
525
|
+
from: 'system',
|
|
526
|
+
to: 'team',
|
|
527
|
+
role: 'agent',
|
|
528
|
+
content: `[跨团队任务已完成] "${collabTask.subject}" — ${collabTask.toTeam} 的交付已通过审核,此跨团队任务结束。`,
|
|
529
|
+
meta: {
|
|
530
|
+
source: 'cross_team_approved',
|
|
531
|
+
dispatchId,
|
|
532
|
+
targetTeam: collabTask.toTeam,
|
|
533
|
+
},
|
|
534
|
+
});
|
|
535
|
+
} catch {}
|
|
536
|
+
|
|
537
|
+
// Notify target agent: approved
|
|
538
|
+
try {
|
|
539
|
+
await this.workspace.appendMessage(collabTask.toTeam, {
|
|
540
|
+
from: 'system',
|
|
541
|
+
to: 'team',
|
|
542
|
+
role: 'agent',
|
|
543
|
+
content: `[跨团队任务审核通过] "${collabTask.subject}" — ${teamSlug} 已通过审核,任务完成。`,
|
|
544
|
+
meta: {
|
|
545
|
+
source: 'cross_team_approved_target',
|
|
546
|
+
dispatchId,
|
|
547
|
+
originTeam: teamSlug,
|
|
548
|
+
},
|
|
549
|
+
});
|
|
550
|
+
} catch {}
|
|
551
|
+
|
|
552
|
+
this.sendFeishuNotification(
|
|
553
|
+
`跨团队任务完成:${collabTask.fromTeam} ← ${collabTask.toTeam}\n${collabTask.subject}\n状态:已完成`
|
|
554
|
+
);
|
|
555
|
+
|
|
556
|
+
return { ok: true };
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
async rejectResult(
|
|
560
|
+
teamSlug: string,
|
|
561
|
+
dispatchId: string,
|
|
562
|
+
feedback: string
|
|
563
|
+
): Promise<{ ok: boolean }> {
|
|
564
|
+
const collabTask = this.collabBoard.getTask(dispatchId);
|
|
565
|
+
if (!collabTask) {
|
|
566
|
+
throw new Error(`No collab task found for dispatchId: ${dispatchId}`);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
const newRevisionCount = collabTask.revisionCount + 1;
|
|
570
|
+
|
|
571
|
+
// Send revision response to target team
|
|
572
|
+
const response: TaskHandshakeResponse = {
|
|
573
|
+
dispatchId,
|
|
574
|
+
type: 'task_revision',
|
|
575
|
+
fromTeam: teamSlug,
|
|
576
|
+
toTeam: collabTask.toTeam,
|
|
577
|
+
feedback,
|
|
578
|
+
};
|
|
579
|
+
|
|
580
|
+
const isLocalTarget = await this.isLocalTeam(collabTask.toTeam);
|
|
581
|
+
if (isLocalTarget) {
|
|
582
|
+
await this.handleLocalResponse(response);
|
|
583
|
+
} else if (this.redis) {
|
|
584
|
+
await this.redis
|
|
585
|
+
.xadd(`task:response:${collabTask.toTeam}`, '*', 'payload', JSON.stringify(response))
|
|
586
|
+
.catch((err: Error) => {
|
|
587
|
+
console.error('[TaskDispatchService] revision xadd failed:', err.message);
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// Update collab board
|
|
592
|
+
const revisionTask = this.collabBoard.transition({
|
|
593
|
+
dispatchId,
|
|
594
|
+
expected: 'delivered',
|
|
595
|
+
next: 'revision',
|
|
596
|
+
actor: { type: 'team', id: teamSlug },
|
|
597
|
+
eventType: 'revision_requested',
|
|
598
|
+
payload: {
|
|
599
|
+
feedback,
|
|
600
|
+
previousResult: collabTask.result,
|
|
601
|
+
revisionCount: newRevisionCount,
|
|
602
|
+
},
|
|
603
|
+
extra: {
|
|
604
|
+
feedback,
|
|
605
|
+
revisionCount: newRevisionCount,
|
|
606
|
+
},
|
|
607
|
+
});
|
|
608
|
+
this.emitCollabChange(dispatchId, 'revision', revisionTask.fromTeam, revisionTask.toTeam);
|
|
609
|
+
|
|
610
|
+
return { ok: true };
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/** Get the collaboration board. */
|
|
614
|
+
getCollabBoard() {
|
|
615
|
+
return this.collabBoard.getBoard();
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
/** Get a single collab task. */
|
|
619
|
+
getCollabTask(dispatchId: string) {
|
|
620
|
+
return this.collabBoard.getTask(dispatchId);
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
/** Get event log for a collab task. */
|
|
624
|
+
getCollabTaskEvents(dispatchId: string) {
|
|
625
|
+
return this.collabBoard.getEvents(dispatchId);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
listPendingRequests(teamSlug: string): TaskDispatchPayload[] {
|
|
629
|
+
const results: TaskDispatchPayload[] = [];
|
|
630
|
+
for (const [, req] of this.pendingRequests) {
|
|
631
|
+
if (req.teamSlug === teamSlug) {
|
|
632
|
+
results.push(req.payload);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
return results;
|
|
151
636
|
}
|
|
152
637
|
|
|
153
638
|
async onTaskCompleted(teamSlug: string, taskId: string): Promise<void> {
|
|
@@ -165,12 +650,10 @@ export class TaskDispatchService {
|
|
|
165
650
|
result: task.result ?? undefined,
|
|
166
651
|
};
|
|
167
652
|
|
|
168
|
-
// Update local dispatchMeta
|
|
169
653
|
await this.workspace.patchTask(teamSlug, taskId, {
|
|
170
654
|
dispatchMeta: { ...meta, status: 'completed', completedAt: update.timestamp },
|
|
171
655
|
} as any);
|
|
172
656
|
|
|
173
|
-
// Notify origin team
|
|
174
657
|
if (this.redis) {
|
|
175
658
|
const channel = `task:status:${meta.originTeam}`;
|
|
176
659
|
await this.redis.publish(channel, JSON.stringify(update)).catch((err: Error) => {
|
|
@@ -179,7 +662,7 @@ export class TaskDispatchService {
|
|
|
179
662
|
}
|
|
180
663
|
}
|
|
181
664
|
|
|
182
|
-
// ── Local
|
|
665
|
+
// ── Local team check ─────────────────────────────────────────
|
|
183
666
|
|
|
184
667
|
private async isLocalTeam(teamSlug: string): Promise<boolean> {
|
|
185
668
|
try {
|
|
@@ -190,21 +673,84 @@ export class TaskDispatchService {
|
|
|
190
673
|
}
|
|
191
674
|
}
|
|
192
675
|
|
|
193
|
-
|
|
676
|
+
// ── Unified Redis dispatch ───────────────────────────────────────
|
|
677
|
+
|
|
678
|
+
private async handleRedisDispatch(
|
|
194
679
|
dispatchMeta: DispatchMeta,
|
|
195
|
-
task: { subject: string; description?: string; prompt?: string }
|
|
680
|
+
task: { subject: string; description?: string; prompt?: string },
|
|
681
|
+
needsHumanReview?: boolean
|
|
196
682
|
): Promise<void> {
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
|
|
683
|
+
const payload: TaskDispatchPayload = {
|
|
684
|
+
dispatchId: dispatchMeta.dispatchId,
|
|
685
|
+
originTeam: dispatchMeta.originTeam,
|
|
686
|
+
targetTeam: dispatchMeta.targetTeam,
|
|
687
|
+
task: { subject: task.subject, description: task.description, prompt: task.prompt },
|
|
688
|
+
dispatchedAt: dispatchMeta.dispatchedAt,
|
|
689
|
+
deadline: dispatchMeta.deadline,
|
|
690
|
+
needsHumanReview,
|
|
691
|
+
};
|
|
692
|
+
|
|
693
|
+
// Store in memory for accept/reject
|
|
694
|
+
this.pendingRequests.set(dispatchMeta.dispatchId, {
|
|
695
|
+
payload,
|
|
696
|
+
msgId: `redis-${Date.now()}`,
|
|
697
|
+
groupName: 'dispatch-group',
|
|
698
|
+
teamSlug: dispatchMeta.targetTeam,
|
|
200
699
|
});
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
700
|
+
|
|
701
|
+
// Publish to Redis stream
|
|
702
|
+
const streamKey = `task:dispatch:${dispatchMeta.targetTeam}`;
|
|
703
|
+
await this.redis!.xadd(streamKey, '*', 'payload', JSON.stringify(payload));
|
|
704
|
+
|
|
705
|
+
// If local team, also write to inbox
|
|
706
|
+
const isLocal = await this.isLocalTeam(dispatchMeta.targetTeam);
|
|
707
|
+
if (isLocal) {
|
|
708
|
+
try {
|
|
709
|
+
await this.workspace.appendMessage(dispatchMeta.targetTeam, {
|
|
710
|
+
from: dispatchMeta.originTeam,
|
|
711
|
+
to: 'team',
|
|
712
|
+
role: 'agent',
|
|
713
|
+
content: `[跨团队任务] ${task.subject}${task.description ? '\n' + task.description : ''}`,
|
|
714
|
+
meta: {
|
|
715
|
+
source: 'cross_team_dispatch',
|
|
716
|
+
dispatchId: dispatchMeta.dispatchId,
|
|
717
|
+
originTeam: dispatchMeta.originTeam,
|
|
718
|
+
needsHumanReview,
|
|
719
|
+
},
|
|
720
|
+
});
|
|
721
|
+
} catch (err) {
|
|
722
|
+
console.error('[TaskDispatchService] inbox write failed:', (err as Error).message);
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
this.sendFeishuNotification(
|
|
727
|
+
`跨团队任务派发:${dispatchMeta.originTeam} → ${dispatchMeta.targetTeam}\n${task.subject}`
|
|
728
|
+
);
|
|
205
729
|
}
|
|
206
730
|
|
|
207
|
-
// ──
|
|
731
|
+
// ── Feishu notification helper ──────────────────────────────────
|
|
732
|
+
|
|
733
|
+
private sendFeishuNotification(text: string): void {
|
|
734
|
+
setTimeout(() => {
|
|
735
|
+
try {
|
|
736
|
+
const { execSync } = require('node:child_process');
|
|
737
|
+
execSync(
|
|
738
|
+
`feishu-cli msg send --receive-id-type chat_id --receive-id oc_e7d4204895f8f9d763d9f0e42ead1e5e --text ${JSON.stringify(text)}`,
|
|
739
|
+
{ timeout: 5000, stdio: 'pipe' }
|
|
740
|
+
);
|
|
741
|
+
} catch {
|
|
742
|
+
// best effort
|
|
743
|
+
}
|
|
744
|
+
}, 0);
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
// ── Local response (same machine) ─────────────────────────────
|
|
748
|
+
|
|
749
|
+
private async handleLocalResponse(response: TaskHandshakeResponse): Promise<void> {
|
|
750
|
+
await this.applyResponse(response);
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
// ── Redis connection ──────────────────────────────────────────
|
|
208
754
|
|
|
209
755
|
private async connectRedis(): Promise<void> {
|
|
210
756
|
if (!this.config?.redis) return;
|
|
@@ -222,8 +768,10 @@ export class TaskDispatchService {
|
|
|
222
768
|
|
|
223
769
|
await this.redis.ping();
|
|
224
770
|
|
|
771
|
+
this.collabBoard.setRedis(this.redis);
|
|
225
772
|
this.startHeartbeat();
|
|
226
773
|
this.startConsumers();
|
|
774
|
+
this.startResponseConsumers();
|
|
227
775
|
this.subscribeStatus();
|
|
228
776
|
} catch (err) {
|
|
229
777
|
console.error('[TaskDispatchService] Redis connect failed:', err);
|
|
@@ -232,27 +780,7 @@ export class TaskDispatchService {
|
|
|
232
780
|
}
|
|
233
781
|
}
|
|
234
782
|
|
|
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 ─────────────────────────────────────────────────
|
|
783
|
+
// ── Heartbeat + agent info ────────────────────────────────────
|
|
256
784
|
|
|
257
785
|
private startHeartbeat(): void {
|
|
258
786
|
this.stopHeartbeat();
|
|
@@ -262,7 +790,21 @@ export class TaskDispatchService {
|
|
|
262
790
|
const localTeams = await this.workspace.listTeams();
|
|
263
791
|
for (const team of localTeams) {
|
|
264
792
|
await this.redis.zadd('task:teams', now, team.slug).catch(() => {});
|
|
793
|
+
await this.redis
|
|
794
|
+
.hset(`task:team:info:${team.slug}`, {
|
|
795
|
+
slug: team.slug,
|
|
796
|
+
displayName: team.displayName ?? team.slug,
|
|
797
|
+
harness: team.harness,
|
|
798
|
+
description: team.description ?? '',
|
|
799
|
+
capabilities: JSON.stringify(this.inferCapabilities(team)),
|
|
800
|
+
collaboration: String(team.collaboration !== false),
|
|
801
|
+
updatedAt: new Date().toISOString(),
|
|
802
|
+
})
|
|
803
|
+
.catch(() => {});
|
|
265
804
|
}
|
|
805
|
+
|
|
806
|
+
// Check deadline timeouts
|
|
807
|
+
await this.checkDeadlines(localTeams);
|
|
266
808
|
};
|
|
267
809
|
beat();
|
|
268
810
|
this.heartbeatTimer = setInterval(beat, 30_000);
|
|
@@ -275,21 +817,47 @@ export class TaskDispatchService {
|
|
|
275
817
|
}
|
|
276
818
|
}
|
|
277
819
|
|
|
278
|
-
|
|
820
|
+
private async checkDeadlines(localTeams: TeamManifest[]): Promise<void> {
|
|
821
|
+
for (const team of localTeams) {
|
|
822
|
+
try {
|
|
823
|
+
const tasks = await this.workspace.readTasks(team.slug);
|
|
824
|
+
for (const task of tasks) {
|
|
825
|
+
if (
|
|
826
|
+
task.dispatchMeta?.status === 'pending_accept' &&
|
|
827
|
+
task.dispatchMeta.deadline &&
|
|
828
|
+
new Date(task.dispatchMeta.deadline).getTime() < Date.now()
|
|
829
|
+
) {
|
|
830
|
+
await this.workspace.patchTask(team.slug, task.id, {
|
|
831
|
+
dispatchMeta: {
|
|
832
|
+
...task.dispatchMeta,
|
|
833
|
+
status: 'failed',
|
|
834
|
+
rejectionReason: 'handshake timeout',
|
|
835
|
+
},
|
|
836
|
+
} as any);
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
} catch {
|
|
840
|
+
/* skip broken teams */
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
// ── Dispatch consumers (XREADGROUP) ───────────────────────────
|
|
279
846
|
|
|
280
847
|
private startConsumers(): void {
|
|
281
848
|
if (!this.redis || !this.redisSub) return;
|
|
282
849
|
|
|
283
850
|
const startForTeam = async (teamSlug: string) => {
|
|
851
|
+
if (this.consumerTeamSlugs.has(teamSlug)) return;
|
|
852
|
+
this.consumerTeamSlugs.add(teamSlug);
|
|
284
853
|
const streamKey = `task:dispatch:${teamSlug}`;
|
|
285
854
|
const groupName = `hermit-${teamSlug}`;
|
|
286
855
|
const consumerId = `consumer-${process.pid}`;
|
|
287
856
|
|
|
288
|
-
// Create consumer group (MKSTREAM creates stream if missing)
|
|
289
857
|
try {
|
|
290
858
|
await this.redis!.xgroup('CREATE', streamKey, groupName, '0', 'MKSTREAM');
|
|
291
859
|
} catch {
|
|
292
|
-
// Group already exists
|
|
860
|
+
// Group already exists
|
|
293
861
|
}
|
|
294
862
|
|
|
295
863
|
const poll = async () => {
|
|
@@ -322,18 +890,19 @@ export class TaskDispatchService {
|
|
|
322
890
|
}
|
|
323
891
|
};
|
|
324
892
|
|
|
325
|
-
// Initial poll, then interval
|
|
326
893
|
poll();
|
|
327
894
|
const timer = setInterval(poll, 5000);
|
|
328
895
|
this.consumerTimers.push(timer);
|
|
329
896
|
};
|
|
330
897
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
898
|
+
const syncConsumers = () =>
|
|
899
|
+
void this.workspace.listTeams().then((teams) => {
|
|
900
|
+
for (const team of teams) {
|
|
901
|
+
void startForTeam(team.slug);
|
|
902
|
+
}
|
|
903
|
+
});
|
|
904
|
+
syncConsumers();
|
|
905
|
+
this.consumerTimers.push(setInterval(syncConsumers, 10_000));
|
|
337
906
|
}
|
|
338
907
|
|
|
339
908
|
private async handleIncomingDispatch(
|
|
@@ -343,44 +912,87 @@ export class TaskDispatchService {
|
|
|
343
912
|
groupName: string
|
|
344
913
|
): Promise<void> {
|
|
345
914
|
try {
|
|
346
|
-
// fields is [key, value, key, value, ...]
|
|
347
915
|
const payloadStr = fields[1]?.toString();
|
|
348
916
|
if (!payloadStr) return;
|
|
349
917
|
|
|
350
918
|
const payload: TaskDispatchPayload = JSON.parse(payloadStr);
|
|
919
|
+
const alreadyPending = this.pendingRequests.has(payload.dispatchId);
|
|
351
920
|
|
|
352
|
-
|
|
353
|
-
const
|
|
354
|
-
|
|
921
|
+
const fromTeamManifest = await this.safeReadManifest(payload.originTeam);
|
|
922
|
+
const toTeamManifest = await this.safeReadManifest(teamSlug);
|
|
923
|
+
const createdAt = payload.dispatchedAt || new Date().toISOString();
|
|
924
|
+
this.collabBoard.addTask({
|
|
925
|
+
id: payload.dispatchId,
|
|
926
|
+
dispatchId: payload.dispatchId,
|
|
927
|
+
subject: payload.task.subject,
|
|
355
928
|
description: payload.task.description,
|
|
929
|
+
fromTeam: payload.originTeam,
|
|
930
|
+
fromTeamDisplay: fromTeamManifest?.displayName ?? payload.originTeam,
|
|
931
|
+
toTeam: teamSlug,
|
|
932
|
+
toTeamDisplay: toTeamManifest?.displayName ?? teamSlug,
|
|
933
|
+
status: 'pending_accept',
|
|
934
|
+
deadline: payload.deadline,
|
|
935
|
+
needsHumanReview: payload.needsHumanReview ?? false,
|
|
936
|
+
revisionCount: 0,
|
|
937
|
+
createdAt,
|
|
938
|
+
updatedAt: createdAt,
|
|
356
939
|
});
|
|
940
|
+
this.emitCollabChange(payload.dispatchId, 'pending_accept', payload.originTeam, teamSlug);
|
|
357
941
|
|
|
358
|
-
const
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
942
|
+
const existingTasks = await this.workspace.readTasks(teamSlug).catch(() => []);
|
|
943
|
+
const existingTask = existingTasks.find(
|
|
944
|
+
(task) => task.dispatchMeta?.dispatchId === payload.dispatchId
|
|
945
|
+
);
|
|
946
|
+
const localTask =
|
|
947
|
+
existingTask ??
|
|
948
|
+
(await this.workspace.createTask(teamSlug, {
|
|
949
|
+
title: payload.task.subject,
|
|
950
|
+
description: payload.task.description ?? payload.task.prompt ?? '',
|
|
951
|
+
status: 'todo',
|
|
952
|
+
dispatchMeta: {
|
|
953
|
+
dispatchId: payload.dispatchId,
|
|
954
|
+
originTeam: payload.originTeam,
|
|
955
|
+
targetTeam: teamSlug,
|
|
956
|
+
status: 'pending_accept',
|
|
957
|
+
dispatchedAt: payload.dispatchedAt,
|
|
958
|
+
receivedAt: new Date().toISOString(),
|
|
959
|
+
deadline: payload.deadline,
|
|
960
|
+
},
|
|
961
|
+
}));
|
|
367
962
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
963
|
+
// Store in pending requests — wait for a human to accept/reject the agent-created dispatch.
|
|
964
|
+
this.pendingRequests.set(payload.dispatchId, {
|
|
965
|
+
payload,
|
|
966
|
+
msgId,
|
|
967
|
+
groupName,
|
|
968
|
+
teamSlug,
|
|
969
|
+
localTaskId: localTask.id,
|
|
970
|
+
});
|
|
371
971
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
972
|
+
if (!alreadyPending) {
|
|
973
|
+
await this.workspace
|
|
974
|
+
.appendMessage(teamSlug, {
|
|
975
|
+
from: payload.originTeam,
|
|
976
|
+
to: 'team',
|
|
977
|
+
role: 'agent',
|
|
978
|
+
content: `[跨团队任务] ${payload.task.subject}${
|
|
979
|
+
payload.task.description ? '\n' + payload.task.description : ''
|
|
980
|
+
}`,
|
|
981
|
+
meta: {
|
|
982
|
+
source: 'cross_team_dispatch',
|
|
983
|
+
dispatchId: payload.dispatchId,
|
|
984
|
+
originTeam: payload.originTeam,
|
|
985
|
+
needsHumanReview: payload.needsHumanReview,
|
|
986
|
+
},
|
|
987
|
+
})
|
|
988
|
+
.catch((err: Error) => {
|
|
989
|
+
console.error('[TaskDispatchService] inbox write failed:', err.message);
|
|
990
|
+
});
|
|
383
991
|
}
|
|
992
|
+
|
|
993
|
+
console.log(
|
|
994
|
+
`[TaskDispatchService] received dispatch request: ${payload.dispatchId} from ${payload.originTeam} → ${teamSlug}`
|
|
995
|
+
);
|
|
384
996
|
} catch (err) {
|
|
385
997
|
console.error('[TaskDispatchService] handleIncomingDispatch error:', err);
|
|
386
998
|
}
|
|
@@ -389,9 +1001,155 @@ export class TaskDispatchService {
|
|
|
389
1001
|
private stopConsumers(): void {
|
|
390
1002
|
for (const t of this.consumerTimers) clearInterval(t);
|
|
391
1003
|
this.consumerTimers = [];
|
|
1004
|
+
this.consumerTeamSlugs.clear();
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
// ── Response consumers (XREADGROUP) ───────────────────────────
|
|
1008
|
+
|
|
1009
|
+
private startResponseConsumers(): void {
|
|
1010
|
+
if (!this.redis || !this.redisSub) return;
|
|
1011
|
+
|
|
1012
|
+
const startForTeam = async (teamSlug: string) => {
|
|
1013
|
+
if (this.responseConsumerTeamSlugs.has(teamSlug)) return;
|
|
1014
|
+
this.responseConsumerTeamSlugs.add(teamSlug);
|
|
1015
|
+
const streamKey = `task:response:${teamSlug}`;
|
|
1016
|
+
const groupName = `hermit-response-${teamSlug}`;
|
|
1017
|
+
const consumerId = `response-consumer-${process.pid}`;
|
|
1018
|
+
|
|
1019
|
+
try {
|
|
1020
|
+
await this.redis!.xgroup('CREATE', streamKey, groupName, '0', 'MKSTREAM');
|
|
1021
|
+
} catch {
|
|
1022
|
+
// Group already exists
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
const poll = async () => {
|
|
1026
|
+
if (this.disposed || !this.redisSub) return;
|
|
1027
|
+
try {
|
|
1028
|
+
const raw: unknown = await (this.redisSub as any).xreadgroup(
|
|
1029
|
+
'GROUP',
|
|
1030
|
+
groupName,
|
|
1031
|
+
consumerId,
|
|
1032
|
+
'BLOCK',
|
|
1033
|
+
5000,
|
|
1034
|
+
'COUNT',
|
|
1035
|
+
1,
|
|
1036
|
+
'STREAMS',
|
|
1037
|
+
streamKey,
|
|
1038
|
+
'>'
|
|
1039
|
+
);
|
|
1040
|
+
const results = raw as [string, [string, (string | Buffer)[]][]][] | null;
|
|
1041
|
+
|
|
1042
|
+
if (!results || results.length === 0) return;
|
|
1043
|
+
|
|
1044
|
+
for (const [, messages] of results) {
|
|
1045
|
+
if (!Array.isArray(messages)) continue;
|
|
1046
|
+
for (const [msgId, fields] of messages) {
|
|
1047
|
+
await this.handleIncomingResponse(teamSlug, msgId, fields, groupName);
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
} catch {
|
|
1051
|
+
// Read error — will retry next poll
|
|
1052
|
+
}
|
|
1053
|
+
};
|
|
1054
|
+
|
|
1055
|
+
poll();
|
|
1056
|
+
const timer = setInterval(poll, 5000);
|
|
1057
|
+
this.responseConsumerTimers.push(timer);
|
|
1058
|
+
};
|
|
1059
|
+
|
|
1060
|
+
const syncConsumers = () =>
|
|
1061
|
+
void this.workspace.listTeams().then((teams) => {
|
|
1062
|
+
for (const team of teams) {
|
|
1063
|
+
void startForTeam(team.slug);
|
|
1064
|
+
}
|
|
1065
|
+
});
|
|
1066
|
+
syncConsumers();
|
|
1067
|
+
this.responseConsumerTimers.push(setInterval(syncConsumers, 10_000));
|
|
392
1068
|
}
|
|
393
1069
|
|
|
394
|
-
|
|
1070
|
+
private async handleIncomingResponse(
|
|
1071
|
+
_teamSlug: string,
|
|
1072
|
+
msgId: string,
|
|
1073
|
+
fields: (string | Buffer)[],
|
|
1074
|
+
groupName: string
|
|
1075
|
+
): Promise<void> {
|
|
1076
|
+
try {
|
|
1077
|
+
const payloadStr = fields[1]?.toString();
|
|
1078
|
+
if (!payloadStr) return;
|
|
1079
|
+
|
|
1080
|
+
const response: TaskHandshakeResponse = JSON.parse(payloadStr);
|
|
1081
|
+
await this.applyResponse(response);
|
|
1082
|
+
|
|
1083
|
+
// ACK
|
|
1084
|
+
if (this.redis) {
|
|
1085
|
+
await this.redis.xack(`task:response:${_teamSlug}`, groupName, msgId).catch(() => {});
|
|
1086
|
+
}
|
|
1087
|
+
} catch (err) {
|
|
1088
|
+
console.error('[TaskDispatchService] handleIncomingResponse error:', err);
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
private async applyResponse(response: TaskHandshakeResponse): Promise<void> {
|
|
1093
|
+
const originTeam = response.toTeam;
|
|
1094
|
+
const tasks = await this.workspace.readTasks(originTeam);
|
|
1095
|
+
const shadowTask = tasks.find((t) => t.dispatchMeta?.dispatchId === response.dispatchId);
|
|
1096
|
+
if (!shadowTask) return;
|
|
1097
|
+
|
|
1098
|
+
const meta = { ...shadowTask.dispatchMeta! };
|
|
1099
|
+
|
|
1100
|
+
if (response.type === 'task_accept') {
|
|
1101
|
+
meta.status = 'accepted';
|
|
1102
|
+
meta.acceptedAt = response.acceptedAt;
|
|
1103
|
+
meta.remoteTaskId = response.remoteTaskId;
|
|
1104
|
+
// Collab board update already done in acceptTask
|
|
1105
|
+
} else if (response.type === 'task_reject') {
|
|
1106
|
+
meta.status = 'rejected';
|
|
1107
|
+
meta.rejectedAt = response.rejectedAt;
|
|
1108
|
+
meta.rejectionReason = response.reason;
|
|
1109
|
+
// Collab board update already done in rejectTask
|
|
1110
|
+
} else if (response.type === 'task_deliver') {
|
|
1111
|
+
meta.status = 'completed';
|
|
1112
|
+
meta.completedAt = response.deliveredAt;
|
|
1113
|
+
await this.workspace.patchTask(originTeam, shadowTask.id, {
|
|
1114
|
+
dispatchMeta: meta,
|
|
1115
|
+
} as any);
|
|
1116
|
+
|
|
1117
|
+
// Auto-approve if no human review needed
|
|
1118
|
+
const collabTask = this.collabBoard.getTask(response.dispatchId);
|
|
1119
|
+
if (collabTask && !collabTask.needsHumanReview && collabTask.status === 'delivered') {
|
|
1120
|
+
const approvedAt = new Date().toISOString();
|
|
1121
|
+
this.collabBoard.transition({
|
|
1122
|
+
dispatchId: response.dispatchId,
|
|
1123
|
+
expected: 'delivered',
|
|
1124
|
+
next: 'approved',
|
|
1125
|
+
actor: { type: 'system', id: 'auto-approve' },
|
|
1126
|
+
eventType: 'task_approved',
|
|
1127
|
+
payload: { auto: true },
|
|
1128
|
+
extra: { approvedAt },
|
|
1129
|
+
});
|
|
1130
|
+
this.emitCollabChange(response.dispatchId, 'approved', response.fromTeam, response.toTeam);
|
|
1131
|
+
}
|
|
1132
|
+
return;
|
|
1133
|
+
} else if (response.type === 'task_approve') {
|
|
1134
|
+
// Received by target team — already handled in approveTask
|
|
1135
|
+
return;
|
|
1136
|
+
} else if (response.type === 'task_revision') {
|
|
1137
|
+
// Received by target team — already handled in rejectResult
|
|
1138
|
+
return;
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
await this.workspace.patchTask(originTeam, shadowTask.id, {
|
|
1142
|
+
dispatchMeta: meta,
|
|
1143
|
+
} as any);
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
private stopResponseConsumers(): void {
|
|
1147
|
+
for (const t of this.responseConsumerTimers) clearInterval(t);
|
|
1148
|
+
this.responseConsumerTimers = [];
|
|
1149
|
+
this.responseConsumerTeamSlugs.clear();
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
// ── Status subscribe (completion notifications) ──────────────
|
|
395
1153
|
|
|
396
1154
|
private subscribeStatus(): void {
|
|
397
1155
|
if (!this.redisSub) return;
|
|
@@ -407,14 +1165,11 @@ export class TaskDispatchService {
|
|
|
407
1165
|
});
|
|
408
1166
|
|
|
409
1167
|
this.redisSub.on('message', (channel: string, message: string) => {
|
|
410
|
-
// channel format: task:status:{teamSlug}
|
|
411
1168
|
if (!channel.startsWith('task:status:')) return;
|
|
412
1169
|
|
|
413
1170
|
try {
|
|
414
1171
|
const update: TaskStatusUpdate = JSON.parse(message);
|
|
415
1172
|
const teamSlug = channel.replace('task:status:', '');
|
|
416
|
-
|
|
417
|
-
// Find shadow task (dispatched from this team) and update status
|
|
418
1173
|
this.handleStatusSync(teamSlug, update);
|
|
419
1174
|
} catch {
|
|
420
1175
|
// Ignore malformed messages
|
|
@@ -437,4 +1192,34 @@ export class TaskDispatchService {
|
|
|
437
1192
|
},
|
|
438
1193
|
} as any);
|
|
439
1194
|
}
|
|
1195
|
+
|
|
1196
|
+
// ── Capability inference ──────────────────────────────────────
|
|
1197
|
+
|
|
1198
|
+
private inferCapabilities(team: TeamManifest): AgentCapability[] {
|
|
1199
|
+
const caps: AgentCapability[] = [];
|
|
1200
|
+
if (team.harness) {
|
|
1201
|
+
caps.push({ skill: team.harness, description: `${team.harness} agent` });
|
|
1202
|
+
}
|
|
1203
|
+
if (team.description) {
|
|
1204
|
+
caps.push({ skill: 'general', description: team.description });
|
|
1205
|
+
}
|
|
1206
|
+
return caps;
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
private async safeReadManifest(teamSlug: string): Promise<TeamManifest | null> {
|
|
1210
|
+
try {
|
|
1211
|
+
return await this.workspace.readTeamManifest(teamSlug);
|
|
1212
|
+
} catch {
|
|
1213
|
+
return null;
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
private emitCollabChange(
|
|
1218
|
+
dispatchId: string,
|
|
1219
|
+
status: string,
|
|
1220
|
+
fromTeam: string,
|
|
1221
|
+
toTeam: string
|
|
1222
|
+
): void {
|
|
1223
|
+
this.onCollabChange?.(dispatchId, status, fromTeam, toTeam);
|
|
1224
|
+
}
|
|
440
1225
|
}
|