@phnx-labs/agents-cli 1.22.26 → 1.22.28

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.
Files changed (183) hide show
  1. package/CHANGELOG.md +293 -0
  2. package/README.md +80 -27
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/bench.d.ts +2 -0
  5. package/dist/commands/bench.js +101 -0
  6. package/dist/commands/doctor.js +24 -10
  7. package/dist/commands/exec.js +71 -26
  8. package/dist/commands/hosts.js +4 -3
  9. package/dist/commands/insights.js +39 -5
  10. package/dist/commands/output.js +100 -25
  11. package/dist/commands/perf.d.ts +10 -0
  12. package/dist/commands/perf.js +14 -6
  13. package/dist/commands/projects.js +24 -10
  14. package/dist/commands/resume.d.ts +11 -0
  15. package/dist/commands/resume.js +51 -0
  16. package/dist/commands/routines.js +31 -4
  17. package/dist/commands/run-account-picker.d.ts +37 -0
  18. package/dist/commands/run-account-picker.js +101 -17
  19. package/dist/commands/secrets.js +86 -2
  20. package/dist/commands/sessions-picker.js +12 -7
  21. package/dist/commands/sessions.d.ts +73 -7
  22. package/dist/commands/sessions.js +391 -49
  23. package/dist/commands/ssh.js +184 -11
  24. package/dist/commands/teams.js +22 -11
  25. package/dist/index.js +16 -20
  26. package/dist/lib/activity.js +45 -99
  27. package/dist/lib/auth-health.d.ts +47 -3
  28. package/dist/lib/auth-health.js +74 -15
  29. package/dist/lib/bench/index.d.ts +4 -0
  30. package/dist/lib/bench/index.js +4 -0
  31. package/dist/lib/bench/runner.d.ts +16 -0
  32. package/dist/lib/bench/runner.js +111 -0
  33. package/dist/lib/bench/schema.d.ts +5 -0
  34. package/dist/lib/bench/schema.js +91 -0
  35. package/dist/lib/bench/storage.d.ts +5 -0
  36. package/dist/lib/bench/storage.js +32 -0
  37. package/dist/lib/bench/types.d.ts +40 -0
  38. package/dist/lib/bench/types.js +1 -0
  39. package/dist/lib/claude-account-token.d.ts +2 -0
  40. package/dist/lib/claude-account-token.js +56 -3
  41. package/dist/lib/cloud/host.js +1 -0
  42. package/dist/lib/crabbox/cli.d.ts +2 -0
  43. package/dist/lib/crabbox/cli.js +2 -0
  44. package/dist/lib/crabbox/lease.js +7 -1
  45. package/dist/lib/daemon.js +20 -0
  46. package/dist/lib/devices/connect.d.ts +2 -0
  47. package/dist/lib/devices/connect.js +7 -0
  48. package/dist/lib/devices/doctor-findings.d.ts +4 -1
  49. package/dist/lib/devices/doctor-findings.js +15 -1
  50. package/dist/lib/devices/harness-inventory.d.ts +97 -0
  51. package/dist/lib/devices/harness-inventory.js +0 -0
  52. package/dist/lib/devices/registry.d.ts +2 -0
  53. package/dist/lib/devices/resolve-target.d.ts +1 -0
  54. package/dist/lib/devices/resolve-target.js +9 -2
  55. package/dist/lib/devices/ssh-config.js +3 -0
  56. package/dist/lib/devices/windows-ssh-enrollment.d.ts +20 -0
  57. package/dist/lib/devices/windows-ssh-enrollment.js +98 -0
  58. package/dist/lib/events.d.ts +1 -1
  59. package/dist/lib/events.js +2 -2
  60. package/dist/lib/exec.js +18 -8
  61. package/dist/lib/fleet/apply.js +9 -7
  62. package/dist/lib/fleet/remote-login.d.ts +4 -3
  63. package/dist/lib/fleet/remote-login.js +11 -9
  64. package/dist/lib/gemini-settings.d.ts +0 -1
  65. package/dist/lib/gemini-settings.js +12 -7
  66. package/dist/lib/git.d.ts +9 -3
  67. package/dist/lib/git.js +39 -22
  68. package/dist/lib/hooks/profile.d.ts +12 -1
  69. package/dist/lib/hooks/profile.js +6 -1
  70. package/dist/lib/hooks.d.ts +8 -5
  71. package/dist/lib/hooks.js +14 -7
  72. package/dist/lib/hosts/dispatch.d.ts +10 -0
  73. package/dist/lib/hosts/dispatch.js +85 -25
  74. package/dist/lib/hosts/logs.js +11 -1
  75. package/dist/lib/hosts/passthrough.d.ts +27 -0
  76. package/dist/lib/hosts/passthrough.js +19 -10
  77. package/dist/lib/hosts/progress.d.ts +12 -1
  78. package/dist/lib/hosts/progress.js +41 -8
  79. package/dist/lib/hosts/providers/devices.js +1 -0
  80. package/dist/lib/hosts/ready.d.ts +45 -4
  81. package/dist/lib/hosts/ready.js +124 -20
  82. package/dist/lib/hosts/reconcile.d.ts +2 -1
  83. package/dist/lib/hosts/reconcile.js +24 -7
  84. package/dist/lib/hosts/reconnect.d.ts +45 -12
  85. package/dist/lib/hosts/reconnect.js +94 -36
  86. package/dist/lib/hosts/registry.d.ts +2 -2
  87. package/dist/lib/hosts/registry.js +4 -5
  88. package/dist/lib/hosts/remote-cmd.d.ts +17 -0
  89. package/dist/lib/hosts/remote-cmd.js +29 -0
  90. package/dist/lib/hosts/tasks.d.ts +3 -0
  91. package/dist/lib/hosts/types.d.ts +1 -0
  92. package/dist/lib/hosts/types.js +3 -0
  93. package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
  94. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  95. package/dist/lib/menubar/install-menubar.d.ts +9 -6
  96. package/dist/lib/menubar/install-menubar.js +20 -9
  97. package/dist/lib/menubar/snapshot.js +15 -2
  98. package/dist/lib/models.d.ts +42 -6
  99. package/dist/lib/models.js +55 -153
  100. package/dist/lib/perf/db.js +16 -3
  101. package/dist/lib/perf/types.d.ts +12 -1
  102. package/dist/lib/pricing/cost.d.ts +9 -0
  103. package/dist/lib/pricing/cost.js +24 -0
  104. package/dist/lib/pricing/index.d.ts +1 -1
  105. package/dist/lib/pricing/index.js +1 -1
  106. package/dist/lib/project-probe.d.ts +22 -3
  107. package/dist/lib/project-probe.js +105 -17
  108. package/dist/lib/project-status.d.ts +9 -0
  109. package/dist/lib/project-status.js +15 -0
  110. package/dist/lib/redact.js +8 -3
  111. package/dist/lib/refresh.js +22 -11
  112. package/dist/lib/remote-agents-json.d.ts +32 -0
  113. package/dist/lib/remote-agents-json.js +47 -16
  114. package/dist/lib/resource-profiles.js +1 -2
  115. package/dist/lib/rotate.d.ts +22 -0
  116. package/dist/lib/rotate.js +26 -0
  117. package/dist/lib/routine-notify-owner.d.ts +102 -0
  118. package/dist/lib/routine-notify-owner.js +232 -0
  119. package/dist/lib/routines.d.ts +10 -0
  120. package/dist/lib/runner.d.ts +9 -1
  121. package/dist/lib/runner.js +182 -17
  122. package/dist/lib/sandbox.d.ts +0 -2
  123. package/dist/lib/sandbox.js +2 -19
  124. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  125. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  126. package/dist/lib/secrets/agent.d.ts +19 -3
  127. package/dist/lib/secrets/agent.js +61 -22
  128. package/dist/lib/secrets/audit.d.ts +1 -1
  129. package/dist/lib/secrets/audit.js +2 -0
  130. package/dist/lib/secrets/bundles.d.ts +3 -3
  131. package/dist/lib/secrets/bundles.js +15 -5
  132. package/dist/lib/secrets/filestore.d.ts +2 -0
  133. package/dist/lib/secrets/filestore.js +4 -0
  134. package/dist/lib/secrets/lease.d.ts +25 -0
  135. package/dist/lib/secrets/lease.js +44 -0
  136. package/dist/lib/secrets/session-store.d.ts +7 -0
  137. package/dist/lib/secrets/session-store.js +21 -0
  138. package/dist/lib/self-update.d.ts +20 -5
  139. package/dist/lib/self-update.js +93 -16
  140. package/dist/lib/session/active.d.ts +49 -0
  141. package/dist/lib/session/active.js +175 -11
  142. package/dist/lib/session/bash-command.d.ts +16 -0
  143. package/dist/lib/session/bash-command.js +65 -0
  144. package/dist/lib/session/db.d.ts +23 -2
  145. package/dist/lib/session/db.js +86 -20
  146. package/dist/lib/session/discover.d.ts +21 -1
  147. package/dist/lib/session/discover.js +124 -32
  148. package/dist/lib/session/insights.d.ts +19 -0
  149. package/dist/lib/session/insights.js +78 -10
  150. package/dist/lib/session/recovery.d.ts +17 -1
  151. package/dist/lib/session/recovery.js +111 -4
  152. package/dist/lib/session/remote-bundle.js +1 -1
  153. package/dist/lib/session/remote-list.d.ts +10 -9
  154. package/dist/lib/session/remote-list.js +25 -23
  155. package/dist/lib/session/resume-owner.d.ts +55 -0
  156. package/dist/lib/session/resume-owner.js +69 -0
  157. package/dist/lib/session/team-filter.d.ts +65 -0
  158. package/dist/lib/session/team-filter.js +98 -3
  159. package/dist/lib/session/types.d.ts +33 -0
  160. package/dist/lib/smart-launch.d.ts +31 -5
  161. package/dist/lib/smart-launch.js +43 -7
  162. package/dist/lib/ssh-exec.d.ts +27 -0
  163. package/dist/lib/ssh-exec.js +34 -1
  164. package/dist/lib/ssh-tunnel.d.ts +3 -2
  165. package/dist/lib/ssh-tunnel.js +25 -16
  166. package/dist/lib/staleness/detectors/skills.d.ts +2 -0
  167. package/dist/lib/staleness/detectors/skills.js +17 -2
  168. package/dist/lib/staleness/index.d.ts +10 -1
  169. package/dist/lib/staleness/index.js +28 -3
  170. package/dist/lib/startup/command-registry.d.ts +1 -0
  171. package/dist/lib/startup/command-registry.js +2 -0
  172. package/dist/lib/teams/agents.d.ts +1 -0
  173. package/dist/lib/teams/agents.js +38 -11
  174. package/dist/lib/teams/placement-probe.d.ts +1 -1
  175. package/dist/lib/teams/placement-probe.js +24 -24
  176. package/dist/lib/teams/remoteWorktree.d.ts +11 -7
  177. package/dist/lib/teams/remoteWorktree.js +29 -27
  178. package/dist/lib/tmux/session.d.ts +8 -0
  179. package/dist/lib/tmux/session.js +22 -0
  180. package/dist/lib/types.d.ts +2 -0
  181. package/dist/lib/versions.d.ts +1 -0
  182. package/dist/lib/versions.js +40 -22
  183. package/package.json +3 -1
@@ -26,7 +26,7 @@ const DB_PATH = getSessionsDbPath();
26
26
  /** Current schema version; bumped when migrations are added. Exported so tests
27
27
  * assert against the constant instead of hardcoding a number that every bump
28
28
  * then has to chase (docs/05-sessions.md calls the constant the source of truth). */
29
- export const SCHEMA_VERSION = 36;
29
+ export const SCHEMA_VERSION = 37;
30
30
  /**
31
31
  * Bump to force `agents sessions backfill resources` to re-derive every
32
32
  * session's skill/slash-command tallies on its next run (resource_scan_ledger
@@ -81,7 +81,11 @@ CREATE TABLE IF NOT EXISTS sessions (
81
81
  message_count INTEGER,
82
82
  token_count INTEGER,
83
83
  output_tokens INTEGER,
84
+ input_tokens INTEGER,
85
+ cache_read_tokens INTEGER,
86
+ cache_write_tokens INTEGER,
84
87
  cost_usd REAL,
88
+ cost_usd_nocache REAL,
85
89
  duration_ms INTEGER,
86
90
  model TEXT,
87
91
  tool_call_count INTEGER,
@@ -308,7 +312,8 @@ CREATE TABLE IF NOT EXISTS session_insights (
308
312
  * re-derives on the next `agents insights` instead of silently reporting stale
309
313
  * numbers alongside fresh ones. Same role as RESOURCE_INDEX_VERSION.
310
314
  */
311
- export const INSIGHTS_EXTRACTOR_VERSION = 4;
315
+ /** Bump when facet extraction changes so cached rows recompute (stalls-by-model v6). */
316
+ export const INSIGHTS_EXTRACTOR_VERSION = 6;
312
317
  let dbInstance = null;
313
318
  /**
314
319
  * Apply schema migrations from `fromVersion` → SCHEMA_VERSION. The new
@@ -875,6 +880,25 @@ function migrateSchema(db, fromVersion) {
875
880
  FROM tool_calls;
876
881
  `);
877
882
  }
883
+ if (fromVersion < 37) {
884
+ // v36 -> v37: persist the burn SPLIT (uncached input / cache-read /
885
+ // cache-write) and a second "no-cache" cost per session, so `agents output`
886
+ // can report the token split and a --pricing no-cache scenario (RUSH-2287).
887
+ // These are new nullable columns — do NOT flush scan_ledger (adding a column
888
+ // must keep warm session ledgers warm, the contract the v33->v34 note above
889
+ // states). Pre-upgrade rows stay NULL for the split until their transcript is
890
+ // re-scanned; `agents output` reports the split only where it is present, so
891
+ // an absent split reads as "not available for this session", never as zero.
892
+ const cols = new Set(db.prepare(`PRAGMA table_info(sessions)`).all().map((c) => c.name));
893
+ if (!cols.has('input_tokens'))
894
+ db.exec(`ALTER TABLE sessions ADD COLUMN input_tokens INTEGER`);
895
+ if (!cols.has('cache_read_tokens'))
896
+ db.exec(`ALTER TABLE sessions ADD COLUMN cache_read_tokens INTEGER`);
897
+ if (!cols.has('cache_write_tokens'))
898
+ db.exec(`ALTER TABLE sessions ADD COLUMN cache_write_tokens INTEGER`);
899
+ if (!cols.has('cost_usd_nocache'))
900
+ db.exec(`ALTER TABLE sessions ADD COLUMN cost_usd_nocache REAL`);
901
+ }
878
902
  }
879
903
  /**
880
904
  * Stamp `account_key` / `account_org` / `account` on every Claude row from its
@@ -1019,6 +1043,7 @@ export function getDB() {
1019
1043
  }
1020
1044
  /** Close the cached database connection. */
1021
1045
  export function closeDB() {
1046
+ clearSessionExistenceCache();
1022
1047
  if (dbInstance) {
1023
1048
  dbInstance.close();
1024
1049
  dbInstance = null;
@@ -1369,7 +1394,8 @@ const upsertSessionStmt = (db) => db.prepare(`
1369
1394
  id, short_id, agent, origin, routine_name, routine_run_id,
1370
1395
  version, account, account_key, account_org, mode, timestamp, last_activity,
1371
1396
  project, cwd, git_branch, topic, label, message_count, token_count,
1372
- output_tokens, cost_usd, duration_ms, model, tool_call_count,
1397
+ output_tokens, input_tokens, cache_read_tokens, cache_write_tokens,
1398
+ cost_usd, cost_usd_nocache, duration_ms, model, tool_call_count,
1373
1399
  file_path, file_mtime_ms, file_size, scanned_at, is_team_origin,
1374
1400
  pr_url, pr_number, worktree_slug, ticket_id, spawned_team, plan, todos,
1375
1401
  recent_directories_touched, linear_project, linear_project_url, machine,
@@ -1378,7 +1404,8 @@ const upsertSessionStmt = (db) => db.prepare(`
1378
1404
  @id, @short_id, @agent, @origin, @routine_name, @routine_run_id,
1379
1405
  @version, @account, @account_key, @account_org, @mode, @timestamp, @last_activity,
1380
1406
  @project, @cwd, @git_branch, @topic, @label, @message_count, @token_count,
1381
- @output_tokens, @cost_usd, @duration_ms, @model, @tool_call_count,
1407
+ @output_tokens, @input_tokens, @cache_read_tokens, @cache_write_tokens,
1408
+ @cost_usd, @cost_usd_nocache, @duration_ms, @model, @tool_call_count,
1382
1409
  @file_path, @file_mtime_ms, @file_size, @scanned_at, @is_team_origin,
1383
1410
  @pr_url, @pr_number, @worktree_slug, @ticket_id, @spawned_team, @plan, @todos,
1384
1411
  @recent_directories_touched, @linear_project, @linear_project_url, @machine,
@@ -1413,7 +1440,11 @@ const upsertSessionStmt = (db) => db.prepare(`
1413
1440
  message_count = excluded.message_count,
1414
1441
  token_count = excluded.token_count,
1415
1442
  output_tokens = excluded.output_tokens,
1443
+ input_tokens = excluded.input_tokens,
1444
+ cache_read_tokens = excluded.cache_read_tokens,
1445
+ cache_write_tokens = excluded.cache_write_tokens,
1416
1446
  cost_usd = excluded.cost_usd,
1447
+ cost_usd_nocache = excluded.cost_usd_nocache,
1417
1448
  duration_ms = excluded.duration_ms,
1418
1449
  model = excluded.model,
1419
1450
  tool_call_count = excluded.tool_call_count,
@@ -1652,7 +1683,11 @@ export function upsertSession(meta, content, scan) {
1652
1683
  message_count: meta.messageCount ?? null,
1653
1684
  token_count: meta.tokenCount ?? null,
1654
1685
  output_tokens: meta.outputTokens ?? null,
1686
+ input_tokens: meta.inputTokens ?? null,
1687
+ cache_read_tokens: meta.cacheReadTokens ?? null,
1688
+ cache_write_tokens: meta.cacheWriteTokens ?? null,
1655
1689
  cost_usd: meta.costUsd ?? null,
1690
+ cost_usd_nocache: meta.costUsdNoCache ?? null,
1656
1691
  duration_ms: meta.durationMs ?? null,
1657
1692
  model: meta.model ?? null,
1658
1693
  tool_call_count: meta.toolCallCount ?? null,
@@ -1832,7 +1867,11 @@ export function upsertSessionsBatch(entries) {
1832
1867
  message_count: meta.messageCount ?? null,
1833
1868
  token_count: meta.tokenCount ?? null,
1834
1869
  output_tokens: meta.outputTokens ?? null,
1870
+ input_tokens: meta.inputTokens ?? null,
1871
+ cache_read_tokens: meta.cacheReadTokens ?? null,
1872
+ cache_write_tokens: meta.cacheWriteTokens ?? null,
1835
1873
  cost_usd: meta.costUsd ?? null,
1874
+ cost_usd_nocache: meta.costUsdNoCache ?? null,
1836
1875
  duration_ms: meta.durationMs ?? null,
1837
1876
  model: meta.model ?? null,
1838
1877
  tool_call_count: meta.toolCallCount ?? null,
@@ -2046,7 +2085,11 @@ function rowToMeta(row) {
2046
2085
  messageCount: row.message_count ?? undefined,
2047
2086
  tokenCount: row.token_count ?? undefined,
2048
2087
  outputTokens: row.output_tokens ?? undefined,
2088
+ inputTokens: row.input_tokens ?? undefined,
2089
+ cacheReadTokens: row.cache_read_tokens ?? undefined,
2090
+ cacheWriteTokens: row.cache_write_tokens ?? undefined,
2049
2091
  costUsd: row.cost_usd ?? undefined,
2092
+ costUsdNoCache: row.cost_usd_nocache ?? undefined,
2050
2093
  durationMs: row.duration_ms ?? undefined,
2051
2094
  model: row.model ?? undefined,
2052
2095
  toolCallCount: row.tool_call_count ?? undefined,
@@ -2235,17 +2278,18 @@ function buildSessionWhere(options) {
2235
2278
  const clause = where.length > 0 ? `WHERE ${where.join(' AND ')}` : '';
2236
2279
  return { clause, params };
2237
2280
  }
2238
- /**
2239
- * Resolve which of the given file paths no longer exist, batching the check
2240
- * per directory instead of one `fs.existsSync` stat syscall per file.
2241
- * Transcript trees put many sessions in the same directory (one Claude
2242
- * `~/.claude/projects/<slug>/` holds every session for that project), so
2243
- * `readdirSync` once per directory and a Set membership test collapses what
2244
- * used to be N stat syscalls into (number of distinct directories) readdir
2245
- * syscalls the same existence answer, far fewer syscalls on a large index
2246
- * (RUSH-2211). Falls back to per-file existsSync only when the directory
2247
- * itself can't be listed (permissions, race with a concurrent delete).
2248
- */
2281
+ const directoryMembershipCache = new Map();
2282
+ let directoryMembershipSweepCount = 0;
2283
+ const DIRECTORY_MTIME_SETTLE_MS = 2_000;
2284
+ /** Process-local diagnostics for the real-filesystem existence-cache tests. */
2285
+ export function getSessionExistenceCacheStats() {
2286
+ return { sweeps: directoryMembershipSweepCount };
2287
+ }
2288
+ /** Clear process-local directory membership state when the session DB closes. */
2289
+ function clearSessionExistenceCache() {
2290
+ directoryMembershipCache.clear();
2291
+ directoryMembershipSweepCount = 0;
2292
+ }
2249
2293
  function findMissingFilePaths(filePaths) {
2250
2294
  const byDir = new Map();
2251
2295
  for (const p of filePaths) {
@@ -2261,21 +2305,36 @@ function findMissingFilePaths(filePaths) {
2261
2305
  for (const [dir, basenames] of byDir) {
2262
2306
  let entries;
2263
2307
  try {
2264
- entries = new Set(fs.readdirSync(dir));
2308
+ const stat = fs.statSync(dir);
2309
+ const cached = directoryMembershipCache.get(dir);
2310
+ const now = Date.now();
2311
+ const settled = now - stat.mtimeMs > DIRECTORY_MTIME_SETTLE_MS;
2312
+ const fresh = cached && now - cached.cachedAtMs <= DIRECTORY_MTIME_SETTLE_MS;
2313
+ if (settled && fresh && cached.mtimeMs === stat.mtimeMs && cached.size === stat.size) {
2314
+ entries = cached.entries;
2315
+ }
2316
+ else {
2317
+ entries = new Set(fs.readdirSync(dir));
2318
+ directoryMembershipCache.set(dir, { mtimeMs: stat.mtimeMs, size: stat.size, cachedAtMs: now, entries });
2319
+ directoryMembershipSweepCount++;
2320
+ }
2265
2321
  }
2266
2322
  catch {
2323
+ directoryMembershipCache.delete(dir);
2267
2324
  // Directory itself is gone (or unreadable) — every file in it is missing.
2268
2325
  // Also covers the race where readdir loses to a concurrent delete: fall
2269
2326
  // back to a direct stat rather than assuming existence.
2270
2327
  for (const base of basenames) {
2271
- if (!fs.existsSync(path.join(dir, base)))
2272
- missing.add(path.join(dir, base));
2328
+ const filePath = path.join(dir, base);
2329
+ if (!fs.existsSync(filePath))
2330
+ missing.add(filePath);
2273
2331
  }
2274
2332
  continue;
2275
2333
  }
2276
2334
  for (const base of basenames) {
2335
+ const filePath = path.join(dir, base);
2277
2336
  if (!entries.has(base))
2278
- missing.add(path.join(dir, base));
2337
+ missing.add(filePath);
2279
2338
  }
2280
2339
  }
2281
2340
  return missing;
@@ -2494,10 +2553,17 @@ export function queryUsageRollup(options) {
2494
2553
  THEN account_org || ' <' || account || '>' END) AS label,`
2495
2554
  : ''}
2496
2555
  IFNULL(SUM(cost_usd), 0) AS costUsd,
2556
+ -- A session with a cost but no persisted no-cache figure records no cache
2557
+ -- split, so its no-cache cost equals its actual cost — fall back to cost_usd
2558
+ -- so it still contributes to the scenario total rather than dropping to 0.
2559
+ IFNULL(SUM(COALESCE(cost_usd_nocache, cost_usd)), 0) AS costUsdNoCache,
2497
2560
  IFNULL(SUM(duration_ms), 0) AS durationMs,
2498
2561
  COUNT(*) AS sessionCount,
2499
2562
  IFNULL(SUM(token_count), 0) AS tokenCount,
2500
- IFNULL(SUM(output_tokens), 0) AS outputTokens
2563
+ IFNULL(SUM(output_tokens), 0) AS outputTokens,
2564
+ IFNULL(SUM(input_tokens), 0) AS inputTokens,
2565
+ IFNULL(SUM(cache_read_tokens), 0) AS cacheReadTokens,
2566
+ IFNULL(SUM(cache_write_tokens), 0) AS cacheWriteTokens
2501
2567
  FROM sessions
2502
2568
  ${clause}
2503
2569
  GROUP BY key
@@ -71,8 +71,14 @@ interface ClaudeSessionScan {
71
71
  tokenCount?: number;
72
72
  /** Real generated (output) tokens, excluding cache-read/-write context. */
73
73
  outputTokens?: number;
74
+ /** Burn split — uncached input / cache-read / cache-write tokens (RUSH-2287). */
75
+ inputTokens?: number;
76
+ cacheReadTokens?: number;
77
+ cacheWriteTokens?: number;
74
78
  /** Total USD cost accumulated from per-(model, direction) token usage. */
75
79
  costUsd?: number;
80
+ /** USD cost with cache read/write repriced at the input rate (RUSH-2287). */
81
+ costUsdNoCache?: number;
76
82
  /** Wall-clock duration in ms between the first and last timestamped event. */
77
83
  durationMs?: number;
78
84
  /** ISO time of the last timestamped event — the session's last activity. */
@@ -122,7 +128,13 @@ interface CodexSessionScan {
122
128
  tokenCount?: number;
123
129
  /** Real generated (output) tokens, excluding cache-read/-write context. */
124
130
  outputTokens?: number;
131
+ /** Burn split — uncached input / cache-read tokens (Codex has no cache-write) (RUSH-2287). */
132
+ inputTokens?: number;
133
+ cacheReadTokens?: number;
134
+ cacheWriteTokens?: number;
125
135
  costUsd?: number;
136
+ /** USD cost with cache read repriced at the input rate (RUSH-2287). */
137
+ costUsdNoCache?: number;
126
138
  durationMs?: number;
127
139
  lastActivity?: string;
128
140
  contentText?: string;
@@ -417,8 +429,12 @@ export interface ClaudeParseState {
417
429
  toolCallCount: number;
418
430
  tokenCount: number;
419
431
  outputTokens: number;
432
+ inputTokens: number;
433
+ cacheReadTokens: number;
434
+ cacheWriteTokens: number;
420
435
  sawTokenCount: boolean;
421
436
  costUsd: number;
437
+ costUsdNoCache: number;
422
438
  sawCost: boolean;
423
439
  firstTsMs?: number;
424
440
  lastTsMs?: number;
@@ -468,7 +484,7 @@ export declare function scanClaudeSession(filePath: string): Promise<ClaudeSessi
468
484
  * exact even when the recent window is smaller than the true count.
469
485
  */
470
486
  export interface ClaudeParserState {
471
- v: 2;
487
+ v: 3;
472
488
  offset: number;
473
489
  jsonlDroppingOversizedLine?: boolean;
474
490
  timestamp?: string;
@@ -487,9 +503,13 @@ export interface ClaudeParserState {
487
503
  toolCallCount: number;
488
504
  tokenCount: number;
489
505
  outputTokens: number;
506
+ inputTokens: number;
507
+ cacheReadTokens: number;
508
+ cacheWriteTokens: number;
490
509
  sawTokenCount: boolean;
491
510
  sawCost: boolean;
492
511
  costUsd: number;
512
+ costUsdNoCache: number;
493
513
  seenIdsSize: number;
494
514
  seenIdsRecent: string[];
495
515
  sawPrCreate: boolean;
@@ -20,6 +20,8 @@ import { parseTimeFilter } from './relative-time.js';
20
20
  const execFileAsync = promisify(execFile);
21
21
  import { AGENTS, agentConfigDirName, getCliVersion } from '../agents.js';
22
22
  import { walkForFilesWithStat } from '../fs-walk.js';
23
+ import { hasCommand } from '../cli-resources.js';
24
+ import { execFileShellSpec } from '../platform/exec.js';
23
25
  import { getConfigSymlinkVersion } from '../shims.js';
24
26
  import { SESSION_AGENTS } from './types.js';
25
27
  import { deriveShortId } from './short-id.js';
@@ -28,7 +30,7 @@ import { extractSessionTopic, extractSlashCommandName, extractSlashCommandFromTo
28
30
  import { isSkillInvocation, extractSkills, extractSlashCommands } from './highlights.js';
29
31
  import { parseAntigravity, parseCursor } from './parse.js';
30
32
  import { extractPrUrl, detectWorktree, detectTicket, isPrCreateCommand, detectSpawnedTeam, isTicketCreateTool, extractCreatedTicket, extractRecentDirectoriesTouched, extractTodoProgressFromEvents } from './state.js';
31
- import { costOfUsage } from '../pricing/index.js';
33
+ import { costOfUsage, costOfUsageNoCache } from '../pricing/index.js';
32
34
  import { machineForSessionFile } from './origin-machine.js';
33
35
  export { machineForSessionFile } from './origin-machine.js';
34
36
  import { mapBounded } from '../concurrency.js';
@@ -1096,7 +1098,11 @@ async function readClaudeMeta(filePath, sessionId, scanStamp, priorRow, label) {
1096
1098
  toolCallCount: scan.toolCallCount,
1097
1099
  tokenCount: scan.tokenCount,
1098
1100
  outputTokens: scan.outputTokens,
1101
+ inputTokens: scan.inputTokens,
1102
+ cacheReadTokens: scan.cacheReadTokens,
1103
+ cacheWriteTokens: scan.cacheWriteTokens,
1099
1104
  costUsd: scan.costUsd,
1105
+ costUsdNoCache: scan.costUsdNoCache,
1100
1106
  durationMs: scan.durationMs,
1101
1107
  isTeamOrigin,
1102
1108
  prUrl: scan.prUrl,
@@ -1130,7 +1136,11 @@ async function readClaudeMeta(filePath, sessionId, scanStamp, priorRow, label) {
1130
1136
  toolCallCount: scan.toolCallCount,
1131
1137
  tokenCount: scan.tokenCount,
1132
1138
  outputTokens: scan.outputTokens,
1139
+ inputTokens: scan.inputTokens,
1140
+ cacheReadTokens: scan.cacheReadTokens,
1141
+ cacheWriteTokens: scan.cacheWriteTokens,
1133
1142
  costUsd: scan.costUsd,
1143
+ costUsdNoCache: scan.costUsdNoCache,
1134
1144
  durationMs: scan.durationMs,
1135
1145
  topic: scan.topic,
1136
1146
  isTeamOrigin,
@@ -1438,7 +1448,11 @@ export async function readCodexMeta(filePath, resolveAccount, currentVersion, sc
1438
1448
  messageCount: scan.messageCount,
1439
1449
  tokenCount: scan.tokenCount,
1440
1450
  outputTokens: scan.outputTokens,
1451
+ inputTokens: scan.inputTokens,
1452
+ cacheReadTokens: scan.cacheReadTokens,
1453
+ cacheWriteTokens: scan.cacheWriteTokens,
1441
1454
  costUsd: scan.costUsd,
1455
+ costUsdNoCache: scan.costUsdNoCache,
1442
1456
  durationMs: scan.durationMs,
1443
1457
  account: resolveAccount?.(),
1444
1458
  prUrl: scan.prUrl,
@@ -1576,8 +1590,11 @@ function readGeminiMeta(filePath, hashDir, projectMap, currentVersion) {
1576
1590
  let messageCount = 0;
1577
1591
  let tokenCount = 0;
1578
1592
  let outputTokens = 0;
1593
+ let inputTokens = 0;
1594
+ let cacheReadTokens = 0;
1579
1595
  let sawTokenCount = false;
1580
1596
  let costUsd = 0;
1597
+ let costUsdNoCache = 0;
1581
1598
  let sawCost = false;
1582
1599
  let firstTsMs;
1583
1600
  let lastTsMs;
@@ -1622,21 +1639,28 @@ function readGeminiMeta(filePath, hashDir, projectMap, currentVersion) {
1622
1639
  (typeof gtk.output === 'number' ? gtk.output : 0) +
1623
1640
  (typeof gtk.thoughts === 'number' ? gtk.thoughts : 0) +
1624
1641
  (typeof gtk.tool === 'number' ? gtk.tool : 0);
1642
+ // Burn split (RUSH-2287): Gemini has uncached input + cache-read, no cache-write.
1643
+ if (typeof gtk.input === 'number')
1644
+ inputTokens += gtk.input;
1645
+ if (typeof gtk.cached === 'number')
1646
+ cacheReadTokens += gtk.cached;
1625
1647
  }
1626
1648
  // Per-message cost: directional tokens × this message's model price.
1627
1649
  const msgModel = (typeof message.model === 'string' ? message.model : undefined) || sessionModel;
1628
1650
  const tk = message.tokens;
1629
1651
  if (msgModel && tk && typeof tk === 'object') {
1630
- const c = costOfUsage({
1652
+ const usage = {
1631
1653
  model: msgModel,
1632
1654
  inputTokens: typeof tk.input === 'number' ? tk.input : undefined,
1633
1655
  outputTokens: (typeof tk.output === 'number' ? tk.output : 0) +
1634
1656
  (typeof tk.thoughts === 'number' ? tk.thoughts : 0) +
1635
1657
  (typeof tk.tool === 'number' ? tk.tool : 0),
1636
1658
  cacheReadTokens: typeof tk.cached === 'number' ? tk.cached : undefined,
1637
- });
1659
+ };
1660
+ const c = costOfUsage(usage);
1638
1661
  if (c > 0) {
1639
1662
  costUsd += c;
1663
+ costUsdNoCache += costOfUsageNoCache(usage);
1640
1664
  sawCost = true;
1641
1665
  }
1642
1666
  }
@@ -1659,7 +1683,10 @@ function readGeminiMeta(filePath, hashDir, projectMap, currentVersion) {
1659
1683
  messageCount,
1660
1684
  tokenCount: sawTokenCount ? tokenCount : undefined,
1661
1685
  outputTokens: sawTokenCount ? outputTokens : undefined,
1686
+ inputTokens: sawTokenCount ? inputTokens : undefined,
1687
+ cacheReadTokens: sawTokenCount ? cacheReadTokens : undefined,
1662
1688
  costUsd: sawCost ? costUsd : undefined,
1689
+ costUsdNoCache: sawCost ? costUsdNoCache : undefined,
1663
1690
  durationMs,
1664
1691
  };
1665
1692
  return { meta, content: userTexts.join('\n') };
@@ -2053,13 +2080,12 @@ async function scanOpenCodeIncremental() {
2053
2080
  // ---------------------------------------------------------------------------
2054
2081
  /** Scan active OpenClaw channels and cron jobs via the openclaw CLI. */
2055
2082
  async function scanOpenClawIncremental() {
2056
- // Check if openclaw is installed — silently skip if not.
2057
- try {
2058
- await execFileAsync('which', ['openclaw']);
2059
- }
2060
- catch {
2083
+ // Check if openclaw is installed — silently skip if not. `which` is POSIX-only
2084
+ // (Windows resolves PATH with `where`), so a bare `which` throws ENOENT on every
2085
+ // Windows run and silently disabled the entire OpenClaw scan there — its sessions
2086
+ // never reached the index (RUSH-2286). hasCommand() probes cross-platform.
2087
+ if (!hasCommand('openclaw'))
2061
2088
  return;
2062
- }
2063
2089
  // TTL cache: skip subprocess calls if we scanned recently. Stored in the
2064
2090
  // meta table so we skip even when no channels/cron exist to produce rows.
2065
2091
  const db = getDB();
@@ -2073,8 +2099,13 @@ async function scanOpenClawIncremental() {
2073
2099
  const scan = { fileMtimeMs: now, fileSize: 0 };
2074
2100
  const entries = [];
2075
2101
  try {
2076
- const { stdout: output } = await execFileAsync('openclaw', ['channels', 'status'], {
2102
+ // On Windows `openclaw` resolves to a .cmd/.ps1 shim that execFile can't launch
2103
+ // directly; execFileShellSpec composes a shell-safe invocation there and is a
2104
+ // no-op passthrough on POSIX (RUSH-2286).
2105
+ const channelsSpec = execFileShellSpec('openclaw', ['channels', 'status']);
2106
+ const { stdout: output } = await execFileAsync(channelsSpec.command, channelsSpec.args, {
2077
2107
  encoding: 'utf-8',
2108
+ shell: channelsSpec.shell,
2078
2109
  });
2079
2110
  for (const line of output.split('\n')) {
2080
2111
  const match = line.match(/^-\s+\w+\s+(\S+)\s+\((\w+)\):\s*(.+)/);
@@ -2103,8 +2134,10 @@ async function scanOpenClawIncremental() {
2103
2134
  /* channels command failed */
2104
2135
  }
2105
2136
  try {
2106
- const { stdout: output } = await execFileAsync('openclaw', ['cron', 'list'], {
2137
+ const cronSpec = execFileShellSpec('openclaw', ['cron', 'list']);
2138
+ const { stdout: output } = await execFileAsync(cronSpec.command, cronSpec.args, {
2107
2139
  encoding: 'utf-8',
2140
+ shell: cronSpec.shell,
2108
2141
  });
2109
2142
  const lines = output.split('\n');
2110
2143
  for (let i = 1; i < lines.length; i++) {
@@ -2610,15 +2643,19 @@ async function readDroidMeta(filePath, currentVersion) {
2610
2643
  const settings = readDroidSettings(filePath.replace(/\.jsonl$/, '.settings.json'));
2611
2644
  const model = settings.model || scan.model;
2612
2645
  const tokenCount = settings.tokenCount;
2613
- const costUsd = model && settings.usage
2614
- ? costOfUsage({
2646
+ // Droid records a full split (input / cache-read / cache-write / output) in its
2647
+ // settings sidecar, so both the actual and no-cache cost are derivable (RUSH-2287).
2648
+ const usageForCost = model && settings.usage
2649
+ ? {
2615
2650
  model,
2616
2651
  inputTokens: settings.usage.inputTokens,
2617
2652
  outputTokens: settings.usage.outputTokens,
2618
2653
  cacheReadTokens: settings.usage.cacheReadTokens,
2619
2654
  cacheCreationTokens: settings.usage.cacheCreationTokens,
2620
- })
2621
- : 0;
2655
+ }
2656
+ : undefined;
2657
+ const costUsd = usageForCost ? costOfUsage(usageForCost) : 0;
2658
+ const costUsdNoCache = usageForCost ? costOfUsageNoCache(usageForCost) : 0;
2622
2659
  const stat = safeStatSync(filePath);
2623
2660
  const cwd = normalizeCwd(scan.cwd || '');
2624
2661
  const meta = {
@@ -2636,7 +2673,11 @@ async function readDroidMeta(filePath, currentVersion) {
2636
2673
  messageCount: scan.messageCount,
2637
2674
  tokenCount,
2638
2675
  outputTokens: settings.usage?.outputTokens,
2676
+ inputTokens: settings.usage?.inputTokens,
2677
+ cacheReadTokens: settings.usage?.cacheReadTokens,
2678
+ cacheWriteTokens: settings.usage?.cacheCreationTokens,
2639
2679
  costUsd: costUsd > 0 ? costUsd : undefined,
2680
+ costUsdNoCache: costUsd > 0 ? costUsdNoCache : undefined,
2640
2681
  durationMs: scan.durationMs,
2641
2682
  };
2642
2683
  return { meta, content: scan.contentText || '' };
@@ -2784,8 +2825,12 @@ export function initClaudeParseState() {
2784
2825
  toolCallCount: 0,
2785
2826
  tokenCount: 0,
2786
2827
  outputTokens: 0,
2828
+ inputTokens: 0,
2829
+ cacheReadTokens: 0,
2830
+ cacheWriteTokens: 0,
2787
2831
  sawTokenCount: false,
2788
2832
  costUsd: 0,
2833
+ costUsdNoCache: 0,
2789
2834
  sawCost: false,
2790
2835
  firstTsMs: undefined,
2791
2836
  lastTsMs: undefined,
@@ -2982,21 +3027,34 @@ export function applyClaudeLine(state, parsed) {
2982
3027
  }
2983
3028
  if (typeof usageObj?.output_tokens === 'number')
2984
3029
  state.outputTokens += usageObj.output_tokens;
3030
+ // Burn split (RUSH-2287): accumulate the raw directional counts so `agents
3031
+ // output` can report uncached-input / cache-read / cache-write separately.
3032
+ if (usageObj && typeof usageObj === 'object') {
3033
+ if (typeof usageObj.input_tokens === 'number')
3034
+ state.inputTokens += usageObj.input_tokens;
3035
+ if (typeof usageObj.cache_read_input_tokens === 'number')
3036
+ state.cacheReadTokens += usageObj.cache_read_input_tokens;
3037
+ if (typeof usageObj.cache_creation_input_tokens === 'number')
3038
+ state.cacheWriteTokens += usageObj.cache_creation_input_tokens;
3039
+ }
2985
3040
  // Per-assistant-message cost: each event carries its own model, so we
2986
3041
  // multiply that event's raw token directions by that model's price.
2987
3042
  const model = parsed.message?.model;
2988
3043
  if (typeof model === 'string' && model)
2989
3044
  state.model = model;
2990
3045
  if (model && usageObj && typeof usageObj === 'object') {
2991
- const eventCost = costOfUsage({
3046
+ const usageForCost = {
2992
3047
  model,
2993
3048
  inputTokens: usageObj.input_tokens,
2994
3049
  outputTokens: usageObj.output_tokens,
2995
3050
  cacheReadTokens: usageObj.cache_read_input_tokens,
2996
3051
  cacheCreationTokens: usageObj.cache_creation_input_tokens,
2997
- });
3052
+ };
3053
+ const eventCost = costOfUsage(usageForCost);
2998
3054
  if (eventCost > 0) {
2999
3055
  state.costUsd += eventCost;
3056
+ // No-cache scenario: reprice this same event's cache tokens at the input rate.
3057
+ state.costUsdNoCache += costOfUsageNoCache(usageForCost);
3000
3058
  state.sawCost = true;
3001
3059
  }
3002
3060
  }
@@ -3026,7 +3084,11 @@ export function finalizeClaudeScan(state) {
3026
3084
  toolCallCount: state.toolCallCount,
3027
3085
  tokenCount: state.sawTokenCount ? state.tokenCount : undefined,
3028
3086
  outputTokens: state.sawTokenCount ? state.outputTokens : undefined,
3087
+ inputTokens: state.sawTokenCount ? state.inputTokens : undefined,
3088
+ cacheReadTokens: state.sawTokenCount ? state.cacheReadTokens : undefined,
3089
+ cacheWriteTokens: state.sawTokenCount ? state.cacheWriteTokens : undefined,
3029
3090
  costUsd: state.sawCost ? state.costUsd : undefined,
3091
+ costUsdNoCache: state.sawCost ? state.costUsdNoCache : undefined,
3030
3092
  durationMs,
3031
3093
  lastActivity: state.lastTsMs !== undefined ? new Date(state.lastTsMs).toISOString() : undefined,
3032
3094
  contentText: state.userTexts.length > 0 ? state.userTexts.join('\n') : undefined,
@@ -3082,7 +3144,7 @@ export function serializeClaudeParserState(state, offset, jsonlDroppingOversized
3082
3144
  : allIds;
3083
3145
  const ticket = detectTicket(state.userTexts.join('\n') || undefined, state.gitBranch);
3084
3146
  return {
3085
- v: 2,
3147
+ v: 3,
3086
3148
  offset,
3087
3149
  jsonlDroppingOversizedLine: jsonlDroppingOversizedLine || undefined,
3088
3150
  timestamp: state.timestamp,
@@ -3101,9 +3163,13 @@ export function serializeClaudeParserState(state, offset, jsonlDroppingOversized
3101
3163
  toolCallCount: state.toolCallCount,
3102
3164
  tokenCount: state.tokenCount,
3103
3165
  outputTokens: state.outputTokens,
3166
+ inputTokens: state.inputTokens,
3167
+ cacheReadTokens: state.cacheReadTokens,
3168
+ cacheWriteTokens: state.cacheWriteTokens,
3104
3169
  sawTokenCount: state.sawTokenCount,
3105
3170
  sawCost: state.sawCost,
3106
3171
  costUsd: state.costUsd,
3172
+ costUsdNoCache: state.costUsdNoCache,
3107
3173
  seenIdsSize: state.seenAssistantIds.size,
3108
3174
  seenIdsRecent,
3109
3175
  sawPrCreate: state.sawPrCreate,
@@ -3163,8 +3229,12 @@ export function hydrateClaudeParseState(prior) {
3163
3229
  toolCallCount: prior.toolCallCount ?? 0,
3164
3230
  tokenCount: prior.tokenCount,
3165
3231
  outputTokens: prior.outputTokens,
3232
+ inputTokens: prior.inputTokens ?? 0,
3233
+ cacheReadTokens: prior.cacheReadTokens ?? 0,
3234
+ cacheWriteTokens: prior.cacheWriteTokens ?? 0,
3166
3235
  sawTokenCount: prior.sawTokenCount,
3167
3236
  costUsd: prior.costUsd,
3237
+ costUsdNoCache: prior.costUsdNoCache ?? 0,
3168
3238
  sawCost: prior.sawCost,
3169
3239
  firstTsMs: prior.firstTsMs,
3170
3240
  lastTsMs: prior.lastTsMs,
@@ -3313,7 +3383,7 @@ function parsePriorClaudeState(row) {
3313
3383
  return null;
3314
3384
  try {
3315
3385
  const parsed = JSON.parse(row.parserState);
3316
- if (parsed?.v !== 2 || typeof parsed.offset !== 'number' || parsed.toolCalls?.v !== 1)
3386
+ if (parsed?.v !== 3 || typeof parsed.offset !== 'number' || parsed.toolCalls?.v !== 1)
3317
3387
  return null;
3318
3388
  return parsed;
3319
3389
  }
@@ -3469,17 +3539,34 @@ export function applyCodexLine(state, parsed) {
3469
3539
  * exact return-building {@link scanCodexSession} used to run inline.
3470
3540
  */
3471
3541
  export function finalizeCodexScan(state) {
3472
- // Price the final cumulative token snapshot once, against the session model.
3542
+ // Codex reports one cumulative snapshot: uncached input, cached (cache-read)
3543
+ // input, and output+reasoning. It has no cache-write bucket. Derive the burn
3544
+ // split and both costs (actual + no-cache) from that final snapshot (RUSH-2287).
3545
+ const snap = state.lastTotalTokenUsage;
3546
+ const outputTokens = snap
3547
+ ? (snap.output_tokens ?? 0) + (snap.reasoning_output_tokens ?? 0)
3548
+ : undefined;
3473
3549
  let costUsd;
3474
- if (state.model && state.lastTotalTokenUsage) {
3475
- const c = costOfUsage({
3550
+ let costUsdNoCache;
3551
+ let inputTokens;
3552
+ let cacheReadTokens;
3553
+ if (snap) {
3554
+ inputTokens = typeof snap.input_tokens === 'number' ? snap.input_tokens : undefined;
3555
+ cacheReadTokens = typeof snap.cached_input_tokens === 'number' ? snap.cached_input_tokens : undefined;
3556
+ }
3557
+ // Price the final cumulative token snapshot once, against the session model.
3558
+ if (state.model && snap) {
3559
+ const usage = {
3476
3560
  model: state.model,
3477
- inputTokens: state.lastTotalTokenUsage.input_tokens,
3478
- outputTokens: (state.lastTotalTokenUsage.output_tokens ?? 0) + (state.lastTotalTokenUsage.reasoning_output_tokens ?? 0),
3479
- cacheReadTokens: state.lastTotalTokenUsage.cached_input_tokens,
3480
- });
3481
- if (c > 0)
3561
+ inputTokens: snap.input_tokens,
3562
+ outputTokens,
3563
+ cacheReadTokens: snap.cached_input_tokens,
3564
+ };
3565
+ const c = costOfUsage(usage);
3566
+ if (c > 0) {
3482
3567
  costUsd = c;
3568
+ costUsdNoCache = costOfUsageNoCache(usage);
3569
+ }
3483
3570
  }
3484
3571
  const durationMs = state.firstTsMs !== undefined && state.lastTsMs !== undefined && state.lastTsMs > state.firstTsMs
3485
3572
  ? state.lastTsMs - state.firstTsMs
@@ -3496,10 +3583,12 @@ export function finalizeCodexScan(state) {
3496
3583
  topic: state.topic,
3497
3584
  messageCount: state.messageCount,
3498
3585
  tokenCount: state.tokenCount,
3499
- outputTokens: state.lastTotalTokenUsage
3500
- ? (state.lastTotalTokenUsage.output_tokens ?? 0) + (state.lastTotalTokenUsage.reasoning_output_tokens ?? 0)
3501
- : undefined,
3586
+ outputTokens,
3587
+ inputTokens,
3588
+ cacheReadTokens,
3589
+ cacheWriteTokens: undefined,
3502
3590
  costUsd,
3591
+ costUsdNoCache,
3503
3592
  durationMs,
3504
3593
  lastActivity: state.lastTsMs !== undefined ? new Date(state.lastTsMs).toISOString() : undefined,
3505
3594
  contentText: state.userTexts.length > 0 ? state.userTexts.join('\n') : undefined,
@@ -4457,10 +4546,13 @@ export function readGrokMeta(filePath, currentVersion) {
4457
4546
  ? summary.num_messages
4458
4547
  : undefined;
4459
4548
  // Grok records its managed home in summary.grok_home
4460
- // (…/versions/grok/<version>/home/.grok) — recover the version from it.
4549
+ // (…/versions/grok/<version>/home/.grok) — recover the version from it. The
4550
+ // value is written by the Grok CLI in the writing host's native separators, so
4551
+ // a Windows-authored summary is backslash-separated; normalize to `/` before
4552
+ // matching or the version never resolves on Windows (RUSH-2286).
4461
4553
  let embeddedVersion;
4462
4554
  if (typeof summary?.grok_home === 'string') {
4463
- embeddedVersion = summary.grok_home.match(/versions\/grok\/([^/]+)\//)?.[1];
4555
+ embeddedVersion = summary.grok_home.replace(/\\/g, '/').match(/versions\/grok\/([^/]+)\//)?.[1];
4464
4556
  }
4465
4557
  const meta = {
4466
4558
  id: sessionId,