Files
ysicing 3575a97de5 fix(ai-usage): 修正子代理回放判定并清理配额时代残留
修复子代理与 fork 会话的 Token 全量丢失:原判定要求 task_started 带
started_at 且不早于会话创建时间才结束回放,但子代理的 started_at 是父级
回合的开始时间,天然早于子会话文件,新版 Codex 更是完全不发该字段。改为
首个 task_started 无条件结束回放——实测 661 个真实 rollout 中没有任何一个
的 token_count 出现在它之前。本机语料 A/B:Token 13.60G → 14.53G(+6.8%),
用量记录 68997 → 73566。

其他修复:
- 两个 Provider 钉到专用串行执行器,冷扫描不再占用协作线程池。把池限制
  成一条线程实测,修复前并行的每秒采样在 43 秒扫描期间一次都没能推进。
- 按日期与模型聚合并定序输出,行数 701 → 272 且 id 唯一;顺带修掉冷扫描
  与检查点恢复顺序不一致导致每次刷新都产生"不相等"报告的问题。
- 缓存行加剪枝,覆盖归档搬走的 rollout、删除的项目目录与旧解析命名空间;
  检查点仅在末行未完时存第二份状态,Claude 事件按保留窗口裁剪。
- 复用 ISO8601 解析器,不再逐行创建。
- 数字与日期改跟随应用语言,弹窗与菜单栏共用同一套 Token 缩写。
- 窄弹窗热力图按设计尺寸铺开后横向滚动,此前被钉在面板宽度上永不溢出。
- 打开菜单栏项一并开启扫描,并为无数据状态补上悬停文案。
- 删除未被应用构造的配额 Provider、凭据读取与响应映射(约 330 行)及其
  全链路管线,并清掉 37 条孤立译文 × 8 个语言文件。
2026-09-23 08:50:41 +08:00

40 lines
1.5 KiB
Swift

// Copyright (C) 2026 ysicing
// SPDX-License-Identifier: AGPL-3.0-or-later
import Dispatch
import Foundation
/// ISO8601DateFormatter 的创建开销远高于一次解析,按解析状态复用一份,不要逐行新建。
struct ISO8601Parser {
private let fractional: ISO8601DateFormatter = {
let formatter = ISO8601DateFormatter()
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
return formatter
}()
private let whole = ISO8601DateFormatter()
func date(_ value: Any?) -> Date? {
guard let text = value as? String else { return nil }
return fractional.date(from: text) ?? whole.date(from: text)
}
}
/// 冷启动要读完整个日志目录,是几十秒的同步 IO。放在专用串行队列上执行,
/// 不占用 Swift 并发的协作线程池,否则首次扫描期间每秒采样的系统指标会被一起卡住。
/// 两个 Provider 共用一条线程:扫描瓶颈在磁盘,并行读并不会更快。
final class ScanExecutor: SerialExecutor {
static let shared = ScanExecutor()
private let queue = DispatchQueue(label: "com.xstats.ai-usage-scan", qos: .utility)
func enqueue(_ job: consuming ExecutorJob) {
let job = UnownedJob(job)
let executor = asUnownedSerialExecutor()
queue.async { job.runSynchronously(on: executor) }
}
func asUnownedSerialExecutor() -> UnownedSerialExecutor {
UnownedSerialExecutor(ordinary: self)
}
}