@yeaft/webchat-agent 0.1.69 → 0.1.70

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
@@ -408,7 +408,7 @@ async function processClaudeOutput(conversationId, claudeQuery, state) {
408
408
 
409
409
  // 计算上下文使用百分比
410
410
  const inputTokens = message.usage?.input_tokens || 0;
411
- const maxContextTokens = 128000; // API max_prompt_tokens 限制
411
+ const maxContextTokens = ctx.CONFIG?.maxContextTokens || 128000;
412
412
  if (inputTokens > 0) {
413
413
  ctx.sendToServer({
414
414
  type: 'context_usage',
@@ -497,6 +497,35 @@ async function processClaudeOutput(conversationId, claudeQuery, state) {
497
497
 
498
498
  console.log(`[RolePlay] Auto-continuing to role: ${to}`);
499
499
 
500
+ // ★ Pre-send compact check for RolePlay auto-continue
501
+ const rpAutoCompactThreshold = ctx.CONFIG?.autoCompactThreshold || 110000;
502
+ const rpEstimatedNewTokens = Math.ceil(prompt.length / 3);
503
+ const rpEstimatedTotal = inputTokens + rpEstimatedNewTokens;
504
+
505
+ if (rpEstimatedTotal > rpAutoCompactThreshold) {
506
+ console.log(`[RolePlay] Pre-send compact: estimated ${rpEstimatedTotal} tokens (last: ${inputTokens} + new: ~${rpEstimatedNewTokens}) exceeds threshold ${rpAutoCompactThreshold}`);
507
+ ctx.sendToServer({
508
+ type: 'compact_status',
509
+ conversationId,
510
+ status: 'compacting',
511
+ message: `Auto-compacting before RolePlay continue: estimated ${rpEstimatedTotal} tokens (threshold: ${rpAutoCompactThreshold})`
512
+ });
513
+ // Store pending message and compact first
514
+ const userMessage = {
515
+ type: 'user',
516
+ message: { role: 'user', content: prompt }
517
+ };
518
+ state._pendingUserMessage = userMessage;
519
+ state.turnActive = true;
520
+ state.turnResultReceived = false;
521
+ state.inputStream.enqueue({
522
+ type: 'user',
523
+ message: { role: 'user', content: '/compact' }
524
+ });
525
+ sendConversationList();
526
+ break;
527
+ }
528
+
500
529
  // Re-activate the turn
501
530
  state.turnActive = true;
502
531
  state.turnResultReceived = false;
@@ -531,6 +560,18 @@ async function processClaudeOutput(conversationId, claudeQuery, state) {
531
560
  workDir: state.workDir
532
561
  });
533
562
  sendConversationList();
563
+
564
+ // ★ Send pending user message after compact completes
565
+ if (state._pendingUserMessage && state.inputStream) {
566
+ const pendingMsg = state._pendingUserMessage;
567
+ state._pendingUserMessage = null;
568
+ console.log(`[${conversationId}] Sending pending message after compact`);
569
+ state.turnActive = true;
570
+ state.turnResultReceived = false;
571
+ sendOutput(conversationId, pendingMsg);
572
+ state.inputStream.enqueue(pendingMsg);
573
+ sendConversationList();
574
+ }
534
575
  continue;
535
576
  }
536
577
 
package/conversation.js CHANGED
@@ -515,6 +515,34 @@ export async function handleUserInput(msg) {
515
515
  };
516
516
 
517
517
  console.log(`[${conversationId}] Sending: ${prompt.substring(0, 100)}...`);
518
+
519
+ // ★ Pre-send compact check: estimate total tokens and compact before sending if needed
520
+ const autoCompactThreshold = ctx.CONFIG?.autoCompactThreshold || 110000;
521
+ const lastInputTokens = state.lastResultInputTokens || 0;
522
+ const estimatedNewTokens = Math.ceil(effectivePrompt.length / 3); // conservative: ~3 chars per token
523
+ const estimatedTotal = lastInputTokens + estimatedNewTokens;
524
+
525
+ if (estimatedTotal > autoCompactThreshold && state.inputStream) {
526
+ console.log(`[${conversationId}] Pre-send compact: estimated ${estimatedTotal} tokens (last: ${lastInputTokens} + new: ~${estimatedNewTokens}) exceeds threshold ${autoCompactThreshold}`);
527
+ ctx.sendToServer({
528
+ type: 'compact_status',
529
+ conversationId,
530
+ status: 'compacting',
531
+ message: `Auto-compacting before send: estimated ${estimatedTotal} tokens (threshold: ${autoCompactThreshold})`
532
+ });
533
+ // Send /compact first, then the user message will be sent after compact completes
534
+ // by storing it as a pending message
535
+ state._pendingUserMessage = userMessage;
536
+ state.turnActive = true;
537
+ state.turnResultReceived = false;
538
+ sendConversationList();
539
+ state.inputStream.enqueue({
540
+ type: 'user',
541
+ message: { role: 'user', content: '/compact' }
542
+ });
543
+ return;
544
+ }
545
+
518
546
  state.turnActive = true;
519
547
  state.turnResultReceived = false; // 重置 per-turn 去重标志
520
548
  sendConversationList(); // 在 turnActive=true 后通知 server,确保 processing 状态正确
@@ -6,10 +6,10 @@ import { sendCrewMessage, sendCrewOutput, sendStatusUpdate, endRoleStreaming } f
6
6
  import { saveRoleSessionId, clearRoleSessionId, classifyRoleError, createRoleQuery } from './role-query.js';
7
7
  import { parseRoutes, executeRoute, dispatchToRole } from './routing.js';
8
8
  import { parseCompletedTasks, updateFeatureIndex, appendChangelog, saveRoleWorkSummary, updateKanban } from './task-files.js';
9
+ import ctx from '../context.js';
9
10
 
10
- // Context 使用率阈值常量
11
- const MAX_CONTEXT = 128000; // API max_prompt_tokens 限制
12
- const CLEAR_THRESHOLD = 0.85; // 85% → 直接 clear + rebuild(不再走 compact)
11
+ // Context 使用率常量(运行时从 ctx.CONFIG 读取)
12
+ const getMaxContext = () => ctx.CONFIG?.maxContextTokens || 128000;
13
13
 
14
14
  /**
15
15
  * 处理角色的流式输出
@@ -99,14 +99,11 @@ export async function processRoleOutput(session, roleName, roleQuery, roleState)
99
99
  sessionId: session.id,
100
100
  role: roleName,
101
101
  inputTokens,
102
- maxTokens: MAX_CONTEXT,
103
- percentage: Math.min(100, Math.round((inputTokens / MAX_CONTEXT) * 100))
102
+ maxTokens: getMaxContext(),
103
+ percentage: Math.min(100, Math.round((inputTokens / getMaxContext()) * 100))
104
104
  });
105
105
  }
106
106
 
107
- const contextPercentage = inputTokens / MAX_CONTEXT;
108
- const needClear = contextPercentage >= CLEAR_THRESHOLD;
109
-
110
107
  // 解析路由
111
108
  const routes = parseRoutes(roleState.accumulatedText);
112
109
 
@@ -136,8 +133,8 @@ export async function processRoleOutput(session, roleName, roleQuery, roleState)
136
133
  }
137
134
  }
138
135
 
139
- // 保存 accumulatedText 供后续 saveRoleWorkSummary 使用(清空前)
140
- const turnText = roleState.accumulatedText;
136
+ // 保存本 turn 文本(供 routing.js 预检时 saveRoleWorkSummary 使用)
137
+ roleState.lastTurnText = roleState.accumulatedText;
141
138
  roleState.accumulatedText = '';
142
139
  roleState.turnActive = false;
143
140
 
@@ -149,57 +146,7 @@ export async function processRoleOutput(session, roleName, roleQuery, roleState)
149
146
 
150
147
  sendStatusUpdate(session);
151
148
 
152
- // Context 超限:保存工作摘要后 clear + rebuild
153
- if (needClear) {
154
- console.log(`[Crew] ${roleName} context at ${Math.round(contextPercentage * 100)}%, clearing and rebuilding`);
155
-
156
- // 保存工作摘要到 feature 文件
157
- await saveRoleWorkSummary(session, roleName, turnText).catch(e =>
158
- console.warn(`[Crew] Failed to save work summary for ${roleName}:`, e.message));
159
-
160
- // Clear 角色
161
- await clearRoleSessionId(session.sharedDir, roleName);
162
- roleState.claudeSessionId = null;
163
-
164
- if (roleState.abortController) roleState.abortController.abort();
165
- roleState.query = null;
166
- roleState.inputStream = null;
167
-
168
- sendCrewMessage({
169
- type: 'crew_role_cleared',
170
- sessionId: session.id,
171
- role: roleName,
172
- contextPercentage: Math.round(contextPercentage * 100),
173
- reason: 'context_limit'
174
- });
175
-
176
- // 继承 task 到路由(如有)
177
- const currentTask = roleState.currentTask;
178
- if (routes.length > 0) {
179
- for (const route of routes) {
180
- if (!route.taskId && currentTask) {
181
- route.taskId = currentTask.taskId;
182
- route.taskTitle = currentTask.taskTitle;
183
- }
184
- }
185
- }
186
-
187
- // 执行路由
188
- if (routes.length > 0) {
189
- session.round++;
190
- const results = await Promise.allSettled(routes.map(route =>
191
- executeRoute(session, roleName, route)
192
- ));
193
- for (const r of results) {
194
- if (r.status === 'rejected') {
195
- console.warn(`[Crew] Route execution failed:`, r.reason);
196
- }
197
- }
198
- }
199
- return; // query 已清空,退出
200
- }
201
-
202
- // 执行路由(无需 clear 时)
149
+ // 执行路由
203
150
  if (routes.length > 0) {
204
151
  session.round++;
205
152
 
package/crew/routing.js CHANGED
@@ -4,8 +4,9 @@
4
4
  */
5
5
  import { join } from 'path';
6
6
  import { sendCrewMessage, sendCrewOutput, sendStatusUpdate } from './ui-messages.js';
7
- import { ensureTaskFile, appendTaskRecord, readTaskFile, updateKanban, readKanban } from './task-files.js';
8
- import { createRoleQuery } from './role-query.js';
7
+ import { ensureTaskFile, appendTaskRecord, readTaskFile, updateKanban, readKanban, saveRoleWorkSummary } from './task-files.js';
8
+ import { createRoleQuery, clearRoleSessionId } from './role-query.js';
9
+ import ctx from '../context.js';
9
10
 
10
11
  /** Format role label */
11
12
  function roleLabel(r) {
@@ -193,6 +194,39 @@ export async function dispatchToRole(session, roleName, content, fromSource, tas
193
194
  timestamp: Date.now()
194
195
  });
195
196
 
197
+ // ★ Pre-send compact check: estimate total tokens and clear+rebuild if needed
198
+ const autoCompactThreshold = ctx.CONFIG?.autoCompactThreshold || 110000;
199
+ const lastInputTokens = roleState.lastInputTokens || 0;
200
+ const estimatedNewTokens = Math.ceil((typeof content === 'string' ? content.length : 0) / 3);
201
+ const estimatedTotal = lastInputTokens + estimatedNewTokens;
202
+
203
+ if (lastInputTokens > 0 && estimatedTotal > autoCompactThreshold) {
204
+ console.log(`[Crew] Pre-send compact for ${roleName}: estimated ${estimatedTotal} tokens (last: ${lastInputTokens} + new: ~${estimatedNewTokens}) exceeds threshold ${autoCompactThreshold}`);
205
+
206
+ // Save work summary before clearing (use lastTurnText since accumulatedText is cleared after result)
207
+ await saveRoleWorkSummary(session, roleName, roleState.lastTurnText || roleState.accumulatedText || '').catch(e =>
208
+ console.warn(`[Crew] Failed to save work summary for ${roleName}:`, e.message));
209
+
210
+ // Clear role session and rebuild
211
+ await clearRoleSessionId(session.sharedDir, roleName);
212
+ roleState.claudeSessionId = null;
213
+
214
+ if (roleState.abortController) roleState.abortController.abort();
215
+ roleState.query = null;
216
+ roleState.inputStream = null;
217
+
218
+ sendCrewMessage({
219
+ type: 'crew_role_cleared',
220
+ sessionId: session.id,
221
+ role: roleName,
222
+ contextPercentage: Math.round((lastInputTokens / (ctx.CONFIG?.maxContextTokens || 128000)) * 100),
223
+ reason: 'pre_send_compact'
224
+ });
225
+
226
+ // Recreate the query (fresh Claude process)
227
+ roleState = await createRoleQuery(session, roleName);
228
+ }
229
+
196
230
  // 发送
197
231
  roleState.lastDispatchContent = content;
198
232
  roleState.lastDispatchFrom = fromSource;
package/index.js CHANGED
@@ -74,7 +74,11 @@ const CONFIG = {
74
74
  return raw.split(',').map(s => s.trim()).filter(Boolean);
75
75
  })(),
76
76
  // disallowedTools 会在 loadMcpServers() 中计算
77
- disallowedTools: []
77
+ disallowedTools: [],
78
+ // 最大上下文 tokens(用于百分比计算的分母)
79
+ maxContextTokens: parseInt(process.env.MAX_CONTEXT_TOKENS || fileConfig.maxContextTokens, 10) || 128000,
80
+ // Auto-compact 阈值(tokens):context 超过此值时自动触发 compact
81
+ autoCompactThreshold: parseInt(process.env.AUTO_COMPACT_THRESHOLD || fileConfig.autoCompactThreshold, 10) || 110000
78
82
  };
79
83
 
80
84
  // 初始化共享上下文
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yeaft/webchat-agent",
3
- "version": "0.1.69",
3
+ "version": "0.1.70",
4
4
  "description": "Remote agent for Yeaft WebChat — connects worker machines to the central server",
5
5
  "main": "index.js",
6
6
  "type": "module",