@winmatrix/daemon 1.0.4 → 1.0.5

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.
Files changed (46) hide show
  1. package/bin/winmatrix-daemon.js +3 -1
  2. package/dist/core/AgentProcessManager.d.ts +111 -0
  3. package/dist/core/AgentProcessManager.d.ts.map +1 -0
  4. package/dist/core/AgentProcessManager.js +701 -0
  5. package/dist/core/AgentProcessManager.js.map +1 -0
  6. package/dist/core/ConfigLoader.d.ts +110 -0
  7. package/dist/core/ConfigLoader.d.ts.map +1 -0
  8. package/dist/core/ConfigLoader.js +296 -0
  9. package/dist/core/ConfigLoader.js.map +1 -0
  10. package/dist/core/DaemonClient.d.ts +102 -0
  11. package/dist/core/DaemonClient.d.ts.map +1 -0
  12. package/dist/core/DaemonClient.js +225 -0
  13. package/dist/core/DaemonClient.js.map +1 -0
  14. package/dist/core/DaemonFileLogger.d.ts +34 -0
  15. package/dist/core/DaemonFileLogger.d.ts.map +1 -0
  16. package/dist/core/DaemonFileLogger.js +192 -0
  17. package/dist/core/DaemonFileLogger.js.map +1 -0
  18. package/dist/core/DaemonLifecycle.d.ts +22 -0
  19. package/dist/core/DaemonLifecycle.d.ts.map +1 -0
  20. package/dist/core/DaemonLifecycle.js +157 -0
  21. package/dist/core/DaemonLifecycle.js.map +1 -0
  22. package/dist/core/DiagnosticsServer.d.ts +28 -0
  23. package/dist/core/DiagnosticsServer.d.ts.map +1 -0
  24. package/dist/core/DiagnosticsServer.js +155 -0
  25. package/dist/core/DiagnosticsServer.js.map +1 -0
  26. package/dist/core/InstallationId.d.ts +14 -0
  27. package/dist/core/InstallationId.d.ts.map +1 -0
  28. package/dist/core/InstallationId.js +50 -0
  29. package/dist/core/InstallationId.js.map +1 -0
  30. package/dist/core/RuntimeDoctor.d.ts +13 -0
  31. package/dist/core/RuntimeDoctor.d.ts.map +1 -0
  32. package/dist/core/RuntimeDoctor.js +86 -0
  33. package/dist/core/RuntimeDoctor.js.map +1 -0
  34. package/dist/core/WinMatrixAgentClient.d.ts +183 -0
  35. package/dist/core/WinMatrixAgentClient.d.ts.map +1 -0
  36. package/dist/core/WinMatrixAgentClient.js +555 -0
  37. package/dist/core/WinMatrixAgentClient.js.map +1 -0
  38. package/dist/core/WorkspaceScanner.d.ts +45 -0
  39. package/dist/core/WorkspaceScanner.d.ts.map +1 -0
  40. package/dist/core/WorkspaceScanner.js +166 -0
  41. package/dist/core/WorkspaceScanner.js.map +1 -0
  42. package/dist/index.d.ts +16 -0
  43. package/dist/index.d.ts.map +1 -0
  44. package/dist/index.js +264 -0
  45. package/dist/index.js.map +1 -0
  46. package/package.json +3 -3
@@ -0,0 +1,701 @@
1
+ /**
2
+ * WinMatrix Daemon - Agent Process Manager (v2)
3
+ *
4
+ * 管理外部 Agent 适配器实例生命周期�? * 收到 server �?agent.create / agent.stop 信令后,
5
+ * 实例化对应的 AgentAdapter 并创�?WinMatrixAgentClient 连接�? *
6
+ * 方案 B:adapter �?daemon 进程内直接运行,不再 spawn 子进程�? */
7
+ import { WinMatrixAgentClient, } from './WinMatrixAgentClient.js';
8
+ import { getGlobalRegistry, } from '@winmatrix/agent-sdk';
9
+ import { METHODS, } from '@winmatrix/protocol';
10
+ import { formatDaemonLocalTimestamp } from './DaemonFileLogger.js';
11
+ /* ── Manager Class ── */
12
+ export class AgentProcessManager {
13
+ agents = new Map();
14
+ pendingCreations = new Map();
15
+ registry;
16
+ taskAudit = [];
17
+ static TASK_AUDIT_MAX = 120;
18
+ onProfilesChanged = null;
19
+ constructor(registry) {
20
+ this.registry = registry ?? getGlobalRegistry();
21
+ }
22
+ /** Set callback invoked when agent profiles change (create/destroy/rescan) */
23
+ setOnProfilesChanged(fn) {
24
+ this.onProfilesChanged = fn;
25
+ }
26
+ recordTaskAudit(entry) {
27
+ this.taskAudit.push(entry);
28
+ if (this.taskAudit.length > AgentProcessManager.TASK_AUDIT_MAX) {
29
+ this.taskAudit.splice(0, this.taskAudit.length - AgentProcessManager.TASK_AUDIT_MAX);
30
+ }
31
+ }
32
+ appendRunEvent(run, event) {
33
+ const envelope = {
34
+ runKey: run.ref.runKey,
35
+ engineId: run.ref.engineId,
36
+ invocationFingerprint: run.ref.invocationFingerprint,
37
+ sequence: run.lastSequence + 1,
38
+ timestamp: formatDaemonLocalTimestamp(),
39
+ event,
40
+ };
41
+ run.lastSequence = envelope.sequence;
42
+ run.events.push(envelope);
43
+ if (run.events.length > 1_000)
44
+ run.events.splice(0, run.events.length - 1_000);
45
+ return envelope;
46
+ }
47
+ async handleAgentEngineRequest(managed, method, paramsValue) {
48
+ const params = paramsValue && typeof paramsValue === 'object'
49
+ ? paramsValue
50
+ : {};
51
+ const runKey = typeof params.runKey === 'string' ? params.runKey : undefined;
52
+ const run = runKey ? managed.runs.get(runKey) : undefined;
53
+ if (method === METHODS.TASK_EVENTS_SUBSCRIBE) {
54
+ if (!run)
55
+ throw new Error(`Run not found: ${runKey ?? ''}`);
56
+ const afterSequence = typeof params.afterSequence === 'number' ? params.afterSequence : 0;
57
+ return { events: run.events.filter((event) => event.sequence > afterSequence) };
58
+ }
59
+ if (method === METHODS.TASK_INSPECT) {
60
+ if (!run)
61
+ throw new Error(`Run not found: ${runKey ?? ''}`);
62
+ return {
63
+ ref: run.ref,
64
+ ...(run.sessionRef ? { sessionRef: run.sessionRef } : {}),
65
+ status: run.status,
66
+ executionDurability: 'connection_bound',
67
+ eventStreamMode: 'replay',
68
+ eventRetention: { maxEvents: 1_000 },
69
+ lastSequence: run.lastSequence,
70
+ pendingInteractions: [],
71
+ ...(run.result ? { result: run.result } : {}),
72
+ recoverable: false,
73
+ protocolVersion: '1',
74
+ };
75
+ }
76
+ if (method === METHODS.TASK_INTERACTION_RESPOND) {
77
+ const response = params.response && typeof params.response === 'object'
78
+ ? params.response
79
+ : {};
80
+ return {
81
+ status: 'rejected',
82
+ responseId: String(response.responseId ?? 'missing'),
83
+ interactionId: String(response.interactionId ?? 'missing'),
84
+ error: {
85
+ code: 'unsupported_capability',
86
+ message: 'External adapter does not support interactive responses',
87
+ },
88
+ };
89
+ }
90
+ if (method === METHODS.TASK_CANCEL_RUN) {
91
+ if (!run)
92
+ throw new Error(`Run not found: ${runKey ?? ''}`);
93
+ if (['completed', 'failed', 'cancelled', 'interrupted'].includes(run.status)) {
94
+ return { status: 'already_terminal' };
95
+ }
96
+ run.status = 'cancel_requested';
97
+ this.appendRunEvent(run, { type: 'status', text: 'cancel_requested' });
98
+ managed.abortController?.abort();
99
+ if (managed.adapter.cancel)
100
+ await managed.adapter.cancel(run.ref.runKey);
101
+ return { status: 'accepted' };
102
+ }
103
+ if (method === METHODS.TASK_ACTIVE_LIST) {
104
+ const limit = typeof params.limit === 'number' && params.limit > 0 ? params.limit : 50;
105
+ const engineId = typeof params.engineId === 'string' ? params.engineId : undefined;
106
+ const runs = [...managed.runs.values()]
107
+ .filter((candidate) => ['accepted', 'starting', 'running', 'waiting_interaction', 'cancel_requested'].includes(candidate.status)
108
+ && (!engineId || candidate.ref.engineId === engineId))
109
+ .slice(0, limit)
110
+ .map((candidate) => ({
111
+ ref: candidate.ref,
112
+ ...(candidate.sessionRef ? { sessionRef: candidate.sessionRef } : {}),
113
+ status: candidate.status,
114
+ startedAt: candidate.startedAt,
115
+ lastSequence: candidate.lastSequence,
116
+ pendingInteractionCount: 0,
117
+ }));
118
+ return { runs };
119
+ }
120
+ const engineId = managed.adapter.meta.agentType === 'claude-code'
121
+ || managed.adapter.meta.agentType === 'codex'
122
+ || managed.adapter.meta.agentType === 'hermes'
123
+ || managed.adapter.meta.agentType === 'openclaw'
124
+ ? managed.adapter.meta.agentType
125
+ : 'custom';
126
+ if (method === METHODS.AGENT_ENGINE_LIST) {
127
+ const health = await managed.adapter.healthCheck();
128
+ return {
129
+ engines: [{
130
+ engineId,
131
+ label: managed.adapter.meta.runtime,
132
+ availability: health.available ? 'ready' : 'unavailable',
133
+ ...(health.version ? { version: health.version } : {}),
134
+ }],
135
+ };
136
+ }
137
+ if (method === METHODS.AGENT_ENGINE_MODELS)
138
+ return { models: [] };
139
+ if (method === METHODS.AGENT_ENGINE_OPTIONS) {
140
+ return { engineId, modes: [], configOptions: [], commands: [], checkedAt: formatDaemonLocalTimestamp() };
141
+ }
142
+ if (method === METHODS.AGENT_ENGINE_PROBE) {
143
+ const requestedEngineId = typeof params.engineId === 'string' ? params.engineId : engineId;
144
+ if (requestedEngineId !== engineId) {
145
+ return {
146
+ engineId: requestedEngineId,
147
+ instanceId: managed.agentId,
148
+ hostKind: 'external_computer',
149
+ checkedAt: formatDaemonLocalTimestamp(),
150
+ status: 'unavailable',
151
+ binaryStatus: 'missing',
152
+ credentialStatus: 'unknown',
153
+ issues: [{ code: 'engine_not_registered', message: `Registered engine is ${engineId}` }],
154
+ };
155
+ }
156
+ const health = await managed.adapter.healthCheck();
157
+ return {
158
+ engineId,
159
+ instanceId: managed.agentId,
160
+ hostKind: 'external_computer',
161
+ checkedAt: formatDaemonLocalTimestamp(),
162
+ status: health.available ? 'ready' : 'unavailable',
163
+ binaryStatus: health.available ? 'found' : 'missing',
164
+ credentialStatus: health.available ? 'ready' : 'unknown',
165
+ ...(health.version ? { version: health.version } : {}),
166
+ issues: health.error ? [{ code: 'adapter_health_failed', message: health.error }] : [],
167
+ };
168
+ }
169
+ throw new Error(`Unsupported Agent Engine method: ${method}`);
170
+ }
171
+ /**
172
+ * 遍历所�?managed agents,收集其 tools/skills/toolCategories 统一上报�? * 返回深拷贝的 profiles,确保修改原 meta 不影响已收集的数据�? */
173
+ async collectAgentProfiles() {
174
+ const profiles = [];
175
+ for (const [agentId, agent] of this.agents) {
176
+ profiles.push({
177
+ agentId,
178
+ agentType: agent.adapter.meta.agentType,
179
+ // 深拷�?tools 数组
180
+ tools: [...agent.adapter.meta.tools],
181
+ // 深拷�?skills 数组及其内部对象
182
+ skills: agent.adapter.meta.skills.map(s => ({ ...s })),
183
+ // 深拷�?toolCategories 及其内部数组
184
+ toolCategories: Object.fromEntries(Object.entries(agent.adapter.meta.toolCategories).map(([k, v]) => [k, [...v]])),
185
+ });
186
+ }
187
+ return profiles;
188
+ }
189
+ /**
190
+ * 重新扫描所�?agent �?skill 目录并触发立即心跳上报�? * 用于诊断页面的「刷新技能」按钮手动触发�? */
191
+ async rescanAndReportProfiles() {
192
+ for (const [, agent] of this.agents) {
193
+ if (agent.adapter.discoverSkills) {
194
+ try {
195
+ const skills = await agent.adapter.discoverSkills({ workDir: agent.workDir });
196
+ agent.adapter.meta.skills.splice(0, agent.adapter.meta.skills.length, ...skills);
197
+ }
198
+ catch {
199
+ // best-effort
200
+ }
201
+ }
202
+ }
203
+ this.onProfilesChanged?.();
204
+ }
205
+ /**
206
+ * 创建 Agent 连接并启�?adapter�? *
207
+ * 每个 agent 拥有独立�?WinMatrixAgentClient + WebSocket 连接�? */
208
+ async createAgent(payload) {
209
+ const existing = this.agents.get(payload.agentId);
210
+ if (existing) {
211
+ return { ok: true, agent: existing };
212
+ }
213
+ const pending = this.pendingCreations.get(payload.agentId);
214
+ if (pending) {
215
+ return pending;
216
+ }
217
+ const creation = this.createAgentInternal(payload);
218
+ this.pendingCreations.set(payload.agentId, creation);
219
+ try {
220
+ return await creation;
221
+ }
222
+ finally {
223
+ if (this.pendingCreations.get(payload.agentId) === creation) {
224
+ this.pendingCreations.delete(payload.agentId);
225
+ }
226
+ }
227
+ }
228
+ async createAgentInternal(payload) {
229
+ if (!this.registry.has(payload.agentType)) {
230
+ return { ok: false, error: `Unknown agent type: ${payload.agentType}. Registered: ${this.registry.listTypes().join(', ')}` };
231
+ }
232
+ const adapter = this.registry.create(payload.agentType, payload.adapterConfig);
233
+ let managed;
234
+ const client = new WinMatrixAgentClient({
235
+ url: payload.serverUrl,
236
+ apiKey: payload.apiKey,
237
+ agentId: payload.agentId,
238
+ name: payload.name ?? payload.agentType,
239
+ agentType: payload.agentType,
240
+ capabilities: adapter.meta.capabilities,
241
+ autoReconnect: true,
242
+ taskAssignAdmission: (paramsValue) => {
243
+ const params = paramsValue && typeof paramsValue === 'object'
244
+ ? paramsValue
245
+ : {};
246
+ const runKey = typeof params.runKey === 'string' ? params.runKey : undefined;
247
+ const fingerprint = typeof params.invocationFingerprint === 'string'
248
+ ? params.invocationFingerprint
249
+ : undefined;
250
+ if (!runKey || !fingerprint)
251
+ return { ok: true };
252
+ const existingRun = managed.runs.get(runKey);
253
+ if (existingRun) {
254
+ return existingRun.ref.invocationFingerprint === fingerprint
255
+ ? { ok: true, disposition: 'existing' }
256
+ : { ok: false, error: `RUN_IDENTITY_MISMATCH: ${runKey}` };
257
+ }
258
+ if (managed.busy)
259
+ return { ok: false, error: 'AGENT_BUSY' };
260
+ return { ok: true, disposition: 'accepted' };
261
+ },
262
+ agentEngineRequestHandler: (method, params) => this.handleAgentEngineRequest(managed, method, params),
263
+ sessionQueryHandler: async (method, params) => {
264
+ if (method === 'session.list') {
265
+ return adapter.listSessions?.(params) ?? { sessions: [], total: 0 };
266
+ }
267
+ if (method === 'session.get') {
268
+ const p = params;
269
+ return (adapter.getSession?.(p.sessionId, p) ?? {
270
+ session: { id: p.sessionId, summary: 'N/A', createdAt: '', lastActivityAt: '' },
271
+ messageCount: 0,
272
+ truncated: false,
273
+ userPrompts: [],
274
+ assistantText: '',
275
+ lastAssistantText: '',
276
+ });
277
+ }
278
+ if (method === 'session.search') {
279
+ // task 6.10:会话搜索。daemon 端 ClaudeAdapter 支持时委派,否则返回空结果。
280
+ const p = params;
281
+ return (adapter.searchSessions?.(p) ?? { sessions: [] });
282
+ }
283
+ if (method === 'session.delete') {
284
+ // task 6.10:幂等删除会话。requestId 支撑上层幂等去重(daemon 端按 sessionId 删除文件)。
285
+ const p = params;
286
+ return (adapter.deleteSession?.(p.sessionId, { requestId: p.requestId ?? p.sessionId }) ?? {
287
+ status: 'rejected',
288
+ error: 'deleteSession not supported by adapter',
289
+ });
290
+ }
291
+ throw new Error(`Unsupported session method: ${method}`);
292
+ },
293
+ });
294
+ managed = {
295
+ agentId: payload.agentId,
296
+ adapter,
297
+ client,
298
+ abortController: null,
299
+ busy: false,
300
+ workDir: payload.workDir,
301
+ runs: new Map(),
302
+ };
303
+ // 断连时不再立即 abort CC 进程 —— 让其继续执行,
304
+ // 完成后等待 client 重连再发送结果。
305
+ // 仅在非 busy 时记录日志。
306
+ client.on('disconnect', (() => {
307
+ if (!managed.busy)
308
+ return;
309
+ console.warn(`[agent:${payload.agentId}] connection disconnected during task execution, ` +
310
+ `CC process will continue; result will be sent after reconnect`);
311
+ }));
312
+ // task.resume 中途递话(harden-external-computer-daemon P2/D5):
313
+ // 运行中 run → adapter.steer(turn 边界注入 stdin,立即排队生效);
314
+ // 无运行中 run / adapter 不支持 → 回 task.error(调用方可走 followup 新 run)
315
+ client.on('resume', ((input) => {
316
+ const resumeInput = input;
317
+ const text = typeof resumeInput.text === 'string' ? resumeInput.text.trim() : '';
318
+ if (!text) {
319
+ client.sendFrame({
320
+ type: 'event',
321
+ event: 'task.error',
322
+ payload: { taskId: resumeInput.taskId, error: 'task.resume 缺少 text', code: 'RESUME_REJECTED' },
323
+ });
324
+ return;
325
+ }
326
+ const result = managed.adapter.steer?.(text) ?? { accepted: false, reason: 'unsupported' };
327
+ if (result.accepted) {
328
+ console.log(`[agent:${payload.agentId}] steer accepted taskId=${resumeInput.taskId} textLength=${text.length}`);
329
+ this.recordTaskAudit({
330
+ ts: formatDaemonLocalTimestamp(),
331
+ agentId: payload.agentId,
332
+ taskId: resumeInput.taskId,
333
+ event: 'task_start',
334
+ detail: `steer textLength=${text.length}`,
335
+ });
336
+ }
337
+ else {
338
+ client.sendFrame({
339
+ type: 'event',
340
+ event: 'task.error',
341
+ payload: {
342
+ taskId: resumeInput.taskId,
343
+ error: `steer 未受理: ${result.reason ?? 'no_active_run'}`,
344
+ code: 'RESUME_REJECTED',
345
+ },
346
+ });
347
+ }
348
+ }));
349
+ // Handle task.assign(design D11.1:支持统一信封 runKey/fingerprint 幂等 launch)
350
+ client.on('task', (async (taskInput) => {
351
+ const task = taskInput;
352
+ const runKey = task.runKey;
353
+ const fingerprint = task.invocationFingerprint;
354
+ if (managed.busy) {
355
+ // 幂等重投:同一 runKey 重复 assign 且 fingerprint 一致 → 不重复执行,按正常处理静默丢弃
356
+ // (task.assign ACK 已在 SDK 收到帧时回 ok,此处无需再发事件)。
357
+ if (runKey && runKey === managed.activeRunKey) {
358
+ if (fingerprint && managed.activeFingerprint && fingerprint !== managed.activeFingerprint) {
359
+ // 同 runKey 不同 fingerprint → 身份冲突
360
+ this.recordTaskAudit({
361
+ ts: formatDaemonLocalTimestamp(),
362
+ agentId: payload.agentId,
363
+ taskId: task.taskId,
364
+ event: 'task_fail',
365
+ detail: 'RUN_IDENTITY_MISMATCH',
366
+ });
367
+ client.sendFrame({
368
+ type: 'event',
369
+ event: 'task.error',
370
+ payload: {
371
+ taskId: task.taskId,
372
+ error: `Run identity mismatch for runKey=${runKey}`,
373
+ code: 'RUN_IDENTITY_MISMATCH',
374
+ },
375
+ });
376
+ }
377
+ // 同 runKey 同 fingerprint:重复 assign,静默幂等 ack(不重复执行)
378
+ return;
379
+ }
380
+ // 不同 runKey 且 busy → 真正的并发拒绝
381
+ this.recordTaskAudit({
382
+ ts: formatDaemonLocalTimestamp(),
383
+ agentId: payload.agentId,
384
+ taskId: task.taskId,
385
+ event: 'task_fail',
386
+ detail: 'AGENT_BUSY',
387
+ });
388
+ client.sendFrame({
389
+ type: 'event',
390
+ event: 'task.error',
391
+ payload: { taskId: task.taskId, error: 'Agent busy', code: 'AGENT_BUSY' },
392
+ });
393
+ return;
394
+ }
395
+ managed.busy = true;
396
+ managed.activeRunKey = runKey;
397
+ managed.activeFingerprint = fingerprint;
398
+ const engineId = task.launchSpec?.engineId
399
+ ?? (adapter.meta.agentType === 'claude-code'
400
+ || adapter.meta.agentType === 'codex'
401
+ || adapter.meta.agentType === 'hermes'
402
+ || adapter.meta.agentType === 'openclaw'
403
+ ? adapter.meta.agentType
404
+ : 'custom');
405
+ const run = runKey && fingerprint
406
+ ? {
407
+ ref: {
408
+ instance: { instanceId: payload.agentId, hostKind: 'external_computer' },
409
+ executionSnapshot: {},
410
+ runKey,
411
+ engineId,
412
+ invocationFingerprint: fingerprint,
413
+ },
414
+ status: 'running',
415
+ startedAt: formatDaemonLocalTimestamp(),
416
+ lastSequence: 0,
417
+ events: [],
418
+ }
419
+ : undefined;
420
+ if (run) {
421
+ managed.runs.set(run.ref.runKey, run);
422
+ if (managed.runs.size > 100) {
423
+ const oldestTerminal = [...managed.runs.entries()].find(([, candidate]) => ['completed', 'failed', 'cancelled', 'interrupted'].includes(candidate.status));
424
+ if (oldestTerminal)
425
+ managed.runs.delete(oldestTerminal[0]);
426
+ }
427
+ this.appendRunEvent(run, { type: 'status', text: 'running' });
428
+ }
429
+ console.log(`[agent:${payload.agentId}] task start taskId=${task.taskId} runKey=${runKey ?? '-'} inputLength=${task.input.length}`);
430
+ this.recordTaskAudit({
431
+ ts: formatDaemonLocalTimestamp(),
432
+ agentId: payload.agentId,
433
+ taskId: task.taskId,
434
+ event: 'task_start',
435
+ // 仅记录长度等非敏感元数据;runKey 非敏感,可记录
436
+ detail: `inputLength=${task.input.length}${runKey ? ` runKey=${runKey}` : ''}`,
437
+ });
438
+ const abortController = new AbortController();
439
+ managed.abortController = abortController;
440
+ /**
441
+ * 尝试通过 client 发送帧。如果当前 disconnected,等待重连后再发送。
442
+ * 超时后放弃(避免无限等待)。
443
+ */
444
+ const waitForReconnectAndSend = async (sendFn, label) => {
445
+ // 先尝试直接发送
446
+ try {
447
+ if (client.getState() === 'registered') {
448
+ sendFn();
449
+ return;
450
+ }
451
+ }
452
+ catch { /* ignore */ }
453
+ // 等待重连,最多 120 秒
454
+ const deadline = Date.now() + 120_000;
455
+ while (Date.now() < deadline) {
456
+ await new Promise((r) => setTimeout(r, 2000));
457
+ try {
458
+ if (client.getState() === 'registered') {
459
+ sendFn();
460
+ console.log(`[agent:${payload.agentId}] ${label} sent after reconnect`);
461
+ return;
462
+ }
463
+ }
464
+ catch { /* ignore */ }
465
+ }
466
+ console.warn(`[agent:${payload.agentId}] ${label} dropped: reconnect timeout (120s)`);
467
+ };
468
+ try {
469
+ for await (const event of adapter.execute({
470
+ taskId: task.taskId,
471
+ input: task.input,
472
+ mode: 'session',
473
+ workDir: managed.workDir ?? payload.workDir ?? task.workDir,
474
+ systemPrompt: task.systemPrompt,
475
+ mcpToken: task.mcpToken,
476
+ sessionId: task.sessionId,
477
+ resumeSession: task.resumeSession,
478
+ env: task.env ?? task.launchSpec?.nonSensitiveEnv,
479
+ claude: task.claude,
480
+ hermes: task.hermes,
481
+ openclaw: task.openclaw,
482
+ }, abortController.signal)) {
483
+ if (event.type === 'thinking') {
484
+ if (run) {
485
+ this.appendRunEvent(run, event.engineEvent ?? { type: 'thinking_delta', text: event.content });
486
+ }
487
+ // thinking delta 断连时可丢弃,不等待重连
488
+ try {
489
+ client.sendFrame({
490
+ type: 'event',
491
+ event: 'task.delta',
492
+ payload: {
493
+ taskId: task.taskId,
494
+ content: event.content,
495
+ meta: { stream: 'thinking' },
496
+ engineEvent: event.engineEvent ?? { type: 'thinking_delta', text: event.content },
497
+ },
498
+ });
499
+ }
500
+ catch { /* best-effort */ }
501
+ continue;
502
+ }
503
+ switch (event.type) {
504
+ case 'delta':
505
+ if (run) {
506
+ this.appendRunEvent(run, event.engineEvent ?? { type: 'content_delta', text: event.content });
507
+ }
508
+ // delta 断连时可丢弃(server 端通过 result 兜底)
509
+ try {
510
+ client.streamDelta(task.taskId, event.content, undefined, event.engineEvent ?? { type: 'content_delta', text: event.content });
511
+ }
512
+ catch { /* best-effort */ }
513
+ break;
514
+ case 'tool_call': {
515
+ if (run) {
516
+ this.appendRunEvent(run, event.engineEvent ?? { type: 'tool_call', name: event.name });
517
+ }
518
+ try {
519
+ await client.callTool(event.name, event.args, task.taskId);
520
+ }
521
+ catch (err) {
522
+ console.error(`[agent:${payload.agentId}] tool call failed tool=${event.name} error=${err instanceof Error ? err.message : String(err)}`);
523
+ }
524
+ break;
525
+ }
526
+ case 'result':
527
+ if (run) {
528
+ const result = {
529
+ success: true,
530
+ text: event.content,
531
+ ...(event.sessionId ? { sessionId: event.sessionId } : {}),
532
+ ...(event.usage ? {
533
+ usage: { inputTokens: event.usage.input, outputTokens: event.usage.output },
534
+ } : {}),
535
+ };
536
+ run.status = 'completed';
537
+ run.result = result;
538
+ if (event.sessionId) {
539
+ run.sessionRef = {
540
+ instance: run.ref.instance,
541
+ engineId: run.ref.engineId,
542
+ sessionId: event.sessionId,
543
+ };
544
+ }
545
+ this.appendRunEvent(run, { type: 'result', result });
546
+ }
547
+ // 关键帧:等待重连后发送,确保服务端收到最终结果
548
+ await waitForReconnectAndSend(() => client.completeTask(task.taskId, event.content, event.sessionId, event.usage), `task complete taskId=${task.taskId}`);
549
+ console.log(`[agent:${payload.agentId}] task complete taskId=${task.taskId} sessionId=${event.sessionId ?? '-'}`);
550
+ this.recordTaskAudit({
551
+ ts: formatDaemonLocalTimestamp(),
552
+ agentId: payload.agentId,
553
+ taskId: task.taskId,
554
+ event: 'task_complete',
555
+ detail: `sessionId=${event.sessionId ?? '-'}`,
556
+ });
557
+ return;
558
+ case 'error':
559
+ if (run) {
560
+ const result = { success: false, error: event.message };
561
+ run.status = 'failed';
562
+ run.result = result;
563
+ this.appendRunEvent(run, { type: 'error', error: event.message, ...(event.code ? { code: event.code } : {}) });
564
+ this.appendRunEvent(run, { type: 'result', result });
565
+ }
566
+ await waitForReconnectAndSend(() => client.failTask(task.taskId, event.message, event.code), `task error taskId=${task.taskId}`);
567
+ console.error(`[agent:${payload.agentId}] task error taskId=${task.taskId} error=${event.message}`);
568
+ this.recordTaskAudit({
569
+ ts: formatDaemonLocalTimestamp(),
570
+ agentId: payload.agentId,
571
+ taskId: task.taskId,
572
+ event: 'task_error',
573
+ detail: event.message,
574
+ });
575
+ return;
576
+ }
577
+ }
578
+ if (run && !run.result) {
579
+ const result = {
580
+ success: false,
581
+ error: 'Adapter stream ended without a terminal result',
582
+ };
583
+ run.status = 'interrupted';
584
+ run.result = result;
585
+ this.appendRunEvent(run, { type: 'result', result });
586
+ }
587
+ }
588
+ catch (err) {
589
+ const msg = err instanceof Error ? err.message : String(err);
590
+ if (run && !run.result) {
591
+ const cancelled = abortController.signal.aborted;
592
+ const result = {
593
+ success: false,
594
+ error: cancelled ? 'cancelled' : msg,
595
+ };
596
+ run.status = cancelled ? 'cancelled' : 'failed';
597
+ run.result = result;
598
+ this.appendRunEvent(run, cancelled
599
+ ? { type: 'status', text: 'cancelled' }
600
+ : { type: 'error', error: msg });
601
+ this.appendRunEvent(run, { type: 'result', result });
602
+ }
603
+ await waitForReconnectAndSend(() => client.failTask(task.taskId, msg), `task fail taskId=${task.taskId}`).catch(() => { });
604
+ console.error(`[agent:${payload.agentId}] task failed taskId=${task.taskId} error=${msg}`);
605
+ this.recordTaskAudit({
606
+ ts: formatDaemonLocalTimestamp(),
607
+ agentId: payload.agentId,
608
+ taskId: task.taskId,
609
+ event: 'task_fail',
610
+ detail: msg,
611
+ });
612
+ }
613
+ finally {
614
+ managed.abortController = null;
615
+ managed.busy = false;
616
+ managed.activeRunKey = undefined;
617
+ managed.activeFingerprint = undefined;
618
+ }
619
+ }));
620
+ // Connect
621
+ try {
622
+ await client.connect();
623
+ }
624
+ catch (err) {
625
+ const msg = err instanceof Error ? err.message : String(err);
626
+ client.disconnect();
627
+ return { ok: false, error: `Agent ${payload.agentId} connection failed: ${msg}` };
628
+ }
629
+ this.agents.set(payload.agentId, managed);
630
+ console.log(`[daemon] Agent ${payload.agentId} connected (type: ${payload.agentType})`);
631
+ // Discover skills on creation and write to adapter meta
632
+ if (adapter.discoverSkills) {
633
+ try {
634
+ const skills = await adapter.discoverSkills({ workDir: payload.workDir });
635
+ adapter.meta.skills.splice(0, adapter.meta.skills.length, ...skills);
636
+ }
637
+ catch {
638
+ // best-effort
639
+ }
640
+ }
641
+ // Notify profile change
642
+ this.onProfilesChanged?.();
643
+ return { ok: true, agent: managed };
644
+ }
645
+ /**
646
+ * 停止 Agent �?取消正在执行的任务,断开连接�? */
647
+ async stopAgent(payload) {
648
+ const agent = this.agents.get(payload.agentId);
649
+ if (!agent) {
650
+ return { ok: false, error: `Agent ${payload.agentId} is not running` };
651
+ }
652
+ // Cancel running task
653
+ if (agent.abortController) {
654
+ agent.abortController.abort();
655
+ }
656
+ // Disconnect
657
+ try {
658
+ await agent.client.disconnect();
659
+ }
660
+ catch {
661
+ // Best-effort
662
+ }
663
+ this.agents.delete(payload.agentId);
664
+ console.log(`[daemon] Agent ${payload.agentId} stopped`);
665
+ // Notify profile change
666
+ this.onProfilesChanged?.();
667
+ return { ok: true };
668
+ }
669
+ /** 停止所�?Agent(shutdown 时调用) */
670
+ async stopAllAgents() {
671
+ for (const [agentId, agent] of this.agents) {
672
+ try {
673
+ if (agent.abortController) {
674
+ agent.abortController.abort();
675
+ }
676
+ await agent.client.disconnect();
677
+ console.log(`[daemon] Agent ${agentId} stopped during shutdown`);
678
+ }
679
+ catch {
680
+ // Best-effort
681
+ }
682
+ }
683
+ this.agents.clear();
684
+ }
685
+ /** 列出当前运行�?Agent */
686
+ listAgents() {
687
+ return [...this.agents.values()].map((a) => ({
688
+ agentId: a.agentId,
689
+ state: a.client.getState(),
690
+ }));
691
+ }
692
+ /** 最近一段时间接�?完成的任务(内存环,供本机诊断页�?*/
693
+ getRecentTaskEvents() {
694
+ return [...this.taskAudit];
695
+ }
696
+ /** 检�?Agent 是否在运�?*/
697
+ isRunning(agentId) {
698
+ return this.agents.has(agentId);
699
+ }
700
+ }
701
+ //# sourceMappingURL=AgentProcessManager.js.map