feat: v0.3.18 上下文压缩机制修复 + MCP 工具就绪竞态修复 + 记忆系统加固

【上下文压缩机制修复】
- engine.ts 新增 lastRealInputTokens 记录 LLM 返回的真实输入 token,压缩判断取 max(估算值, 真实值),避免估算偏低导致不压缩但 API 413
- 修复 effectiveContextWindow 缺少默认值导致 compressionThreshold 变 NaN、压缩永不触发的 bug(添加 ?? 128_000 兜底)
- compressMessages 保留区从固定 10 条改为按 token 预算动态截断(50% 上下文窗口)
- 二次截断 charsPerToken 从 2 调整为 1.0,与 CJK_TOKEN_RATIO 一致
- 压缩后重置 lastRealInputTokens,避免跨迭代污染
- 每个 run 开始时重置 lastRealInputTokens

【前端 Token 显示修复】
- 区分"累计消耗"和"上下文占用"语义——之前 totalTokens(累计) / contextWindow(单次窗口) 得出无意义百分比
- TokenUsage.tsx 上下文占用改用 lastInputTokens,新增压缩节省行(绿色,仅当 > 0 时显示)
- agent-store.ts TokenUsage 接口新增 lastInputTokens 和 lastCompressedSaved 字段,4 处初始值统一更新
- useAgentStream.ts usage 事件 lastInputTokens 替换不累加,compressed 事件通过 streamEvent 接收 savedTokens
- handlers.ts onCompressed 同时发 toast + streamEvent,解决"压缩触发但前端 token 显示不降"缺陷
- 旧数据兼容使用 ?? 0,保证历史会话加载不崩溃

【MCP 工具就绪竞态修复】
- 修复输入框永久显示"工具加载中"的竞态条件:MCP initialize 几乎立即 resolve(connectServer 不 await),tools:ready 事件在前端监听器注册前已发出
- main.ts 维护 toolsReady 标志 + 注册 tools:isReady IPC handler 查询当前状态
- preload.ts 暴露 tools.isReady() 方法
- App.tsx 注册 onReady 监听器后立即查询 isReady(),无论事件是否错过都能恢复正确状态

【记忆系统加固】
- consolidator.ts 新增 runningPromise + waitForCompletion(35s),before-quit 等待固化完成,防止退出时异步 consolidate 数据丢失
- 固化到 MEMORY.md 的同时写入 semantic_memories 表,解决双轨存储无交叉验证问题
- manager.ts tokenize 按中英文标点切分子句后再做 bigram,优化中文分词
- 清理正则冗余括号

【SOUL.md 降级处理】
- context-builder.ts SOUL.md 为空或不存在时降级到默认身份,向前端发 toast 提示用户
- 新增 fallbackRoleNotified 去重标志,仅首次降级通知,避免每次发消息都弹 toast
- SOUL.md 恢复内容时重置标志

【Token 估算调整】
- token-estimator.ts CJK_TOKEN_RATIO 从 1.5 调整为 1.0

【MCP 异步初始化】
- main.ts MCP 完成后广播 tools:ready 事件,前端 UI 据以控制输入框可用性
- preload.ts + global.d.ts 暴露 tools.onReady() 监听器
- App.tsx + ChatInput.tsx toolsReady 状态控制输入框

【版本号】
- package.json + package-lock.json 从 0.3.16 升级到 0.3.18
This commit is contained in:
2026-07-22 15:17:58 +08:00
parent 3e5ddea72d
commit f3a0e751ba
16 changed files with 471 additions and 49 deletions
+25 -2
View File
@@ -146,6 +146,15 @@ export function registerAllIPCHandlers(
const workspaceFiles = workspaceService.getFiles();
const systemPrompt = contextBuilder.buildSystemPrompt(workspaceFiles, workspaceService.getPath());
// v0.3.18 修复: SOUL.md 为空或不存在时降级到默认身份,向前端发 toast 提示用户
// 避免用户自定义人格丢失但不知情的"安静失败"问题
if (contextBuilder.isUsingFallbackRole() && !mainWindow.isDestroyed()) {
mainWindow.webContents.send('toast:show', {
type: 'info',
message: '未找到 SOUL.md 或内容为空,已使用默认 Metona 身份。可在工作空间根目录创建 SOUL.md 自定义 Agent 人格',
});
}
// 检索与用户消息相关的记忆,注入到 System Prompt 动态区
try {
const memories = memoryManager.search(userMessage.content, { topK: 5, minImportance: 0.3 });
@@ -265,12 +274,26 @@ export function registerAllIPCHandlers(
}
};
// M-6: 转发上下文压缩事件为 toast 通知
// M-6: 转发上下文压缩事件为 toast 通知 + streamEvent 通知
// v0.3.18 修复: 同时通过 streamEvent 转发压缩数据,前端 store 据以更新 token 显示
// 解决"压缩触发但前端 token 显示不降"的缺陷
const onCompressed = (data: { iteration?: number; originalTokens?: number; compressedTokens?: number }) => {
const savedTokens = Math.max(0, (data.originalTokens ?? 0) - (data.compressedTokens ?? 0));
if (!mainWindow.isDestroyed()) {
// toast 通知用户压缩已发生
mainWindow.webContents.send('toast:show', {
type: 'info',
message: `上下文压缩: ${data.originalTokens ?? '?'}${data.compressedTokens ?? '?'} tokens`,
message: `上下文压缩: ${data.originalTokens ?? '?'}${data.compressedTokens ?? '?'} tokens(节省 ${savedTokens}`,
});
// 通过 streamEvent 转发,前端 useAgentStream 监听 'compressed' 类型后更新 store
mainWindow.webContents.send('agent:streamEvent', {
type: 'compressed',
sessionId,
iteration: data.iteration ?? 0,
originalTokens: data.originalTokens,
compressedTokens: data.compressedTokens,
savedTokens,
timestamp: Date.now(),
});
}
};