@yeaft/webchat-agent 0.0.166 → 0.0.168

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/claude.js CHANGED
@@ -364,7 +364,7 @@ async function processClaudeOutput(conversationId, claudeQuery, state) {
364
364
 
365
365
  // 计算上下文使用百分比
366
366
  const inputTokens = message.usage?.input_tokens || 0;
367
- const maxContextTokens = 200000; // Claude 模型 context window
367
+ const maxContextTokens = 128000; // API max_prompt_tokens 限制
368
368
  if (inputTokens > 0) {
369
369
  ctx.sendToServer({
370
370
  type: 'context_usage',
package/connection.js CHANGED
@@ -33,7 +33,8 @@ const BUFFERABLE_TYPES = new Set([
33
33
  'background_task_started', 'background_task_output',
34
34
  'crew_output', 'crew_status', 'crew_turn_completed',
35
35
  'crew_session_created', 'crew_session_restored', 'crew_human_needed',
36
- 'crew_role_added', 'crew_role_removed'
36
+ 'crew_role_added', 'crew_role_removed',
37
+ 'crew_role_compact', 'crew_context_usage'
37
38
  ]);
38
39
 
39
40
  // Send message to server (with encryption if available)
package/crew.js CHANGED
@@ -1066,7 +1066,7 @@ async function clearRoleSessionId(sharedDir, roleName) {
1066
1066
  function classifyRoleError(error) {
1067
1067
  const msg = error.message || '';
1068
1068
  if (/context.*(window|limit|exceeded)|token.*limit|too.*(long|large)|max.*token/i.test(msg)) {
1069
- return { recoverable: true, reason: 'context_exceeded', skipResume: true };
1069
+ return { recoverable: true, reason: 'context_exceeded', skipResume: true, needContentTrim: true };
1070
1070
  }
1071
1071
  if (/compact|compress|context.*reduc/i.test(msg)) {
1072
1072
  return { recoverable: true, reason: 'compact_failed', skipResume: true };
@@ -1083,6 +1083,22 @@ function classifyRoleError(error) {
1083
1083
  return { recoverable: true, reason: 'unknown', skipResume: false };
1084
1084
  }
1085
1085
 
1086
+ /**
1087
+ * context 超限时精简重派内容
1088
+ */
1089
+ function trimContentForRetry(content) {
1090
+ if (typeof content === 'string' && content.length > 2000) {
1091
+ return `[注意: 上一轮因 context 超限被截断重发]\n\n${content.substring(0, 2000)}\n\n[内容已截断,请基于已知信息继续工作]`;
1092
+ }
1093
+ if (Array.isArray(content)) {
1094
+ return content.filter(b => b.type === 'text').map(b => ({
1095
+ ...b,
1096
+ text: b.text.length > 2000 ? b.text.substring(0, 2000) + '\n[已截断]' : b.text
1097
+ }));
1098
+ }
1099
+ return content;
1100
+ }
1101
+
1086
1102
  // =====================================================================
1087
1103
  // Role Query Management
1088
1104
  // =====================================================================
@@ -1142,7 +1158,12 @@ async function createRoleQuery(session, roleName) {
1142
1158
  lastDispatchContent: null,
1143
1159
  lastDispatchFrom: null,
1144
1160
  lastDispatchTaskId: null,
1145
- lastDispatchTaskTitle: null
1161
+ lastDispatchTaskTitle: null,
1162
+ // compact 状态
1163
+ _compacting: false, // 是否正在 compact
1164
+ _compactSummaryPending: false, // 是否等待过滤 compact summary
1165
+ _pendingCompactRoutes: null, // compact 期间缓存的待执行路由 Array|null
1166
+ _fromRole: null // 缓存路由的来源角色
1146
1167
  };
1147
1168
 
1148
1169
  session.roleStates.set(roleName, roleState);
@@ -1355,6 +1376,11 @@ ${allRoles.map(r => `- ${r.name}: ${roleLabel(r)} - ${r.description}`).join('\n'
1355
1376
  // Role Output Processing
1356
1377
  // =====================================================================
1357
1378
 
1379
+ // Context 使用率阈值常量
1380
+ const MAX_CONTEXT = 128000; // API max_prompt_tokens 限制
1381
+ const COMPACT_THRESHOLD = 0.85; // 85% → 触发预防性 compact
1382
+ const CLEAR_THRESHOLD = 0.95; // 95% → compact 后仍超限则 clear + rebuild
1383
+
1358
1384
  /**
1359
1385
  * 处理角色的流式输出
1360
1386
  */
@@ -1370,6 +1396,22 @@ async function processRoleOutput(session, roleName, roleQuery, roleState) {
1370
1396
  continue;
1371
1397
  }
1372
1398
 
1399
+ // ★ compact 消息过滤(compact 期间只放行 result,其余过滤)
1400
+ if (roleState._compacting && message.type !== 'result') {
1401
+ if (message.type === 'system') {
1402
+ if (message.subtype === 'compact_boundary') {
1403
+ roleState._compactSummaryPending = true;
1404
+ }
1405
+ continue; // 过滤所有 compact 期间的 system 消息
1406
+ }
1407
+ if (message.type === 'user' && roleState._compactSummaryPending) {
1408
+ roleState._compactSummaryPending = false;
1409
+ continue; // 过滤 compact summary
1410
+ }
1411
+ // 其他消息(assistant 等)在 compact 期间也过滤
1412
+ continue;
1413
+ }
1414
+
1373
1415
  if (message.type === 'assistant') {
1374
1416
  // 累积文本用于路由解析
1375
1417
  const content = message.message?.content;
@@ -1424,12 +1466,99 @@ async function processRoleOutput(session, roleName, roleQuery, roleState) {
1424
1466
  roleState.lastOutputTokens = message.usage.output_tokens || 0;
1425
1467
  }
1426
1468
 
1469
+ // ★ compact turn 完成的处理
1470
+ if (roleState._compacting) {
1471
+ roleState._compacting = false;
1472
+ const postCompactTokens = message.usage?.input_tokens || 0;
1473
+ const postCompactPercentage = postCompactTokens / MAX_CONTEXT;
1474
+ console.log(`[Crew] ${roleName} compact completed, context now at ${Math.round(postCompactPercentage * 100)}%`);
1475
+
1476
+ sendCrewMessage({
1477
+ type: 'crew_role_compact',
1478
+ sessionId: session.id,
1479
+ role: roleName,
1480
+ contextPercentage: Math.round(postCompactPercentage * 100),
1481
+ status: 'completed'
1482
+ });
1483
+
1484
+ // Layer 2: compact 后仍超 95% → clear + rebuild
1485
+ if (postCompactPercentage >= CLEAR_THRESHOLD) {
1486
+ console.warn(`[Crew] ${roleName} still at ${Math.round(postCompactPercentage * 100)}% after compact, escalating to clear`);
1487
+
1488
+ await clearRoleSessionId(session.sharedDir, roleName);
1489
+ roleState.claudeSessionId = null;
1490
+
1491
+ if (roleState.abortController) roleState.abortController.abort();
1492
+ roleState.query = null;
1493
+ roleState.inputStream = null;
1494
+
1495
+ sendCrewMessage({
1496
+ type: 'crew_role_compact',
1497
+ sessionId: session.id,
1498
+ role: roleName,
1499
+ status: 'cleared'
1500
+ });
1501
+
1502
+ // 重新 dispatch 缓存的路由(用新会话)
1503
+ if (roleState._pendingCompactRoutes) {
1504
+ const routes = roleState._pendingCompactRoutes;
1505
+ const fromRole = roleState._fromRole;
1506
+ roleState._pendingCompactRoutes = null;
1507
+ roleState._fromRole = null;
1508
+ session.round++;
1509
+ const results = await Promise.allSettled(routes.map(route =>
1510
+ executeRoute(session, fromRole, route)
1511
+ ));
1512
+ for (const r of results) {
1513
+ if (r.status === 'rejected') {
1514
+ console.warn(`[Crew] Route execution failed:`, r.reason);
1515
+ }
1516
+ }
1517
+ }
1518
+ return; // abort 后 query 已清空,退出 processRoleOutput
1519
+ }
1520
+
1521
+ // 执行之前缓存的路由
1522
+ if (roleState._pendingCompactRoutes) {
1523
+ const routes = roleState._pendingCompactRoutes;
1524
+ const fromRole = roleState._fromRole;
1525
+ roleState._pendingCompactRoutes = null;
1526
+ roleState._fromRole = null;
1527
+ session.round++;
1528
+ const results = await Promise.allSettled(routes.map(route =>
1529
+ executeRoute(session, fromRole, route)
1530
+ ));
1531
+ for (const r of results) {
1532
+ if (r.status === 'rejected') {
1533
+ console.warn(`[Crew] Route execution failed:`, r.reason);
1534
+ }
1535
+ }
1536
+ }
1537
+ continue; // 不要重复处理这个 compact result
1538
+ }
1539
+
1427
1540
  // ★ 持久化 sessionId(每次 turn 完成后保存,用于后续 resume)
1428
1541
  if (roleState.claudeSessionId) {
1429
1542
  saveRoleSessionId(session.sharedDir, roleName, roleState.claudeSessionId)
1430
1543
  .catch(e => console.warn(`[Crew] Failed to save sessionId for ${roleName}:`, e.message));
1431
1544
  }
1432
1545
 
1546
+ // ★ context 使用率监控
1547
+ const inputTokens = message.usage?.input_tokens || 0;
1548
+ if (inputTokens > 0) {
1549
+ sendCrewMessage({
1550
+ type: 'crew_context_usage',
1551
+ sessionId: session.id,
1552
+ role: roleName,
1553
+ inputTokens,
1554
+ maxTokens: MAX_CONTEXT,
1555
+ percentage: Math.min(100, Math.round((inputTokens / MAX_CONTEXT) * 100))
1556
+ });
1557
+ }
1558
+
1559
+ const contextPercentage = inputTokens / MAX_CONTEXT;
1560
+ const needCompact = contextPercentage >= COMPACT_THRESHOLD;
1561
+
1433
1562
  // 解析路由(支持多 ROUTE 块)
1434
1563
  const routes = parseRoutes(roleState.accumulatedText);
1435
1564
  roleState.accumulatedText = '';
@@ -1445,7 +1574,44 @@ async function processRoleOutput(session, roleName, roleQuery, roleState) {
1445
1574
  // 发送状态更新
1446
1575
  sendStatusUpdate(session);
1447
1576
 
1448
- // 执行路由
1577
+ // ★ 需要 compact:缓存路由,先执行 compact
1578
+ if (needCompact) {
1579
+ console.log(`[Crew] ${roleName} context at ${Math.round(contextPercentage * 100)}%, compacting before next dispatch`);
1580
+
1581
+ roleState._pendingCompactRoutes = routes.length > 0 ? routes : null;
1582
+ roleState._compacting = true;
1583
+ roleState._compactSummaryPending = false;
1584
+ roleState._fromRole = roleName;
1585
+
1586
+ // task 继承
1587
+ const currentTask = roleState.currentTask;
1588
+ if (roleState._pendingCompactRoutes) {
1589
+ for (const route of roleState._pendingCompactRoutes) {
1590
+ if (!route.taskId && currentTask) {
1591
+ route.taskId = currentTask.taskId;
1592
+ route.taskTitle = currentTask.taskTitle;
1593
+ }
1594
+ }
1595
+ }
1596
+
1597
+ // 发送 /compact
1598
+ roleState.inputStream.enqueue({
1599
+ type: 'user',
1600
+ message: { role: 'user', content: '/compact' }
1601
+ });
1602
+
1603
+ sendCrewMessage({
1604
+ type: 'crew_role_compact',
1605
+ sessionId: session.id,
1606
+ role: roleName,
1607
+ contextPercentage: Math.round(contextPercentage * 100),
1608
+ status: 'compacting'
1609
+ });
1610
+
1611
+ continue; // 等 compact turn 完成
1612
+ }
1613
+
1614
+ // 执行路由(无需 compact 时)
1449
1615
  if (routes.length > 0) {
1450
1616
  // ★ 修复1: 多 ROUTE 只增 1 轮(round++ 从 executeRoute 移到这里)
1451
1617
  session.round++;
@@ -1495,6 +1661,9 @@ async function processRoleOutput(session, roleName, roleQuery, roleState) {
1495
1661
  roleState.inputStream = null;
1496
1662
  roleState.turnActive = false;
1497
1663
  roleState.accumulatedText = '';
1664
+ // 重置 compact 状态(防止 compact 期间出错导致后续消息被永久过滤)
1665
+ roleState._compacting = false;
1666
+ roleState._compactSummaryPending = false;
1498
1667
 
1499
1668
  // Step 2: 错误分类
1500
1669
  const classification = classifyRoleError(error);
@@ -1547,12 +1716,19 @@ async function processRoleOutput(session, roleName, roleQuery, roleState) {
1547
1716
  });
1548
1717
 
1549
1718
  if (roleState.lastDispatchContent) {
1719
+ let retryContent = roleState.lastDispatchContent;
1720
+
1721
+ // ★ context 超限时精简重派内容
1722
+ if (classification.needContentTrim) {
1723
+ retryContent = trimContentForRetry(retryContent);
1724
+ }
1725
+
1550
1726
  if (classification.skipResume) {
1551
1727
  await clearRoleSessionId(session.sharedDir, roleName);
1552
1728
  }
1553
1729
  await dispatchToRole(
1554
1730
  session, roleName,
1555
- roleState.lastDispatchContent,
1731
+ retryContent,
1556
1732
  roleState.lastDispatchFrom || 'system',
1557
1733
  roleState.lastDispatchTaskId,
1558
1734
  roleState.lastDispatchTaskTitle
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yeaft/webchat-agent",
3
- "version": "0.0.166",
3
+ "version": "0.0.168",
4
4
  "description": "Remote agent for Yeaft WebChat — connects worker machines to the central server",
5
5
  "main": "index.js",
6
6
  "type": "module",