release: v1.2.6 — 全量代码审计修复、滚动与流式渲染优化、MCP 协议升级 - #21
Conversation
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
MemoryExtractionService.extractAndStore 从未被调用(原设计由 MainCoordinator 调度, 实际未接线),连带删除其专属依赖: - memory/skill-extraction 两个 prompt 模板注册(PromptTemplateRepository ID + 定义 + title/desc 字符串 + asset 文件) - memory_extraction_json_only 字符串(3 个 locale) - 对应单测 MemoryExtractionServiceTest - CLAUDE.md/README 中的相关引用同步更新
- 版本号 1.2.6-rc.2 -> 1.2.6(正式版),versionCode 30 -> 31 - 全量代码审计修复:高危 2 项、中危 30 项 - file_read KB 分页只返回第一行 / end_kb 放开 / 分块统计 - Shell 输出只回最后一行 / 超时进程树击杀 / readFile 上限 - UrlPolicy 字面 IPv4 私网判定 / SimpleHttpClient 响应体上限 - 图片下载与响应上限生效 / Markdown base64 防 OOM / 中文标签截字 - 错误日志脱敏 / 导入路径穿越防护 - Codex 压缩 Token 基线重置 / SSH 连接池竞态 / ToolRegistry 读写锁 - 持久化后台化 / 工具预算只计实际执行 / Agent 计数并入全局上限 - 主线程 IO 迁移(Skill 安装/导出/统计)/ 屏幕缓存上限 / UI 细节修复 - update.md: v1.2.6 正式版日志(含 1.2.5 起全部 21 个提交的变更)
Reviewer's GuideRelease v1.2.6 formal build focusing on protocol/tooling features, scroll/stream rendering UX, and a broad security/stability audit (file_read, shell/SSH, HTTP/image limits, context compaction, concurrency, and UI). Sequence diagram for shell execution timeout and process-tree killsequenceDiagram
actor User
participant ShellExecuteTool
participant TerminalProviderService
User->>ShellExecuteTool: executeViaTerminalProvider(command, cwd, timeoutMs)
ShellExecuteTool->>TerminalProviderService: executeShell(command, cwd, timeoutMs, callback)
TerminalProviderService->>TerminalProviderService: executor.submit(shell task)
TerminalProviderService->>TerminalProviderService: process.start()
TerminalProviderService->>TerminalProviderService: processRef.set(process)
par StreamOutput
TerminalProviderService->>callback: onOutput(line) [!finished]
TerminalProviderService->>callback: onOutput(line) [!finished]
end
alt completes before timeout
TerminalProviderService->>callback: onComplete(exitCode) [!finished]
TerminalProviderService->>TerminalProviderService: process.destroy()
else timeout
TerminalProviderService->>TerminalProviderService: future.cancel(true)
TerminalProviderService->>TerminalProviderService: killProcessTree(processRef.get())
TerminalProviderService->>TerminalProviderService: finished.compareAndSet(false, true)
TerminalProviderService->>callback: onError("命令执行超时")
end
Sequence diagram for async latest-wins conversation persistencesequenceDiagram
actor User
participant GenerationFlowController
participant ConversationPersistenceController
participant ConversationStore
participant LearningContextStore
User->>GenerationFlowController: send message / tools complete
GenerationFlowController->>ConversationPersistenceController: persistCurrentConversation()
ConversationPersistenceController->>ConversationPersistenceController: build ConversationRecord
ConversationPersistenceController->>ConversationPersistenceController: persistSnapshot = conversation
ConversationPersistenceController->>ConversationPersistenceController: persistScheduled?
alt first persist
ConversationPersistenceController->>ConversationPersistenceController: persistScheduled = true
ConversationPersistenceController->>ConversationPersistenceController: persistExecutor.execute(runPersist)
else subsequent persists
ConversationPersistenceController->>ConversationPersistenceController: update persistSnapshot (latest-wins)
end
ConversationPersistenceController->>ConversationStore: saveConversation(snapshot) (background)
alt learning mode enabled
ConversationPersistenceController->>LearningContextStore: indexConversation(snapshot.getProjectId(), snapshot)
end
User->>ConversationPersistenceController: applyConversation(newConversation)
ConversationPersistenceController->>ConversationPersistenceController: awaitPendingPersist()
ConversationPersistenceController->>ConversationStore: (all queued writes finished before switch)
Sequence diagram for global tool-call budget including Agent internal toolssequenceDiagram
actor User
participant GenerationFlowController
participant AgentExecutionController
participant ModelClient
User->>GenerationFlowController: startInitialModelRequest(...)
GenerationFlowController->>AgentExecutionController: resetExecutedAgentToolCalls()
GenerationFlowController->>ModelClient: complete(..., messages)
ModelClient-->>GenerationFlowController: toolCalls
GenerationFlowController->>GenerationFlowController: effectiveUsedToolCalls(mainFlowUsed)
GenerationFlowController->>GenerationFlowController: generationController.canExecuteToolCalls(selectedModel, effectiveUsed, toolCalls.size())
alt over limit
GenerationFlowController->>GenerationFlowController: generationController.toolLimitMessage(selectedModel, effectiveUsed, toolCalls.size())
GenerationFlowController->>ModelClient: (no further tool execution)
else within limit
GenerationFlowController->>AgentExecutionController: runAgentTool / runAgentPipelineTool(..., toolCallBudget)
AgentExecutionController->>AgentExecutionController: accumulateExecutedAgentToolCalls(result.getToolCallCount())
AgentExecutionController-->>GenerationFlowController: ToolResult / AgentRunResult
GenerationFlowController->>GenerationFlowController: effectiveUsedToolCalls(updatedMainFlowUsed)
GenerationFlowController->>ModelClient: continueModelAfterTools(..., effectiveUsed)
end
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
Hey - I've found 1 issue, and left some high level feedback:
- The asynchronous skill creation/installation methods in ExtensionManagementController now always return null despite a SkillRecord return type; consider changing these to return void or a more appropriate async result to avoid misleading callers.
- ConversationPersistenceController creates its own single-thread ExecutorService without any shutdown; if controllers are recreated over the app lifecycle, consider sharing a long-lived executor or explicitly managing shutdown to avoid leaking threads.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- The asynchronous skill creation/installation methods in ExtensionManagementController now always return null despite a SkillRecord return type; consider changing these to return void or a more appropriate async result to avoid misleading callers.
- ConversationPersistenceController creates its own single-thread ExecutorService without any shutdown; if controllers are recreated over the app lifecycle, consider sharing a long-lived executor or explicitly managing shutdown to avoid leaking threads.
## Individual Comments
### Comment 1
<location path="app/src/test/java/cn/lineai/data/repository/MemoryRankerRagInjectionTest.java" line_range="15-24" />
<code_context>
+public final class MemoryRankerRagInjectionTest {
</code_context>
<issue_to_address>
**suggestion (testing):** Extend `MemoryRankerRagInjectionTest` to assert max-count limiting and relevance threshold behavior.
To more completely specify `MemoryRanker.rank`’s behavior, please also add:
- A case with more candidates than `maxCount`, asserting the list is truncated to `maxCount` and that ordering (by relevance/recency) is preserved.
- A case with a non-zero relevance threshold, asserting that only candidates above the threshold are returned.
These will make ranking semantics more robust against future changes to the heuristic.
Suggested implementation:
```java
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.assertEquals;
```
```java
public final class MemoryRankerRagInjectionTest {
@Test
public void maxCountLimitsNumberOfRankedCandidatesAndPreservesOrdering() {
// 构造 4 个候选,其中两个与查询高度相关,两个仅用于测试截断与排序
long now = System.currentTimeMillis();
MemoryRanker.Candidate oldUnrelated = new MemoryRanker.Candidate(
"c1", "很久以前的无关记忆:天气预报和网页搜索配置。", now - 1000L * 60 * 60, "");
MemoryRanker.Candidate recentUnrelated = new MemoryRanker.Candidate(
"c2", "刚刚发生的无关记忆:午饭吃了什么。", now - 1000L * 60, "");
MemoryRanker.Candidate highRelevanceOlder = new MemoryRanker.Candidate(
"c3", "当前项目不能使用 AndroidX,必须保持 Java 原生 View。", now - 1000L * 30, "");
MemoryRanker.Candidate highRelevanceNewest = new MemoryRanker.Candidate(
"c4", "项目升级到 AndroidX 后,需要更新所有传统 View 的适配。", now, "");
List<MemoryRanker.Candidate> result = MemoryRanker.rank(
Arrays.asList(oldUnrelated, recentUnrelated, highRelevanceOlder, highRelevanceNewest),
"AndroidX 项目 View",
2, // maxCount:只返回两个候选
0.0, // relevance threshold:不过滤掉低相关,依赖排序与截断
true); // recency fallback 打开以保持既有行为
// 截断到 maxCount
assertEquals(2, result.size());
// 排序按照相关度/新鲜度,最相关且最新的候选优先
assertEquals("c4", result.get(0).getId());
assertEquals("c3", result.get(1).getId());
}
@Test
public void relevanceThresholdFiltersOutLowRelevanceCandidates() {
long now = System.currentTimeMillis();
MemoryRanker.Candidate unrelated = new MemoryRanker.Candidate(
"u1", "与 AndroidX 完全无关的记忆:天气预报和网页搜索配置。", now - 1000L * 60 * 60, "");
MemoryRanker.Candidate weakMatch = new MemoryRanker.Candidate(
"w1", "曾经考虑过升级,但没有提到 AndroidX,只是泛泛而谈项目升级。", now - 1000L * 30, "");
MemoryRanker.Candidate strongMatch = new MemoryRanker.Candidate(
"s1", "当前项目已经切换到 AndroidX,需要更新所有旧的 View 实现。", now, "");
List<MemoryRanker.Candidate> result = MemoryRanker.rank(
Arrays.asList(unrelated, weakMatch, strongMatch),
"AndroidX 项目 View 升级",
10, // 足够大的 maxCount,不触发截断
0.5, // relevance threshold:仅返回相关度足够高的候选
true); // recency fallback 打开以保持既有行为
// 不相关或弱相关的候选应被过滤,只留下强匹配候选
assertEquals(1, result.size());
assertEquals("s1", result.get(0).getId());
// 结果中的候选应该是与查询最相关的记忆
assertTrue(result.get(0).getText().contains("AndroidX"));
}
```
These changes assume `MemoryRanker.rank` has the signature:
`rank(List<MemoryRanker.Candidate> candidates, String query, int maxCount, double relevanceThreshold, boolean enableRecencyFallback)` and that `MemoryRanker.Candidate` exposes `getId()` and `getText()` accessors.
If your actual `rank` signature differs (e.g., different parameter order, missing threshold parameter, or different fallback flag type), you will need to:
1. Adjust the arguments passed to `MemoryRanker.rank` in both new tests to match the real signature.
2. If relevance threshold is configured elsewhere (e.g., via configuration object), pass that object instead of the raw `double` shown here and update assertions accordingly.
3. If `Candidate` does not have `getId()`/`getText()` methods, replace those calls with the appropriate accessors or fields used in your existing tests.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
| public final class MemoryRankerRagInjectionTest { | ||
|
|
||
| @Test | ||
| public void matchingMemoryCandidateIsRankedFirstWithPositiveRelevance() { | ||
| MemoryRanker.Candidate unrelated = new MemoryRanker.Candidate( | ||
| "u1", "天气预报和网页搜索配置。", System.currentTimeMillis(), ""); | ||
| MemoryRanker.Candidate match = new MemoryRanker.Candidate( | ||
| "m1", "当前项目不能使用 AndroidX,必须保持 Java 原生 View。", | ||
| System.currentTimeMillis(), ""); | ||
|
|
There was a problem hiding this comment.
suggestion (testing): Extend MemoryRankerRagInjectionTest to assert max-count limiting and relevance threshold behavior.
To more completely specify MemoryRanker.rank’s behavior, please also add:
- A case with more candidates than
maxCount, asserting the list is truncated tomaxCountand that ordering (by relevance/recency) is preserved. - A case with a non-zero relevance threshold, asserting that only candidates above the threshold are returned.
These will make ranking semantics more robust against future changes to the heuristic.
Suggested implementation:
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.assertEquals;public final class MemoryRankerRagInjectionTest {
@Test
public void maxCountLimitsNumberOfRankedCandidatesAndPreservesOrdering() {
// 构造 4 个候选,其中两个与查询高度相关,两个仅用于测试截断与排序
long now = System.currentTimeMillis();
MemoryRanker.Candidate oldUnrelated = new MemoryRanker.Candidate(
"c1", "很久以前的无关记忆:天气预报和网页搜索配置。", now - 1000L * 60 * 60, "");
MemoryRanker.Candidate recentUnrelated = new MemoryRanker.Candidate(
"c2", "刚刚发生的无关记忆:午饭吃了什么。", now - 1000L * 60, "");
MemoryRanker.Candidate highRelevanceOlder = new MemoryRanker.Candidate(
"c3", "当前项目不能使用 AndroidX,必须保持 Java 原生 View。", now - 1000L * 30, "");
MemoryRanker.Candidate highRelevanceNewest = new MemoryRanker.Candidate(
"c4", "项目升级到 AndroidX 后,需要更新所有传统 View 的适配。", now, "");
List<MemoryRanker.Candidate> result = MemoryRanker.rank(
Arrays.asList(oldUnrelated, recentUnrelated, highRelevanceOlder, highRelevanceNewest),
"AndroidX 项目 View",
2, // maxCount:只返回两个候选
0.0, // relevance threshold:不过滤掉低相关,依赖排序与截断
true); // recency fallback 打开以保持既有行为
// 截断到 maxCount
assertEquals(2, result.size());
// 排序按照相关度/新鲜度,最相关且最新的候选优先
assertEquals("c4", result.get(0).getId());
assertEquals("c3", result.get(1).getId());
}
@Test
public void relevanceThresholdFiltersOutLowRelevanceCandidates() {
long now = System.currentTimeMillis();
MemoryRanker.Candidate unrelated = new MemoryRanker.Candidate(
"u1", "与 AndroidX 完全无关的记忆:天气预报和网页搜索配置。", now - 1000L * 60 * 60, "");
MemoryRanker.Candidate weakMatch = new MemoryRanker.Candidate(
"w1", "曾经考虑过升级,但没有提到 AndroidX,只是泛泛而谈项目升级。", now - 1000L * 30, "");
MemoryRanker.Candidate strongMatch = new MemoryRanker.Candidate(
"s1", "当前项目已经切换到 AndroidX,需要更新所有旧的 View 实现。", now, "");
List<MemoryRanker.Candidate> result = MemoryRanker.rank(
Arrays.asList(unrelated, weakMatch, strongMatch),
"AndroidX 项目 View 升级",
10, // 足够大的 maxCount,不触发截断
0.5, // relevance threshold:仅返回相关度足够高的候选
true); // recency fallback 打开以保持既有行为
// 不相关或弱相关的候选应被过滤,只留下强匹配候选
assertEquals(1, result.size());
assertEquals("s1", result.get(0).getId());
// 结果中的候选应该是与查询最相关的记忆
assertTrue(result.get(0).getText().contains("AndroidX"));
}These changes assume MemoryRanker.rank has the signature:
rank(List<MemoryRanker.Candidate> candidates, String query, int maxCount, double relevanceThreshold, boolean enableRecencyFallback) and that MemoryRanker.Candidate exposes getId() and getText() accessors.
If your actual rank signature differs (e.g., different parameter order, missing threshold parameter, or different fallback flag type), you will need to:
- Adjust the arguments passed to
MemoryRanker.rankin both new tests to match the real signature. - If relevance threshold is configured elsewhere (e.g., via configuration object), pass that object instead of the raw
doubleshown here and update assertions accordingly. - If
Candidatedoes not havegetId()/getText()methods, replace those calls with the appropriate accessors or fields used in your existing tests.
- ExtensionManagementController/ExtensionController: 异步 skill 创建/安装方法返回类型 SkillRecord -> void(此前恒返回 null 误导调用方),保留 throws 声明兼容调用方 try/catch - ConversationPersistenceController: 持久化 executor 改为进程级静态共享(控制器重建不再重复创建线程),daemon 线程无需显式关闭 - MemoryRankerRagInjectionTest: 新增 maxCount 截断 + 无关候选过滤两个用例
版本
1.2.6-rc.2→1.2.6,versionCode 30 → 31)update.md(覆盖 1.2.5 起全部 21 个提交的变更)功能与改进
Mcp-Session-Id会话管理、Mcp-Protocol-Version协议头ToolContext新增进度监听器 accessor 并透传给工具执行器SimpleHttpClient捕获并暴露响应头(如 MCP 会话头)memory_update可见性 - 工具在学习模式开启后可见MemoryExtractionService(587 行)及专属模板/字符串聊天滚动体验优化(嵌套滚动冲突修复)
BoundedScrollView(:ui-theme),保守抢占 + 按方向动态交还手势工具卡片流式渲染性能优化
ToolCallCardView.updateContent增量更新钩子(默认回退bind,向后兼容)ToolCallBlockView结构/内容签名拆分:仅内容变化不再销毁重建整棵视图树全量代码审计修复(高危 2 项、中危 30 项)
文件读取(
file_read)end_kb放开 50KB 硬限制,可按文件大小分页读取(单次跨度 1MB 上限)Shell 执行
nohup &后台任务readFile32MB 上限防 OOM;Terminal Provider 定位开放插件(不设签名/包名校验,兼容 GPLv3 重签名分发)安全
UrlPolicy私网判定改字面 IPv4(10.evil.com等公网域名不再放行明文 HTTP)SimpleHttpClient响应体 32MB 上限、3 参 download 上限生效、int 溢出修复data:imagebase64 5MB 上限 + 降采样.linecode导入fileName消毒 + 规范路径校验(防../穿越)[图片]不再被截字(两处)上下文压缩
并发与稳定性
SshConnectionPool并发首借竞态(连接/锁泄漏、IllegalMonitorStateException)ToolRegistry读写锁(后台 reload 与渲染读取竞争)UI 与细节
MainChatView屏幕缓存上限 12(动态 screen id 泄漏 View/Context)%2$s参数错误修复(三语言)测试
BoundedScrollViewTest、ToolCallBlockViewSignatureTest、ToolCallAgentViewLayoutSignatureTest、MemoryRankerRagInjectionTestToolBuiltinsTest加强至 23 例(KB 分页多行、中间页连续行号、end_kb>50):app:assembleDebug/:terminal-provider:assembleDebugBUILD SUCCESSFULSummary by Sourcery
Release v1.2.6 with protocol, tooling, performance, and safety improvements across chat, tools, and infrastructure.
New Features:
memory_updategated on Learning Mode.Bug Fixes:
Enhancements:
Build:
Documentation:
Tests: