@yeaft/webchat-agent 0.1.225 → 0.1.226

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.
@@ -1,19 +1,16 @@
1
1
  /**
2
- * Conductor — Session 核心
2
+ * Conductor — Session Core (V5 Singleton)
3
3
  *
4
- * 管理 Conductor session 的完整生命周期。
5
- * 与 V1 Crew 的关键区别:
6
- * - 不绑定固定工作路径,workDir 可动态切换
7
- * - 不定义固定角色列表,task 创建时由 orchestrator 决定
8
- * - session 级元数据存在 ~/.claude/conductor/<sessionId>/
4
+ * Each Agent has exactly one Conductor (1:1 binding).
5
+ * No multi-session Map — just a single conductor instance.
6
+ *
7
+ * Conductor has no workDir; tasks bind to workDir.
9
8
  */
10
- import { randomUUID } from 'crypto';
11
9
  import ctx from '../context.js';
12
10
  import {
13
- loadConductorIndex, upsertConductorIndex,
14
- loadSessionMeta, saveSessionMeta, loadSessionMessages,
15
- getMaxShardIndex, getSessionDataDir, initSessionDataDir,
16
- cleanupMessageShards
11
+ ensureConductorHome, loadConductorMeta, saveConductorMeta,
12
+ loadConductorMessages, loadState, getMaxShardIndex,
13
+ getConductorHome, cleanupMessageShards
17
14
  } from './persistence.js';
18
15
  import {
19
16
  sendConductorMessage, sendConductorOutput, sendStatusUpdate, recordUserMessage
@@ -21,335 +18,191 @@ import {
21
18
  import { createConductorClaude, sendToConductor, stopConductorClaude } from './conductor-claude.js';
22
19
 
23
20
  // =====================================================================
24
- // Data Structures
21
+ // Singleton Conductor Instance
25
22
  // =====================================================================
26
23
 
27
- /** @type {Map<string, ConductorSession>} */
28
- export const conductorSessions = new Map();
29
-
30
- // =====================================================================
31
- // Session Lifecycle
32
- // =====================================================================
24
+ /** @type {Conductor|null} */
25
+ let conductor = null;
33
26
 
34
27
  /**
35
- * 创建 Conductor Session
28
+ * Get the current Conductor instance (or null if not initialized)
36
29
  */
37
- export async function createConductorSession(msg) {
38
- const {
39
- sessionId = randomUUID(),
40
- name = '',
41
- workDir = null,
42
- scenarioId = null,
43
- userId,
44
- username
45
- } = msg;
46
-
47
- const session = {
48
- id: sessionId,
49
- name: name || 'Conductor',
50
- workDir,
51
- scenarioId,
52
- status: 'running',
53
- tasks: new Map(),
54
- conductorState: null,
55
- costUsd: 0,
56
- totalInputTokens: 0,
57
- totalOutputTokens: 0,
58
- activeClaudes: 0,
59
- uiMessages: [],
60
- userId,
61
- username,
62
- agentId: ctx.CONFIG?.agentName || null,
63
- createdAt: Date.now(),
64
- _rotating: false,
65
- _conductorSemRelease: null
66
- };
67
-
68
- conductorSessions.set(sessionId, session);
69
-
70
- // 初始化数据目录
71
- await initSessionDataDir(sessionId);
72
-
73
- // 通知前端
74
- sendConductorMessage({
75
- type: 'conductor_session_created',
76
- sessionId,
77
- name: session.name,
78
- workDir: session.workDir,
79
- scenarioId: session.scenarioId,
80
- userId,
81
- username
82
- });
83
-
84
- sendStatusUpdate(session);
85
-
86
- // 注册到全局 index
87
- await upsertConductorIndex(session);
88
- await saveSessionMeta(session);
89
-
90
- // 启动 Conductor Claude
91
- try {
92
- await createConductorClaude(session);
93
- console.log(`[Conductor] Session ${sessionId} created, Claude ready`);
94
- } catch (e) {
95
- console.error('[Conductor] Failed to start Claude:', e.message);
96
- sendConductorOutput(session, 'system', {
97
- message: { role: 'assistant', content: `Conductor 启动失败: ${e.message}` }
98
- });
99
- }
100
-
101
- return session;
30
+ export function getConductor() {
31
+ return conductor;
102
32
  }
103
33
 
104
- /**
105
- * 列出所有 conductor sessions
106
- */
107
- export async function handleListConductorSessions(msg) {
108
- const { requestId, _requestClientId } = msg;
109
- const index = await loadConductorIndex();
110
-
111
- const agentId = ctx.CONFIG?.agentName || null;
112
- const filtered = agentId
113
- ? index.filter(e => !e.agentId || e.agentId === agentId)
114
- : index;
115
-
116
- // 更新活跃 session 的状态
117
- for (const entry of filtered) {
118
- const active = conductorSessions.get(entry.sessionId);
119
- if (active) {
120
- entry.status = active.status;
121
- }
122
- }
123
-
124
- ctx.sendToServer({
125
- type: 'conductor_sessions_list',
126
- requestId,
127
- _requestClientId,
128
- sessions: filtered.filter(e => !e.hidden)
129
- });
130
- }
34
+ // =====================================================================
35
+ // Conductor Lifecycle
36
+ // =====================================================================
131
37
 
132
38
  /**
133
- * 恢复已停止的 conductor session
39
+ * Initialize or restore the single Conductor instance.
40
+ * Called when user opens the Conductor UI.
134
41
  */
135
- export async function resumeConductorSession(msg) {
136
- const { sessionId, userId, username } = msg;
137
-
138
- // 已在内存中
139
- if (conductorSessions.has(sessionId)) {
140
- const session = conductorSessions.get(sessionId);
141
- // 加载 UI 消息
142
- if (!session.uiMessages || session.uiMessages.length === 0) {
143
- const loaded = await loadSessionMessages(sessionId);
144
- session.uiMessages = loaded.messages;
42
+ export async function initConductor(msg) {
43
+ const { userId, username } = msg;
44
+
45
+ // Already initialized in memory — just send current state
46
+ if (conductor) {
47
+ if (!conductor.uiMessages || conductor.uiMessages.length === 0) {
48
+ const loaded = await loadConductorMessages();
49
+ conductor.uiMessages = loaded.messages;
145
50
  }
146
- const cleaned = (session.uiMessages || []).map(m => {
51
+ const cleaned = (conductor.uiMessages || []).map(m => {
147
52
  const { _streaming, ...rest } = m;
148
53
  return rest;
149
54
  });
150
- const dir = getSessionDataDir(sessionId);
55
+ const dir = getConductorHome();
151
56
  const hasOlderMessages = await getMaxShardIndex(dir) > 0;
152
57
 
58
+ // Load state.json for task info
59
+ const state = await loadState();
60
+
153
61
  sendConductorMessage({
154
- type: 'conductor_session_restored',
155
- sessionId,
156
- name: session.name,
157
- workDir: session.workDir,
158
- scenarioId: session.scenarioId,
159
- tasks: Array.from(session.tasks.values()),
160
- userId: session.userId,
161
- username: session.username,
62
+ type: 'conductor_opened',
63
+ tasks: state.tasks,
64
+ userId: conductor.userId,
65
+ username: conductor.username,
162
66
  uiMessages: cleaned,
163
- hasOlderMessages
67
+ hasOlderMessages,
68
+ status: conductor.status
164
69
  });
165
- sendStatusUpdate(session);
166
- return;
70
+ sendStatusUpdate(conductor);
71
+ return conductor;
167
72
  }
168
73
 
169
- // 从磁盘恢复
170
- const meta = await loadSessionMeta(sessionId);
171
- if (!meta) {
172
- sendConductorMessage({
173
- type: 'error', sessionId,
174
- message: 'Conductor session not found'
175
- });
176
- return;
177
- }
74
+ // Ensure conductor home directory exists
75
+ await ensureConductorHome();
178
76
 
179
- const session = {
180
- id: sessionId,
181
- name: meta.name || '',
182
- workDir: meta.workDir || null,
183
- scenarioId: meta.scenarioId || null,
77
+ // Try restore from disk
78
+ const meta = await loadConductorMeta();
79
+
80
+ conductor = {
184
81
  status: 'running',
185
- tasks: new Map((meta.tasks || []).map(t => [t.taskId, t])),
82
+ tasks: new Map(),
186
83
  conductorState: null,
187
- costUsd: meta.costUsd || 0,
188
- totalInputTokens: meta.totalInputTokens || 0,
189
- totalOutputTokens: meta.totalOutputTokens || 0,
84
+ costUsd: meta?.costUsd || 0,
85
+ totalInputTokens: meta?.totalInputTokens || 0,
86
+ totalOutputTokens: meta?.totalOutputTokens || 0,
190
87
  activeClaudes: 0,
191
88
  uiMessages: [],
192
- userId: userId || meta.userId,
193
- username: username || meta.username,
194
- agentId: meta.agentId || ctx.CONFIG?.agentName || null,
195
- createdAt: meta.createdAt || Date.now(),
89
+ userId: userId || meta?.userId,
90
+ username: username || meta?.username,
91
+ createdAt: meta?.createdAt || Date.now(),
196
92
  _rotating: false,
197
93
  _conductorSemRelease: null
198
94
  };
199
95
 
200
- conductorSessions.set(sessionId, session);
96
+ // Load state.json to populate tasks map
97
+ const state = await loadState();
98
+ for (const [taskId, entry] of Object.entries(state.tasks || {})) {
99
+ conductor.tasks.set(taskId, entry);
100
+ }
101
+
102
+ // Load UI messages from disk
103
+ const loaded = await loadConductorMessages();
104
+ conductor.uiMessages = loaded.messages;
201
105
 
202
- const loaded = await loadSessionMessages(sessionId);
203
- session.uiMessages = loaded.messages;
106
+ const isResume = !!meta;
204
107
 
205
108
  sendConductorMessage({
206
- type: 'conductor_session_restored',
207
- sessionId,
208
- name: session.name,
209
- workDir: session.workDir,
210
- scenarioId: session.scenarioId,
211
- tasks: Array.from(session.tasks.values()),
212
- userId: session.userId,
213
- username: session.username,
214
- uiMessages: session.uiMessages,
215
- hasOlderMessages: loaded.hasOlderMessages
109
+ type: 'conductor_opened',
110
+ tasks: state.tasks,
111
+ userId: conductor.userId,
112
+ username: conductor.username,
113
+ uiMessages: conductor.uiMessages,
114
+ hasOlderMessages: loaded.hasOlderMessages,
115
+ status: conductor.status,
116
+ resumed: isResume
216
117
  });
217
- sendStatusUpdate(session);
118
+ sendStatusUpdate(conductor);
218
119
 
219
- // 启动 Conductor Claude
120
+ // Start Conductor Claude
220
121
  try {
221
- await createConductorClaude(session);
222
- console.log(`[Conductor] Session ${sessionId} resumed, Claude ready`);
122
+ await createConductorClaude(conductor);
123
+ console.log(`[Conductor] ${isResume ? 'Resumed' : 'Created'}, Claude ready`);
223
124
  } catch (e) {
224
- console.error('[Conductor] Failed to resume Claude:', e.message);
125
+ console.error('[Conductor] Failed to start Claude:', e.message);
126
+ sendConductorOutput(conductor, 'system', {
127
+ message: { role: 'assistant', content: `Conductor 启动失败: ${e.message}` }
128
+ });
225
129
  }
226
130
 
227
- await upsertConductorIndex(session);
228
- await saveSessionMeta(session);
131
+ await saveConductorMeta(conductor);
132
+ return conductor;
229
133
  }
230
134
 
231
135
  /**
232
- * 处理用户输入
136
+ * Handle user input to Conductor
233
137
  */
234
138
  export async function handleConductorUserInput(msg) {
235
- const { sessionId, content } = msg;
236
- const session = conductorSessions.get(sessionId);
237
- if (!session) {
238
- console.warn(`[Conductor] Session not found: ${sessionId}`);
139
+ const { content } = msg;
140
+ if (!conductor) {
141
+ console.warn('[Conductor] Not initialized, ignoring input');
239
142
  return;
240
143
  }
241
144
 
242
- // 确保 session 在运行态(先检查再记录,避免停止态也记录消息)
243
- if (session.status === 'stopped') {
145
+ if (conductor.status === 'stopped') {
244
146
  sendConductorMessage({
245
147
  type: 'conductor_error',
246
- sessionId,
247
- error: 'Session is stopped'
148
+ error: 'Conductor is stopped'
248
149
  });
249
150
  return;
250
151
  }
251
152
 
252
- // 记录到 UI
253
- recordUserMessage(session, content);
254
-
255
- session.status = 'running';
256
-
257
- // 发给 Conductor Claude
258
- await sendToConductor(session, content);
259
- }
260
-
261
- /**
262
- * 更新工作路径
263
- */
264
- export async function handleUpdateWorkDir(msg) {
265
- const { sessionId, workDir } = msg;
266
- const session = conductorSessions.get(sessionId);
267
- if (!session) return;
268
-
269
- session.workDir = workDir;
270
- console.log(`[Conductor] Session ${sessionId} workDir updated: ${workDir}`);
271
-
272
- sendConductorMessage({
273
- type: 'conductor_workdir_updated',
274
- sessionId,
275
- workDir
276
- });
277
- sendStatusUpdate(session);
278
- await saveSessionMeta(session);
279
- }
280
-
281
- /**
282
- * 更新 session name
283
- */
284
- export async function handleUpdateConductorSession(msg) {
285
- const { sessionId, name, workDir } = msg;
286
- const session = conductorSessions.get(sessionId);
287
- if (!session) return;
288
-
289
- if (name !== undefined) session.name = name;
290
- if (workDir !== undefined) session.workDir = workDir;
291
- await saveSessionMeta(session);
292
- await upsertConductorIndex(session);
153
+ recordUserMessage(conductor, content);
154
+ conductor.status = 'running';
155
+ await sendToConductor(conductor, content);
293
156
  }
294
157
 
295
158
  /**
296
- * 停止 session
159
+ * Stop Conductor
297
160
  */
298
- export async function stopConductorSession(sessionId) {
299
- const session = conductorSessions.get(sessionId);
300
- if (!session) return;
301
-
302
- session.status = 'stopped';
161
+ export async function stopConductor() {
162
+ if (!conductor) return;
303
163
 
304
- // 停止 Conductor Claude
305
- await stopConductorClaude(session);
164
+ conductor.status = 'stopped';
165
+ await stopConductorClaude(conductor);
306
166
 
307
- sendConductorOutput(session, 'system', {
308
- message: { role: 'assistant', content: 'Session 已停止' }
167
+ sendConductorOutput(conductor, 'system', {
168
+ message: { role: 'assistant', content: 'Conductor 已停止' }
309
169
  });
310
- sendStatusUpdate(session);
311
- await saveSessionMeta(session);
312
- await upsertConductorIndex(session);
170
+ sendStatusUpdate(conductor);
171
+ await saveConductorMeta(conductor);
313
172
 
314
- conductorSessions.delete(sessionId);
315
- console.log(`[Conductor] Session ${sessionId} stopped`);
173
+ conductor = null;
174
+ console.log('[Conductor] Stopped');
316
175
  }
317
176
 
318
177
  /**
319
- * 清空 session
178
+ * Clear Conductor (reset messages, restart Claude)
320
179
  */
321
- export async function clearConductorSession(sessionId) {
322
- const session = conductorSessions.get(sessionId);
323
- if (!session) return;
180
+ export async function clearConductor() {
181
+ if (!conductor) return;
324
182
 
325
- // 停止 Claude
326
- await stopConductorClaude(session);
183
+ await stopConductorClaude(conductor);
327
184
 
328
- // 清空数据
329
- session.tasks.clear();
330
- session.uiMessages = [];
331
- session.costUsd = 0;
332
- session.totalInputTokens = 0;
333
- session.totalOutputTokens = 0;
185
+ // Clear data but keep tasks
186
+ conductor.uiMessages = [];
187
+ conductor.costUsd = 0;
188
+ conductor.totalInputTokens = 0;
189
+ conductor.totalOutputTokens = 0;
334
190
 
335
- const dir = getSessionDataDir(sessionId);
191
+ const dir = getConductorHome();
336
192
  await cleanupMessageShards(dir);
337
193
 
338
- session.status = 'running';
194
+ conductor.status = 'running';
339
195
 
340
- sendConductorMessage({
341
- type: 'conductor_session_cleared',
342
- sessionId
343
- });
344
- sendStatusUpdate(session);
196
+ sendConductorMessage({ type: 'conductor_cleared' });
197
+ sendStatusUpdate(conductor);
345
198
 
346
- // 重启 Claude
199
+ // Restart Claude
347
200
  try {
348
- await createConductorClaude(session);
201
+ await createConductorClaude(conductor);
349
202
  } catch (e) {
350
203
  console.error('[Conductor] Failed to restart Claude after clear:', e.message);
351
204
  }
352
205
 
353
- await saveSessionMeta(session);
354
- console.log(`[Conductor] Session ${sessionId} cleared`);
206
+ await saveConductorMeta(conductor);
207
+ console.log('[Conductor] Cleared');
355
208
  }
@@ -1,14 +1,16 @@
1
1
  /**
2
- * Conductor — UI 消息辅助函数
2
+ * Conductor — UI Message Helpers (V5)
3
3
  *
4
- * 与 crew/ui-messages.js 结构一致,但用 conductor_ 前缀的消息类型。
5
- * 核心职责:向 server 发送 WebSocket 消息,维护 uiMessages 持久化列表。
4
+ * Sends WebSocket messages to server/frontend.
5
+ * Maintains uiMessages persistence list on the conductor instance.
6
+ *
7
+ * No sessionId — Conductor is a singleton per Agent.
6
8
  */
7
9
  import ctx from '../context.js';
8
- import { upsertConductorIndex, saveSessionMeta } from './persistence.js';
10
+ import { saveConductorMeta } from './persistence.js';
9
11
 
10
12
  /**
11
- * 发送 conductor 消息到 server(透传到 Web 前端)
13
+ * Send conductor message to server (forwarded to Web frontend)
12
14
  */
13
15
  export function sendConductorMessage(msg) {
14
16
  if (ctx.sendToServer) {
@@ -17,19 +19,18 @@ export function sendConductorMessage(msg) {
17
19
  }
18
20
 
19
21
  /**
20
- * 发送 Conductor Claude 的输出到前端
22
+ * Send Conductor Claude output to frontend
21
23
  * outputType: 'text' | 'tool_use' | 'tool_result' | 'system' | 'task_created' | 'task_forwarded'
22
24
  */
23
- export function sendConductorOutput(session, outputType, rawMessage, extra = {}) {
25
+ export function sendConductorOutput(conductor, outputType, rawMessage, extra = {}) {
24
26
  sendConductorMessage({
25
27
  type: 'conductor_output',
26
- sessionId: session.id,
27
28
  outputType,
28
29
  data: rawMessage,
29
30
  ...extra
30
31
  });
31
32
 
32
- // 记录精简 UI 消息
33
+ // Record trimmed UI messages
33
34
  if (outputType === 'text') {
34
35
  const content = rawMessage?.message?.content;
35
36
  let text = '';
@@ -39,10 +40,9 @@ export function sendConductorOutput(session, outputType, rawMessage, extra = {})
39
40
  text = content.filter(b => b.type === 'text').map(b => b.text).join('');
40
41
  }
41
42
  if (!text) return;
42
- // 反向查找 streaming 消息
43
43
  let found = false;
44
- for (let i = session.uiMessages.length - 1; i >= 0; i--) {
45
- const msg = session.uiMessages[i];
44
+ for (let i = conductor.uiMessages.length - 1; i >= 0; i--) {
45
+ const msg = conductor.uiMessages[i];
46
46
  if (msg.source === 'conductor' && msg.type === 'text' && msg._streaming) {
47
47
  msg.content += text;
48
48
  found = true;
@@ -50,7 +50,7 @@ export function sendConductorOutput(session, outputType, rawMessage, extra = {})
50
50
  }
51
51
  }
52
52
  if (!found) {
53
- session.uiMessages.push({
53
+ conductor.uiMessages.push({
54
54
  source: 'conductor', type: 'text', content: text,
55
55
  _streaming: true, timestamp: Date.now()
56
56
  });
@@ -61,25 +61,25 @@ export function sendConductorOutput(session, outputType, rawMessage, extra = {})
61
61
  if (typeof content === 'string') text = content;
62
62
  else if (Array.isArray(content)) text = content.filter(b => b.type === 'text').map(b => b.text).join('');
63
63
  if (!text) return;
64
- session.uiMessages.push({
64
+ conductor.uiMessages.push({
65
65
  source: 'conductor', type: 'system', content: text, timestamp: Date.now()
66
66
  });
67
67
  } else if (outputType === 'task_created') {
68
- session.uiMessages.push({
68
+ conductor.uiMessages.push({
69
69
  source: 'conductor', type: 'task_created',
70
70
  taskId: extra.taskId, taskTitle: extra.taskTitle,
71
71
  content: `Created task: ${extra.taskTitle}`,
72
72
  timestamp: Date.now()
73
73
  });
74
74
  } else if (outputType === 'task_forwarded') {
75
- session.uiMessages.push({
75
+ conductor.uiMessages.push({
76
76
  source: 'conductor', type: 'task_forwarded',
77
77
  taskId: extra.taskId,
78
78
  content: `Forwarded message to task: ${extra.taskId}`,
79
79
  timestamp: Date.now()
80
80
  });
81
81
  } else if (outputType === 'tool_use') {
82
- endConductorStreaming(session);
82
+ endConductorStreaming(conductor);
83
83
  const content = rawMessage?.message?.content;
84
84
  if (Array.isArray(content)) {
85
85
  for (const block of content) {
@@ -89,7 +89,7 @@ export function sendConductorOutput(session, outputType, rawMessage, extra = {})
89
89
  if (input.file_path) trimmedInput.file_path = input.file_path;
90
90
  if (input.command) trimmedInput.command = input.command.substring(0, 200);
91
91
  if (input.pattern) trimmedInput.pattern = input.pattern;
92
- session.uiMessages.push({
92
+ conductor.uiMessages.push({
93
93
  source: 'conductor', type: 'tool',
94
94
  toolName: block.name, toolId: block.id,
95
95
  toolInput: Object.keys(trimmedInput).length > 0 ? trimmedInput : null,
@@ -103,9 +103,9 @@ export function sendConductorOutput(session, outputType, rawMessage, extra = {})
103
103
  } else if (outputType === 'tool_result') {
104
104
  const toolId = rawMessage?.message?.tool_use_id;
105
105
  if (toolId) {
106
- for (let i = session.uiMessages.length - 1; i >= 0; i--) {
107
- if (session.uiMessages[i].type === 'tool' && session.uiMessages[i].toolId === toolId) {
108
- session.uiMessages[i].hasResult = true;
106
+ for (let i = conductor.uiMessages.length - 1; i >= 0; i--) {
107
+ if (conductor.uiMessages[i].type === 'tool' && conductor.uiMessages[i].toolId === toolId) {
108
+ conductor.uiMessages[i].hasResult = true;
109
109
  break;
110
110
  }
111
111
  }
@@ -114,56 +114,57 @@ export function sendConductorOutput(session, outputType, rawMessage, extra = {})
114
114
  }
115
115
 
116
116
  /**
117
- * 结束 Conductor 的 streaming 消息
117
+ * End Conductor streaming message
118
118
  */
119
- export function endConductorStreaming(session) {
120
- for (let i = session.uiMessages.length - 1; i >= 0; i--) {
121
- if (session.uiMessages[i].source === 'conductor' && session.uiMessages[i]._streaming) {
122
- delete session.uiMessages[i]._streaming;
119
+ export function endConductorStreaming(conductor) {
120
+ for (let i = conductor.uiMessages.length - 1; i >= 0; i--) {
121
+ if (conductor.uiMessages[i].source === 'conductor' && conductor.uiMessages[i]._streaming) {
122
+ delete conductor.uiMessages[i]._streaming;
123
123
  break;
124
124
  }
125
125
  }
126
126
  }
127
127
 
128
128
  /**
129
- * 记录用户消息到 uiMessages
129
+ * Record user message to uiMessages
130
130
  */
131
- export function recordUserMessage(session, content) {
132
- session.uiMessages.push({
131
+ export function recordUserMessage(conductor, content) {
132
+ conductor.uiMessages.push({
133
133
  source: 'user', type: 'text', content,
134
134
  timestamp: Date.now()
135
135
  });
136
136
  }
137
137
 
138
138
  /**
139
- * 发送 session 状态更新到前端
139
+ * Send conductor status update to frontend
140
140
  */
141
- export function sendStatusUpdate(session) {
142
- const tasks = Array.from(session.tasks.values()).map(t => ({
143
- taskId: t.taskId,
144
- title: t.title,
145
- workDir: t.workDir,
146
- status: t.status,
147
- phase: t.phase,
148
- progress: t.progress,
149
- activeActors: t.activeActors || [],
150
- createdAt: t.createdAt,
151
- updatedAt: t.updatedAt
152
- }));
141
+ export function sendStatusUpdate(conductor) {
142
+ const tasks = {};
143
+ for (const [taskId, t] of conductor.tasks) {
144
+ tasks[taskId] = {
145
+ taskId: t.taskId,
146
+ title: t.title,
147
+ workDir: t.workDir,
148
+ scenario: t.scenario,
149
+ status: t.status,
150
+ activeActors: t.activeActors || [],
151
+ currentStep: t.currentStep || '',
152
+ lastUpdate: t.lastUpdate || t.updatedAt
153
+ };
154
+ }
153
155
 
154
156
  sendConductorMessage({
155
157
  type: 'conductor_status',
156
- sessionId: session.id,
157
- status: session.status,
158
- workDir: session.workDir,
158
+ status: conductor.status,
159
159
  tasks,
160
- costUsd: session.costUsd,
161
- totalInputTokens: session.totalInputTokens,
162
- totalOutputTokens: session.totalOutputTokens,
163
- activeClaudes: session.activeClaudes || 0
160
+ costUsd: conductor.costUsd,
161
+ totalInputTokens: conductor.totalInputTokens,
162
+ totalOutputTokens: conductor.totalOutputTokens,
163
+ activeClaudes: conductor.activeClaudes || 0
164
164
  });
165
165
 
166
- // 异步持久化
167
- upsertConductorIndex(session).catch(e => console.warn('[Conductor] Failed to update index:', e.message));
168
- saveSessionMeta(session).catch(e => console.warn('[Conductor] Failed to save meta:', e.message));
166
+ // Async persist
167
+ saveConductorMeta(conductor).catch(e =>
168
+ console.warn('[Conductor] Failed to save meta:', e.message)
169
+ );
169
170
  }