@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.
@@ -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
- const SUPERVISOR_ROOT = process.env.WINMATRIX_SUPERVISOR_ROOT || '/tmp/agent-engine-supervisor';
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
- fs.writeFileSync(activeRunsFile, JSON.stringify(data, null, 2), 'utf8');
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
- fs.writeFileSync(statusFile, JSON.stringify(metadata, null, 2), 'utf8');
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
- fs.writeFileSync(resultFile, JSON.stringify(result, null, 2), 'utf8');
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
- return {
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: typeof result?.sessionId === 'string' ? result.sessionId : undefined,
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
- return new Promise((resolve, reject) => {
328
- const url = new URL(callbackUrl);
329
- const data = JSON.stringify(payload);
330
- const options = {
331
- hostname: url.hostname,
332
- port: url.port || 80,
333
- path: url.pathname + url.search,
334
- method: 'POST',
335
- headers: {
336
- 'Content-Type': 'application/json',
337
- 'Content-Length': Buffer.byteLength(data),
338
- 'Authorization': `Bearer ${token}`,
339
- },
340
- timeout: CALLBACK_TIMEOUT_MS,
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
- const req = http.request(options, (res) => {
344
- if (res.statusCode >= 200 && res.statusCode < 300) {
345
- console.log(`[run-agent-engine] Callback sent successfully: ${callbackUrl}`);
346
- resolve();
347
- } else {
348
- reject(new Error(`Callback failed with status ${res.statusCode}`));
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
- req.on('error', (error) => {
353
- console.error(`[run-agent-engine] Callback error:`, error.message);
354
- reject(error);
355
- });
405
+ req.on('error', (error) => {
406
+ reject(error);
407
+ });
356
408
 
357
- req.on('timeout', () => {
358
- req.destroy();
359
- reject(new Error('Callback timeout'));
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
- req.write(data);
363
- req.end();
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
- child.stdin.write(stdinData);
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: cancelled ? 'cancelled' : code === 0 ? 'completed' : 'failed',
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
- // D11.4: 构建包含 deliverable usage 的结果
724
+ // D2:success 综合 SDK is_error / 失败 subtype,exitCode === 0 不得单独判成功
632
725
  const result = {
633
- success: !cancelled && code === 0,
726
+ success: outcome.success,
634
727
  text: resultText || undefined,
635
728
  ...(resultSessionId ? { sessionId: resultSessionId } : {}),
636
729
  ...(usage ? { usage } : {}),
637
- ...(cancelled ? { error: 'cancelled' } : code === 0 ? {} : { error: `Engine exited with code ${code}` }),
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
- console.error(`[run-agent-engine] Callback failed for ${runKey}:`, error.message);
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() + 2_000;
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(`Claimed run did not start: ${runKey}`);
894
+ throw new Error(`Launch ack timeout: runKey=${runKey} code=launch_ack_timeout`);
750
895
  };
751
- let claim;
752
896
  try {
753
- claim = fs.openSync(claimFile, 'wx', 0o600);
754
- fs.writeFileSync(claim, invocationFingerprint, 'utf8');
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 claimedFingerprint = fs.readFileSync(claimFile, 'utf8');
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
- await waitForClaimedRun();
762
- return buildRunAck(payload, 'existing');
763
- } finally {
764
- if (claim !== undefined) fs.closeSync(claim);
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', '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() + 2_000;
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 (!loadRunMetadata(runKey)) {
963
+ if (loadRunMetadata(runKey)) {
964
+ return buildRunAck(payload, 'accepted');
965
+ }
966
+ if (workerError || workerExited) {
789
967
  fs.rmSync(claimFile, { force: true });
790
- throw new Error(`Run worker failed to start: ${runKey}`);
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
- return buildRunAck(payload, 'accepted');
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
- throw new Error(`Run not found: ${runKey}`);
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' };