Self Checks / 自检
Related / 相关:#3938 reports the same symptom on 3.16.2 + Gemini CLI 0.45.2 (now stale). This issue adds the source-level root cause, which turns out to be two independent defects, plus measured data on how much usage is lost.
#3938 在 3.16.2 + Gemini CLI 0.45.2 上报告过同样的现象(现已被标记 stale)。本 issue 补充源码级根因——实际是两个独立缺陷——以及漏算量的实测数据。
CC Switch Version / 版本号
3.20.3
Operating System / 操作系统
macOS 14.4 (darwin 23.4.0, arm64)
Related App / 涉及应用
Gemini CLI
Steps to Reproduce / 重现步骤
- Install Gemini CLI 0.59.0 and run a few sessions so that
~/.gemini/tmp/<project>/chats/session-*.jsonl files exist.
- Open CC Switch with
visibleApps.gemini: true and sessionAutoSyncEnabled: true.
- Open the Usage dashboard and the session history list.
Expected Behavior / 期望行为
Gemini CLI sessions appear in the session list, and their token usage is imported into the usage dashboard.
Gemini CLI 的会话出现在会话列表里,token 消耗被导入用量统计。
Actual Behavior / 实际行为
Neither happens. [GEMINI-SYNC] never appears in the log at all — not even a parse warning:
两者都没有发生。日志里 [GEMINI-SYNC] 一次都没出现过,连解析告警都没有:
$ for t in SESSION CODEX GEMINI OPENCODE PI; do printf "%-10s %s\n" "$t" "$(grep -c "\[$t-SYNC\]" ~/.cc-switch/logs/cc-switch.log)"; done
SESSION 12848
CODEX 511
GEMINI 0 <-- 33 session files on disk
OPENCODE 0
PI 7007
$ sqlite3 ~/.cc-switch/cc-switch.db "SELECT COUNT(*) FROM session_log_sync WHERE file_path LIKE '%.gemini%';"
0
Additional Context / 补充信息
Root cause / 根因
There are two independent defects. Fixing only the first still yields wrong numbers.
有两个独立缺陷,只修第一个数字仍然是错的。
1. The extension filter rejects .jsonl
src-tauri/src/services/session_usage_gemini.rs:120
.map(|n| n.starts_with("session-") && n.ends_with(".json"))
"session-….jsonl".ends_with(".json") is false, so zero files are scanned. src-tauri/src/session_manager/providers/gemini.rs:42 has the same problem via path.extension() != Some("json").
Gemini CLI writes .jsonl — from its own bundle (@google/gemini-cli@0.59.0, bundle/chunk-YSBB75DZ.js):
`session-${safeSessionId}.jsonl`
2. The file body is no longer a single JSON object
Both parsers do a whole-file serde_json::from_str and then look for a top-level messages array (session_usage_gemini.rs:154-167, session_manager/providers/gemini.rs:59-65). Current Gemini CLI writes an append-only JSONL made of three record kinds:
两个解析器都对整个文件做 serde_json::from_str,再找顶层 messages 数组。现在的 Gemini CLI 写的是追加式 JSONL,由三类记录组成:
{"sessionId":"…","projectHash":"…","startTime":"…","lastUpdated":"…","kind":"main"}
{"$set":{"messages":[…],"lastUpdated":"…"}}
{"id":"…","type":"gemini","model":"…","tokens":{"input":…,"output":…,"cached":…,"thoughts":…},"toolCalls":[…]}
Renaming a real session file to .json so it passes filter #1 shows defect #2 on its own:
把一个真实会话文件改名成 .json 绕过缺陷 #1,缺陷 #2 就单独暴露出来:
[GEMINI-SYNC] Gemini 会话文件解析失败 …/session-…json:
配置错误: JSON 解析失败: trailing characters at line 2 column 1
Two semantics that are easy to get wrong / 两处容易写错的语义
Both matter for token accuracy, and I verified them against 33 real session files:
两处都直接影响 token 准确性,已用 33 个真实会话文件验证:
-
$set.messages is a full replacement, not an append. When a session is resumed or compacted, the whole history is rewritten as a snapshot. In my data one snapshot carries 118 messages. Treating $set lines as "skip" loses all of them; treating them as "append" double-counts the overlap (one of my files has 8 ids present both in a snapshot and as standalone lines).
$set.messages 是全量替换而非追加。 会话被 resume 或压缩时整段历史以快照重写。我的数据里有个快照带 118 条消息。把 $set 行当作"跳过"会丢掉全部;当作"追加"则会重复计算重叠部分(我有个文件里 8 个 id 同时出现在快照和独立行中)。
-
A standalone line with an existing id rewrites that message. Gemini CLI writes a message once without toolCalls, then rewrites it with the same id once the tool calls resolve — same timestamp, same tokens. 13 of 55 messages in one of my sessions are such rewrites.
独立行若 id 已存在,是对该消息的改写。 Gemini CLI 先写一条不带 toolCalls 的消息,工具调用完成后用同一个 id 重写——时间戳和 tokens 都相同。我某个会话 55 条消息里有 13 条是这种改写。
Measured impact / 漏算量实测
Replaying my 33 session files with the correct semantics vs. skipping $set lines:
按正确语义回放我本机 33 个会话文件,对比"跳过 $set 行"的做法:
|
messages |
input |
output |
cached |
correct ($set replaces + upsert by id) |
213 |
10,232,753 |
136,973 |
7,427,224 |
skipping $set lines |
155 |
6,152,380 |
91,558 |
4,057,979 |
That is 40% of input tokens lost, concentrated in one resumed session where 58 of 59 messages live only inside a $set.messages snapshot.
即 40% 的 input token 丢失,集中在一个 resume 过的会话里——59 条消息中有 58 条只存在于 $set.messages 快照中。
Note on pricing / 定价表补充
Even after parsing is fixed, cost shows $0 for most models: of the 5 models in my sessions, only gemini-3.5-flash exists in model-pricing.json. gemma-4-31b-it (my highest-volume model), gemini-2.5-pro, gemini-3.1-flash-lite and gemini-3.1-pro-preview have no pricing entry, so tokens will show but cost will not. Probably worth a separate issue.
即使解析修好,多数模型的费用仍显示 $0:我会话里的 5 个模型只有 gemini-3.5-flash 在 model-pricing.json 中。gemma-4-31b-it(用量最大)、gemini-2.5-pro、gemini-3.1-flash-lite、gemini-3.1-pro-preview 都没有定价条目,会有 token 数但算不出金额。这个大概值得单开一个 issue。
Prior art / 已有工作
PR #2771 addresses defect #1 and part of #2, but it is currently mergeable_state: dirty (it was written against get_sync_state, which main has since replaced with load_sync_cursors) and it skips $set lines, which is the 40% gap above. I have a fix that handles both defects and both semantics; I will open a PR referencing this issue.
PR #2771 处理了缺陷 #1 和部分 #2,但目前 mergeable_state: dirty(它基于 get_sync_state,而 main 已改为 load_sync_cursors),且它跳过 $set 行——就是上面那 40% 的缺口。我写了一个同时处理两个缺陷和两处语义的修复,会另开 PR 关联本 issue。
Self Checks / 自检
CC Switch Version / 版本号
3.20.3
Operating System / 操作系统
macOS 14.4 (darwin 23.4.0, arm64)
Related App / 涉及应用
Gemini CLI
Steps to Reproduce / 重现步骤
~/.gemini/tmp/<project>/chats/session-*.jsonlfiles exist.visibleApps.gemini: trueandsessionAutoSyncEnabled: true.Expected Behavior / 期望行为
Gemini CLI sessions appear in the session list, and their token usage is imported into the usage dashboard.
Gemini CLI 的会话出现在会话列表里,token 消耗被导入用量统计。
Actual Behavior / 实际行为
Neither happens.
[GEMINI-SYNC]never appears in the log at all — not even a parse warning:两者都没有发生。日志里
[GEMINI-SYNC]一次都没出现过,连解析告警都没有:Additional Context / 补充信息
Root cause / 根因
There are two independent defects. Fixing only the first still yields wrong numbers.
有两个独立缺陷,只修第一个数字仍然是错的。
1. The extension filter rejects
.jsonlsrc-tauri/src/services/session_usage_gemini.rs:120"session-….jsonl".ends_with(".json")isfalse, so zero files are scanned.src-tauri/src/session_manager/providers/gemini.rs:42has the same problem viapath.extension() != Some("json").Gemini CLI writes
.jsonl— from its own bundle (@google/gemini-cli@0.59.0,bundle/chunk-YSBB75DZ.js):`session-${safeSessionId}.jsonl`2. The file body is no longer a single JSON object
Both parsers do a whole-file
serde_json::from_strand then look for a top-levelmessagesarray (session_usage_gemini.rs:154-167,session_manager/providers/gemini.rs:59-65). Current Gemini CLI writes an append-only JSONL made of three record kinds:两个解析器都对整个文件做
serde_json::from_str,再找顶层messages数组。现在的 Gemini CLI 写的是追加式 JSONL,由三类记录组成:{"sessionId":"…","projectHash":"…","startTime":"…","lastUpdated":"…","kind":"main"} {"$set":{"messages":[…],"lastUpdated":"…"}} {"id":"…","type":"gemini","model":"…","tokens":{"input":…,"output":…,"cached":…,"thoughts":…},"toolCalls":[…]}Renaming a real session file to
.jsonso it passes filter #1 shows defect #2 on its own:把一个真实会话文件改名成
.json绕过缺陷 #1,缺陷 #2 就单独暴露出来:Two semantics that are easy to get wrong / 两处容易写错的语义
Both matter for token accuracy, and I verified them against 33 real session files:
两处都直接影响 token 准确性,已用 33 个真实会话文件验证:
$set.messagesis a full replacement, not an append. When a session is resumed or compacted, the whole history is rewritten as a snapshot. In my data one snapshot carries 118 messages. Treating$setlines as "skip" loses all of them; treating them as "append" double-counts the overlap (one of my files has 8 ids present both in a snapshot and as standalone lines).$set.messages是全量替换而非追加。 会话被 resume 或压缩时整段历史以快照重写。我的数据里有个快照带 118 条消息。把$set行当作"跳过"会丢掉全部;当作"追加"则会重复计算重叠部分(我有个文件里 8 个 id 同时出现在快照和独立行中)。A standalone line with an existing
idrewrites that message. Gemini CLI writes a message once withouttoolCalls, then rewrites it with the sameidonce the tool calls resolve — same timestamp, same tokens. 13 of 55 messages in one of my sessions are such rewrites.独立行若
id已存在,是对该消息的改写。 Gemini CLI 先写一条不带toolCalls的消息,工具调用完成后用同一个id重写——时间戳和 tokens 都相同。我某个会话 55 条消息里有 13 条是这种改写。Measured impact / 漏算量实测
Replaying my 33 session files with the correct semantics vs. skipping
$setlines:按正确语义回放我本机 33 个会话文件,对比"跳过
$set行"的做法:$setreplaces + upsert by id)$setlinesThat is 40% of input tokens lost, concentrated in one resumed session where 58 of 59 messages live only inside a
$set.messagessnapshot.即 40% 的 input token 丢失,集中在一个 resume 过的会话里——59 条消息中有 58 条只存在于
$set.messages快照中。Note on pricing / 定价表补充
Even after parsing is fixed, cost shows
$0for most models: of the 5 models in my sessions, onlygemini-3.5-flashexists inmodel-pricing.json.gemma-4-31b-it(my highest-volume model),gemini-2.5-pro,gemini-3.1-flash-liteandgemini-3.1-pro-previewhave no pricing entry, so tokens will show but cost will not. Probably worth a separate issue.即使解析修好,多数模型的费用仍显示 $0:我会话里的 5 个模型只有
gemini-3.5-flash在model-pricing.json中。gemma-4-31b-it(用量最大)、gemini-2.5-pro、gemini-3.1-flash-lite、gemini-3.1-pro-preview都没有定价条目,会有 token 数但算不出金额。这个大概值得单开一个 issue。Prior art / 已有工作
PR #2771 addresses defect #1 and part of #2, but it is currently
mergeable_state: dirty(it was written againstget_sync_state, whichmainhas since replaced withload_sync_cursors) and it skips$setlines, which is the 40% gap above. I have a fix that handles both defects and both semantics; I will open a PR referencing this issue.PR #2771 处理了缺陷 #1 和部分 #2,但目前
mergeable_state: dirty(它基于get_sync_state,而main已改为load_sync_cursors),且它跳过$set行——就是上面那 40% 的缺口。我写了一个同时处理两个缺陷和两处语义的修复,会另开 PR 关联本 issue。