@praeviso/code-env-switch 0.1.6 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -67,6 +67,8 @@ npm link
67
67
  > `codenv init`, the shell wrapper applies them automatically.
68
68
  > The snippet also wraps `codex`/`claude` to bind sessions to profiles; use
69
69
  > `command codex` / `command claude` to bypass.
70
+ > When you switch profiles within the same session, usage tracking and cost
71
+ > display follow the most recently applied profile for subsequent tokens.
70
72
 
71
73
  ### Common commands
72
74
 
@@ -90,36 +92,6 @@ codenv usage-reset --yes
90
92
 
91
93
  This deletes usage history files (`usage.jsonl`, usage state, `profile-log.jsonl`, `statusline-debug.jsonl`) plus any backup variants in the config directory.
92
94
 
93
- ### Usage tracking
94
-
95
- Usage stats are derived from two sources (statusline input + session logs) and appended to `usage.jsonl`.
96
-
97
- - Files and paths
98
- - `usage.jsonl`: JSONL records with `ts`, `type`, `profileKey`/`profileName`, `model`, `sessionId`, and token breakdowns.
99
- - `usage.jsonl.state.json`: per-session totals + per-session-file metadata (mtime/size) used to compute deltas and avoid double counting.
100
- - `profile-log.jsonl`: profile usage + session binding log (`use` and `session` events).
101
- - `statusline-debug.jsonl`: optional debug capture when `CODE_ENV_STATUSLINE_DEBUG` is enabled.
102
- - Paths can be overridden via `usagePath`, `usageStatePath`, `profileLogPath`, `codexSessionsPath`, `claudeSessionsPath`.
103
- - Session binding (profile -> session)
104
- - `codenv init` installs a shell wrapper so `codex`/`claude` run via `codenv launch`.
105
- - `codenv launch` logs profile usage and then finds the latest unbound session file (prefers matching `cwd`, within a small grace window) and records the binding in `profile-log.jsonl`.
106
- - Sync uses these bindings to attribute session usage to a profile.
107
- - Statusline sync (`codenv statusline --sync-usage`)
108
- - Requires `sessionId`, model, and a profile (`profileKey` or `profileName`) to write usage.
109
- - Reads stdin JSON for totals and computes a delta against the last stored totals in the state file.
110
- - If totals decrease (session reset), the current totals are treated as fresh usage; otherwise negative sub-deltas are clamped to zero.
111
- - If the token breakdown exceeds the reported total, the breakdown sum wins.
112
- - Appends a delta record to `usage.jsonl` and updates the per-session totals in the state file.
113
- - Session log sync (`--sync-usage` and `codenv list`)
114
- - Scans Codex sessions under `CODEX_HOME/sessions` (or `~/.codex/sessions`) and Claude sessions under `CLAUDE_HOME/projects` (or `~/.claude/projects`).
115
- - Codex: parses `event_msg` token_count records and uses max totals; cached input is recorded as cache read and subtracted from input when possible.
116
- - Claude: sums `message.usage` input/output/cache tokens across the file.
117
- - Deltas are computed against prior file metadata and per-session maxima in the state file; files without a resolved binding are skipped.
118
- - Daily totals
119
- - "Today" is computed in local time between 00:00 and the next 00:00.
120
- - Cost calculation
121
- - Uses pricing from the profile (or `pricing.models`, plus defaults) and requires token splits; if splits are missing, cost is omitted.
122
-
123
95
  ### Add / update a profile
124
96
 
125
97
  ```bash
package/README_zh.md CHANGED
@@ -67,6 +67,8 @@ npm link
67
67
  > shell 包装函数会自动在当前终端生效。
68
68
  > 该片段还会包装 `codex`/`claude` 以绑定会话到 profile;如需绕过,
69
69
  > 可使用 `command codex` / `command claude`。
70
+ > 同一个 session 内切换 profile 时,后续用量与金额会跟随最新应用的
71
+ > profile 统计。
70
72
 
71
73
  ### 常用命令
72
74
 
@@ -90,36 +92,6 @@ codenv usage-reset --yes
90
92
 
91
93
  该命令会删除用量历史文件(`usage.jsonl`、用量 state、`profile-log.jsonl`、`statusline-debug.jsonl`)以及配置目录中的相关备份文件。
92
94
 
93
- ### 用量统计逻辑
94
-
95
- 用量来自两条路径(状态栏输入同步 + 会话日志解析),最终追加到 `usage.jsonl`。
96
-
97
- - 文件与路径
98
- - `usage.jsonl`:JSONL 记录,包含 `ts`/`type`/`profileKey`/`profileName`/`model`/`sessionId` 和 token 拆分字段。
99
- - `usage.jsonl.state.json`:保存每个 session 的累计 totals + session 文件的 mtime/size,用于计算增量并避免重复统计。
100
- - `profile-log.jsonl`:profile 使用与 session 绑定日志(`use`/`session`)。
101
- - `statusline-debug.jsonl`:当 `CODE_ENV_STATUSLINE_DEBUG` 开启时写入的调试信息。
102
- - 可通过 `usagePath`/`usageStatePath`/`profileLogPath`/`codexSessionsPath`/`claudeSessionsPath` 覆盖默认路径。
103
- - 会话绑定(profile -> session)
104
- - `codenv init` 安装 shell 包装函数,使 `codex`/`claude` 实际走 `codenv launch`。
105
- - `codenv launch` 记录 profile 使用,并在启动后短时间内(默认约 5 秒、每秒轮询)找到最新未绑定的 session 文件(优先 `cwd` 匹配),写入 `profile-log.jsonl`。
106
- - 后续同步会用该绑定将 session 归因到对应 profile。
107
- - 状态栏同步(`codenv statusline --sync-usage`)
108
- - 需要 `sessionId`、model,以及 profile(`profileKey` 或 `profileName`)才会写入用量。
109
- - 从 stdin JSON 读取 totals,与 state 中的上次 totals 做差得到增量。
110
- - 如果 totals 回退(session reset),直接把当前 totals 当作新增;否则对负数子项做 0 处理。
111
- - 当拆分合计大于 total 时,以拆分合计为准。
112
- - 把增量写入 `usage.jsonl`,并更新 state 中的 session totals。
113
- - 会话日志同步(`--sync-usage` 与 `codenv list`)
114
- - 扫描 Codex 的 `CODEX_HOME/sessions`(或 `~/.codex/sessions`)与 Claude 的 `CLAUDE_HOME/projects`(或 `~/.claude/projects`)。
115
- - Codex:读取 `event_msg` 的 token_count 记录,取累计最大值;cached input 计为 cache read,并在可判断时从 input 中扣除。
116
- - Claude:汇总 `message.usage` 中的 input/output/cache tokens。
117
- - 与 state 中的文件元数据和 session 最大值做差,生成增量并写入 `usage.jsonl`;无法解析到绑定的文件会被跳过。
118
- - 今日统计
119
- - “今日”按本地时区 00:00 到次日 00:00 计算。
120
- - 费用换算
121
- - 使用 profile 定价或 `pricing.models`(含默认值)换算,依赖 input/output/cache 拆分;缺少拆分则不显示金额。
122
-
123
95
  ### 添加 / 更新 profile
124
96
 
125
97
  ```bash
@@ -449,6 +449,8 @@ function syncUsageFromStatuslineInput(config, configPath, type, profileKey, prof
449
449
  endTs: now,
450
450
  cwd: cwd || (prev ? prev.cwd : null),
451
451
  model: resolvedModel,
452
+ profileKey: profileKey || null,
453
+ profileName: profileName || null,
452
454
  };
453
455
  state.sessions = sessions;
454
456
  updateUsageStateMetadata(state, usagePath);
@@ -1142,6 +1144,10 @@ function buildUsageSessionsFromRecords(records) {
1142
1144
  entry.cacheReadTokens += toUsageNumber(record.cacheReadTokens);
1143
1145
  entry.cacheWriteTokens += toUsageNumber(record.cacheWriteTokens);
1144
1146
  entry.totalTokens += toUsageNumber(record.totalTokens);
1147
+ if (record.profileKey)
1148
+ entry.profileKey = record.profileKey;
1149
+ if (record.profileName)
1150
+ entry.profileName = record.profileName;
1145
1151
  if (!entry.model && record.model)
1146
1152
  entry.model = record.model;
1147
1153
  if (record.ts) {
@@ -1300,11 +1306,19 @@ function syncUsageFromSessions(config, configPath, usagePath) {
1300
1306
  catch {
1301
1307
  return;
1302
1308
  }
1303
- const resolved = resolveProfileForSession(config, logEntries, type, filePath, stats.sessionId);
1304
- if (!resolved.match)
1305
- return;
1306
1309
  const sessionKey = stats.sessionId ? buildSessionKey(type, stats.sessionId) : null;
1307
1310
  const sessionPrev = sessionKey ? sessions[sessionKey] : null;
1311
+ const sessionProfile = sessionPrev && (sessionPrev.profileKey || sessionPrev.profileName)
1312
+ ? {
1313
+ profileKey: sessionPrev.profileKey || null,
1314
+ profileName: sessionPrev.profileName || null,
1315
+ }
1316
+ : null;
1317
+ const resolved = sessionProfile
1318
+ ? { match: sessionProfile, ambiguous: false }
1319
+ : resolveProfileForSession(config, logEntries, type, filePath, stats.sessionId);
1320
+ if (!resolved.match)
1321
+ return;
1308
1322
  const resolvedModel = (sessionPrev && sessionPrev.model) ||
1309
1323
  stats.model ||
1310
1324
  (prev && prev.model) ||
@@ -1397,6 +1411,8 @@ function syncUsageFromSessions(config, configPath, usagePath) {
1397
1411
  endTs: stats.endTs || (sessionPrev ? sessionPrev.endTs : null),
1398
1412
  cwd: stats.cwd || (sessionPrev ? sessionPrev.cwd : null),
1399
1413
  model: resolvedModel,
1414
+ profileKey: resolved.match.profileKey,
1415
+ profileName: resolved.match.profileName,
1400
1416
  };
1401
1417
  }
1402
1418
  files[filePath] = {
package/docs/README.md ADDED
@@ -0,0 +1,3 @@
1
+ # Docs Index
2
+
3
+ - [usage.md](usage.md): Usage tracking and statusline input details (Codex/Claude).
@@ -0,0 +1,3 @@
1
+ # 文档索引
2
+
3
+ - [usage_zh.md](usage_zh.md):用量统计逻辑与状态栏输入说明。
package/docs/usage.md ADDED
@@ -0,0 +1,126 @@
1
+ # Usage tracking
2
+
3
+ Usage stats are derived from two sources (statusline input + session logs) and appended to `usage.jsonl`.
4
+
5
+ ## Files and paths
6
+
7
+ - `usage.jsonl`: JSONL records with `ts`, `type`, `profileKey`/`profileName`, `model`, `sessionId`, and token breakdowns.
8
+ - `usage.jsonl.state.json`: per-session totals + per-session-file metadata (mtime/size) used to compute deltas and avoid double counting.
9
+ - `profile-log.jsonl`: profile usage + session binding log (`use` and `session` events).
10
+ - `statusline-debug.jsonl`: optional debug capture when `CODE_ENV_STATUSLINE_DEBUG` is enabled.
11
+ - Paths can be overridden via `usagePath`, `usageStatePath`, `profileLogPath`, `codexSessionsPath`, `claudeSessionsPath`.
12
+
13
+ ## Session binding (profile -> session)
14
+
15
+ - `codenv init` installs a shell wrapper so `codex`/`claude` run via `codenv launch`.
16
+ - `codenv launch` logs profile usage and then finds the latest unbound session file (prefers matching `cwd`, within a small grace window) and records the binding in `profile-log.jsonl`.
17
+ - Sync uses these bindings to attribute session usage to a profile.
18
+
19
+ ## Statusline sync (`codenv statusline --sync-usage`)
20
+
21
+ - Requires `sessionId`, model, and a profile (`profileKey` or `profileName`) to write usage.
22
+ - Reads stdin JSON for totals and computes a delta against the last stored totals in the state file.
23
+ - If totals decrease (session reset), the current totals are treated as fresh usage; otherwise negative sub-deltas are clamped to zero.
24
+ - If the token breakdown exceeds the reported total, the breakdown sum wins.
25
+ - Appends a delta record to `usage.jsonl` and updates the per-session totals in the state file.
26
+
27
+ ## Session log sync (`--sync-usage` and `codenv list`)
28
+
29
+ - Scans Codex sessions under `CODEX_HOME/sessions` (or `~/.codex/sessions`) and Claude sessions under `CLAUDE_HOME/projects` (or `~/.claude/projects`).
30
+ - Codex: parses `event_msg` token_count records and uses max totals; cached input is recorded as cache read and subtracted from input when possible.
31
+ - Claude: sums `message.usage` input/output/cache tokens across the file.
32
+ - Deltas are computed against prior file metadata and per-session maxima in the state file; files without a resolved binding are skipped.
33
+
34
+ ## Daily totals
35
+
36
+ - "Today" is computed in local time between 00:00 and the next 00:00.
37
+
38
+ ## Cost calculation
39
+
40
+ - Uses pricing from the profile (or `pricing.models`, plus defaults) and requires token splits; if splits are missing, cost is omitted.
41
+
42
+ ## Examples
43
+
44
+ ### `usage.jsonl` record
45
+
46
+ Each line is a JSON object (fields may be null/omitted depending on the source):
47
+
48
+ ```json
49
+ {"ts":"2026-01-25T12:34:56.789Z","type":"codex","profileKey":"p_a1b2c3","profileName":"primary","model":"gpt-5.1-codex","sessionId":"a6f9c4d8-1234-5678-9abc-def012345678","inputTokens":1200,"outputTokens":300,"cacheReadTokens":200,"cacheWriteTokens":0,"totalTokens":1700}
50
+ ```
51
+
52
+ `totalTokens` is the max of the reported total and the breakdown sum, so it can be >= input + output + cache.
53
+
54
+ ## Statusline input examples (Codex/Claude)
55
+
56
+ To see real payloads, set `CODE_ENV_STATUSLINE_DEBUG=1` and read the JSONL entries in
57
+ `statusline-debug.jsonl` (or the path from `CODE_ENV_STATUSLINE_DEBUG_PATH`).
58
+
59
+ ### Codex (token_usage totals)
60
+
61
+ ```json
62
+ {
63
+ "type": "codex",
64
+ "session_id": "a6f9c4d8-1234-5678-9abc-def012345678",
65
+ "profile": { "key": "p_a1b2c3", "name": "primary", "type": "codex" },
66
+ "model": "gpt-5.1-codex",
67
+ "token_usage": {
68
+ "total_token_usage": {
69
+ "input_tokens": 1200,
70
+ "output_tokens": 300,
71
+ "cached_input_tokens": 200,
72
+ "cache_creation_input_tokens": 50,
73
+ "total_tokens": 1750
74
+ },
75
+ "last_token_usage": {
76
+ "input_tokens": 100,
77
+ "output_tokens": 20,
78
+ "cached_input_tokens": 10,
79
+ "cache_creation_input_tokens": 0,
80
+ "total_tokens": 130
81
+ }
82
+ }
83
+ }
84
+ ```
85
+
86
+ Accepted aliases (non-exhaustive):
87
+ - `token_usage.total_token_usage` or `token_usage.totalTokenUsage`
88
+ - `last_token_usage` or `lastTokenUsage`
89
+ - `input_tokens` / `inputTokens` / `input`
90
+ - `output_tokens` / `outputTokens` / `output` / `reasoning_output_tokens`
91
+ - `cached_input_tokens` / `cache_read_input_tokens`
92
+ - `cache_creation_input_tokens` / `cache_write_input_tokens`
93
+ - `total_tokens` / `totalTokens` / `total`
94
+
95
+ `token_usage` can also be a number, or the payload can provide `usage` directly.
96
+
97
+ ### Claude (context_window totals)
98
+
99
+ ```json
100
+ {
101
+ "type": "claude",
102
+ "session_id": "1f2e3d4c-5678-90ab-cdef-1234567890ab",
103
+ "profile": { "key": "p_c3d4e5", "name": "default", "type": "claude" },
104
+ "model": { "display_name": "Claude Sonnet 4.5" },
105
+ "context_window": {
106
+ "total_input_tokens": 800,
107
+ "total_output_tokens": 250,
108
+ "current_usage": {
109
+ "cache_read_input_tokens": 100,
110
+ "cache_creation_input_tokens": 40
111
+ },
112
+ "context_window_size": 200000
113
+ }
114
+ }
115
+ ```
116
+
117
+ Accepted aliases (non-exhaustive):
118
+ - `context_window` or `contextWindow`
119
+ - `current_usage` or `currentUsage`
120
+ - `total_input_tokens` / `totalInputTokens`
121
+ - `total_output_tokens` / `totalOutputTokens`
122
+ - `cache_read_input_tokens` / `cacheReadInputTokens`
123
+ - `cache_creation_input_tokens` / `cacheWriteInputTokens`
124
+
125
+ `usage` can also be provided directly with `todayTokens` / `totalTokens` / `inputTokens` /
126
+ `outputTokens` / `cacheReadTokens` / `cacheWriteTokens`.
@@ -0,0 +1,126 @@
1
+ # 用量统计逻辑
2
+
3
+ 用量来自两条路径(状态栏输入同步 + 会话日志解析),最终追加到 `usage.jsonl`。
4
+
5
+ ## 文件与路径
6
+
7
+ - `usage.jsonl`:JSONL 记录,包含 `ts`/`type`/`profileKey`/`profileName`/`model`/`sessionId` 和 token 拆分字段。
8
+ - `usage.jsonl.state.json`:保存每个 session 的累计 totals + session 文件的 mtime/size,用于计算增量并避免重复统计。
9
+ - `profile-log.jsonl`:profile 使用与 session 绑定日志(`use`/`session`)。
10
+ - `statusline-debug.jsonl`:当 `CODE_ENV_STATUSLINE_DEBUG` 开启时写入的调试信息。
11
+ - 可通过 `usagePath`/`usageStatePath`/`profileLogPath`/`codexSessionsPath`/`claudeSessionsPath` 覆盖默认路径。
12
+
13
+ ## 会话绑定(profile -> session)
14
+
15
+ - `codenv init` 安装 shell 包装函数,使 `codex`/`claude` 实际走 `codenv launch`。
16
+ - `codenv launch` 记录 profile 使用,并在启动后短时间内(默认约 5 秒、每秒轮询)找到最新未绑定的 session 文件(优先 `cwd` 匹配),写入 `profile-log.jsonl`。
17
+ - 后续同步会用该绑定将 session 归因到对应 profile。
18
+
19
+ ## 状态栏同步(`codenv statusline --sync-usage`)
20
+
21
+ - 需要 `sessionId`、model,以及 profile(`profileKey` 或 `profileName`)才会写入用量。
22
+ - 从 stdin JSON 读取 totals,与 state 中的上次 totals 做差得到增量。
23
+ - 如果 totals 回退(session reset),直接把当前 totals 当作新增;否则对负数子项做 0 处理。
24
+ - 当拆分合计大于 total 时,以拆分合计为准。
25
+ - 把增量写入 `usage.jsonl`,并更新 state 中的 session totals。
26
+
27
+ ## 会话日志同步(`--sync-usage` 与 `codenv list`)
28
+
29
+ - 扫描 Codex 的 `CODEX_HOME/sessions`(或 `~/.codex/sessions`)与 Claude 的 `CLAUDE_HOME/projects`(或 `~/.claude/projects`)。
30
+ - Codex:读取 `event_msg` 的 token_count 记录,取累计最大值;cached input 计为 cache read,并在可判断时从 input 中扣除。
31
+ - Claude:汇总 `message.usage` 中的 input/output/cache tokens。
32
+ - 与 state 中的文件元数据和 session 最大值做差,生成增量并写入 `usage.jsonl`;无法解析到绑定的文件会被跳过。
33
+
34
+ ## 今日统计
35
+
36
+ - “今日”按本地时区 00:00 到次日 00:00 计算。
37
+
38
+ ## 费用换算
39
+
40
+ - 使用 profile 定价或 `pricing.models`(含默认值)换算,依赖 input/output/cache 拆分;缺少拆分则不显示金额。
41
+
42
+ ## 示例
43
+
44
+ ### `usage.jsonl` 记录
45
+
46
+ 每行一条 JSON(字段可能为空或缺省,取决于来源):
47
+
48
+ ```json
49
+ {"ts":"2026-01-25T12:34:56.789Z","type":"codex","profileKey":"p_a1b2c3","profileName":"primary","model":"gpt-5.1-codex","sessionId":"a6f9c4d8-1234-5678-9abc-def012345678","inputTokens":1200,"outputTokens":300,"cacheReadTokens":200,"cacheWriteTokens":0,"totalTokens":1700}
50
+ ```
51
+
52
+ `totalTokens` 会取“上报 total”和“拆分合计”的较大值,因此可能 >= input + output + cache。
53
+
54
+ ## 状态栏输入示例(Codex/Claude)
55
+
56
+ 可通过设置 `CODE_ENV_STATUSLINE_DEBUG=1`,在 `statusline-debug.jsonl`
57
+ (或 `CODE_ENV_STATUSLINE_DEBUG_PATH` 指定路径)看到实际 JSON。
58
+
59
+ ### Codex(token_usage totals)
60
+
61
+ ```json
62
+ {
63
+ "type": "codex",
64
+ "session_id": "a6f9c4d8-1234-5678-9abc-def012345678",
65
+ "profile": { "key": "p_a1b2c3", "name": "primary", "type": "codex" },
66
+ "model": "gpt-5.1-codex",
67
+ "token_usage": {
68
+ "total_token_usage": {
69
+ "input_tokens": 1200,
70
+ "output_tokens": 300,
71
+ "cached_input_tokens": 200,
72
+ "cache_creation_input_tokens": 50,
73
+ "total_tokens": 1750
74
+ },
75
+ "last_token_usage": {
76
+ "input_tokens": 100,
77
+ "output_tokens": 20,
78
+ "cached_input_tokens": 10,
79
+ "cache_creation_input_tokens": 0,
80
+ "total_tokens": 130
81
+ }
82
+ }
83
+ }
84
+ ```
85
+
86
+ 可识别字段(节选):
87
+ - `token_usage.total_token_usage` 或 `token_usage.totalTokenUsage`
88
+ - `last_token_usage` 或 `lastTokenUsage`
89
+ - `input_tokens` / `inputTokens` / `input`
90
+ - `output_tokens` / `outputTokens` / `output` / `reasoning_output_tokens`
91
+ - `cached_input_tokens` / `cache_read_input_tokens`
92
+ - `cache_creation_input_tokens` / `cache_write_input_tokens`
93
+ - `total_tokens` / `totalTokens` / `total`
94
+
95
+ `token_usage` 也可以是数字,或直接提供 `usage`。
96
+
97
+ ### Claude(context_window totals)
98
+
99
+ ```json
100
+ {
101
+ "type": "claude",
102
+ "session_id": "1f2e3d4c-5678-90ab-cdef-1234567890ab",
103
+ "profile": { "key": "p_c3d4e5", "name": "default", "type": "claude" },
104
+ "model": { "display_name": "Claude Sonnet 4.5" },
105
+ "context_window": {
106
+ "total_input_tokens": 800,
107
+ "total_output_tokens": 250,
108
+ "current_usage": {
109
+ "cache_read_input_tokens": 100,
110
+ "cache_creation_input_tokens": 40
111
+ },
112
+ "context_window_size": 200000
113
+ }
114
+ }
115
+ ```
116
+
117
+ 可识别字段(节选):
118
+ - `context_window` 或 `contextWindow`
119
+ - `current_usage` 或 `currentUsage`
120
+ - `total_input_tokens` / `totalInputTokens`
121
+ - `total_output_tokens` / `totalOutputTokens`
122
+ - `cache_read_input_tokens` / `cacheReadInputTokens`
123
+ - `cache_creation_input_tokens` / `cacheWriteInputTokens`
124
+
125
+ 也可直接提供 `usage`,字段包括 `todayTokens` / `totalTokens` / `inputTokens` /
126
+ `outputTokens` / `cacheReadTokens` / `cacheWriteTokens`。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@praeviso/code-env-switch",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
4
4
  "description": "Switch between Claude Code and Codex environment variables from a single CLI",
5
5
  "bin": {
6
6
  "codenv": "bin/index.js"
@@ -91,6 +91,8 @@ interface UsageSessionEntry {
91
91
  endTs: string | null;
92
92
  cwd: string | null;
93
93
  model?: string | null;
94
+ profileKey?: string | null;
95
+ profileName?: string | null;
94
96
  }
95
97
 
96
98
  interface UsageStateFile {
@@ -638,6 +640,8 @@ export function syncUsageFromStatuslineInput(
638
640
  endTs: now,
639
641
  cwd: cwd || (prev ? prev.cwd : null),
640
642
  model: resolvedModel,
643
+ profileKey: profileKey || null,
644
+ profileName: profileName || null,
641
645
  };
642
646
  state.sessions = sessions;
643
647
  updateUsageStateMetadata(state, usagePath);
@@ -1387,6 +1391,8 @@ function buildUsageSessionsFromRecords(
1387
1391
  entry.cacheReadTokens += toUsageNumber(record.cacheReadTokens);
1388
1392
  entry.cacheWriteTokens += toUsageNumber(record.cacheWriteTokens);
1389
1393
  entry.totalTokens += toUsageNumber(record.totalTokens);
1394
+ if (record.profileKey) entry.profileKey = record.profileKey;
1395
+ if (record.profileName) entry.profileName = record.profileName;
1390
1396
  if (!entry.model && record.model) entry.model = record.model;
1391
1397
  if (record.ts) {
1392
1398
  const range = { start: entry.startTs, end: entry.endTs };
@@ -1563,17 +1569,26 @@ export function syncUsageFromSessions(
1563
1569
  } catch {
1564
1570
  return;
1565
1571
  }
1566
- const resolved = resolveProfileForSession(
1567
- config,
1568
- logEntries,
1569
- type,
1570
- filePath,
1571
- stats.sessionId
1572
- );
1573
- if (!resolved.match) return;
1574
1572
  const sessionKey =
1575
1573
  stats.sessionId ? buildSessionKey(type, stats.sessionId) : null;
1576
1574
  const sessionPrev = sessionKey ? sessions[sessionKey] : null;
1575
+ const sessionProfile =
1576
+ sessionPrev && (sessionPrev.profileKey || sessionPrev.profileName)
1577
+ ? {
1578
+ profileKey: sessionPrev.profileKey || null,
1579
+ profileName: sessionPrev.profileName || null,
1580
+ }
1581
+ : null;
1582
+ const resolved = sessionProfile
1583
+ ? { match: sessionProfile, ambiguous: false }
1584
+ : resolveProfileForSession(
1585
+ config,
1586
+ logEntries,
1587
+ type,
1588
+ filePath,
1589
+ stats.sessionId
1590
+ );
1591
+ if (!resolved.match) return;
1577
1592
  const resolvedModel =
1578
1593
  (sessionPrev && sessionPrev.model) ||
1579
1594
  stats.model ||
@@ -1683,6 +1698,8 @@ export function syncUsageFromSessions(
1683
1698
  endTs: stats.endTs || (sessionPrev ? sessionPrev.endTs : null),
1684
1699
  cwd: stats.cwd || (sessionPrev ? sessionPrev.cwd : null),
1685
1700
  model: resolvedModel,
1701
+ profileKey: resolved.match.profileKey,
1702
+ profileName: resolved.match.profileName,
1686
1703
  };
1687
1704
  }
1688
1705
  files[filePath] = {