@winmatrix/supervisor 1.0.5 → 1.0.7
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/package.json +1 -5
- package/src/claude-session-lib.mjs +84 -0
- package/src/engine-run-success.mjs +118 -0
- package/src/env-sanitizer.mjs +4 -0
- package/src/openclaw-device-store.mjs +208 -0
- package/src/openclaw-pair-workstation.mjs +190 -0
- package/src/run-agent-engine.mjs +318 -77
- package/src/run-engine.mjs +139 -7
package/src/run-agent-engine.mjs
CHANGED
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
cloneMessagesForJson,
|
|
40
40
|
buildCliReplayGuide,
|
|
41
41
|
buildDiagnostics,
|
|
42
|
+
compactSessionDetailForTransport,
|
|
42
43
|
maybeTruncate,
|
|
43
44
|
resolveDirViaGlobalListSessions,
|
|
44
45
|
} from './claude-session-lib.mjs';
|
|
@@ -50,19 +51,30 @@ import { filterBlockedArgs, formatBlockedArgsAudit } from './blocked-args-filter
|
|
|
50
51
|
import { createDeliverableState, updateDeliverable, extractDeliverable, computeDeliverableFromEvents } from './deliverable-tracker.mjs';
|
|
51
52
|
import { extractTokenUsage } from './token-usage.mjs';
|
|
52
53
|
import { isResumeRejected, buildContinuityNotice, buildResumeDegradedEvent, isResumeFailureOnExit } from './resume-degradation.mjs';
|
|
54
|
+
import { extractSdkResultSignals, resolveEngineRunOutcome } from './engine-run-success.mjs';
|
|
53
55
|
|
|
54
56
|
const require = createRequire(import.meta.url);
|
|
55
57
|
const fs = require('node:fs');
|
|
56
58
|
const path = require('node:path');
|
|
57
59
|
const { spawn, spawnSync } = require('node:child_process');
|
|
58
60
|
const http = require('node:http');
|
|
61
|
+
const https = require('node:https');
|
|
59
62
|
const readline = require('node:readline');
|
|
60
63
|
const SCRIPT_PATH = fileURLToPath(import.meta.url);
|
|
61
64
|
|
|
62
|
-
// Supervisor 根目录(每个 runKey
|
|
63
|
-
|
|
65
|
+
// Supervisor 根目录(每个 runKey 一个子目录)。必须位于 Pod 的持久 agent-home,
|
|
66
|
+
// 容器重建后可继续读取状态与事件;禁止回退到 /tmp,避免表面成功但状态丢失。
|
|
67
|
+
const SUPERVISOR_HOME = process.env.HOME?.trim();
|
|
68
|
+
const SUPERVISOR_ROOT = process.env.WINMATRIX_SUPERVISOR_ROOT?.trim()
|
|
69
|
+
|| (SUPERVISOR_HOME ? path.join(SUPERVISOR_HOME, '.winmatrix', 'agent-engine-supervisor') : '');
|
|
70
|
+
if (!SUPERVISOR_ROOT) {
|
|
71
|
+
throw new Error('WINMATRIX_SUPERVISOR_ROOT 或 HOME 必须指向持久 agent-home');
|
|
72
|
+
}
|
|
64
73
|
const CALLBACK_TIMEOUT_MS = Number(process.env.WINMATRIX_CALLBACK_TIMEOUT_MS) || 5000;
|
|
65
74
|
const MAX_ACTIVE_RUNS = Number(process.env.WINMATRIX_MAX_ACTIVE_RUNS) || 100;
|
|
75
|
+
const LAUNCH_ACK_TIMEOUT_MS = Number(process.env.WINMATRIX_LAUNCH_ACK_TIMEOUT_MS) || 10_000;
|
|
76
|
+
/** 单次 events 请求返回的最大事件数,避免大量事件堆积导致响应超限。 */
|
|
77
|
+
const MAX_EVENTS_PAGE_SIZE = 500;
|
|
66
78
|
|
|
67
79
|
// 确保根目录存在
|
|
68
80
|
if (!fs.existsSync(SUPERVISOR_ROOT)) {
|
|
@@ -81,7 +93,27 @@ function saveActiveRuns() {
|
|
|
81
93
|
runKey,
|
|
82
94
|
metadata,
|
|
83
95
|
}));
|
|
84
|
-
|
|
96
|
+
atomicWriteFile(activeRunsFile, JSON.stringify(data, null, 2));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function atomicWriteFile(filePath, content) {
|
|
100
|
+
const tempPath = `${filePath}.tmp-${process.pid}-${Math.random().toString(16).slice(2)}`;
|
|
101
|
+
fs.writeFileSync(tempPath, content, 'utf8');
|
|
102
|
+
try {
|
|
103
|
+
fs.renameSync(tempPath, filePath);
|
|
104
|
+
} finally {
|
|
105
|
+
try { fs.rmSync(tempPath, { force: true }); } catch { /* best effort */ }
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function atomicCreateExclusive(filePath, content) {
|
|
110
|
+
const tempPath = `${filePath}.tmp-${process.pid}-${Math.random().toString(16).slice(2)}`;
|
|
111
|
+
fs.writeFileSync(tempPath, content, { encoding: 'utf8', mode: 0o600 });
|
|
112
|
+
try {
|
|
113
|
+
fs.linkSync(tempPath, filePath);
|
|
114
|
+
} finally {
|
|
115
|
+
try { fs.rmSync(tempPath, { force: true }); } catch { /* best effort */ }
|
|
116
|
+
}
|
|
85
117
|
}
|
|
86
118
|
|
|
87
119
|
/**
|
|
@@ -188,7 +220,7 @@ function saveRunMetadata(runKey, metadata) {
|
|
|
188
220
|
fs.mkdirSync(runDir, { recursive: true });
|
|
189
221
|
}
|
|
190
222
|
const statusFile = path.join(runDir, 'status.json');
|
|
191
|
-
|
|
223
|
+
atomicWriteFile(statusFile, JSON.stringify(metadata, null, 2));
|
|
192
224
|
}
|
|
193
225
|
|
|
194
226
|
/**
|
|
@@ -221,7 +253,7 @@ function saveResult(runKey, result) {
|
|
|
221
253
|
fs.mkdirSync(runDir, { recursive: true });
|
|
222
254
|
}
|
|
223
255
|
const resultFile = path.join(runDir, 'result.json');
|
|
224
|
-
|
|
256
|
+
atomicWriteFile(resultFile, JSON.stringify(result, null, 2));
|
|
225
257
|
}
|
|
226
258
|
|
|
227
259
|
/**
|
|
@@ -280,7 +312,7 @@ function hasCancelMarker(runKey) {
|
|
|
280
312
|
* @param {RunMetadata} startMetadata - 启动元数据(startTime)
|
|
281
313
|
* @returns {object} Callback payload
|
|
282
314
|
*/
|
|
283
|
-
function buildTerminalCallbackPayload(runKey, endMetadata, result, startMetadata) {
|
|
315
|
+
function buildTerminalCallbackPayload(runKey, endMetadata, result, startMetadata) {
|
|
284
316
|
const sep = runKey.lastIndexOf(':');
|
|
285
317
|
const recordId = sep > 0 ? runKey.slice(0, sep) : runKey;
|
|
286
318
|
const parsedAttempt = sep > 0 ? Number(runKey.slice(sep + 1)) : NaN;
|
|
@@ -293,8 +325,10 @@ function buildTerminalCallbackPayload(runKey, endMetadata, result, startMetadata
|
|
|
293
325
|
? Math.round((endMs - startMs) / 1000)
|
|
294
326
|
: undefined;
|
|
295
327
|
|
|
296
|
-
const text = typeof result?.text === 'string' ? result.text : undefined;
|
|
297
|
-
|
|
328
|
+
const text = typeof result?.text === 'string' ? result.text : undefined;
|
|
329
|
+
const engineId = typeof startMetadata?.engineId === 'string' ? startMetadata.engineId : undefined;
|
|
330
|
+
const engineSessionId = typeof result?.sessionId === 'string' ? result.sessionId : undefined;
|
|
331
|
+
return {
|
|
298
332
|
recordId,
|
|
299
333
|
attemptNo,
|
|
300
334
|
runKey,
|
|
@@ -303,7 +337,7 @@ function buildTerminalCallbackPayload(runKey, endMetadata, result, startMetadata
|
|
|
303
337
|
result: text,
|
|
304
338
|
error: typeof result?.error === 'string' ? result.error : undefined,
|
|
305
339
|
durationSeconds,
|
|
306
|
-
claudeSessionId:
|
|
340
|
+
claudeSessionId: engineId === 'claude-code' ? engineSessionId : undefined,
|
|
307
341
|
statusFilePath: path.join(runDir, 'status.json'),
|
|
308
342
|
resultFilePath: path.join(runDir, 'result.json'),
|
|
309
343
|
completedAt: endMetadata.endTime,
|
|
@@ -311,10 +345,16 @@ function buildTerminalCallbackPayload(runKey, endMetadata, result, startMetadata
|
|
|
311
345
|
exitCode: endMetadata.exitCode,
|
|
312
346
|
...(endMetadata.signal ? { signal: endMetadata.signal } : {}),
|
|
313
347
|
...(endMetadata.resumeDegraded ? { resumeDegraded: true } : {}),
|
|
314
|
-
...(result?.usage ? { usage: result.usage } : {}),
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
}
|
|
348
|
+
...(result?.usage ? { usage: result.usage } : {}),
|
|
349
|
+
...(engineId && engineId !== 'claude-code' && engineSessionId
|
|
350
|
+
? { agentEngine: { engineId, sessionId: engineSessionId } }
|
|
351
|
+
: {}),
|
|
352
|
+
},
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/** 重定向最大跳数,防重定向环。 */
|
|
357
|
+
const MAX_CALLBACK_REDIRECTS = 5;
|
|
318
358
|
|
|
319
359
|
/**
|
|
320
360
|
* 发送 callback 到 Server
|
|
@@ -324,44 +364,76 @@ function buildTerminalCallbackPayload(runKey, endMetadata, result, startMetadata
|
|
|
324
364
|
* @returns {Promise<void>}
|
|
325
365
|
*/
|
|
326
366
|
async function sendCallback(callbackUrl, token, payload) {
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
367
|
+
const data = JSON.stringify(payload);
|
|
368
|
+
let currentUrl = new URL(callbackUrl);
|
|
369
|
+
const originalHostname = currentUrl.hostname;
|
|
370
|
+
|
|
371
|
+
for (let redirects = 0; redirects <= MAX_CALLBACK_REDIRECTS; redirects += 1) {
|
|
372
|
+
const result = await new Promise((resolve, reject) => {
|
|
373
|
+
const isHttps = currentUrl.protocol === 'https:';
|
|
374
|
+
const client = isHttps ? https : http;
|
|
375
|
+
const options = {
|
|
376
|
+
hostname: currentUrl.hostname,
|
|
377
|
+
port: currentUrl.port || (isHttps ? 443 : 80),
|
|
378
|
+
path: currentUrl.pathname + currentUrl.search,
|
|
379
|
+
method: 'POST',
|
|
380
|
+
headers: {
|
|
381
|
+
'Content-Type': 'application/json',
|
|
382
|
+
'Content-Length': Buffer.byteLength(data),
|
|
383
|
+
'Authorization': `Bearer ${token}`,
|
|
384
|
+
},
|
|
385
|
+
timeout: CALLBACK_TIMEOUT_MS,
|
|
386
|
+
};
|
|
342
387
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
388
|
+
const req = client.request(options, (res) => {
|
|
389
|
+
const redirectStatus = [301, 302, 307, 308];
|
|
390
|
+
const location = res.headers.location;
|
|
391
|
+
if (redirectStatus.includes(res.statusCode) && location) {
|
|
392
|
+
res.resume();
|
|
393
|
+
resolve({ redirect: location });
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
if (res.statusCode >= 200 && res.statusCode < 300) {
|
|
397
|
+
res.resume();
|
|
398
|
+
resolve({ ok: true });
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
res.resume();
|
|
402
|
+
resolve({ ok: false, status: res.statusCode });
|
|
403
|
+
});
|
|
351
404
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
});
|
|
405
|
+
req.on('error', (error) => {
|
|
406
|
+
reject(error);
|
|
407
|
+
});
|
|
356
408
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
409
|
+
req.on('timeout', () => {
|
|
410
|
+
req.destroy();
|
|
411
|
+
reject(new Error('Callback timeout'));
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
req.write(data);
|
|
415
|
+
req.end();
|
|
360
416
|
});
|
|
361
417
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
418
|
+
if (result.ok) {
|
|
419
|
+
console.log(`[run-agent-engine] Callback sent successfully: ${callbackUrl}`);
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
if (result.ok === false) {
|
|
423
|
+
throw new Error(`Callback failed with status ${result.status}`);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// 跟随重定向:仅允许同 hostname 的 http↔https 跳转(防 SSRF)。
|
|
427
|
+
const nextUrl = new URL(result.redirect, currentUrl);
|
|
428
|
+
if (nextUrl.hostname !== originalHostname) {
|
|
429
|
+
throw new Error(`Callback redirect to different host rejected: ${nextUrl.hostname}`);
|
|
430
|
+
}
|
|
431
|
+
if (redirects === MAX_CALLBACK_REDIRECTS) {
|
|
432
|
+
throw new Error(`Callback redirect limit exceeded (${MAX_CALLBACK_REDIRECTS})`);
|
|
433
|
+
}
|
|
434
|
+
currentUrl = nextUrl;
|
|
435
|
+
}
|
|
436
|
+
throw new Error(`Callback redirect limit exceeded (${MAX_CALLBACK_REDIRECTS})`);
|
|
365
437
|
}
|
|
366
438
|
|
|
367
439
|
/**
|
|
@@ -449,12 +521,12 @@ async function runOwned(payload) {
|
|
|
449
521
|
// D11.2: detached=true 用于创建独立进程组,两阶段关闭时可 SIGTERM 整组
|
|
450
522
|
detached: process.platform !== 'win32',
|
|
451
523
|
});
|
|
524
|
+
// 链路日志走 stderr:stdout 是协议通道(ack JSON),console.log 会污染
|
|
525
|
+
console.error(`[run-agent-engine] runKey=${runKey} engine spawn: engineId=${launchSpec.engineId} pid=${child.pid} argv=${argv.join(' ')} workDir=${launchSpec.workDir || '(cwd)'}`);
|
|
452
526
|
|
|
453
527
|
// 写入 stdin
|
|
454
|
-
if (stdinData)
|
|
455
|
-
|
|
456
|
-
child.stdin.end();
|
|
457
|
-
}
|
|
528
|
+
if (stdinData) child.stdin.write(stdinData);
|
|
529
|
+
child.stdin.end();
|
|
458
530
|
|
|
459
531
|
// 读取 stdout JSONL 事件,同时写入 stdout.log
|
|
460
532
|
const stdoutWriteStream = fs.createWriteStream(logFile, { flags: 'a' });
|
|
@@ -487,6 +559,10 @@ async function runOwned(payload) {
|
|
|
487
559
|
instanceId: payload.instanceId,
|
|
488
560
|
lastSequence: 0,
|
|
489
561
|
};
|
|
562
|
+
const testStartDelayMs = Number(process.env.WINMATRIX_AGENT_ENGINE_TEST_START_DELAY_MS) || 0;
|
|
563
|
+
if (testStartDelayMs > 0) {
|
|
564
|
+
await new Promise((resolve) => setTimeout(resolve, testStartDelayMs));
|
|
565
|
+
}
|
|
490
566
|
saveRunMetadata(runKey, metadata);
|
|
491
567
|
activeRuns.set(runKey, { child, metadata });
|
|
492
568
|
saveActiveRuns();
|
|
@@ -585,6 +661,7 @@ async function runOwned(payload) {
|
|
|
585
661
|
let resultText = '';
|
|
586
662
|
let usage = null;
|
|
587
663
|
let resultSessionId = null;
|
|
664
|
+
let sdkSignals = extractSdkResultSignals([]);
|
|
588
665
|
try {
|
|
589
666
|
const eventsFile = path.join(getRunDir(runKey), 'events.jsonl');
|
|
590
667
|
if (fs.existsSync(eventsFile)) {
|
|
@@ -611,14 +688,30 @@ async function runOwned(payload) {
|
|
|
611
688
|
break;
|
|
612
689
|
}
|
|
613
690
|
}
|
|
691
|
+
|
|
692
|
+
sdkSignals = extractSdkResultSignals(events);
|
|
693
|
+
if (!resultSessionId && sdkSignals.sessionId) {
|
|
694
|
+
resultSessionId = sdkSignals.sessionId;
|
|
695
|
+
}
|
|
696
|
+
if (!resultText?.trim() && sdkSignals.visibleText) {
|
|
697
|
+
resultText = sdkSignals.visibleText;
|
|
698
|
+
}
|
|
614
699
|
}
|
|
615
700
|
} catch (err) {
|
|
616
701
|
console.error(`[run-agent-engine] Failed to compute deliverable/usage for ${runKey}:`, err.message);
|
|
617
702
|
}
|
|
703
|
+
const outcome = resolveEngineRunOutcome({
|
|
704
|
+
cancelled,
|
|
705
|
+
exitCode: code,
|
|
706
|
+
isError: sdkSignals.isError,
|
|
707
|
+
subtype: sdkSignals.subtype,
|
|
708
|
+
visibleText: resultText,
|
|
709
|
+
existingError: sdkSignals.error,
|
|
710
|
+
});
|
|
618
711
|
|
|
619
712
|
const endMetadata = {
|
|
620
713
|
...metadata,
|
|
621
|
-
status:
|
|
714
|
+
status: outcome.status,
|
|
622
715
|
endTime: new Date().toISOString(),
|
|
623
716
|
exitCode: code,
|
|
624
717
|
...(signal ? { signal } : {}),
|
|
@@ -628,35 +721,56 @@ async function runOwned(payload) {
|
|
|
628
721
|
activeRuns.delete(runKey);
|
|
629
722
|
saveActiveRuns();
|
|
630
723
|
|
|
631
|
-
//
|
|
724
|
+
// D2:success 综合 SDK is_error / 失败 subtype,exitCode === 0 不得单独判成功
|
|
632
725
|
const result = {
|
|
633
|
-
success:
|
|
726
|
+
success: outcome.success,
|
|
634
727
|
text: resultText || undefined,
|
|
635
728
|
...(resultSessionId ? { sessionId: resultSessionId } : {}),
|
|
636
729
|
...(usage ? { usage } : {}),
|
|
637
|
-
...(
|
|
730
|
+
...(outcome.error ? { error: outcome.error } : {}),
|
|
638
731
|
metadata: {
|
|
639
732
|
exitCode: code,
|
|
640
733
|
...(signal ? { signal } : {}),
|
|
641
734
|
...(resumeFailed ? { resumeDegraded: true } : {}),
|
|
642
735
|
...(usage?.source ? { usageSource: usage.source } : {}),
|
|
736
|
+
...(sdkSignals.isError ? { is_error: true } : {}),
|
|
737
|
+
...(sdkSignals.subtype ? { subtype: sdkSignals.subtype } : {}),
|
|
643
738
|
},
|
|
644
739
|
};
|
|
645
740
|
saveResult(runKey, result);
|
|
646
741
|
appendEvent(runKey, endMetadata, { type: 'result', result });
|
|
742
|
+
console.error(`[run-agent-engine] runKey=${runKey} run terminal: status=${endMetadata.status} exitCode=${code}${signal ? ` signal=${signal}` : ''} success=${result.success} deliverableLen=${(resultText || '').length} sessionId=${resultSessionId ?? 'none'}${resumeFailed ? ' resumeDegraded=true' : ''}`);
|
|
647
743
|
|
|
648
744
|
// 发送 callback(如果有)
|
|
649
745
|
// merge-workstation-tools R11 / 9b.5:payload 对齐 V1 run-claude-agent.mjs
|
|
650
746
|
// 字段语义,满足 /api/v1/workstation-task-callbacks 的 zod schema(recordId 必填),
|
|
651
747
|
// 使 HTTP callback 快路径对 V2 run 也能闭合(此前仅 observer 投影 + reconcile 兜底)。
|
|
652
748
|
if (privateBindings?.callback) {
|
|
749
|
+
console.error(`[run-agent-engine] runKey=${runKey} callback 发送: url=${privateBindings.callback.url} status=${endMetadata.status}`);
|
|
653
750
|
sendCallback(
|
|
654
751
|
privateBindings.callback.url,
|
|
655
752
|
privateBindings.callback.token,
|
|
656
753
|
buildTerminalCallbackPayload(runKey, endMetadata, result, metadata),
|
|
657
754
|
).catch((error) => {
|
|
658
|
-
|
|
755
|
+
// callback 失败必须可观测:失败原因写入既有 result.json(reconcile 已读该文件),
|
|
756
|
+
// 而非仅 console.error(worker stdio 被上层 ignore,会静默吞掉)。
|
|
757
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
758
|
+
console.error(`[run-agent-engine] Callback failed for ${runKey}:`, errorMsg);
|
|
759
|
+
try {
|
|
760
|
+
const current = loadResult(runKey) ?? {};
|
|
761
|
+
saveResult(runKey, {
|
|
762
|
+
...current,
|
|
763
|
+
metadata: {
|
|
764
|
+
...(current.metadata && typeof current.metadata === 'object' ? current.metadata : {}),
|
|
765
|
+
callbackError: errorMsg,
|
|
766
|
+
},
|
|
767
|
+
});
|
|
768
|
+
} catch (persistError) {
|
|
769
|
+
console.error(`[run-agent-engine] Callback failed to persist for ${runKey}:`, persistError.message);
|
|
770
|
+
}
|
|
659
771
|
});
|
|
772
|
+
} else {
|
|
773
|
+
console.error(`[run-agent-engine] runKey=${runKey} 无 callback 配置,终态依赖 observer 投影/reconcile`);
|
|
660
774
|
}
|
|
661
775
|
resolve();
|
|
662
776
|
}));
|
|
@@ -714,7 +828,7 @@ function buildRunAck(payload, disposition) {
|
|
|
714
828
|
};
|
|
715
829
|
}
|
|
716
830
|
|
|
717
|
-
async function launch(payload) {
|
|
831
|
+
async function launch(payload, retryCount = 0) {
|
|
718
832
|
const { runKey, invocationFingerprint, launchSpec, instanceId } = payload;
|
|
719
833
|
if (!runKey || !invocationFingerprint || !launchSpec || !instanceId) {
|
|
720
834
|
throw new Error('Missing required fields: runKey, invocationFingerprint, launchSpec, instanceId');
|
|
@@ -733,8 +847,32 @@ async function launch(payload) {
|
|
|
733
847
|
}
|
|
734
848
|
|
|
735
849
|
const claimFile = path.join(SUPERVISOR_ROOT, `${runKey.replace(/:/g, '_')}.launch`);
|
|
850
|
+
const readClaim = () => {
|
|
851
|
+
let raw;
|
|
852
|
+
try {
|
|
853
|
+
raw = fs.readFileSync(claimFile, 'utf8');
|
|
854
|
+
} catch (error) {
|
|
855
|
+
if (error?.code === 'ENOENT') return null;
|
|
856
|
+
throw error;
|
|
857
|
+
}
|
|
858
|
+
try {
|
|
859
|
+
const parsed = JSON.parse(raw);
|
|
860
|
+
return { fingerprint: parsed.fingerprint, ownerPid: parsed.ownerPid };
|
|
861
|
+
} catch {
|
|
862
|
+
return { fingerprint: raw.trim(), ownerPid: undefined };
|
|
863
|
+
}
|
|
864
|
+
};
|
|
865
|
+
const isPidAlive = (pid) => {
|
|
866
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
867
|
+
try {
|
|
868
|
+
process.kill(pid, 0);
|
|
869
|
+
return true;
|
|
870
|
+
} catch {
|
|
871
|
+
return false;
|
|
872
|
+
}
|
|
873
|
+
};
|
|
736
874
|
const waitForClaimedRun = async () => {
|
|
737
|
-
const deadline = Date.now() +
|
|
875
|
+
const deadline = Date.now() + LAUNCH_ACK_TIMEOUT_MS;
|
|
738
876
|
while (Date.now() < deadline) {
|
|
739
877
|
const metadata = loadRunMetadata(runKey);
|
|
740
878
|
if (metadata) {
|
|
@@ -744,24 +882,44 @@ async function launch(payload) {
|
|
|
744
882
|
return metadata;
|
|
745
883
|
}
|
|
746
884
|
if (!fs.existsSync(claimFile)) break;
|
|
885
|
+
const claim = readClaim();
|
|
886
|
+
if (!claim) continue;
|
|
887
|
+
const ownerPid = claim.ownerPid;
|
|
888
|
+
if (ownerPid && !isPidAlive(ownerPid)) {
|
|
889
|
+
fs.rmSync(claimFile, { force: true });
|
|
890
|
+
break;
|
|
891
|
+
}
|
|
747
892
|
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
748
893
|
}
|
|
749
|
-
throw new Error(`
|
|
894
|
+
throw new Error(`Launch ack timeout: runKey=${runKey} code=launch_ack_timeout`);
|
|
750
895
|
};
|
|
751
|
-
let claim;
|
|
752
896
|
try {
|
|
753
|
-
|
|
754
|
-
|
|
897
|
+
atomicCreateExclusive(claimFile, JSON.stringify({
|
|
898
|
+
fingerprint: invocationFingerprint,
|
|
899
|
+
ownerPid: process.pid,
|
|
900
|
+
createdAt: new Date().toISOString(),
|
|
901
|
+
}));
|
|
755
902
|
} catch (error) {
|
|
756
903
|
if (error?.code !== 'EEXIST') throw error;
|
|
757
|
-
const
|
|
904
|
+
const claimed = readClaim();
|
|
905
|
+
if (!claimed) {
|
|
906
|
+
if (retryCount < 1) return launch(payload, retryCount + 1);
|
|
907
|
+
throw new Error(`Launch claim disappeared: runKey=${runKey}`);
|
|
908
|
+
}
|
|
909
|
+
const claimedFingerprint = claimed.fingerprint;
|
|
758
910
|
if (claimedFingerprint !== invocationFingerprint) {
|
|
759
911
|
throw new Error(`Fingerprint mismatch for runKey=${runKey}`);
|
|
760
912
|
}
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
913
|
+
try {
|
|
914
|
+
await waitForClaimedRun();
|
|
915
|
+
return buildRunAck(payload, 'existing');
|
|
916
|
+
} catch (waitError) {
|
|
917
|
+
if (String(waitError?.message ?? '').includes('launch_ack_timeout') && !fs.existsSync(claimFile)) {
|
|
918
|
+
if (retryCount < 1) return launch(payload, retryCount + 1);
|
|
919
|
+
throw waitError;
|
|
920
|
+
}
|
|
921
|
+
throw waitError;
|
|
922
|
+
}
|
|
765
923
|
}
|
|
766
924
|
|
|
767
925
|
if (process.env.WINMATRIX_AGENT_ENGINE_TEST_INLINE === 'true') {
|
|
@@ -776,20 +934,41 @@ async function launch(payload) {
|
|
|
776
934
|
|
|
777
935
|
const worker = spawn(process.execPath, [SCRIPT_PATH, 'supervise-run'], {
|
|
778
936
|
detached: true,
|
|
779
|
-
stdio: ['pipe', 'ignore', '
|
|
937
|
+
stdio: ['pipe', 'ignore', fs.openSync(path.join(SUPERVISOR_ROOT, `${runKey.replace(/:/g, '_')}.worker.log`), 'a')],
|
|
780
938
|
env: process.env,
|
|
781
939
|
});
|
|
940
|
+
atomicWriteFile(claimFile, JSON.stringify({
|
|
941
|
+
fingerprint: invocationFingerprint,
|
|
942
|
+
ownerPid: worker.pid,
|
|
943
|
+
createdAt: new Date().toISOString(),
|
|
944
|
+
}));
|
|
945
|
+
let workerExited = false;
|
|
946
|
+
let workerExitCode;
|
|
947
|
+
let workerSignal;
|
|
948
|
+
let workerError;
|
|
949
|
+
worker.once('error', (error) => {
|
|
950
|
+
workerError = error;
|
|
951
|
+
});
|
|
952
|
+
worker.once('exit', (code, signal) => {
|
|
953
|
+
workerExited = true;
|
|
954
|
+
workerExitCode = code;
|
|
955
|
+
workerSignal = signal;
|
|
956
|
+
});
|
|
782
957
|
worker.stdin.end(JSON.stringify(payload));
|
|
783
958
|
worker.unref();
|
|
784
|
-
const deadline = Date.now() +
|
|
785
|
-
while (!loadRunMetadata(runKey) && Date.now() < deadline) {
|
|
959
|
+
const deadline = Date.now() + LAUNCH_ACK_TIMEOUT_MS;
|
|
960
|
+
while (!loadRunMetadata(runKey) && !workerExited && !workerError && Date.now() < deadline) {
|
|
786
961
|
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
787
962
|
}
|
|
788
|
-
if (
|
|
963
|
+
if (loadRunMetadata(runKey)) {
|
|
964
|
+
return buildRunAck(payload, 'accepted');
|
|
965
|
+
}
|
|
966
|
+
if (workerError || workerExited) {
|
|
789
967
|
fs.rmSync(claimFile, { force: true });
|
|
790
|
-
|
|
968
|
+
const detail = workerError?.message ?? `exitCode=${workerExitCode ?? 'null'} signal=${workerSignal ?? 'none'}`;
|
|
969
|
+
throw new Error(`Run worker failed to start: runKey=${runKey} code=worker_start_failed ${detail}; log=${path.join(SUPERVISOR_ROOT, `${runKey.replace(/:/g, '_')}.worker.log`)}`);
|
|
791
970
|
}
|
|
792
|
-
|
|
971
|
+
throw new Error(`Launch ack timeout: runKey=${runKey} code=launch_ack_timeout workerPid=${worker.pid ?? 'unknown'}`);
|
|
793
972
|
}
|
|
794
973
|
|
|
795
974
|
function listEvents(runKey, afterSequence = 0) {
|
|
@@ -797,7 +976,8 @@ function listEvents(runKey, afterSequence = 0) {
|
|
|
797
976
|
if (!fs.existsSync(file)) return [];
|
|
798
977
|
return fs.readFileSync(file, 'utf8').split('\n').filter(Boolean)
|
|
799
978
|
.map((line) => JSON.parse(line))
|
|
800
|
-
.filter((event) => event.sequence > afterSequence)
|
|
979
|
+
.filter((event) => event.sequence > afterSequence)
|
|
980
|
+
.slice(0, MAX_EVENTS_PAGE_SIZE);
|
|
801
981
|
}
|
|
802
982
|
|
|
803
983
|
/**
|
|
@@ -847,6 +1027,11 @@ function buildEngineArgv(launchSpec) {
|
|
|
847
1027
|
}
|
|
848
1028
|
}
|
|
849
1029
|
|
|
1030
|
+
// G9: 透传上下文预算
|
|
1031
|
+
if (launchSpec.contextBudget) {
|
|
1032
|
+
argv.push('--context-budget', JSON.stringify(launchSpec.contextBudget));
|
|
1033
|
+
}
|
|
1034
|
+
|
|
850
1035
|
// 添加 input(第一个 text block)
|
|
851
1036
|
if (input?.blocks) {
|
|
852
1037
|
for (const block of input.blocks) {
|
|
@@ -886,7 +1071,26 @@ function buildEngineArgv(launchSpec) {
|
|
|
886
1071
|
function status(runKey) {
|
|
887
1072
|
const metadata = loadRunMetadata(runKey);
|
|
888
1073
|
if (!metadata) {
|
|
889
|
-
|
|
1074
|
+
// status.json 尚未写入,说明 run 还在启动阶段(launch 中 saveRunMetadata 之前的耗时操作)。
|
|
1075
|
+
// 返回 starting 状态而非抛异常,避免 inspect 轮询返回 502。
|
|
1076
|
+
return {
|
|
1077
|
+
ref: {
|
|
1078
|
+
instance: { instanceId: '', hostKind: 'workstation' },
|
|
1079
|
+
executionSnapshot: {},
|
|
1080
|
+
runKey,
|
|
1081
|
+
engineId: '',
|
|
1082
|
+
invocationFingerprint: '',
|
|
1083
|
+
},
|
|
1084
|
+
status: 'starting',
|
|
1085
|
+
executionDurability: 'host_persistent',
|
|
1086
|
+
eventStreamMode: 'replay',
|
|
1087
|
+
eventRetention: { maxEvents: 10000 },
|
|
1088
|
+
lastSequence: 0,
|
|
1089
|
+
pendingInteractions: [],
|
|
1090
|
+
recoverable: true,
|
|
1091
|
+
runtimeVersion: 'run-agent-engine/1',
|
|
1092
|
+
protocolVersion: '1',
|
|
1093
|
+
};
|
|
890
1094
|
}
|
|
891
1095
|
|
|
892
1096
|
const result = loadResult(runKey);
|
|
@@ -1194,6 +1398,8 @@ async function sessionGet() {
|
|
|
1194
1398
|
const includeRawMessages = hasFlag(argv, '--include-raw-messages');
|
|
1195
1399
|
const includeSystem = hasFlag(argv, '--include-system');
|
|
1196
1400
|
const noTruncate = hasFlag(argv, '--no-truncate');
|
|
1401
|
+
const noMessages = hasFlag(argv, '--no-messages');
|
|
1402
|
+
const joinedMessages = hasFlag(argv, '--joined-messages');
|
|
1197
1403
|
const normalizedWorkDir = getOptValue(argv, '--normalized-work-dir');
|
|
1198
1404
|
|
|
1199
1405
|
const { sdk, error } = loadSdk();
|
|
@@ -1239,6 +1445,20 @@ async function sessionGet() {
|
|
|
1239
1445
|
}
|
|
1240
1446
|
}
|
|
1241
1447
|
|
|
1448
|
+
// --no-messages:仅返回 session 元数据,跳过消息体(避免大 payload 触发 sandbox stdout 截断)。
|
|
1449
|
+
if (noMessages) {
|
|
1450
|
+
return {
|
|
1451
|
+
session: sessionSummary(sessionMeta, engineId),
|
|
1452
|
+
messages: [],
|
|
1453
|
+
rawMessagesIncluded: false,
|
|
1454
|
+
metadata: {
|
|
1455
|
+
messageCount: sessionMeta.messageCount,
|
|
1456
|
+
sessionMessagesProjectDir: msgOpts.dir != null ? String(msgOpts.dir) : undefined,
|
|
1457
|
+
sessionMessagesDirSource: dirSource,
|
|
1458
|
+
},
|
|
1459
|
+
};
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1242
1462
|
/** @type {unknown[]} */
|
|
1243
1463
|
let messages = [];
|
|
1244
1464
|
try {
|
|
@@ -1307,12 +1527,34 @@ async function sessionGet() {
|
|
|
1307
1527
|
const cliReplayGuide = buildCliReplayGuide(sessionMeta, sessionId);
|
|
1308
1528
|
const diagnostics = deepRedactSessionValue(buildDiagnostics(messages, noTruncate));
|
|
1309
1529
|
|
|
1530
|
+
// --joined-messages:拼接消息文本,替代结构化 messages[],避免大会话 stdout 截断。
|
|
1531
|
+
let joinedMessagesText = '';
|
|
1532
|
+
if (joinedMessages) {
|
|
1533
|
+
const parts = [];
|
|
1534
|
+
for (const m of messages) {
|
|
1535
|
+
if (m == null || typeof m !== 'object') continue;
|
|
1536
|
+
const rec = /** @type {Record<string, unknown>} */ (m);
|
|
1537
|
+
const type = String(rec.type ?? '');
|
|
1538
|
+
const text = extractText(rec.message);
|
|
1539
|
+
if (text) {
|
|
1540
|
+
parts.push(`${type}: ${text}`);
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1543
|
+
joinedMessagesText = parts.join('\n\n');
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1310
1546
|
/** @type {Record<string, unknown>} */
|
|
1311
1547
|
const detail = {
|
|
1312
1548
|
session: sessionSummary(sessionMeta, engineId),
|
|
1313
|
-
messages: structuredMessages,
|
|
1314
1549
|
rawMessagesIncluded: includeRawMessages,
|
|
1315
1550
|
};
|
|
1551
|
+
// joinedMessages 模式:messages 置空数组(schema 必填),正文走 joinedMessages 文本。
|
|
1552
|
+
if (joinedMessages) {
|
|
1553
|
+
detail.messages = [];
|
|
1554
|
+
detail.joinedMessages = joinedMessagesText;
|
|
1555
|
+
} else {
|
|
1556
|
+
detail.messages = structuredMessages;
|
|
1557
|
+
}
|
|
1316
1558
|
|
|
1317
1559
|
/** @type {Record<string, unknown>} */
|
|
1318
1560
|
const metadata = {
|
|
@@ -1333,7 +1575,7 @@ async function sessionGet() {
|
|
|
1333
1575
|
if (!ok && messages.length > 0) metadata.rawMessagesSerializationFailed = true;
|
|
1334
1576
|
}
|
|
1335
1577
|
|
|
1336
|
-
return { ...detail, metadata };
|
|
1578
|
+
return compactSessionDetailForTransport({ ...detail, metadata });
|
|
1337
1579
|
}
|
|
1338
1580
|
|
|
1339
1581
|
/**
|
|
@@ -1349,7 +1591,6 @@ async function sessionSearch() {
|
|
|
1349
1591
|
const cursor = getOptValue(argv, '--cursor');
|
|
1350
1592
|
const offset = decodeOffsetCursor(cursor);
|
|
1351
1593
|
const normalizedWorkDir = getOptValue(argv, '--normalized-work-dir');
|
|
1352
|
-
|
|
1353
1594
|
const { sdk, error } = loadSdk();
|
|
1354
1595
|
if (!sdk || typeof sdk.listSessions !== 'function') {
|
|
1355
1596
|
return { hits: [], error: error ?? 'SDK 不支持 listSessions' };
|