Repository navigation
Conversation
|
@Liang-HZ 你好!非常感谢这个 PR 🙏 这两个问题都抓得很准,也都真实存在:Qoder 会话中途换了模型,详情弹窗还停在开头的模型上;子代理花掉的 token 也完全没有进统计。PR 描述是拿真实的 1.1.65 会话核对的,数字给得很具体(48 次模型调用,主线程 12 次、两个 Explore 子代理 36 次;面板「总计 961.3K」,真实全量约 4.4M)。模型的根因从 我在本地跑了 PR 自带的测试(Qoder 解析器的 35 个测试全部通过,server 模式的 clippy 也是干净的),对照 Qoder 1.1.65 的 bundle 看了子代理的用量具体写在哪里,还用 1.1.65 的
所以子代理这部分建议改完再合。下面把证据、问题和建议的做法一次列全,方便你一轮改完。哪里判断得不对,也欢迎直接指出 🙂 概要
1. 模型跟随最后一轮:成立,改一处就可以方向是对的。 一处小问题:注释写的是 "Last NON-EMPTY model wins",但代码只判断了 if entry_model.as_deref().is_some_and(|m| !m.trim().is_empty()) {
model.clone_from(&entry_model);
}另外两点,都不需要在这个 PR 里做什么:
如果子代理那部分要多花些时间,模型这部分也可以先拆成一个单独的 PR。 2. 子代理的用量其实在转写里PR 描述里说,子代理的模型调用「不进 transcript」。主转写
回放得到的子代理记录(节选)// <sessionId>/subagents/agent-aExplore-4d0975332279676f.jsonl 里的 assistant 记录,省略了部分字段
{
"type": "assistant",
"timestamp": "2026-10-10T10:26:22.042Z",
"isSidechain": true,
"agentId": "aExplore-4d0975332279676f",
"parent_tool_use_id": "toolu_665c64da-d907-4d12-a465-c0b0e305a0d1",
"message": {
"id": "msg_2802da67-e6d4-4eed-8b4f-3ed1dc2a177a",
"model": "auto",
"stop_reason": "end_turn",
"content": [{ "type": "text", "text": "hello from the sub-agent" }],
"usage": {
"input_tokens": 500,
"cache_creation_input_tokens": 0,
"cache_read_input_tokens": 0,
"output_tokens": 5
}
},
"version": "1.1.65"
}同一目录下的 {"agentType":"Explore","toolUseId":"toolu_665c64da-d907-4d12-a465-c0b0e305a0d1","description":"probe explore","invocationName":"Explore","color":"cyan"}你那个真实会话的目录下应该也有 对照脚本用法: import glob, json, os, sys
KEYS = ("input_tokens", "cache_read_input_tokens", "cache_creation_input_tokens", "output_tokens")
def records(path):
with open(path, encoding="utf-8") as f:
for line in f:
try:
yield json.loads(line)
except ValueError:
pass
def add(total, usage):
for k in KEYS:
total[k] += usage.get(k) or 0
# 参数:主转写路径 ~/.qoder/projects/<encoded-cwd>/<sessionId>.jsonl
transcript = os.path.expanduser(sys.argv[1])
session_dir = transcript[: -len(".jsonl")]
encoded_cwd = os.path.basename(os.path.dirname(transcript))
session_id = os.path.basename(session_dir)
config_root = os.path.dirname(os.path.dirname(os.path.dirname(transcript)))
# 1) 子代理转写:同一个 message.id 的几条记录重复同一份 usage,每次调用只算一次
tx_total, tx_calls = dict.fromkeys(KEYS, 0), 0
for path in sorted(glob.glob(os.path.join(session_dir, "subagents", "agent-*.jsonl"))):
per_call = {}
for rec in records(path):
msg = rec.get("message") or {}
if rec.get("type") == "assistant" and msg.get("id") and msg.get("usage"):
per_call[msg["id"]] = msg["usage"]
for usage in per_call.values():
add(tx_total, usage)
tx_calls += len(per_call)
print(f"{os.path.basename(path)}: {len(per_call)} calls")
print(f"sub-agent transcripts: {tx_calls} calls {tx_total}")
# 2) 段日志:和这个 PR 相同的选法(is_subagent 的 turn)
log_total, log_calls = dict.fromkeys(KEYS, 0), 0
segments = os.path.join(config_root, "logs", "sessions", encoded_cwd, session_id, "segments")
for path in sorted(glob.glob(os.path.join(segments, "*.jsonl"))):
sub_turns, calls = set(), []
for rec in records(path):
data = rec.get("data") or {}
if rec.get("type") == "turn.started" and data.get("is_subagent"):
sub_turns.add(rec.get("turn_id"))
elif rec.get("type") == "model.response.completed":
calls.append((rec.get("turn_id"), data))
for turn_id, data in calls:
if turn_id in sub_turns:
add(log_total, data)
log_calls += 1
print(f"segment log (is_subagent): {log_calls} calls {log_total}")3. 读段日志这一版的问题3.1 缓存读取算了两次段日志 我本机一个真实会话里的一次调用,转写和段日志记的都是 套用 PR 描述里的数字,详情弹窗的「总计」会是:
新测试固定下来的也是重复计算之后的数(qoder.rs L1324-L1330):子代理那次调用的 35,000 输入里有 34,000 是缓存,没命中缓存的只有 1,000。所以 3.2 通常只进详情弹窗,Token 看板看不到这个 PR 只改了 3.3 按
|
6e770cb to
a3a6f97
Compare
… its transcripts Two session-display defects, both verified against real 1.1.65 sessions: 1. summary.model now tracks the LAST NON-EMPTY model of the transcript. The parser used to pin the first assistant record's model, and the details dialog reads summary.model ahead of the turns, so a mid-session switch kept displaying the opening model next to the new model's context window. Blank `model` strings are skipped rather than stored: an empty one would overwrite the last valid model and block the launch-model fallback. 2. Sub-agent spend now reads each run's OWN transcript (<sessionId>/subagents/agent-<agentId>.jsonl — the same layout claude uses) instead of the run's segment log. One API response streams as several records sharing `message.id`, each repeating the same counters, so calls are deduped by that id; `qoder_turn_usage` splits the cached prefix out (Qoder's counters are Anthropic-named over OpenAI semantics); each call lands on the assistant turn current at ITS OWN timestamp. The fold runs after the context-window occupancy is read — a sub-agent's context is its own. Details dialog, session totals and the Token board now read one number (the board's facts come from turn usage). 3. FACT_SCHEMA_VERSION 4 -> 5: totals change for unchanged transcripts. The segment-log path is gone: it double-counted the cached prefix, could not be told apart from main-chain calls by `turn_id` in every entrypoint, and Qoder rotates it after 30 days. Tests: model switch + blank-model guard; transcript fold (dedup, cache split, per-call turn placement); context window untouched; no-subagents directory unchanged.
a3a6f97 to
8c23817
Compare
|
@xintaofei 感谢这么细的评审——段日志 vs 转写那几段分析(缓存口径、 逐条回复: §1 模型:已改成只让非空 §2 + §4.2 子代理用量改读转写:段日志版整体移除,新的
§4.3 归到回合:按每条调用自己的时间戳归到当时最近的 assistant 回合(无时间戳回退到最后一个 assistant 回合);调用点放在上下文窗口已用量读取之后。注释写明两个已知口径:看板会把子代理花费记在父回合的模型名下(与 claude 相同);回退分支上启动过的子代理,其转写仍计入(目录即事实,与 claude 的规则一致)。 §4.4 版本: §4.5 段日志兜底:按你说的先不做。 §5 测试(qoder 解析器 41 个全过):
“ 另外:clippy(server 模式 + 默认 lib)干净;全量 lib 测试 5115 过(本机环境相关的 麻烦再看一眼 🙏 |
问题(用真实 1.1.65 会话核对)
Qoder 会话详情弹窗两处显示错误:
1. 中途切换模型后,「模型」停在会话开头的模型
dfmodel→qfmodel):最后一段的用量与「上下文窗口 87.3K / 200K (43.7%)」都来自 Qwen3.8-Flash(200K 窗口);if model.is_none()),而详情弹窗优先读summary.model(commands/conversations.rs里该链路的注释也说明这一点)→ 切换永远不反映。已改为最后一条非空 model 生效。2. 子代理用量完全没算进会话统计
~/.qoder/logs/sessions/<encoded-cwd>/<sessionId>/segments/*.jsonl;统计从 transcript 计算,天然看不到。turn.started{is_subagent:true}的 turn 的model.response.completed用量并入统计;主链 turn 不加(transcript 已计),日志缺失/轮转时退回原数字。测试
summary.model跟随最后一个;②segment 日志存在时并入子代理用量、主链不重复计。cargo test --release -p codeg --lib:5083 通过;仅computer::local::tests的 2 个 helper 查找用例在本机环境失败(与本次改动无关)。评审后修订(2026-10-10)
按 @xintaofei 的评审改完并 force-push:
model覆盖(含新测试summary_model_ignores_a_blank_later_model);<sessionId>/subagents/agent-*.jsonl(按message.id去重、qoder_turn_usage拆缓存、按每条调用自己的时间戳归到回合、放在上下文用量计算之后);段日志路径整体移除;FACT_SCHEMA_VERSION4 → 5,相关注释同步修正;