From d55941561fa39597102c8de6bfaa2dea37b65041 Mon Sep 17 00:00:00 2001 From: wuguangan Date: Sun, 9 Aug 2026 03:39:28 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E9=AA=8C=E6=94=B6=E6=8A=8A=E8=AE=AD?= =?UTF-8?q?=E7=BB=83=E5=8D=A1=E6=AD=BB=E4=BA=86=20=E2=80=94=E2=80=94=20?= =?UTF-8?q?=E5=8A=A0=E5=86=B7=E5=8D=B4=E3=80=81=E9=99=8D=E6=9D=A1=E6=95=B0?= =?UTF-8?q?=E3=80=81=E8=BF=BD=E9=97=AE=E6=94=B6=E7=AA=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 实测:agentx 清单涨到 175 条,它每次收工被拦下答 6 条,答完又被拦。 22:02–22:08 六分钟全在答验收,一点活没干。 **验收本来是为了让工作更实,不是替代工作。** 三处一起收: - 拦截加 15 分钟冷却(卸膛仍在最前面,防死循环结构不变) - 注入上限 6 → 3 - 追问清单 77 条 → 5 条,全部以「继续」开头,只保证不停 + 目标通道打通 (完整版备份在 scripts/probes/agentx-training-full.txt.bak) 用户原话:「只要保证最后的目标通道打通,让他围绕训练就好, 不要加另外的要求。」 468 个测试。 Co-Authored-By: Claude Opus 5 (1M context) --- HubKit/Sources/HubUI/AcceptanceStore.swift | 29 +++++++++++++++++-- .../HubUITests/AcceptanceStoreTests.swift | 6 ++-- 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/HubKit/Sources/HubUI/AcceptanceStore.swift b/HubKit/Sources/HubUI/AcceptanceStore.swift index 9dff29a..514a47f 100644 --- a/HubKit/Sources/HubUI/AcceptanceStore.swift +++ b/HubKit/Sources/HubUI/AcceptanceStore.swift @@ -61,6 +61,17 @@ public final class AcceptanceStore { @ObservationIgnored private var armedSessions: Set = [] + /// 上一次拦下某个会话的时间。 + /// + /// **拦截本身是有成本的**:它每被拦一次就要停下手上的活、逐条答一遍。 + /// 实测清单涨到 175 条之后,它六分钟里全在答验收、一点活没干 —— + /// 验收本来是为了让工作更实,不是替代工作。 + @ObservationIgnored + private var lastInterceptAt: [String: Date] = [:] + + /// 两次拦截的最小间隔。到不了就放行,让它有整块时间干活。 + private let interceptCooldown: TimeInterval = 900 + /// 用户说话了 —— 给这个会话上膛。**唯一的上膛入口。** public func arm(sessionId: String) { armedSessions.insert(sessionId) @@ -73,8 +84,21 @@ public final class AcceptanceStore { public func disarmAndShouldIntercept(sessionId: String, projectPath: String) -> Bool { let wasArmed = armedSessions.remove(sessionId) != nil guard wasArmed else { return false } + + // 冷却期内直接放行。**卸膛在前面已经做了**,所以这里 return false + // 不会破坏防死循环的结构 —— 只是这一次不拦。 + let now = Date() + if let last = lastInterceptAt[sessionId], + now.timeIntervalSince(last) < interceptCooldown { + return false + } + // 清单里没有待办就别打扰 —— 没启用这功能的项目必须完全无感。 - return ledger(for: projectPath).items.contains(where: \.needsAttention) + guard ledger(for: projectPath).items.contains(where: \.needsAttention) else { + return false + } + lastInterceptAt[sessionId] = now + return true } /// 只读地看一眼有没有上膛。给测试和排障用,不改状态。 @@ -350,7 +374,8 @@ public final class AcceptanceStore { // 实测在一个真实项目上拦了 12 条,带完整验收条件之后糊了满屏 —— // 要点一多就没人看(Claude 也一样),反而比不拦更容易被忽略。 // 存疑的排前面:那一档是「自报做完但代码里找不到」,最该先说清楚。 - let cap = 6 + // 从 6 降到 3:一次问太多它要停很久才能答完,而它本该在干活。 + let cap = 3 // 存疑的优先;其余按**最久没问过**的排前面。 // // 不轮换的话每轮问的都是同一批:老实答「没做」的条目保持 .open, diff --git a/HubKit/Tests/HubUITests/AcceptanceStoreTests.swift b/HubKit/Tests/HubUITests/AcceptanceStoreTests.swift index 66394f9..c5ef566 100644 --- a/HubKit/Tests/HubUITests/AcceptanceStoreTests.swift +++ b/HubKit/Tests/HubUITests/AcceptanceStoreTests.swift @@ -235,8 +235,8 @@ final class AcceptanceStoreTests: XCTestCase { let text = store.injectionText(for: project) ?? "" let listed = text.split(separator: "\n").filter { $0.hasPrefix("- [") }.count - XCTAssertEqual(listed, 6) - XCTAssertTrue(text.contains("另有 6 条未列出"), "省略了多少必须说出来") + XCTAssertEqual(listed, 3) + XCTAssertTrue(text.contains("另有 9 条未列出"), "省略了多少必须说出来") } /// **老实回答「没做」不能被惩罚。** @@ -259,7 +259,7 @@ final class AcceptanceStoreTests: XCTestCase { .filter { second.contains($0.id) }.map(\.id) XCTAssertEqual(Set(firstIDs).intersection(secondIDs).count, 0, "第二轮该问没问过的那批") - XCTAssertEqual(secondIDs.count, 6) + XCTAssertEqual(secondIDs.count, 3) } /// 问过 3 次还说没做的,停止再问并标成「疑似误拆」。