evolcore 0.0.15 → 0.0.17

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.
@@ -46,6 +46,7 @@ import { registerBuiltinModes } from '../../response-system/modes/index.js';
46
46
  import { deriveSessionTitle, shouldAutoFillSessionTitle } from '../session/session-title.js';
47
47
  import { SessionTurnCoordinator } from '../session/session-turn-coordinator.js';
48
48
  import { recordTriggerExecutionAnomaly } from '../../trigger/anomaly-store.js';
49
+ import { classifyToolErrorCode } from '../permission/tool-error-code.js';
49
50
  function isShowActivitiesMode(value) {
50
51
  return value === 'all' || value === 'text' || value === 'none';
51
52
  }
@@ -1388,13 +1389,15 @@ export class ResponseEngine {
1388
1389
  }
1389
1390
  }, 30000);
1390
1391
  });
1391
- const totalExecutionPromise = new Promise((_, reject) => {
1392
- totalExecutionTimer = setTimeout(() => {
1393
- logger.warn(`[ResponseEngine] Total execution timeout after ${totalExecutionMs}ms, stream: ${streamKey}`);
1394
- rejectAfterInterruptBarrier(new Error('TOTAL_EXECUTION_TIMEOUT'));
1395
- }, totalExecutionMs);
1396
- totalExecutionTimer.unref?.();
1397
- });
1392
+ const totalExecutionPromise = totalExecutionMs === undefined
1393
+ ? undefined
1394
+ : new Promise((_, reject) => {
1395
+ totalExecutionTimer = setTimeout(() => {
1396
+ logger.warn(`[ResponseEngine] Total execution timeout after ${totalExecutionMs}ms, stream: ${streamKey}`);
1397
+ rejectAfterInterruptBarrier(new Error('TOTAL_EXECUTION_TIMEOUT'));
1398
+ }, totalExecutionMs);
1399
+ totalExecutionTimer.unref?.();
1400
+ });
1398
1401
  try {
1399
1402
  const processingPromise = this._processMessageInternal(message, session, absoluteProjectPath, resetTimer, shouldSuppress, () => lastIdleSec, outputState, timeoutControl);
1400
1403
  const guardedProcessingPromise = processingPromise.then(async () => {
@@ -1403,11 +1406,10 @@ export class ResponseEngine {
1403
1406
  await timeoutControl.barrier;
1404
1407
  throw timeoutControl.error;
1405
1408
  });
1406
- await Promise.race([
1407
- guardedProcessingPromise,
1408
- timeoutPromise,
1409
- totalExecutionPromise,
1410
- ]);
1409
+ const processingPromises = [guardedProcessingPromise, timeoutPromise];
1410
+ if (totalExecutionPromise)
1411
+ processingPromises.push(totalExecutionPromise);
1412
+ await Promise.race(processingPromises);
1411
1413
  }
1412
1414
  catch (error) {
1413
1415
  if (error instanceof Error && (error.message === 'SDK_TIMEOUT' || error.message === 'TOTAL_EXECUTION_TIMEOUT')) {
@@ -1467,7 +1469,7 @@ export class ResponseEngine {
1467
1469
  && Number.isFinite(configuredSeconds)
1468
1470
  && configuredSeconds > 0
1469
1471
  ? configuredSeconds * 1000
1470
- : 60 * 60 * 1000;
1472
+ : undefined;
1471
1473
  }
1472
1474
  retryAttemptTimeoutMs() {
1473
1475
  if (this.globalSettings.idleMonitor?.enabled === false)
@@ -1807,12 +1809,14 @@ export class ResponseEngine {
1807
1809
  const statusPayload = {
1808
1810
  kind: 'status.timeout',
1809
1811
  metadata: isTotalExecutionTimeout
1810
- ? { totalExecutionMs }
1812
+ ? (totalExecutionMs === undefined ? {} : { totalExecutionMs })
1811
1813
  : { idleSec: getLastIdleSec?.() || undefined },
1812
1814
  };
1813
1815
  const idleSec = getLastIdleSec?.() || 0;
1814
1816
  const userMessage = isTotalExecutionTimeout
1815
- ? `⚠️ 任务超过总执行时限(${Math.round(totalExecutionMs / 1000)}秒),已自动中断`
1817
+ ? (totalExecutionMs === undefined
1818
+ ? '⚠️ 任务超过总执行时限,已自动中断'
1819
+ : `⚠️ 任务超过总执行时限(${Math.round(totalExecutionMs / 1000)}秒),已自动中断`)
1816
1820
  : idleSec > 0
1817
1821
  ? `⚠️ 任务超时(${idleSec}秒无响应),已自动中断`
1818
1822
  : '⚠️ 任务超时,已自动中断';
@@ -2198,6 +2202,7 @@ export class ResponseEngine {
2198
2202
  role: peerRole,
2199
2203
  chatType: authChatType,
2200
2204
  selfAid: session.selfAID || message.selfAID,
2205
+ allowReadonlySourceDiagnostics: effectiveAgentConfig?.readonlySourceDiagnostics === true,
2201
2206
  peerKey: authPeerKey,
2202
2207
  causation: taskCausation,
2203
2208
  approvalRouting,
@@ -2770,6 +2775,7 @@ export class ResponseEngine {
2770
2775
  taskId,
2771
2776
  sessionId: session.id,
2772
2777
  messageId: message.messageId,
2778
+ baseagent: normalizedBaseagent.canonical,
2773
2779
  channel: configChannelType,
2774
2780
  channelId: message.channelId,
2775
2781
  chatType: configChatType,
@@ -3688,7 +3694,7 @@ export class ResponseEngine {
3688
3694
  const daemonTrigger = this.isTrustedDaemonTrigger(message);
3689
3695
  const statusPayload = procStatus === 'timeout'
3690
3696
  ? { kind: 'status.timeout', metadata: isTotalExecutionTimeout
3691
- ? { totalExecutionMs }
3697
+ ? (totalExecutionMs === undefined ? {} : { totalExecutionMs })
3692
3698
  : { idleSec: getLastIdleSec?.() || undefined } }
3693
3699
  : procStatus === 'interrupted'
3694
3700
  ? { kind: 'status.interrupted', metadata: { reason: 'stream_error' } }
@@ -3756,7 +3762,9 @@ export class ResponseEngine {
3756
3762
  : modelFallbackExhaustedMessage
3757
3763
  ? modelFallbackExhaustedMessage
3758
3764
  : isTotalExecutionTimeout
3759
- ? `⚠️ 任务超过总执行时限(${Math.round(totalExecutionMs / 1000)}秒),已自动中断`
3765
+ ? (totalExecutionMs === undefined
3766
+ ? '⚠️ 任务超过总执行时限,已自动中断'
3767
+ : `⚠️ 任务超过总执行时限(${Math.round(totalExecutionMs / 1000)}秒),已自动中断`)
3760
3768
  : isTimeout
3761
3769
  ? (idleSec > 0 ? `⚠️ 任务超时(${idleSec}秒无响应),已自动中断` : '⚠️ 任务超时,已自动中断')
3762
3770
  : getErrorMessage(error, undefined);
@@ -4097,6 +4105,12 @@ export class ResponseEngine {
4097
4105
  if (event.type === 'complete') {
4098
4106
  event = normalizeCompleteAgentEvent(event);
4099
4107
  }
4108
+ if (event.type === 'tool_result' && event.isError && !event.errorCode) {
4109
+ event = {
4110
+ ...event,
4111
+ errorCode: classifyToolErrorCode({ error: event.error, result: event.result }),
4112
+ };
4113
+ }
4100
4114
  // 每收到事件重置空闲超时
4101
4115
  const toolName = event.type === 'tool_use' ? event.name : undefined;
4102
4116
  resetTimer(event.type, toolName);
@@ -4413,8 +4427,8 @@ export class ResponseEngine {
4413
4427
  input: event.input,
4414
4428
  ...(event.callId ? { callId: event.callId } : {}),
4415
4429
  ...(event.correlationId || event.callId ? { correlationId: event.correlationId ?? event.callId } : {}),
4416
- ...(session.selfAID ? { agentAid: session.selfAID } : {}),
4417
- ...(permissionMode ? { permissionMode } : {}),
4430
+ agentAid: session.selfAID ?? 'unknown',
4431
+ permissionMode: permissionMode ?? 'unknown',
4418
4432
  timestamp: Date.now(),
4419
4433
  causation,
4420
4434
  });
@@ -4478,11 +4492,12 @@ export class ResponseEngine {
4478
4492
  sessionId: session.id,
4479
4493
  toolName: event.name,
4480
4494
  isError: event.isError,
4495
+ ...(event.isError ? { errorCode: classifyToolErrorCode({ errorCode: event.errorCode, error: event.error, result: event.result }) } : {}),
4481
4496
  agentName: agentNameForStats,
4482
4497
  ...(event.callId ? { callId: event.callId } : {}),
4483
4498
  ...(event.correlationId || event.callId ? { correlationId: event.correlationId ?? event.callId } : {}),
4484
- ...(session.selfAID ? { agentAid: session.selfAID } : {}),
4485
- ...(permissionMode ? { permissionMode } : {}),
4499
+ agentAid: session.selfAID ?? 'unknown',
4500
+ permissionMode: permissionMode ?? 'unknown',
4486
4501
  timestamp: Date.now(),
4487
4502
  causation,
4488
4503
  });
@@ -0,0 +1,47 @@
1
+ export function normalizeToolErrorCode(value) {
2
+ if (typeof value !== 'string')
3
+ return undefined;
4
+ const code = value.trim().toUpperCase().replace(/[ .-]+/g, '_');
5
+ if ([
6
+ 'POLICY_DENIED', 'ROLE_DENIED', 'USER_DENIED', 'APPROVAL_TIMEOUT',
7
+ 'DELEGATION_FAILED', 'CAPABILITY_UNAVAILABLE', 'INVALID_ARGUMENT',
8
+ 'EXECUTION_FAILED',
9
+ ].includes(code))
10
+ return code;
11
+ return undefined;
12
+ }
13
+ export function classifyToolErrorCode(input) {
14
+ const explicit = normalizeToolErrorCode(input.errorCode);
15
+ if (explicit)
16
+ return explicit;
17
+ const text = [input.error, input.result]
18
+ .map(value => {
19
+ if (typeof value === 'string')
20
+ return value;
21
+ if (value === undefined || value === null)
22
+ return '';
23
+ try {
24
+ return JSON.stringify(value);
25
+ }
26
+ catch {
27
+ return String(value);
28
+ }
29
+ })
30
+ .join(' ')
31
+ .toLowerCase();
32
+ if (/delegat(?:ion|ed)|carrier|command hash|not armed/.test(text))
33
+ return 'DELEGATION_FAILED';
34
+ if (/approval.*(?:timeout|timed out)|timed out.*approval|审批.*超时/.test(text))
35
+ return 'APPROVAL_TIMEOUT';
36
+ if (/user.*(?:denied|declined|cancel)|用户.*(?:拒绝|取消)|cancelled by user/.test(text))
37
+ return 'USER_DENIED';
38
+ if (/role|no_permission|not_allowed|visitor|member.*(?:denied|forbidden)|角色.*(?:拒绝|无权)/.test(text))
39
+ return 'ROLE_DENIED';
40
+ if (/policy|preflight|h[ .-]?class|l[ .-]?class|protected|readonly|dangerous|permission[_ ](?:denied|rejected|forbidden)|权限.*拒绝|策略.*拒绝/.test(text))
41
+ return 'POLICY_DENIED';
42
+ if (/capability|unsupported|unavailable|未找到.*工具|能力.*不可用/.test(text))
43
+ return 'CAPABILITY_UNAVAILABLE';
44
+ if (/invalid|argument|parameter|参数|用法/.test(text))
45
+ return 'INVALID_ARGUMENT';
46
+ return 'EXECUTION_FAILED';
47
+ }
@@ -484,13 +484,19 @@ function analyzeShellProtectedOperands(command, options) {
484
484
  analysis,
485
485
  hClass: containsHClassReference(command),
486
486
  lClass: containsLClassReference(command),
487
+ pathHClass: false,
488
+ lockDiagnostic: false,
487
489
  };
488
490
  }
489
491
  const operands = queryPathOperands(analysis);
492
+ const lockDiagnostic = /(?:^|[\/\s'"`])(?:[^\s'"`/]*\.lock|EVOLCORE_RUNTIME_LOCK_DIR)(?=$|[\/\s'"`=])/i.test(command);
493
+ const pathHClass = operands.some(({ operand, accessKind }) => shellOperandMatchesClass(operand, 'h', options, accessKind));
490
494
  return {
491
495
  analysis,
492
- hClass: operands.some(({ operand, accessKind }) => shellOperandMatchesClass(operand, 'h', options, accessKind)),
496
+ hClass: lockDiagnostic || pathHClass,
493
497
  lClass: operands.some(({ operand, accessKind }) => shellOperandMatchesClass(operand, 'l', options, accessKind)),
498
+ pathHClass,
499
+ lockDiagnostic,
494
500
  };
495
501
  }
496
502
  function protectedReadToolPaths(toolName, input) {
@@ -586,6 +592,45 @@ function readonlyShellStaysInWorkspace(analysis, projectPath, managedTempDir) {
586
592
  || (!!managedTemp && isSameOrDescendant(candidate, managedTemp));
587
593
  })());
588
594
  }
595
+ const READONLY_SOURCE_ROOTS = new Set([
596
+ 'src', 'lib', 'app', 'apps', 'packages', 'package', 'scripts',
597
+ 'test', 'tests', 'ecagent', 'ecweb',
598
+ ]);
599
+ /**
600
+ * Optional source-diagnostic capability. It is deliberately opt-in and only
601
+ * admits bounded, proven-readonly queries rooted at a named source directory;
602
+ * project root, home, dependency, config, and session trees remain outside it.
603
+ */
604
+ function readonlySourceDiagnosticAllowed(analysis, projectPath) {
605
+ if (analysis.kind !== 'proven-readonly')
606
+ return false;
607
+ const workspace = resolveProtectedCandidate(projectPath);
608
+ const operands = queryPathOperands(analysis);
609
+ if (operands.length === 0)
610
+ return false;
611
+ return operands.every(({ operand, accessKind }) => {
612
+ if (accessKind === 'metadata')
613
+ return false;
614
+ const candidate = resolveProtectedCandidate(operand.value, projectPath);
615
+ if (!isSameOrDescendant(candidate, workspace))
616
+ return false;
617
+ const relative = path.relative(workspace, candidate);
618
+ const segments = relative.split(path.sep).filter(Boolean);
619
+ const first = segments[0];
620
+ if (!first || !READONLY_SOURCE_ROOTS.has(first) || segments.includes('node_modules'))
621
+ return false;
622
+ return !containsHClassReference(operand.value) && !containsLClassReference(operand.value)
623
+ && !containsLockPathReference(operand.value)
624
+ && !isHClassPath(candidate, { root: resolveRoot() })
625
+ && !isLClassPath(candidate, { root: resolveRoot() });
626
+ });
627
+ }
628
+ /** A lock filename used as a query pattern is safe; an operand naming the
629
+ * actual lock path is still protected. */
630
+ function containsLockPathReference(value) {
631
+ return /(?:^|[\\/])(?:[^\\/]*\.lock|EVOLCORE_RUNTIME_LOCK_DIR)(?:$|[\\/])/i.test(value)
632
+ || /^(?:[^\\/]*\.lock|EVOLCORE_RUNTIME_LOCK_DIR)$/i.test(value);
633
+ }
589
634
  /**
590
635
  * 只读模式检查(用于 PreToolUse hook 和 canUseTool callback)。普通
591
636
  * 路径上的受限查询 Bash 可自动执行;H/L-class 和未建模 Shell 仍拒绝。
@@ -631,6 +676,13 @@ export function checkReadonly(toolName, input, projectPath, context) {
631
676
  managedTempDir,
632
677
  allowProtectedMetadata: context?.allowProtectedMetadata,
633
678
  });
679
+ const sourceDiagnosticAllowed = context?.allowReadonlySourceDiagnostics
680
+ && readonlySourceDiagnosticAllowed(protectedOperands.analysis, projectPath)
681
+ && !protectedOperands.lClass
682
+ && (!protectedOperands.hClass || (protectedOperands.lockDiagnostic && !protectedOperands.pathHClass));
683
+ if (sourceDiagnosticAllowed) {
684
+ return { behavior: 'allow' };
685
+ }
634
686
  if (protectedOperands.analysis.kind === 'proven-readonly'
635
687
  && !protectedOperands.hClass
636
688
  && !protectedOperands.lClass
@@ -929,7 +981,12 @@ export function checkHClassWrite(toolName, input, context) {
929
981
  managedTempDir: context?.managedTempDir,
930
982
  allowProtectedMetadata: context?.allowProtectedMetadata,
931
983
  });
932
- if (protectedOperands.hClass) {
984
+ const sourceDiagnosticAllowed = context?.permissionMode === 'readonly'
985
+ && context.allowReadonlySourceDiagnostics === true
986
+ && readonlySourceDiagnosticAllowed(protectedOperands.analysis, context.projectPath ?? process.cwd())
987
+ && !protectedOperands.lClass
988
+ && (!protectedOperands.hClass || (protectedOperands.lockDiagnostic && !protectedOperands.pathHClass));
989
+ if (protectedOperands.hClass && !sourceDiagnosticAllowed) {
933
990
  logger.warn(`[H-Class Protection] 🔒 Protected filesystem operand in shell command: tool=${toolName} ` +
934
991
  `session=${context?.sessionId} channel=${context?.channel} peer=${context?.peerId} role=${context?.role}`);
935
992
  return {
@@ -1398,6 +1455,8 @@ export function evaluateToolPreflight(toolName, input, context) {
1398
1455
  workspacePath: context.workspacePath,
1399
1456
  root: context.root,
1400
1457
  managedTempDir: context.managedTempDir,
1458
+ permissionMode: context.permissionMode,
1459
+ allowReadonlySourceDiagnostics: context.allowReadonlySourceDiagnostics,
1401
1460
  allowProtectedMetadata: context.allowProtectedMetadata,
1402
1461
  });
1403
1462
  if (hClass.behavior === 'deny') {
package/dist/index.js CHANGED
@@ -227,18 +227,21 @@ export function resolvePersistedRestartDelivery(opts) {
227
227
  ? { valid: true, delivery: opts.delivery }
228
228
  : { valid: false };
229
229
  }
230
- function daemonConversationWatchdogMs(settings) {
230
+ function daemonIdleTimeoutMs(settings) {
231
231
  const idleTimeoutSec = settings.idleMonitor?.timeout;
232
- const idleMs = typeof idleTimeoutSec === 'number' && Number.isFinite(idleTimeoutSec) && idleTimeoutSec > 0
232
+ return typeof idleTimeoutSec === 'number' && Number.isFinite(idleTimeoutSec) && idleTimeoutSec > 0
233
233
  ? idleTimeoutSec * 1000
234
234
  : 120_000;
235
+ }
236
+ function daemonConversationWatchdogMs(settings) {
237
+ const idleMs = daemonIdleTimeoutMs(settings);
235
238
  return Math.ceil(idleMs * 5 + 60_000);
236
239
  }
237
240
  function daemonConversationTotalExecutionMs(settings) {
238
241
  const seconds = settings.idleMonitor?.maxExecutionTime;
239
242
  return typeof seconds === 'number' && Number.isFinite(seconds) && seconds > 0
240
243
  ? seconds * 1000
241
- : 60 * 60 * 1000;
244
+ : undefined;
242
245
  }
243
246
  function originFromActorSession(session, authenticatedPeerId, authenticatedChannelKey, authenticatedChatType) {
244
247
  const peerId = authenticatedPeerId || session.metadata?.peerId;
@@ -1227,6 +1230,7 @@ async function main() {
1227
1230
  // 回填 messageQueue 引用
1228
1231
  cmdHandler.setMessageQueue(messageQueue);
1229
1232
  processor.setMessageQueue(messageQueue);
1233
+ const handoffQueueTtlMs = daemonIdleTimeoutMs(globalSettings);
1230
1234
  const taskExecutionTtlMs = daemonConversationTotalExecutionMs(globalSettings);
1231
1235
  const handoffRuntime = new HandoffRuntime(sessionManager, messageQueue, async (handoff) => {
1232
1236
  const targetSession = await sessionManager.getSessionById(handoff.target_session_id);
@@ -1277,7 +1281,7 @@ async function main() {
1277
1281
  })
1278
1282
  : { ok: false, error: 'AUN channel does not support daemon private sends' };
1279
1283
  return { ok: result.ok, message_id: result.message_id, error: result.error };
1280
- }, undefined, { queueTtlMs: taskExecutionTtlMs });
1284
+ }, undefined, { queueTtlMs: handoffQueueTtlMs });
1281
1285
  responseEngine.setHandoffRuntime(handoffRuntime);
1282
1286
  cmdHandler.setHandoffRuntime(handoffRuntime);
1283
1287
  // Trigger runtime: daemon-level script + feedback scheduler.
package/dist/ipc.js CHANGED
@@ -1047,6 +1047,7 @@ export class IpcServer {
1047
1047
  * Query the running EvolCore daemon via Unix socket.
1048
1048
  * Returns null if the service is not running or the socket is unreachable.
1049
1049
  */
1050
+ /** Pass `null` as timeoutMs to wait without a client-side deadline. */
1050
1051
  export function ipcQuery(socketPath, cmd, timeoutMs = 3000) {
1051
1052
  let authenticatedCmd = cmd;
1052
1053
  if (!process.env.EVOLCORE_SESSION_ID
@@ -1067,10 +1068,12 @@ export function ipcQuery(socketPath, cmd, timeoutMs = 3000) {
1067
1068
  return new Promise((resolve) => {
1068
1069
  const conn = net.connect(socketPath);
1069
1070
  let buf = '';
1070
- const timer = setTimeout(() => {
1071
- conn.destroy();
1072
- resolve(null);
1073
- }, timeoutMs);
1071
+ const timer = timeoutMs === null
1072
+ ? undefined
1073
+ : setTimeout(() => {
1074
+ conn.destroy();
1075
+ resolve(null);
1076
+ }, timeoutMs);
1074
1077
  conn.on('connect', () => {
1075
1078
  conn.write(JSON.stringify(request) + '\n');
1076
1079
  });
@@ -1078,7 +1081,8 @@ export function ipcQuery(socketPath, cmd, timeoutMs = 3000) {
1078
1081
  buf += data.toString();
1079
1082
  const idx = buf.indexOf('\n');
1080
1083
  if (idx !== -1) {
1081
- clearTimeout(timer);
1084
+ if (timer)
1085
+ clearTimeout(timer);
1082
1086
  try {
1083
1087
  resolve(JSON.parse(buf.slice(0, idx)));
1084
1088
  }
@@ -1089,7 +1093,8 @@ export function ipcQuery(socketPath, cmd, timeoutMs = 3000) {
1089
1093
  }
1090
1094
  });
1091
1095
  conn.on('error', () => {
1092
- clearTimeout(timer);
1096
+ if (timer)
1097
+ clearTimeout(timer);
1093
1098
  resolve(null);
1094
1099
  });
1095
1100
  });
package/dist/paths.js CHANGED
@@ -169,7 +169,24 @@ function resolveInstanceSocketPath(root) {
169
169
  const hash = crypto.createHash('sha1').update(root).digest('hex').slice(0, 12);
170
170
  return `\\\\.\\pipe\\${WINDOWS_PIPE_PREFIX}-${hash}`;
171
171
  }
172
- return path.join(root, 'data', 'instance', INSTANCE_SOCKET_FILENAME);
172
+ const filesystemPath = path.join(root, 'data', 'instance', INSTANCE_SOCKET_FILENAME);
173
+ // Linux/macOS Unix-domain sockets have a small sockaddr path limit
174
+ // (typically 108 bytes on Linux). Long managed TMPDIR prefixes can make a
175
+ // perfectly valid EVOLCORE_HOME impossible to bind. Keep the fallback a
176
+ // normal filesystem socket whenever possible: the endpoint is passed to
177
+ // child processes through environment variables, which cannot contain NUL
178
+ // bytes. The short path remains inside the managed temporary directory and
179
+ // uses the runtime root hash for isolation.
180
+ if (Buffer.byteLength(filesystemPath) < 100)
181
+ return filesystemPath;
182
+ const hash = crypto.createHash('sha1').update(root).digest('hex').slice(0, 20);
183
+ const shortFilesystemPath = path.join(os.tmpdir(), `.ec-${hash.slice(0, 15)}.sock`);
184
+ if (Buffer.byteLength(shortFilesystemPath) < 100)
185
+ return shortFilesystemPath;
186
+ // This is only reachable when the managed temporary directory itself is
187
+ // unusually long. Node's in-process IPC can use the abstract namespace;
188
+ // callers that cross a process boundary must provide an encoded endpoint.
189
+ return `\0evolcore-${hash}`;
173
190
  }
174
191
  export function ensureDataDirs() {
175
192
  const p = resolvePaths();
@@ -1064,9 +1064,10 @@ export class TriggerRuntimeScheduler {
1064
1064
  && !!run.attemptId
1065
1065
  && typeof run.executionSessionId === 'string'
1066
1066
  && !!run.executionSessionId
1067
- && typeof run.executionDeadlineAt === 'number'
1068
- && Number.isFinite(run.executionDeadlineAt)
1069
- && run.executionDeadlineAt > Date.now()
1067
+ && (run.executionDeadlineAt === undefined
1068
+ || (typeof run.executionDeadlineAt === 'number'
1069
+ && Number.isFinite(run.executionDeadlineAt)
1070
+ && run.executionDeadlineAt > Date.now()))
1070
1071
  && run.definitionRevision === definitionRevision(definition);
1071
1072
  }
1072
1073
  async continueRecoveredRun(definition, runtime, session) {
@@ -1668,7 +1669,7 @@ export class TriggerRuntimeScheduler {
1668
1669
  return this.running.get(triggerId)?.has(runId) === true;
1669
1670
  }
1670
1671
  daemonExecutionDeadlineAt(startedAt) {
1671
- return this.daemonChannel.executionDeadlineAt?.(startedAt) ?? startedAt + 60 * 60 * 1_000;
1672
+ return this.daemonChannel.executionDeadlineAt?.(startedAt);
1672
1673
  }
1673
1674
  clearTimer(triggerId) {
1674
1675
  const timer = this.timers.get(triggerId);
@@ -4,6 +4,7 @@ import { execFileSync, execFile, spawn, spawnSync } from 'child_process';
4
4
  import { promisify } from 'util';
5
5
  import fs from 'fs';
6
6
  import { getProcessStartTime, parseCimDate } from './process-introspect.js';
7
+ import { decodeWindowsOutput } from './windows-output.js';
7
8
  const execFileAsync = promisify(execFile);
8
9
  export const isWindows = process.platform === 'win32';
9
10
  /**
@@ -287,30 +288,6 @@ function parseDateString(value) {
287
288
  const parsed = Date.parse(value);
288
289
  return Number.isNaN(parsed) ? null : parsed;
289
290
  }
290
- /** Decode PowerShell output consistently across Windows 5.1 and pwsh. */
291
- function decodeWindowsOutput(value) {
292
- if (!value)
293
- return '';
294
- const bytes = Buffer.isBuffer(value) ? value : Buffer.from(value);
295
- if (bytes.length >= 2 && bytes[0] === 0xff && bytes[1] === 0xfe) {
296
- return bytes.subarray(2).toString('utf16le');
297
- }
298
- if (bytes.length >= 2 && bytes[0] === 0xfe && bytes[1] === 0xff) {
299
- const swapped = Buffer.allocUnsafe(bytes.length - 2);
300
- for (let i = 2; i + 1 < bytes.length; i += 2) {
301
- swapped[i - 2] = bytes[i + 1];
302
- swapped[i - 1] = bytes[i];
303
- }
304
- return swapped.toString('utf16le');
305
- }
306
- const utf8 = bytes.toString('utf8').replace(/^\uFEFF/, '');
307
- if (utf8.includes('\u0000')) {
308
- const utf16 = bytes.toString('utf16le').replace(/^\uFEFF/, '');
309
- if (utf16.includes('{') || utf16.includes('[') || utf16.includes('CommandLine'))
310
- return utf16;
311
- }
312
- return utf8;
313
- }
314
291
  /**
315
292
  * Cross-platform command existence check.
316
293
  */
@@ -3,6 +3,32 @@ import path from 'path';
3
3
  import { getPackageRoot, resolvePaths } from '../paths.js';
4
4
  import { logger } from './logger.js';
5
5
  import { isSandboxInitializationFailure } from '../core/permission/sandbox-runtime.js';
6
+ export const CLAUDE_STARTUP_ARGUMENTS_TOO_LONG = 'claude_startup_arguments_too_long';
7
+ /** Identify a Claude process-start failure without treating ordinary tool errors as startup failures. */
8
+ export function isClaudeStartupArgumentsTooLong(error) {
9
+ const code = error && typeof error === 'object' && 'code' in error
10
+ ? String(error.code ?? '')
11
+ : '';
12
+ const message = error && typeof error === 'object' && 'message' in error
13
+ ? String(error.message ?? '')
14
+ : String(error ?? '');
15
+ const text = `${code}\n${message}`.toLowerCase();
16
+ return text.includes(CLAUDE_STARTUP_ARGUMENTS_TOO_LONG)
17
+ || text.includes('enametoolong')
18
+ || text.includes('e2big')
19
+ || text.includes('argument list too long')
20
+ || text.includes('command line too long');
21
+ }
22
+ export function createClaudeStartupArgumentsTooLongError(error) {
23
+ const wrapped = new Error(`${CLAUDE_STARTUP_ARGUMENTS_TOO_LONG}: Claude 启动参数过长,任务未执行`);
24
+ wrapped.name = 'ClaudeStartupArgumentsTooLongError';
25
+ wrapped.code = CLAUDE_STARTUP_ARGUMENTS_TOO_LONG;
26
+ // Keep the original error available to diagnostics without echoing argv,
27
+ // settings contents, or absolute paths to users.
28
+ if (error !== undefined)
29
+ wrapped.cause = error;
30
+ return wrapped;
31
+ }
6
32
  export var ErrorType;
7
33
  (function (ErrorType) {
8
34
  ErrorType["SDK_TIMEOUT"] = "sdk_timeout";
@@ -13,6 +39,7 @@ export var ErrorType;
13
39
  ErrorType["CONTEXT_TOO_LONG"] = "context_too_long";
14
40
  ErrorType["MODEL_UNAVAILABLE"] = "model_unavailable";
15
41
  ErrorType["SANDBOX_INITIALIZATION_FAILED"] = "sandbox_initialization_failed";
42
+ ErrorType["CLAUDE_STARTUP_ARGUMENTS_TOO_LONG"] = "claude_startup_arguments_too_long";
16
43
  ErrorType["UNKNOWN"] = "unknown";
17
44
  })(ErrorType || (ErrorType = {}));
18
45
  /**
@@ -251,6 +278,11 @@ function hasRetryableHttpStatus(text) {
251
278
  }
252
279
  export function classifyError(error) {
253
280
  const msg = (error?.message || '').toLowerCase();
281
+ // Startup-size failures are deterministic local failures and must win over
282
+ // dictionary rules that would otherwise turn them into generic retries.
283
+ if (isClaudeStartupArgumentsTooLong(error)) {
284
+ return ErrorType.CLAUDE_STARTUP_ARGUMENTS_TOO_LONG;
285
+ }
254
286
  // 字典优先 — 命中则直接返回
255
287
  const rule = matchErrorRule(msg);
256
288
  if (rule) {
@@ -314,6 +346,10 @@ export function isRetryableError(error) {
314
346
  return false;
315
347
  }
316
348
  export function getErrorMessage(error, terminalReason, includeEmoji = true) {
349
+ if (isClaudeStartupArgumentsTooLong(error)) {
350
+ const prefix = includeEmoji ? '⚠️ ' : '';
351
+ return `${prefix}当前 Claude 启动参数过长,任务未执行`;
352
+ }
317
353
  // terminalReason 提供更精确的错误提示(SDK 0.2.100+)
318
354
  if (terminalReason) {
319
355
  const prefix = includeEmoji ? '❌ ' : '';
@@ -335,6 +371,8 @@ export function getErrorMessage(error, terminalReason, includeEmoji = true) {
335
371
  return `${prefix}权限被拒绝,操作已取消`;
336
372
  case 'sandbox_initialization_failed':
337
373
  return `${warnPrefix}只读执行沙箱启动失败(sandbox_initialization_failed),当前任务未执行;请检查 user namespace/seccomp 配置后重试`;
374
+ case 'claude_startup_arguments_too_long':
375
+ return `${warnPrefix}当前 Claude 启动参数过长,任务未执行`;
338
376
  case 'aborted_streaming':
339
377
  case 'aborted_tools':
340
378
  return `${prefix}任务已中断`;
@@ -11,10 +11,11 @@
11
11
  import fs from 'fs';
12
12
  import path from 'path';
13
13
  import { spawnSync } from 'child_process';
14
- import { resolvePaths } from '../paths.js';
14
+ import { getPackageRoot, resolvePaths } from '../paths.js';
15
15
  import { isProcessRunning, killProcess, isWindows, findProcesses } from './cross-platform.js';
16
16
  import { getProcessStartTime, startTimeMatches } from './process-introspect.js';
17
17
  import { isConfirmedLeakedTestDaemon, runtimeHomeFromEnv } from './restart-safety.js';
18
+ import { decodeWindowsOutput } from './windows-output.js';
18
19
  // ── Helpers ──
19
20
  function instanceDir() {
20
21
  return resolvePaths().instanceDir;
@@ -334,8 +335,37 @@ export function removeAll(pid) {
334
335
  function killPid(pid) {
335
336
  killProcess(pid, true);
336
337
  }
338
+ function normalizeProcessPath(value) {
339
+ return value.replace(/[\\/]+/g, '/').toLowerCase();
340
+ }
341
+ function escapeRegExp(value) {
342
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
343
+ }
337
344
  /**
338
- * 扫所有 node 进程中跑 dist/index.js PID,减去当前 HOME 已登记的 main PID。
345
+ * Return whether a process command line contains an exact EvolCore package
346
+ * main entry point. The current package path is accepted verbatim, while
347
+ * other installations are accepted only when their package directory is
348
+ * explicitly named `evolcore`.
349
+ *
350
+ * Matching the package entry instead of any `dist/index.js` path prevents
351
+ * unrelated Node services (for example BrowserMCP) from being classified as
352
+ * EvolCore orphans.
353
+ */
354
+ export function isEvolCoreMainCommand(cmdline, packageRoot = getPackageRoot()) {
355
+ const mainEntry = normalizeProcessPath(path.join(packageRoot, 'dist', 'index.js'));
356
+ const command = normalizeProcessPath(cmdline);
357
+ const entryPattern = escapeRegExp(mainEntry);
358
+ if (new RegExp(`(?:^|[\\s"'=])${entryPattern}(?=$|[\\s"'])`).test(command))
359
+ return true;
360
+ // Cross-install detection (notably on Windows/macOS where process
361
+ // environments may be unavailable): only an exact `evolcore/dist/index.js`
362
+ // package suffix qualifies. This deliberately excludes BrowserMCP and
363
+ // every other package that happens to use the same entry filename.
364
+ return /(?:^|[\s"'=])(?:[^"'=]*\/)?evolcore\/dist\/index\.js(?=$|[\s"'])/.test(command);
365
+ }
366
+ /**
367
+ * 扫所有 node 进程中运行当前 EvolCore 包 dist/index.js 的 PID,减去当前 HOME
368
+ * 已登记的 main PID。
339
369
  *
340
370
  * 用途:检测跨 HOME 残留的 evolcore 主进程(例如测试套件 spawn 后未清理、
341
371
  * 旧版本 pidfile 模式遗留等),由 cmdStart/cmdRestart 在启动前提示用户。
@@ -358,7 +388,7 @@ export function findOrphanProcesses() {
358
388
  if (m.alive)
359
389
  known.add(m.record.pid);
360
390
  }
361
- // 2. 系统中所有跑 dist/index.js 的 node 进程
391
+ // 2. 先按入口文件名找候选,再用完整包路径做严格校验。
362
392
  // Use a stable filename anchor for the Windows CIM query. The full path
363
393
  // regex contains escaped separators and is applied after command lines have
364
394
  // been fetched; embedding it in the CIM pre-filter can silently miss a
@@ -371,11 +401,8 @@ export function findOrphanProcesses() {
371
401
  if (!isProcessRunning(pid))
372
402
  continue;
373
403
  const cmdline = readCmdline(pid);
374
- // 二次验证:确实是 evolcore dist/index.js
375
- if (!/dist[\\/]index\.js/.test(cmdline))
376
- continue;
377
- // 三次验证:排除内嵌 ecweb 与独立 npm 包 ec-web。
378
- if (/[\\/](?:ecweb|ec-web)[\\/]dist[\\/]index\.js/.test(cmdline))
404
+ // 二次验证:入口必须是当前 EvolCore 包的 dist/index.js
405
+ if (!isEvolCoreMainCommand(cmdline))
379
406
  continue;
380
407
  const processEnv = readProcessEnvironment(pid);
381
408
  orphans.push({
@@ -440,25 +467,6 @@ function readCmdline(pid) {
440
467
  }
441
468
  }
442
469
  }
443
- function decodeWindowsOutput(value) {
444
- if (!value)
445
- return '';
446
- const bytes = Buffer.isBuffer(value) ? value : Buffer.from(value);
447
- if (bytes.length >= 2 && bytes[0] === 0xff && bytes[1] === 0xfe)
448
- return bytes.subarray(2).toString('utf16le');
449
- if (bytes.length >= 2 && bytes[0] === 0xfe && bytes[1] === 0xff) {
450
- const swapped = Buffer.allocUnsafe(bytes.length - 2);
451
- for (let i = 2; i + 1 < bytes.length; i += 2) {
452
- swapped[i - 2] = bytes[i + 1];
453
- swapped[i - 1] = bytes[i];
454
- }
455
- return swapped.toString('utf16le');
456
- }
457
- const utf8 = bytes.toString('utf8').replace(/^\uFEFF/, '');
458
- if (utf8.includes('\u0000'))
459
- return bytes.toString('utf16le').replace(/^\uFEFF/, '');
460
- return utf8;
461
- }
462
470
  function readProcessEnvironment(pid) {
463
471
  // Linux: /proc/<pid>/environ
464
472
  if (!isWindows && process.platform !== 'darwin') {