@winmatrix/daemon 0.2.3 → 0.2.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 (32) hide show
  1. package/README.md +28 -29
  2. package/dist/core/AgentProcessManager.d.ts +22 -29
  3. package/dist/core/AgentProcessManager.d.ts.map +1 -1
  4. package/dist/core/AgentProcessManager.js +62 -31
  5. package/dist/core/AgentProcessManager.js.map +1 -1
  6. package/dist/core/ConfigLoader.d.ts +3 -1
  7. package/dist/core/ConfigLoader.d.ts.map +1 -1
  8. package/dist/core/ConfigLoader.js +15 -10
  9. package/dist/core/ConfigLoader.js.map +1 -1
  10. package/dist/core/DaemonFileLogger.d.ts.map +1 -1
  11. package/dist/core/DaemonFileLogger.js +38 -3
  12. package/dist/core/DaemonFileLogger.js.map +1 -1
  13. package/dist/core/DaemonLifecycle.d.ts.map +1 -1
  14. package/dist/core/DaemonLifecycle.js +6 -5
  15. package/dist/core/DaemonLifecycle.js.map +1 -1
  16. package/dist/wrapper/AgentProcessManager.d.ts +80 -0
  17. package/dist/wrapper/AgentProcessManager.d.ts.map +1 -0
  18. package/dist/wrapper/AgentProcessManager.js +474 -0
  19. package/dist/wrapper/AgentProcessManager.js.map +1 -0
  20. package/dist/wrapper/AgentWrapper.d.ts +2 -18
  21. package/dist/wrapper/AgentWrapper.d.ts.map +1 -1
  22. package/dist/wrapper/AgentWrapper.js +32 -679
  23. package/dist/wrapper/AgentWrapper.js.map +1 -1
  24. package/dist/wrapper/claudeCommand.d.ts +1 -1
  25. package/dist/wrapper/claudeCommand.d.ts.map +1 -1
  26. package/dist/wrapper/claudeCommand.js +2 -2
  27. package/dist/wrapper/claudeCommand.js.map +1 -1
  28. package/package.json +4 -2
  29. package/dist/wrapper/ClaudeStreamParser.d.ts +0 -63
  30. package/dist/wrapper/ClaudeStreamParser.d.ts.map +0 -1
  31. package/dist/wrapper/ClaudeStreamParser.js +0 -104
  32. package/dist/wrapper/ClaudeStreamParser.js.map +0 -1
@@ -1,56 +1,24 @@
1
1
  /**
2
- * @deprecated 方案 B 下不再使用(adapter daemon 进程内直接运行,不再 spawn 子进程)。
3
- * 保留供未来 multiprocess 模式参考,所有维护性修改应在此注明原因。
2
+ * @deprecated Scheme B runs adapters in-process; this multiprocess wrapper remains for reference.
4
3
  *
5
- * Agent Wrapper - Daemon 子进程入口
6
- *
7
- * 由 Daemon 通过 agent.create 信令 spawn 启动。
8
- * 通过 stdin/stdout JSON 帧与 Daemon 通信,Daemon 负责通过 WS 多路复用转发给 Server。
9
- *
10
- * 协议(stdin/stdout 均为 JSON Lines):
11
- * stdin: {"type":"fwd","data":<Server 发来的帧>}
12
- * stdout: {"type":"fwd","agentId":"...","data":<发往 Server 的帧>}
13
- * stdout: {"type":"event","event":"agent.ready","payload":{...}}
14
- * stdout: {"type":"event","event":"agent.stopped","payload":{...}}
15
- * stdout: {"type":"event","event":"agent.error","payload":{...}}
16
- *
17
- * 任务模型:按 agentType 分发到不同执行模式:
18
- * claude-code → spawn claude --print --output-format stream-json ...
19
- * hermes → fetch() POST 到 HTTP endpoint
20
- * default → spawn(runtime, ['--print', '--output-format', 'json', input])
4
+ * Agent Wrapper stdin/stdout JSON Lines IPC with Daemon; task execution delegated to WrapperProcessManager.
21
5
  */
22
- import { spawn } from 'node:child_process';
23
- import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
24
- import { join } from 'node:path';
25
- import { homedir } from 'node:os';
26
- import { randomUUID } from 'node:crypto';
27
- import { unwrapOpenClawPayloads } from '@winmatrix/agent-sdk';
28
- import { parseClaudeLine, splitLines } from './ClaudeStreamParser.js';
29
- import { resolveClaudeRuntimeCommand } from './claudeCommand.js';
30
6
  import { scanWorkDir, detectRuntimeInfo } from '../core/WorkspaceScanner.js';
31
- /* ── State ── */
7
+ import { WrapperProcessManager, } from './AgentProcessManager.js';
32
8
  let config = null;
33
- const runningTasks = new Map();
34
- /* ── Helpers ── */
9
+ let processManager = null;
35
10
  function send(frame) {
36
11
  process.stdout.write(JSON.stringify(frame) + '\n');
37
12
  }
38
13
  function parseStdin(line) {
39
14
  try {
40
15
  const parsed = JSON.parse(line);
41
- if (parsed && typeof parsed === 'object') {
42
- // Handle tool_result frames from daemon (Tool Proxy Fallback)
43
- if (parsed.type === 'tool_result') {
44
- handleToolResult(parsed);
45
- return null; // Not a server frame, don't forward
46
- }
47
- if (parsed.type === 'fwd') {
48
- return parsed;
49
- }
16
+ if (parsed && typeof parsed === 'object' && parsed.type === 'fwd') {
17
+ return parsed;
50
18
  }
51
19
  }
52
20
  catch {
53
- // ignore malformed lines
21
+ /* ignore malformed lines */
54
22
  }
55
23
  return null;
56
24
  }
@@ -76,11 +44,7 @@ function sendTaskDelta(taskId, content, meta) {
76
44
  send({
77
45
  type: 'fwd',
78
46
  agentId: config.agentId,
79
- data: {
80
- type: 'event',
81
- event: 'task.delta',
82
- payload,
83
- },
47
+ data: { type: 'event', event: 'task.delta', payload },
84
48
  });
85
49
  }
86
50
  function sendTaskComplete(taskId, result, sessionId, usage) {
@@ -94,576 +58,9 @@ function sendTaskComplete(taskId, result, sessionId, usage) {
94
58
  send({
95
59
  type: 'fwd',
96
60
  agentId: config.agentId,
97
- data: {
98
- type: 'event',
99
- event: 'task.complete',
100
- payload,
101
- },
61
+ data: { type: 'event', event: 'task.complete', payload },
102
62
  });
103
63
  }
104
- function cleanupTask(taskId) {
105
- const entry = runningTasks.get(taskId);
106
- if (!entry)
107
- return;
108
- runningTasks.delete(taskId);
109
- }
110
- /* ── Task Routing ── */
111
- function routeTask(task) {
112
- if (!config)
113
- return;
114
- const taskId = task.taskId;
115
- if (runningTasks.has(taskId)) {
116
- sendTaskError(taskId, 'Task already running');
117
- return;
118
- }
119
- switch (config.agentType) {
120
- case 'claude-code':
121
- spawnClaudeCodeTask(task);
122
- break;
123
- case 'hermes':
124
- spawnHermesTask(task);
125
- break;
126
- case 'openclaw':
127
- spawnOpenClawTask(task);
128
- break;
129
- default:
130
- spawnGenericTask(task);
131
- break;
132
- }
133
- }
134
- /* ── Claude Code Task ── */
135
- /**
136
- * 在 workDir 下生成 .claude/mcp.json 配置,让 Claude Code 能通过 MCP Bridge 调用 WinMatrix 工具。
137
- * 如果 .claude/mcp.json 已存在,使用 mcp.winmatrix.json 并通过 --mcp-config 指定。
138
- */
139
- function generateClaudeMcpConfig(mcpBridgeUrl, mcpApiKey, workDir) {
140
- const mcpConfig = {
141
- mcpServers: {
142
- winmatrix: {
143
- type: 'http',
144
- url: mcpBridgeUrl,
145
- ...(mcpApiKey ? { headers: { Authorization: `Bearer ${mcpApiKey}` } } : {}),
146
- },
147
- },
148
- };
149
- const configDir = workDir ? join(workDir, '.claude') : join(homedir(), '.claude');
150
- const primaryPath = join(configDir, 'mcp.json');
151
- const fallbackPath = join(configDir, 'mcp.winmatrix.json');
152
- try {
153
- if (!existsSync(configDir)) {
154
- mkdirSync(configDir, { recursive: true });
155
- }
156
- if (existsSync(primaryPath)) {
157
- writeFileSync(fallbackPath, JSON.stringify(mcpConfig, null, 2), 'utf-8');
158
- return ['--mcp-config', fallbackPath];
159
- }
160
- writeFileSync(primaryPath, JSON.stringify(mcpConfig, null, 2), 'utf-8');
161
- return null;
162
- }
163
- catch (err) {
164
- console.warn(`MCP 配置写入失败: ${err instanceof Error ? err.message : String(err)}`);
165
- return null;
166
- }
167
- }
168
- const CLAUDE_BASE_ARGS = [
169
- '--print',
170
- '--verbose',
171
- '--input-format', 'stream-json',
172
- '--output-format', 'stream-json',
173
- '--include-partial-messages',
174
- ];
175
- function spawnClaudeCodeTask(task) {
176
- if (!config)
177
- return;
178
- const taskId = task.taskId;
179
- const workDir = task.workDir ?? config.workDir;
180
- const claude = task.claude ?? {};
181
- // Validate workDir
182
- if (workDir && !existsSync(workDir)) {
183
- sendTaskError(taskId, `Work directory does not exist: ${workDir}`);
184
- return;
185
- }
186
- // Assemble CLI args
187
- const args = [...CLAUDE_BASE_ARGS];
188
- // Permission mode (from task config, default 'auto')
189
- const permissionMode = claude.permissionMode ?? 'bypassPermissions';
190
- args.push('--permission-mode', permissionMode);
191
- // workDir → --add-dir
192
- if (workDir) {
193
- args.push('--add-dir', workDir);
194
- }
195
- // Session management: oneshot skips --session-id entirely
196
- const isOneshot = task.mode === 'oneshot';
197
- let sessionId;
198
- if (!isOneshot) {
199
- if (claude.forkSession) {
200
- sessionId = randomUUID();
201
- args.push('--session-id', sessionId);
202
- }
203
- else if (claude.resumeSession && claude.sessionId) {
204
- sessionId = claude.sessionId;
205
- args.push('--resume', sessionId);
206
- }
207
- else if (claude.sessionId) {
208
- sessionId = claude.sessionId;
209
- args.push('--session-id', sessionId);
210
- }
211
- else {
212
- sessionId = randomUUID();
213
- args.push('--session-id', sessionId);
214
- }
215
- }
216
- // Tool restriction
217
- if (claude.tools && claude.tools.length > 0) {
218
- args.push('--allowedTools', claude.tools.join(','));
219
- }
220
- if (claude.disallowedTools && claude.disallowedTools.length > 0) {
221
- args.push('--disallowedTools', claude.disallowedTools.join(','));
222
- }
223
- // Budget
224
- if (claude.maxBudgetUsd !== undefined) {
225
- args.push('--max-budget-usd', String(claude.maxBudgetUsd));
226
- }
227
- // System prompt: task.systemPrompt 优先于 claude.systemPrompt
228
- const effectiveSystemPrompt = task.systemPrompt ?? claude.systemPrompt;
229
- if (effectiveSystemPrompt) {
230
- args.push('--append-system-prompt', effectiveSystemPrompt);
231
- }
232
- // MCP Bridge 配置:在 workDir 下生成 .claude/mcp.json 或 mcp.winmatrix.json
233
- if (config.mcpBridgeUrl) {
234
- const effectiveMcpKey = task.mcpToken ?? config.mcpApiKey;
235
- const mcpArgs = generateClaudeMcpConfig(config.mcpBridgeUrl, effectiveMcpKey, workDir);
236
- if (mcpArgs) {
237
- args.push(...mcpArgs);
238
- }
239
- }
240
- // Build env
241
- const env = {
242
- ...process.env,
243
- WINMATRIX_AGENT_ID: config.agentId,
244
- WINMATRIX_API_KEY: config.apiKey,
245
- WINMATRIX_AGENT_NAME: config.name,
246
- WINMATRIX_AGENT_TYPE: config.agentType,
247
- WINMATRIX_TASK_ID: taskId,
248
- ...task.env,
249
- };
250
- let proc;
251
- try {
252
- const spawnSpec = resolveClaudeRuntimeCommand(config.runtime);
253
- proc = spawn(spawnSpec.command, args, {
254
- stdio: ['pipe', 'pipe', 'pipe'],
255
- env,
256
- cwd: workDir ?? process.cwd(),
257
- shell: spawnSpec.shell,
258
- });
259
- }
260
- catch (err) {
261
- const message = err instanceof Error ? err.message : String(err);
262
- sendTaskError(taskId, `Failed to spawn Claude Code: ${message}`);
263
- return;
264
- }
265
- const entry = {
266
- taskId,
267
- process: proc,
268
- abortController: null,
269
- startedAt: Date.now(),
270
- stdoutChunks: [],
271
- completed: false,
272
- };
273
- runningTasks.set(taskId, entry);
274
- proc.on('error', (err) => {
275
- if (entry.completed)
276
- return;
277
- entry.completed = true;
278
- sendTaskError(taskId, err.message);
279
- cleanupTask(taskId);
280
- });
281
- // Send user message via stdin (stream-json protocol)
282
- const userMessage = JSON.stringify({
283
- type: 'user',
284
- message: { role: 'user', content: task.input },
285
- }) + '\n';
286
- proc.stdin?.write(userMessage);
287
- proc.stdin?.end();
288
- // Track extracted fields from result event for task.complete
289
- let extractedSessionId;
290
- let extractedResultText;
291
- let extractedUsage;
292
- // Parse stdout stream-json events
293
- let stdoutBuffer = '';
294
- proc.stdout?.on('data', (data) => {
295
- const chunk = data.toString('utf-8');
296
- const { lines, remainder } = splitLines(stdoutBuffer, chunk);
297
- stdoutBuffer = remainder;
298
- for (const line of lines) {
299
- entry.stdoutChunks.push(line + '\n');
300
- const action = parseClaudeLine(line);
301
- switch (action.type) {
302
- case 'delta':
303
- sendTaskDelta(taskId, action.text);
304
- break;
305
- case 'tool_use':
306
- sendTaskDelta(taskId, `[调用工具: ${action.toolName}]`, { toolUse: { name: action.toolName, input: action.toolInput } });
307
- break;
308
- case 'result':
309
- if (action.sessionId) {
310
- extractedSessionId = action.sessionId;
311
- }
312
- if (action.result) {
313
- extractedResultText = action.result;
314
- }
315
- if (action.usage) {
316
- extractedUsage = action.usage;
317
- }
318
- break;
319
- case 'error':
320
- entry.completed = true;
321
- sendTaskError(taskId, action.message);
322
- return;
323
- case 'skip':
324
- break;
325
- }
326
- }
327
- });
328
- // Forward stderr as task.delta with meta marker to distinguish from stdout content
329
- proc.stderr?.on('data', (data) => {
330
- const text = data.toString('utf-8').trimEnd();
331
- if (!text)
332
- return;
333
- sendTaskDelta(taskId, text, { source: 'stderr' });
334
- });
335
- proc.on('exit', (code, signal) => {
336
- if (entry.completed)
337
- return;
338
- entry.completed = true;
339
- if (extractedSessionId || code === 0) {
340
- // Use result text extracted from stream-json result event
341
- sendTaskComplete(taskId, { result: extractedResultText ?? null, exitCode: code ?? 0 }, isOneshot ? undefined : extractedSessionId, extractedUsage);
342
- }
343
- else {
344
- sendTaskError(taskId, signal
345
- ? `Claude Code killed by signal ${signal}`
346
- : `Claude Code exited with code ${code ?? 'null'}`);
347
- }
348
- cleanupTask(taskId);
349
- });
350
- }
351
- /* ── HTTP Task (shared by hermes / openclaw) ── */
352
- const DEFAULT_HTTP_TIMEOUT_MS = 180000;
353
- /**
354
- * Shared HTTP POST task execution.
355
- * Hermes and OpenClaw both POST to an endpoint; OpenClaw additionally unwraps the payload.
356
- *
357
- * Tool Proxy Fallback: when the HTTP response contains "unsupported mcp biz type" or
358
- * errcode 846610, the Wrapper sends tool_call IPC frames to the daemon for each
359
- * fallback tool call, aggregates results, and completes the task with mode: 'fallback'.
360
- */
361
- function spawnHttpTask(task, options) {
362
- if (!config)
363
- return;
364
- const taskId = task.taskId;
365
- // Task-level endpoint overrides config-level; fallback to deprecated hermesEndpoint
366
- const endpoint = task.endpoint ?? config.endpoint ?? config.hermesEndpoint;
367
- if (!endpoint) {
368
- sendTaskError(taskId, 'HTTP endpoint not configured');
369
- return;
370
- }
371
- const timeoutMs = task.hermes?.timeoutMs ?? DEFAULT_HTTP_TIMEOUT_MS;
372
- const abortController = new AbortController();
373
- const timeoutId = setTimeout(() => abortController.abort(), timeoutMs);
374
- const entry = {
375
- taskId,
376
- process: null,
377
- abortController,
378
- startedAt: Date.now(),
379
- stdoutChunks: [],
380
- completed: false,
381
- };
382
- runningTasks.set(taskId, entry);
383
- // Build request headers
384
- const headers = { 'Content-Type': 'application/json' };
385
- const token = task.endpointToken ?? config.endpointToken;
386
- if (token) {
387
- headers['Authorization'] = `Bearer ${token}`;
388
- }
389
- // systemPrompt 拼接到 input 前方(Hermes/OpenClaw Gateway 不支持独立 system prompt 参数)
390
- const effectiveInput = task.systemPrompt
391
- ? `${task.systemPrompt}\n\n---\n\n${task.input}`
392
- : task.input;
393
- entry.completionPromise = fetch(endpoint, {
394
- method: 'POST',
395
- headers,
396
- body: JSON.stringify({
397
- taskId: task.taskId,
398
- input: effectiveInput,
399
- context: task.context,
400
- ...(task.systemPrompt ? { systemPrompt: task.systemPrompt } : {}),
401
- ...(config.mcpBridgeUrl ? { mcpBridgeUrl: config.mcpBridgeUrl } : {}),
402
- ...((task.mcpToken ?? config.mcpApiKey) ? { mcpToken: (task.mcpToken ?? config.mcpApiKey) } : {}),
403
- }),
404
- signal: abortController.signal,
405
- })
406
- .then(async (res) => {
407
- clearTimeout(timeoutId);
408
- if (entry.completed)
409
- return;
410
- entry.completed = true;
411
- if (!res.ok) {
412
- sendTaskError(taskId, `HTTP ${res.status}: ${res.statusText}`);
413
- cleanupTask(taskId);
414
- return;
415
- }
416
- const bodyText = await res.text();
417
- let parsed = bodyText;
418
- try {
419
- parsed = JSON.parse(bodyText);
420
- }
421
- catch {
422
- // Non-JSON response, keep raw text
423
- }
424
- // Apply unwrap if configured (openclaw) — unwrap expects raw string, internally JSON.parses
425
- if (options.unwrap && typeof parsed === 'object' && parsed !== null) {
426
- try {
427
- parsed = options.unwrap(bodyText);
428
- }
429
- catch {
430
- // unwrap failed, keep original
431
- }
432
- }
433
- // Tool Proxy Fallback detection
434
- const responseStr = typeof parsed === 'string' ? parsed : JSON.stringify(parsed);
435
- if (/unsupported mcp biz type|errcode.*846610/i.test(responseStr)) {
436
- const fallbackCalls = extractFallbackToolCalls(parsed);
437
- if (fallbackCalls.length > 0) {
438
- await executeFallbackToolCalls(taskId, fallbackCalls);
439
- cleanupTask(taskId);
440
- return;
441
- }
442
- }
443
- sendTaskComplete(taskId, parsed);
444
- cleanupTask(taskId);
445
- })
446
- .catch((err) => {
447
- clearTimeout(timeoutId);
448
- if (entry.completed)
449
- return;
450
- entry.completed = true;
451
- const message = err instanceof Error ? err.message : String(err);
452
- if (err.name === 'AbortError') {
453
- sendTaskError(taskId, `HTTP request timed out after ${timeoutMs}ms`);
454
- }
455
- else {
456
- sendTaskError(taskId, `HTTP request failed: ${message}`);
457
- }
458
- cleanupTask(taskId);
459
- });
460
- }
461
- function spawnHermesTask(task) {
462
- spawnHttpTask(task, {});
463
- }
464
- function spawnOpenClawTask(task) {
465
- spawnHttpTask(task, { unwrap: unwrapOpenClawPayloads });
466
- }
467
- function extractFallbackToolCalls(parsed) {
468
- const calls = [];
469
- // Try to extract from common response shapes
470
- let data = parsed;
471
- if (typeof data === 'string') {
472
- try {
473
- data = JSON.parse(data);
474
- }
475
- catch {
476
- return calls;
477
- }
478
- }
479
- if (!data || typeof data !== 'object')
480
- return calls;
481
- // Shape: { tool_calls: [{ name, arguments }, ...] }
482
- const rawCalls = data.tool_calls ?? data.toolCalls ?? data.calls;
483
- if (Array.isArray(rawCalls)) {
484
- for (const c of rawCalls) {
485
- if (typeof c !== 'object' || c === null)
486
- continue;
487
- const name = c.name ?? c.tool_name ?? c.function;
488
- if (typeof name !== 'string')
489
- continue;
490
- let args = {};
491
- const rawArgs = c.arguments ?? c.args ?? c.parameters;
492
- if (typeof rawArgs === 'string') {
493
- try {
494
- args = JSON.parse(rawArgs);
495
- }
496
- catch {
497
- args = { raw: rawArgs };
498
- }
499
- }
500
- else if (typeof rawArgs === 'object' && rawArgs !== null) {
501
- args = rawArgs;
502
- }
503
- calls.push({ toolName: name, args });
504
- }
505
- }
506
- // Shape: { tool_query: "..." } → single tool call
507
- if (calls.length === 0 && typeof data.tool_query === 'string') {
508
- calls.push({ toolName: 'tool_query', args: { query: data.tool_query } });
509
- }
510
- return calls;
511
- }
512
- async function executeFallbackToolCalls(taskId, calls) {
513
- const results = [];
514
- for (const call of calls) {
515
- const callId = randomUUID();
516
- // Send tool_call IPC to daemon
517
- process.stdout.write(JSON.stringify({
518
- type: 'tool_call',
519
- callId,
520
- taskId,
521
- toolName: call.toolName,
522
- args: call.args,
523
- }) + '\n');
524
- // Wait for tool_result from daemon via stdin
525
- try {
526
- const result = await waitForToolResult(callId, 30000);
527
- results.push({ toolName: call.toolName, ok: true, result });
528
- }
529
- catch (err) {
530
- const message = err instanceof Error ? err.message : String(err);
531
- results.push({ toolName: call.toolName, ok: false, error: message });
532
- }
533
- }
534
- sendTaskComplete(taskId, { mode: 'fallback', calls: results });
535
- }
536
- /** Pending tool_result resolvers keyed by callId */
537
- const toolResultPending = new Map();
538
- function waitForToolResult(callId, timeoutMs) {
539
- return new Promise((resolve, reject) => {
540
- const timer = setTimeout(() => {
541
- toolResultPending.delete(callId);
542
- reject(new Error('Tool call timed out'));
543
- }, timeoutMs);
544
- toolResultPending.set(callId, { resolve, reject, timer });
545
- });
546
- }
547
- /** Called from stdin handler when a tool_result frame arrives from daemon */
548
- function handleToolResult(frame) {
549
- const pending = toolResultPending.get(frame.callId);
550
- if (!pending)
551
- return;
552
- clearTimeout(pending.timer);
553
- toolResultPending.delete(frame.callId);
554
- if (frame.ok) {
555
- pending.resolve(frame.result);
556
- }
557
- else {
558
- pending.reject(new Error(frame.error ?? 'Tool call failed'));
559
- }
560
- }
561
- /* ── Generic Task (fallback) ── */
562
- function spawnGenericTask(task) {
563
- if (!config)
564
- return;
565
- const taskId = task.taskId;
566
- const workDir = task.workDir ?? config.workDir;
567
- const args = ['--print', '--output-format', 'json', task.input];
568
- let proc;
569
- try {
570
- proc = spawn(config.runtime, args, {
571
- stdio: ['pipe', 'pipe', 'pipe'],
572
- env: {
573
- ...process.env,
574
- WINMATRIX_AGENT_ID: config.agentId,
575
- WINMATRIX_API_KEY: config.apiKey,
576
- WINMATRIX_AGENT_NAME: config.name,
577
- WINMATRIX_AGENT_TYPE: config.agentType,
578
- WINMATRIX_TASK_ID: taskId,
579
- ...task.env,
580
- },
581
- cwd: workDir ?? process.cwd(),
582
- });
583
- }
584
- catch (err) {
585
- const message = err instanceof Error ? err.message : String(err);
586
- sendTaskError(taskId, `Failed to spawn runtime: ${message}`);
587
- return;
588
- }
589
- const entry = {
590
- taskId,
591
- process: proc,
592
- abortController: null,
593
- startedAt: Date.now(),
594
- stdoutChunks: [],
595
- completed: false,
596
- };
597
- runningTasks.set(taskId, entry);
598
- proc.stdout?.on('data', (data) => {
599
- const text = data.toString('utf-8');
600
- entry.stdoutChunks.push(text);
601
- const trimmed = text.trimEnd();
602
- if (!trimmed)
603
- return;
604
- sendTaskDelta(taskId, trimmed);
605
- });
606
- proc.stderr?.on('data', (data) => {
607
- const text = data.toString('utf-8').trimEnd();
608
- if (!text)
609
- return;
610
- sendTaskDelta(taskId, text, { source: 'stderr' });
611
- });
612
- proc.on('exit', (code, signal) => {
613
- if (entry.completed)
614
- return;
615
- entry.completed = true;
616
- if (code === 0) {
617
- const rawOutput = entry.stdoutChunks.join('');
618
- let parsedResult = rawOutput;
619
- try {
620
- parsedResult = JSON.parse(rawOutput);
621
- }
622
- catch {
623
- // Non-JSON output, keep raw
624
- }
625
- sendTaskComplete(taskId, parsedResult);
626
- }
627
- else {
628
- sendTaskError(taskId, signal
629
- ? `Process killed by signal ${signal}`
630
- : `Process exited with code ${code ?? 'null'}`);
631
- }
632
- cleanupTask(taskId);
633
- });
634
- proc.on('error', (err) => {
635
- if (entry.completed)
636
- return;
637
- entry.completed = true;
638
- sendTaskError(taskId, err.message);
639
- cleanupTask(taskId);
640
- });
641
- }
642
- /* ── Task Cancellation ── */
643
- function cancelTask(taskId) {
644
- const entry = runningTasks.get(taskId);
645
- if (!entry)
646
- return;
647
- if (entry.abortController) {
648
- entry.abortController.abort();
649
- }
650
- if (entry.process) {
651
- try {
652
- entry.process.kill('SIGTERM');
653
- setTimeout(() => {
654
- try {
655
- entry.process?.kill('SIGKILL');
656
- }
657
- catch { /* already dead */ }
658
- }, 10000);
659
- }
660
- catch {
661
- // process already exited
662
- }
663
- }
664
- cleanupTask(taskId);
665
- }
666
- /* ── Register Flow ── */
667
64
  function sendRegister() {
668
65
  if (!config)
669
66
  return;
@@ -688,13 +85,11 @@ function sendRegister() {
688
85
  },
689
86
  });
690
87
  }
691
- /* ── Stdin Event Handling ── */
692
88
  async function sendWorkspaceReport() {
693
89
  if (!config?.workDir)
694
90
  return;
695
91
  try {
696
92
  const workdir = await scanWorkDir(config.workDir);
697
- // Detect key runtime versions
698
93
  const runtimes = {};
699
94
  const runtimeBinaries = config.runtime ? [config.runtime] : [];
700
95
  for (const bin of runtimeBinaries) {
@@ -708,30 +103,24 @@ async function sendWorkspaceReport() {
708
103
  send({
709
104
  type: 'fwd',
710
105
  agentId: config.agentId,
711
- data: {
712
- type: 'event',
713
- event: 'workspace.report',
714
- payload,
715
- },
106
+ data: { type: 'event', event: 'workspace.report', payload },
716
107
  });
717
108
  }
718
109
  catch {
719
- // workspace scan failed silently - workDir may be inaccessible
110
+ /* workspace scan failed silently */
720
111
  }
721
112
  }
722
113
  function handleServerFrame(data) {
723
- if (!config)
114
+ if (!config || !processManager)
724
115
  return;
725
- // Handle register response
726
116
  if (data.type === 'res' && data.ok === true) {
727
117
  send({
728
118
  type: 'event',
729
119
  event: 'agent.ready',
730
120
  payload: { agentId: config.agentId, pid: process.pid },
731
121
  });
732
- // Fire-and-forget workspace scan after agent is ready
733
122
  if (config.workDir) {
734
- sendWorkspaceReport();
123
+ void sendWorkspaceReport();
735
124
  }
736
125
  return;
737
126
  }
@@ -747,29 +136,24 @@ function handleServerFrame(data) {
747
136
  process.exit(1);
748
137
  return;
749
138
  }
750
- // Handle task.assign event
751
139
  if (data.type === 'event' && data.event === 'task.assign') {
752
140
  const payload = (data.payload ?? {});
753
141
  if (payload.taskId && payload.input) {
754
- routeTask(payload);
142
+ processManager.routeTask(payload);
755
143
  }
756
144
  return;
757
145
  }
758
- // Handle task.cancel event
759
146
  if (data.type === 'event' && data.event === 'task.cancel') {
760
147
  const payload = data.payload;
761
148
  if (payload?.taskId) {
762
- cancelTask(payload.taskId);
149
+ processManager.cancelTask(payload.taskId);
763
150
  }
764
151
  return;
765
152
  }
766
- // Handle reconnect event (Daemon reconnected, re-register)
767
153
  if (data.type === 'event' && data.event === 'reconnect') {
768
154
  sendRegister();
769
- return;
770
155
  }
771
156
  }
772
- /* ── Main ── */
773
157
  function main() {
774
158
  const args = process.argv.slice(2);
775
159
  const parsed = {};
@@ -797,16 +181,17 @@ function main() {
797
181
  send({
798
182
  type: 'event',
799
183
  event: 'agent.error',
800
- payload: {
801
- error: 'Missing required --agent-id or WINMATRIX_API_KEY env',
802
- },
184
+ payload: { error: 'Missing required --agent-id or WINMATRIX_API_KEY env' },
803
185
  });
804
186
  process.exit(1);
805
187
  return;
806
188
  }
807
- // Step 1: Send register frame via stdout (Daemon forwards to Server)
189
+ processManager = new WrapperProcessManager(config, {
190
+ onDelta: sendTaskDelta,
191
+ onComplete: sendTaskComplete,
192
+ onError: sendTaskError,
193
+ });
808
194
  sendRegister();
809
- // Step 2: Wait for register response and task events via stdin
810
195
  process.stdin.setEncoding('utf-8');
811
196
  let buffer = '';
812
197
  process.stdin.on('data', (chunk) => {
@@ -824,53 +209,21 @@ function main() {
824
209
  }
825
210
  });
826
211
  process.stdin.on('end', () => {
827
- shutdown();
212
+ void shutdown();
828
213
  });
829
214
  process.stdin.resume();
830
- // Handle termination signals
831
- function shutdown() {
832
- // First pass: abort fetch tasks and SIGTERM child processes
833
- for (const [, entry] of runningTasks) {
834
- if (entry.abortController) {
835
- entry.abortController.abort();
836
- }
837
- if (entry.process) {
838
- try {
839
- entry.process.kill('SIGTERM');
840
- }
841
- catch { /* already dead */ }
842
- }
843
- }
844
- // Wait for in-flight fetch tasks to settle (with 5s timeout),
845
- // then SIGKILL any remaining child processes.
846
- const fetchPromises = [...runningTasks.values()]
847
- .filter((e) => e.completionPromise)
848
- .map((e) => e.completionPromise);
849
- const finalize = () => {
850
- for (const [, entry] of runningTasks) {
851
- if (entry.process) {
852
- try {
853
- entry.process.kill('SIGKILL');
854
- }
855
- catch { /* already dead */ }
856
- }
857
- }
858
- runningTasks.clear();
859
- process.exit(0);
860
- };
861
- if (fetchPromises.length > 0) {
862
- Promise.race([
863
- Promise.allSettled(fetchPromises),
864
- new Promise((resolve) => setTimeout(resolve, 5000)),
865
- ]).finally(finalize);
866
- }
867
- else {
868
- // No fetch tasks: 10s grace for child process SIGTERM → SIGKILL
869
- setTimeout(finalize, 10000);
215
+ async function shutdown() {
216
+ if (processManager) {
217
+ await processManager.shutdown();
870
218
  }
219
+ process.exit(0);
871
220
  }
872
- process.on('SIGTERM', shutdown);
873
- process.on('SIGINT', shutdown);
221
+ process.on('SIGTERM', () => {
222
+ void shutdown();
223
+ });
224
+ process.on('SIGINT', () => {
225
+ void shutdown();
226
+ });
874
227
  }
875
228
  main();
876
229
  //# sourceMappingURL=AgentWrapper.js.map