@yeaft/webchat-agent 0.0.153 → 0.0.155

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 (2) hide show
  1. package/crew.js +122 -15
  2. package/package.json +1 -1
package/crew.js CHANGED
@@ -926,6 +926,43 @@ async function loadRoleSessionId(sharedDir, roleName) {
926
926
  }
927
927
  }
928
928
 
929
+
930
+ /**
931
+ * 清除角色的 savedSessionId(用于强制新建 conversation)
932
+ */
933
+ async function clearRoleSessionId(sharedDir, roleName) {
934
+ const filePath = join(sharedDir, 'sessions', `${roleName}.json`);
935
+ try {
936
+ await fs.unlink(filePath);
937
+ console.log(`[Crew] Cleared sessionId for ${roleName} (force new conversation)`);
938
+ } catch {
939
+ // 文件不存在也正常
940
+ }
941
+ }
942
+
943
+ /**
944
+ * 判断角色错误是否可恢复
945
+ */
946
+ function classifyRoleError(error) {
947
+ const msg = error.message || '';
948
+ if (/context.*(window|limit|exceeded)|token.*limit|too.*(long|large)|max.*token/i.test(msg)) {
949
+ return { recoverable: true, reason: 'context_exceeded', skipResume: true };
950
+ }
951
+ if (/compact|compress|context.*reduc/i.test(msg)) {
952
+ return { recoverable: true, reason: 'compact_failed', skipResume: true };
953
+ }
954
+ if (/rate.?limit|429|overloaded|503|502|timeout|ECONNRESET|ETIMEDOUT/i.test(msg)) {
955
+ return { recoverable: true, reason: 'transient_api_error', skipResume: false };
956
+ }
957
+ if (/exited with code [1-9]/i.test(msg) && msg.length < 100) {
958
+ return { recoverable: true, reason: 'process_crashed', skipResume: false };
959
+ }
960
+ if (/spawn|ENOENT|not found/i.test(msg)) {
961
+ return { recoverable: false, reason: 'spawn_failed' };
962
+ }
963
+ return { recoverable: true, reason: 'unknown', skipResume: false };
964
+ }
965
+
929
966
  // =====================================================================
930
967
  // Role Query Management
931
968
  // =====================================================================
@@ -980,7 +1017,12 @@ async function createRoleQuery(session, roleName) {
980
1017
  claudeSessionId: savedSessionId,
981
1018
  lastCostUsd: 0,
982
1019
  lastInputTokens: 0,
983
- lastOutputTokens: 0
1020
+ lastOutputTokens: 0,
1021
+ consecutiveErrors: 0,
1022
+ lastDispatchContent: null,
1023
+ lastDispatchFrom: null,
1024
+ lastDispatchTaskId: null,
1025
+ lastDispatchTaskTitle: null
984
1026
  };
985
1027
 
986
1028
  session.roleStates.set(roleName, roleState);
@@ -1241,6 +1283,7 @@ async function processRoleOutput(session, roleName, roleQuery, roleState) {
1241
1283
  } else if (message.type === 'result') {
1242
1284
  // ★ Turn 完成!
1243
1285
  console.log(`[Crew] ${roleName} turn completed`);
1286
+ roleState.consecutiveErrors = 0;
1244
1287
 
1245
1288
  // ★ 修复2: 反向搜索该角色最后一条 streaming 消息并结束
1246
1289
  endRoleStreaming(session, roleName);
@@ -1325,21 +1368,80 @@ async function processRoleOutput(session, roleName, roleQuery, roleState) {
1325
1368
  }
1326
1369
  } else {
1327
1370
  console.error(`[Crew] ${roleName} error:`, error.message);
1328
- // 通知决策者
1329
- if (roleName !== session.decisionMaker) {
1330
- const errorMsg = `角色 ${roleName} 发生错误: ${error.message}`;
1331
- await dispatchToRole(session, session.decisionMaker, errorMsg, roleName);
1371
+
1372
+ // Step 1: 清理 roleState(防止后续写入死进程)
1373
+ endRoleStreaming(session, roleName);
1374
+ roleState.query = null;
1375
+ roleState.inputStream = null;
1376
+ roleState.turnActive = false;
1377
+ roleState.accumulatedText = '';
1378
+
1379
+ // Step 2: 错误分类
1380
+ const classification = classifyRoleError(error);
1381
+ roleState.consecutiveErrors++;
1382
+
1383
+ // Step 3: 通知前端
1384
+ sendCrewMessage({
1385
+ type: 'crew_role_error',
1386
+ sessionId: session.id,
1387
+ role: roleName,
1388
+ error: error.message.substring(0, 500),
1389
+ reason: classification.reason,
1390
+ recoverable: classification.recoverable,
1391
+ retryCount: roleState.consecutiveErrors
1392
+ });
1393
+ sendStatusUpdate(session);
1394
+
1395
+ // Step 4: 判断是否重试
1396
+ const MAX_RETRIES = 3;
1397
+ if (!classification.recoverable || roleState.consecutiveErrors > MAX_RETRIES) {
1398
+ const exhausted = roleState.consecutiveErrors > MAX_RETRIES;
1399
+ const errDetail = exhausted
1400
+ ? `角色 ${roleName} 连续 ${MAX_RETRIES} 次错误后停止重试。最后错误: ${error.message}`
1401
+ : `角色 ${roleName} 不可恢复错误: ${error.message}`;
1402
+ if (roleName !== session.decisionMaker) {
1403
+ await dispatchToRole(session, session.decisionMaker, errDetail, 'system');
1404
+ } else {
1405
+ session.status = 'waiting_human';
1406
+ sendCrewMessage({
1407
+ type: 'crew_human_needed',
1408
+ sessionId: session.id,
1409
+ fromRole: roleName,
1410
+ reason: 'error',
1411
+ message: errDetail
1412
+ });
1413
+ sendStatusUpdate(session);
1414
+ }
1415
+ return;
1416
+ }
1417
+
1418
+ // Step 5: 可恢复 → 自动重建并重试
1419
+ console.log(`[Crew] ${roleName} attempting recovery (${classification.reason}), retry ${roleState.consecutiveErrors}/${MAX_RETRIES}`);
1420
+
1421
+ sendCrewOutput(session, 'system', 'system', {
1422
+ type: 'assistant',
1423
+ message: { role: 'assistant', content: [{
1424
+ type: 'text',
1425
+ text: `${roleName} 遇到 ${classification.reason},正在自动恢复 (${roleState.consecutiveErrors}/${MAX_RETRIES})...`
1426
+ }] }
1427
+ });
1428
+
1429
+ if (roleState.lastDispatchContent) {
1430
+ if (classification.skipResume) {
1431
+ await clearRoleSessionId(session.sharedDir, roleName);
1432
+ }
1433
+ await dispatchToRole(
1434
+ session, roleName,
1435
+ roleState.lastDispatchContent,
1436
+ roleState.lastDispatchFrom || 'system',
1437
+ roleState.lastDispatchTaskId,
1438
+ roleState.lastDispatchTaskTitle
1439
+ );
1332
1440
  } else {
1333
- // 决策者自己出错了,通知人
1334
- sendCrewMessage({
1335
- type: 'crew_human_needed',
1336
- sessionId: session.id,
1337
- fromRole: roleName,
1338
- reason: 'error',
1339
- message: `决策者 ${roleName} 发生错误: ${error.message}`
1340
- });
1341
- session.status = 'waiting_human';
1342
- sendStatusUpdate(session);
1441
+ const msg = `角色 ${roleName} 已恢复(${classification.reason}),但无待重试消息。`;
1442
+ if (roleName !== session.decisionMaker) {
1443
+ await dispatchToRole(session, session.decisionMaker, msg, 'system');
1444
+ }
1343
1445
  }
1344
1446
  }
1345
1447
  }
@@ -1491,6 +1593,10 @@ async function dispatchToRole(session, roleName, content, fromSource, taskId, ta
1491
1593
  });
1492
1594
 
1493
1595
  // 发送
1596
+ roleState.lastDispatchContent = content;
1597
+ roleState.lastDispatchFrom = fromSource;
1598
+ roleState.lastDispatchTaskId = taskId || null;
1599
+ roleState.lastDispatchTaskTitle = taskTitle || null;
1494
1600
  roleState.turnActive = true;
1495
1601
  roleState.accumulatedText = '';
1496
1602
  roleState.inputStream.enqueue({
@@ -1498,6 +1604,7 @@ async function dispatchToRole(session, roleName, content, fromSource, taskId, ta
1498
1604
  message: { role: 'user', content }
1499
1605
  });
1500
1606
 
1607
+ sendStatusUpdate(session);
1501
1608
  console.log(`[Crew] Dispatched to ${roleName} from ${fromSource}${taskId ? ` (task: ${taskId})` : ''}`);
1502
1609
  }
1503
1610
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yeaft/webchat-agent",
3
- "version": "0.0.153",
3
+ "version": "0.0.155",
4
4
  "description": "Remote agent for Yeaft WebChat — connects worker machines to the central server",
5
5
  "main": "index.js",
6
6
  "type": "module",