Repository navigation
fix(usage): count Codex compaction usage from token_usage_record - #8479
Conversation
…record A remote compaction writes token_usage_record and then a token_count whose last usage is zero. The test expects those tokens to be imported once. Co-authored-by: Cursor <cursoragent@cursor.com>
A zero last_token_usage after token_usage_record is the compaction call that token_count never bills. Keep that row off event_index so later requests stay put, and reread unchanged files once so an existing cursor does not leave the row behind. Co-authored-by: Cursor <cursoragent@cursor.com>
farion1231
left a comment
There was a problem hiding this comment.
Thanks for the fix — the pairing between token_usage_record and the following token_count looks right. Replaying a copy of my local ~/.codex (3,038 files) against main, this PR only adds the 102 missing compaction rows and leaves every other row unchanged.
One change before merging: please drop the upgrade backfill (the codex_token_usage_record_backfill marker and the forced reread of already-scanned files) and let the new logic apply only to lines read after the upgrade. Existing cursors stay where they are, so compaction usage from before the upgrade stays as it is on main.
The reasons:
- The first sync after upgrading rereads every already-scanned file. On those 3,038 files that took about 70 s in a debug build.
- The marker is written only when the whole pass succeeds. If one file keeps failing to parse, the marker is never written and the full reread repeats on every periodic sync.
- For users who also run the local route, rows older than 30 days have already been rolled into
usage_daily_rollups, so the backfilled detail rows count that usage a second time.
Removing that layer also removes its tests, so the change should get noticeably smaller.
A forced reread of already-scanned files counted compaction rows twice once they had rolled into usage_daily_rollups. Co-authored-by: Cursor <cursoragent@cursor.com>
|
Dropped the upgrade backfill in 3c97d68. |
farion1231
left a comment
There was a problem hiding this comment.
LGTM, thanks for your contribution!
|
Thanks for tracking this down! Compaction calls showing up as 0 tokens was an easy one to miss, and pairing each Two small edge cases I noticed, neither blocking, in case you'd like to follow up:
Merged, thanks again! |
* fix(tray): keep tray panel rows stable and fit scaled displays (farion1231#8572) Size the provider column to the main panel's height and scroll long lists inside it, so hovering between apps no longer shifts rows under the cursor. Convert between WebView CSS pixels and screen logical pixels using the monitor scale factor on Windows, narrow and clamp the panel to the monitor work area, and let footer buttons shrink. On Windows, left-click on the tray icon now always opens the main window; right-click shows the selected tray style. Fixes farion1231#8553 Fixes farion1231#8549 * fix(tray): refresh DPI coordinates after cross-monitor show (farion1231#8595) * fix(windows): remove stale portable marker during MSI installation (farion1231#8529) * fix(usage): count Codex compaction usage from token_usage_record (farion1231#8479) * fix(provider-form): stop macOS from capitalizing typed API keys The API key field is a plain input, so it missed the autocorrect, auto-capitalization and spellcheck opt-outs the shared Input component already sets. With the key shown in plain text, a typed "sk-..." key could be saved as "Sk-..." and fail authentication. Pasted keys were not affected. * fix(tray): align quota bars and show time until reset Lay out each quota block as one grid (label | bar | reset) so bars start and end at the same x across tiers, and let the quota row extend under the chevron / "In use" column so bars are no longer squeezed. Replace the absolute reset date with a compact time-until-reset (at most two units, e.g. "1d 23h"), with unit strings added to all panel text branches, and keep quota labels from wrapping. * feat(tray): mark the balance amount in panel quota lines Carry an `emphasis` field on tray quota lines and the panel payload, set to the amount only when a pay-as-you-go balance is left (matching the card's `balanceLine`), so the panel can color just the number. * feat(tray): show remaining balances in green in the tray panel Color only the balance amount green, like the provider card's quota line, while the label stays muted and a used-up balance keeps its warning color. * fix(proxy): stop child processes from inheriting the listener socket on Windows On Windows the proxy port could stay LISTENING after CC Switch exited, owned by the PID of the dead process, so the next start failed to bind with os error 10048 and fell back to direct mode until a reboot or a port change. The listener comes from tokio -> mio, and the locked mio 1.1.1 created Windows sockets with plain socket(), which yields an inheritable handle. CC Switch spawns long-lived children (terminals via `cmd /c start`, CLI tools), and std's Command passes inheritable handles to them, so those children kept the listening socket open after CC Switch was gone. Bump mio to 1.2.1, which creates sockets with WSA_FLAG_NO_HANDLE_INHERIT (tokio-rs/mio#1946), and add a Windows-only test asserting the listener handle is not inheritable so the lockfile cannot silently regress. Refs farion1231#8552 * fix(webdav): upload the sync manifest as application/octet-stream CSTCloud Data Capsule's WebDAV server answers PUT manifest.json with 500 "Uploaded object size does not match the upload session" whenever the body is multi-line JSON sent as application/json. The same pretty JSON is stored byte-for-byte when sent as application/octet-stream, and db.sql / skills.zip already upload fine with their own types. Send the manifest with application/octet-stream. The manifest bytes and hash stay the same and readers never look at Content-Type, so old and new versions keep reading each other's manifests. S3 sync is unchanged. Refs farion1231#8554 * fix(toml): only normalize smart quotes used as string delimiters normalizeTomlText replaced every curly/fullwidth quote in the text before TOML parsing, including quotes inside already-valid strings. MCP import and validation (tomlUtils, mcpDraft) and the Codex config editor therefore silently rewrote values such as args = ["don’t"] to "don't", and rejected valid configs like args = ["say“hello”"] or args = ['don’t'] because the inner quotes became delimiters. Scan the text instead: smart quotes outside strings are delimiters and become ASCII; strings opened by an ASCII quote keep smart quotes as content; strings opened by a smart quote may close with either form; multi-line strings and comments are handled. To keep the Chinese IME case working (the editor normalizes each keystroke, so the left quote is already ASCII when the right smart quote is typed), an ASCII-opened string left unterminated at end of line is re-scanned with smart closers, and if the strict result still fails to parse, a lenient scan is used only when it parses. Escaped smart double quotes in basic strings become \". Text without smart quotes is returned unchanged. Refs farion1231#8559 * fix(codex): don't give an expired stashed login back to the proxy Since farion1231#8431, entering or attaching the proxy for a third-party Codex route gives back the most recently stashed ChatGPT login when auth.json has none. The stash can hold a login set aside months ago by a direct switch with "keep official login" off. Restoring it made the route table require OpenAI auth, Codex desktop tried to refresh the dead token and got stuck on the sign-in page, and every exit/enter put it back again. The restore now skips a login whose access_token JWT has already expired. Codex refreshes the token every 8 days and it lives about 10, so an expired one means the login has not been used for a long time. Tokens that cannot be decoded are still restored, and the skipped login stays in the stash so switching back to an official card returns it. Refs farion1231#8573 * fix(mcp): project url-only servers without a type to Hermes and OpenCode A unified MCP spec can lack a `type` field when it was pasted in JSON mode or created via a deeplink, e.g. `{"url": "https://..."}`. Hermes' and OpenCode's projections defaulted a missing type to stdio, whose branch only copies command/args/env. Hermes therefore got an entry with nothing but `enabled: true`, and OpenCode got `type: local` with an empty command, while the UI reported a successful sync. Codex's projection already handled this in `spec_transport_type` (missing or stdio type, no command, non-empty url => http). Move that helper unchanged into `mcp/validation.rs` and use it from the Codex, Hermes and OpenCode projections, so all three infer the transport the same way. Add regression tests for url-only and command-only specs. Refs farion1231#8579 * fix(mcp): take the app's switch lock before toggling a server toggle_mcp_app and set_mcp_enabled called McpService::toggle_app without the per-app switch lock. Each single-server write reads the app's live config, edits it and writes it back, so two toggles in flight at once (or a toggle racing a provider switch, a proxy enter or an MCP resync, which all hold that lock) could overwrite each other and silently drop servers from ~/.claude.json, config.toml and friends while the database still showed them enabled. Add McpService::toggle_app_locked, which takes the same switch lock resync_mcp_to_apps already uses and then runs toggle_app, and route both commands through it. toggle_app itself stays lock-free because the tokio lock is not reentrant and internal callers may already hold it; MiniMax Code and Pi keep their own file lock inside sync_and_commit, which never takes the switch lock. A test fires 16 concurrent toggles at Claude and checks every server lands in the live file (it fails without the lock). Refs farion1231#8580 * fix(tray): release the tray panel WebView in lightweight mode Lightweight mode frees memory by destroying the main window's WebView. Since the tray panel became the default tray UI, the panel is a second WebView window that is created on the first tray click and afterwards only hidden, so a WebView stays alive and on Windows the WebView2 processes never exit after entering lightweight mode. In lightweight mode the panel is now destroyed whenever it is hidden (and right away when entering lightweight mode while it is already hidden); the next tray click creates it again. The destroy is deferred to a later main-thread turn and re-checks that the panel is still hidden and not about to be revealed, so it never tears the WebView down inside its own window-event callback or IPC command. toggle() decides "close" before creating the window so a click that only closes the panel does not recreate it, and a recreated window starts with a fresh theme state and, on macOS, fresh glass views. Refs farion1231#8612 * feat(hermes): warn when a provider key is a Hermes built-in provider id Hermes routes model.provider to its built-in provider whenever the name is a canonical built-in id (runtime_provider_custom._shadowed_by_builtin), so a card keyed "deepseek" or "openrouter" is ignored: Hermes asks for the built-in's own API key env var and never uses the card's endpoint or key. farion1231#8538 only fixed the two MiniMax presets by prefilling cc-switch-minimax. The provider key field now shows a warning for any of the 52 canonical ids (generated from hermes-agent rc.3-v0.22.0) and suggests cc-switch-<key>. A locked key gets a variant that says to re-add the provider under another key. Aliases such as kimi still reach the custom provider and are not flagged. Saving is not blocked. * chore(release): v4.0.17 * docs(release): add v4.0.17 release notes * docs: record the v4.0.17 upstream sync The ledger lists the three version conflicts, the patches that stayed, and the Windows checks still left for a real machine. The owner-line script now compares v4.0.16 with v4.0.17. Co-authored-by: Chris-Xie369 <Chris-Xie369@users.noreply.github.com> * test(codex): follow one parent file in the timeline cache cargo test runs these tests in one process, so another test can put a second path in the parent timeline map. The checks now follow this file. Two cutoffs still have to reuse the same timeline. Co-authored-by: Chris-Xie369 <Chris-Xie369@users.noreply.github.com> * docs: record the v4.0.17 test results The ledger now has the Linux test counts and the Codex cache assertion that failed when cargo test shared one process. Co-authored-by: Chris-Xie369 <Chris-Xie369@users.noreply.github.com> * docs: point the owner-line script at the v4.0.16 base The comparison tag was already v4.0.16. The opening sentence still named v4.0.14. Co-authored-by: Chris-Xie369 <Chris-Xie369@users.noreply.github.com> * fix(codex): keep compaction usage on one thread A child rollout imported a replayed remote-compaction record again, and a usage line scanned before its confirming token count was dropped on the next pass. The event now uses the confirming line, and a replayed confirmation is skipped. Co-authored-by: Chris-Xie369 <Chris-Xie369@users.noreply.github.com> --------- Co-authored-by: funkpopo <funkpopoisme@gmail.com> Co-authored-by: chonngyi <111365239+chonngyi@users.noreply.github.com> Co-authored-by: Chaoqi <136579807+Chaoqi31@users.noreply.github.com> Co-authored-by: Jason <farion1231@gmail.com> Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Chris-Xie369 <Chris-Xie369@users.noreply.github.com>
* fix(tray): keep tray panel rows stable and fit scaled displays (farion1231#8572) Size the provider column to the main panel's height and scroll long lists inside it, so hovering between apps no longer shifts rows under the cursor. Convert between WebView CSS pixels and screen logical pixels using the monitor scale factor on Windows, narrow and clamp the panel to the monitor work area, and let footer buttons shrink. On Windows, left-click on the tray icon now always opens the main window; right-click shows the selected tray style. Fixes farion1231#8553 Fixes farion1231#8549 * fix(tray): refresh DPI coordinates after cross-monitor show (farion1231#8595) * fix(windows): remove stale portable marker during MSI installation (farion1231#8529) * fix(usage): count Codex compaction usage from token_usage_record (farion1231#8479) * fix(provider-form): stop macOS from capitalizing typed API keys The API key field is a plain input, so it missed the autocorrect, auto-capitalization and spellcheck opt-outs the shared Input component already sets. With the key shown in plain text, a typed "sk-..." key could be saved as "Sk-..." and fail authentication. Pasted keys were not affected. * fix(tray): align quota bars and show time until reset Lay out each quota block as one grid (label | bar | reset) so bars start and end at the same x across tiers, and let the quota row extend under the chevron / "In use" column so bars are no longer squeezed. Replace the absolute reset date with a compact time-until-reset (at most two units, e.g. "1d 23h"), with unit strings added to all panel text branches, and keep quota labels from wrapping. * feat(tray): mark the balance amount in panel quota lines Carry an `emphasis` field on tray quota lines and the panel payload, set to the amount only when a pay-as-you-go balance is left (matching the card's `balanceLine`), so the panel can color just the number. * feat(tray): show remaining balances in green in the tray panel Color only the balance amount green, like the provider card's quota line, while the label stays muted and a used-up balance keeps its warning color. * fix(proxy): stop child processes from inheriting the listener socket on Windows On Windows the proxy port could stay LISTENING after CC Switch exited, owned by the PID of the dead process, so the next start failed to bind with os error 10048 and fell back to direct mode until a reboot or a port change. The listener comes from tokio -> mio, and the locked mio 1.1.1 created Windows sockets with plain socket(), which yields an inheritable handle. CC Switch spawns long-lived children (terminals via `cmd /c start`, CLI tools), and std's Command passes inheritable handles to them, so those children kept the listening socket open after CC Switch was gone. Bump mio to 1.2.1, which creates sockets with WSA_FLAG_NO_HANDLE_INHERIT (tokio-rs/mio#1946), and add a Windows-only test asserting the listener handle is not inheritable so the lockfile cannot silently regress. Refs farion1231#8552 * fix(webdav): upload the sync manifest as application/octet-stream CSTCloud Data Capsule's WebDAV server answers PUT manifest.json with 500 "Uploaded object size does not match the upload session" whenever the body is multi-line JSON sent as application/json. The same pretty JSON is stored byte-for-byte when sent as application/octet-stream, and db.sql / skills.zip already upload fine with their own types. Send the manifest with application/octet-stream. The manifest bytes and hash stay the same and readers never look at Content-Type, so old and new versions keep reading each other's manifests. S3 sync is unchanged. Refs farion1231#8554 * fix(toml): only normalize smart quotes used as string delimiters normalizeTomlText replaced every curly/fullwidth quote in the text before TOML parsing, including quotes inside already-valid strings. MCP import and validation (tomlUtils, mcpDraft) and the Codex config editor therefore silently rewrote values such as args = ["don’t"] to "don't", and rejected valid configs like args = ["say“hello”"] or args = ['don’t'] because the inner quotes became delimiters. Scan the text instead: smart quotes outside strings are delimiters and become ASCII; strings opened by an ASCII quote keep smart quotes as content; strings opened by a smart quote may close with either form; multi-line strings and comments are handled. To keep the Chinese IME case working (the editor normalizes each keystroke, so the left quote is already ASCII when the right smart quote is typed), an ASCII-opened string left unterminated at end of line is re-scanned with smart closers, and if the strict result still fails to parse, a lenient scan is used only when it parses. Escaped smart double quotes in basic strings become \". Text without smart quotes is returned unchanged. Refs farion1231#8559 * fix(codex): don't give an expired stashed login back to the proxy Since farion1231#8431, entering or attaching the proxy for a third-party Codex route gives back the most recently stashed ChatGPT login when auth.json has none. The stash can hold a login set aside months ago by a direct switch with "keep official login" off. Restoring it made the route table require OpenAI auth, Codex desktop tried to refresh the dead token and got stuck on the sign-in page, and every exit/enter put it back again. The restore now skips a login whose access_token JWT has already expired. Codex refreshes the token every 8 days and it lives about 10, so an expired one means the login has not been used for a long time. Tokens that cannot be decoded are still restored, and the skipped login stays in the stash so switching back to an official card returns it. Refs farion1231#8573 * fix(mcp): project url-only servers without a type to Hermes and OpenCode A unified MCP spec can lack a `type` field when it was pasted in JSON mode or created via a deeplink, e.g. `{"url": "https://..."}`. Hermes' and OpenCode's projections defaulted a missing type to stdio, whose branch only copies command/args/env. Hermes therefore got an entry with nothing but `enabled: true`, and OpenCode got `type: local` with an empty command, while the UI reported a successful sync. Codex's projection already handled this in `spec_transport_type` (missing or stdio type, no command, non-empty url => http). Move that helper unchanged into `mcp/validation.rs` and use it from the Codex, Hermes and OpenCode projections, so all three infer the transport the same way. Add regression tests for url-only and command-only specs. Refs farion1231#8579 * fix(mcp): take the app's switch lock before toggling a server toggle_mcp_app and set_mcp_enabled called McpService::toggle_app without the per-app switch lock. Each single-server write reads the app's live config, edits it and writes it back, so two toggles in flight at once (or a toggle racing a provider switch, a proxy enter or an MCP resync, which all hold that lock) could overwrite each other and silently drop servers from ~/.claude.json, config.toml and friends while the database still showed them enabled. Add McpService::toggle_app_locked, which takes the same switch lock resync_mcp_to_apps already uses and then runs toggle_app, and route both commands through it. toggle_app itself stays lock-free because the tokio lock is not reentrant and internal callers may already hold it; MiniMax Code and Pi keep their own file lock inside sync_and_commit, which never takes the switch lock. A test fires 16 concurrent toggles at Claude and checks every server lands in the live file (it fails without the lock). Refs farion1231#8580 * fix(tray): release the tray panel WebView in lightweight mode Lightweight mode frees memory by destroying the main window's WebView. Since the tray panel became the default tray UI, the panel is a second WebView window that is created on the first tray click and afterwards only hidden, so a WebView stays alive and on Windows the WebView2 processes never exit after entering lightweight mode. In lightweight mode the panel is now destroyed whenever it is hidden (and right away when entering lightweight mode while it is already hidden); the next tray click creates it again. The destroy is deferred to a later main-thread turn and re-checks that the panel is still hidden and not about to be revealed, so it never tears the WebView down inside its own window-event callback or IPC command. toggle() decides "close" before creating the window so a click that only closes the panel does not recreate it, and a recreated window starts with a fresh theme state and, on macOS, fresh glass views. Refs farion1231#8612 * feat(hermes): warn when a provider key is a Hermes built-in provider id Hermes routes model.provider to its built-in provider whenever the name is a canonical built-in id (runtime_provider_custom._shadowed_by_builtin), so a card keyed "deepseek" or "openrouter" is ignored: Hermes asks for the built-in's own API key env var and never uses the card's endpoint or key. farion1231#8538 only fixed the two MiniMax presets by prefilling cc-switch-minimax. The provider key field now shows a warning for any of the 52 canonical ids (generated from hermes-agent rc.3-v0.22.0) and suggests cc-switch-<key>. A locked key gets a variant that says to re-add the provider under another key. Aliases such as kimi still reach the custom provider and are not flagged. Saving is not blocked. * chore(release): v4.0.17 * docs(release): add v4.0.17 release notes * feat(agy): integrate Antigravity CLI management (farion1231#8268) Add Antigravity CLI (agy) as a managed app: provider switching for the native login and API-key modes, MCP and Skills sync through ~/.gemini/config, session browsing and session-log usage import. Co-authored-by: peterLwk888 <lwk1124@outlook.com> * refactor(agy): launch API-key providers from the terminal instead of a shell wrapper agy only reads GEMINI_API_KEY and GOOGLE_GEMINI_BASE_URL from the process environment, and modelProvider is global. The previous approach wrote the key to a file, installed a wrapper script and edited .zshenv/.zprofile/.zshrc so every agy picked it up. Now the native settings.json always stays in login mode: login cards set the model (display name) and drop modelProvider; key cards only drop modelProvider. A key card applies to agy started from the card's "Open Terminal": `agy --gemini_dir=<private dir>` under ~/.cc-switch/agy/<id>, whose settings carry modelProvider and the model and whose config links back to ~/.gemini/config so MCP, Skills and prompts stay shared. The key and endpoint reach that one process through the environment only. The terminal launcher now takes a per-app launch description (command, env, temp files) so Claude Code and agy share the terminal layer. On macOS/Linux env values are read from a 0600 file that is deleted right away and passed via `env`, so values exported by the user's rc files cannot override them; on Windows they live in a setlocal block. Claude's temporary settings file is now written 0600 as well, and temp files are removed before handing over to the interactive shell (exec skipped the EXIT trap). On startup, wrappers, env files and PATH blocks left by earlier test builds are removed (only CC Switch-marked content) and the current provider is reapplied so modelProvider returns to login mode. * feat(agy): show the model on cards and attribute session usage to it Ported from farion1231#8608: - Model discovery works before agy has written a settings.json; the official login lives outside that file. - Configurations imported from live are marked official: the key is never on disk, so an import is always agy's own login. - Antigravity provider cards show the configured model. - Session usage rows take their model from the `Model Selection` step agy injects at the start of a conversation, since responses carry no model. Not ported: running `agy models` while importing on startup or when the form opens. It takes several seconds on a signed-in machine and may prompt for sign-in on one that never logged in; the Fetch Models button stays explicit. Co-authored-by: ajia1206 <491387123@qq.com> * feat(agy): share prompts with key profiles, resume key sessions, price usage - Store the agy prompt in ~/.gemini/config/AGENTS.md, which agy reads as a standalone customization root and every private key profile links to. - List sessions recorded under each key profile and resume them with the matching --gemini_dir and the card's key in a launched terminal. - Fetch the model list for API-key cards from agy's own offline catalog in a throwaway profile, so the form offers display names agy actually accepts. - Record agy session usage under priced model IDs (display name kept as the request model) and estimate cost from Gemini API prices. - Treat an imported settings file with a model as the official login card. * feat(agy): show the signed-in quota on the official card - Ask agy's own /usage command (agy -p /usage --output-format json) instead of reading credentials; it makes no model call and opens no conversation. - Keep the Gemini group and the Claude/GPT group, each with its 5-hour and weekly window, as four tiers; unknown buckets are skipped. - On the card each group shows only its window with the least left, since four tiers do not fit; the expanded view lists all four. - A missing agy is treated as no login (nothing shown); a timeout propagates so the last good value is kept; other failures do not echo agy's output. * fix(agy): show only the typed prompt in sessions and describe the quota check - agy stores every prompt wrapped in <USER_REQUEST>, followed by context it adds itself (<ADDITIONAL_METADATA>, <USER_SETTINGS_CHANGE>). Session titles and user messages now show only the text inside <USER_REQUEST>; an empty request keeps the original content. - The official-quota hint said CC Switch reads the CLI's OAuth credentials. For agy it runs agy's own /usage command instead, so the usage dialog now says that (and that a check takes about 10 seconds). * feat(agy): give Antigravity its own icon and manage the CLI on the Apps page - Use the Antigravity logo for the app instead of reusing Gemini's. - List Antigravity CLI on the Apps page: install with the official script, update with `agy update` (also for the Homebrew cask, which is auto_updates), read the latest version from the release manifest, and uninstall official-script and cask installs. - Find install.ps1's %LOCALAPPDATA%\agy\bin on Windows. * fix(agy): accept the empty mcp_config.json agy creates on first run agy writes a zero-byte ~/.gemini/config/mcp_config.json the first time it starts. Parsing it as JSON failed, so enabling any MCP server for Antigravity showed "Invalid Antigravity MCP JSON" and importing from it failed too. Treat a blank file as having no servers. * feat(agy): replace the enable button with Open Terminal CC Switch can only reach the agy it starts itself: a third-party key travels in that process's environment, so "enabling" a key card never changed what a plain `agy` used. Antigravity cards now only launch. - Every card's primary action is Open Terminal; there is no "in use" state, the official quota shows on official cards at all times, and any card can be deleted. - Official cards start `agy --model=<display name>`, which applies to that session only. Key cards keep their private --gemini_dir. - The real ~/.gemini/antigravity-cli/settings.json is never written any more: switching is refused, adding the first card, full syncs and deep links with enabled=true leave it alone. The only remaining write removes the modelProvider left behind by the farion1231#8268 preview. - The terminal dialog and form hints no longer talk about switching. * feat(agy): trim the form hint and require a key on custom cards The long paragraph above the Antigravity form explained a case that should not exist: a non-official card without a key is just the official login card again. Drop the paragraph and warn on save when a custom card has no API key, as Gemini CLI already does. A key without an endpoint stays valid and uses Google's default API. The endpoint and model rules move into "?" tips next to their fields. The card's terminal button now reads "Open Terminal with This Provider"; the shorter "Open Terminal" label stays for the Claude Code card menu. * fix(windows): stop in-app updates from rebooting the machine (farion1231#8628) The MSI now carries REBOOT=ReallySuppress, so Windows Installer never restarts the machine when a file is in use during an in-app update, including updates driven by older updaters. The updater also passes REBOOT=ReallySuppress, a leftover instance is closed on the updater path, and the release workflow fails if the built MSI lacks the property. Fixes farion1231#8534 * fix(ui): show compact mode-colored connection badge in header (farion1231#8634) * fix(ui): trim redundant provider and settings helper copy (farion1231#8633) * fix(installer): make Windows desktop shortcut optional (farion1231#8601) Add a "Create a desktop shortcut" checkbox to the MSI install-directory page and a public CREATEDESKTOPSHORTCUT property for silent installs. Upgrades keep the current state of the desktop shortcut, and clearing the option during an upgrade removes the old one. The desktop shortcut is now copied from the Start Menu shortcut by custom actions with Return="ignore", so a Desktop that cannot be written no longer blocks or rolls back the install. Fixes farion1231#5667 Co-authored-by: thefool <thefoolyuzhe@gmail.com> * docs(aggregation): correct the workaround for the exhausted-quota lock Since route and aggregation mode keep the ChatGPT login (farion1231#8431), the route table writes requires_openai_auth = true whenever a login is on disk, so making a third-party card the aggregation default no longer keeps Codex from locking the send button to Luna Reserve once the official 5-hour quota runs out. The guide and the 4.6 manual pages now describe the method that still works: switch to Direct, keep "Keep official login for direct switches" off, switch to a third-party card and fully restart Codex; switch back to Aggregation once the quota resets. The old method is marked as no longer working. Refs farion1231#7962 * fix(usage): only list apps shown on the Apps page in the app filter The usage dashboard listed every known app as a filter chip regardless of the visibility switches on the Apps page. Filter the chips the same way the session manager does: Claude Code stays while Claude Desktop is shown, and the currently selected app is kept even if it has been hidden. * feat(agy): manage Antigravity official accounts in the Auth Center backend agy keeps a single global login in the system keychain (service "gemini", account "antigravity"). CC Switch now keeps its own copy of each account and switches agy by writing the chosen one back into that entry. - Sign in through the browser with agy's own OAuth client (PKCE, loopback callback on 127.0.0.1), requesting the same scopes agy does, so agy can keep refreshing the token on its own. - Read and write the keychain through /usr/bin/security, the program the entry already trusts: the value goes over `security -i` stdin, `-U` updates in place and keeps the entry's access control, and the write is read back and checked. - Before overwriting, take back the current login: refresh tokens agy renewed go to their account, an unknown account is added, and an unreadable value is stashed to agy-login-stash.json. - Removing an account only drops CC Switch's copy; agy is never signed out. - Exposed as the "agy_oauth" provider of the generic auth_* commands, where the default account is the one agy is signed in with. macOS only for now. * feat(agy): add an Antigravity group to the Auth Center On macOS the Auth Center now lists Antigravity accounts next to Copilot, ChatGPT and xAI. The account agy is signed in with is tagged "In use by agy", and "Switch agy to this account" in the row menu replaces agy's sign-in. After switching, CC Switch checks whether agy is running and warns that open sessions keep the old account and may write it back. Adding an account opens Google's sign-in page in the browser. The shared login hook treats a response without a user code as this browser flow: it leaves the clipboard alone, polls the local result every 1.5 seconds and cancels the backend flow so the callback port is released. The pending block shows a "reopen sign-in page" button instead of a device code. Removing an Antigravity account says that agy stays signed in. The backend now records the sign-in time in seconds, which is what the Auth Center formats. * feat(agy): bind official cards to an Antigravity account An official Antigravity card can now pick one of the Auth Center accounts (macOS only). With an account picked, Open Terminal first switches agy's global sign-in to it; when it is already signed in, nothing is written. Without one, the card keeps following whatever agy is signed in with. The binding is stored as authBinding with authProvider "agy_oauth". The account id is the Google subject, so a card synced to another computer works once the same account is added there; if it is missing, opening the terminal stops with a message instead of launching under another account. Platforms that cannot switch ignore the binding and keep it on the card. The Open Terminal dialog says when opening will switch agy's sign-in, and warns when agy is already running. The Auth Center counts bound cards as users of an account, so the removal dialog lists them. * fix(pricing): price deepseek-v4.1-flash as V4.1 Flash Third-party upstreams such as OpenCode Go, OpenRouter (deepseek/ namespace) and the Hermes presets call DeepSeek V4.1 Flash deepseek-v4.1-flash. Pricing had no row for that id: none of the lookup fallbacks turn it into deepseek-flash, and the prefix fallback only matches longer ids. These requests showed "Unpriced" and were counted at $0. Add a deepseek-v4.1-flash row at the same official peak price as deepseek-flash (0.3 / 1.2 / 0.006 per million). Default prices are added again on every start, so existing installs get the row on next launch. Requests already logged are not repriced. * feat(agy): query each Antigravity account's quota directly The Auth Center can now show quota for every Antigravity account, not only the one agy is signed in with. The new agy_account_quota command calls Google's retrieveUserQuotaSummary with the account's own token, so no switch is needed. The response carries the same buckets as agy's /usage, and both now go through one tier builder. The endpoint only answers clients that identify as Antigravity, so the User-Agent is "cc-switch/<version> (antigravity)": it names CC Switch and adds the word the server checks for. It only reads quota; it never calls a model, warms up accounts or switches on low quota. Tokens that expire within two minutes are refreshed first. The refreshed token is saved only to CC Switch's copy, never to the keychain: agy keeps refreshing its own login, and Google does not rotate the refresh token, so both access tokens stay valid. For the account agy is using, the keychain login is absorbed first so its newer token is used. A rejected token triggers one forced refresh; invalid_grant marks the account as needing sign-in again. * fix(agy): explain why an account is not eligible for Antigravity Accounts that Google does not allow to use Antigravity, such as those restricted by age, get the same 403 "You do not have a valid license, contact your administrator" from the quota endpoint. That reads like a CC Switch or licensing problem. On a 403 the quota query now asks loadCodeAssist once, and when the account only has ineligible tiers it shows Google's own reason instead. * feat(agy): show each account's quota in the Auth Center Every Antigravity row in the Auth Center now has a quota column, the same one Copilot, ChatGPT and xAI rows use: Gemini and Claude/GPT, each with a 5-hour and a weekly window. It is queried once when the page opens and on the refresh button, through agy_account_quota, so accounts agy is not signed in with show quota too. Accounts that need to sign in again get no quota column. * perf(agy): query the signed-in account's quota without starting agy Official Antigravity cards asked agy's own /usage command for quota, which starts agy and takes seven to nine seconds. On macOS the card now takes the account agy is signed in with from the keychain, adds it to the Auth Center if it is new, and asks Google directly with CC Switch's copy of the token, the same way the Auth Center does (about one second). agy's /usage is still used when the keychain holds no recognizable login, when the user removed that account from CC Switch, and on platforms that cannot switch accounts. * feat(agy): show the bound account's quota on official cards An official Antigravity card bound to an Auth Center account showed the quota of whatever account agy was signed in with, which is a different account until the card's terminal is opened. It now shows the bound account's quota, shared with that account's row in the Auth Center, and like Codex account cards it shows even when the card is not current. Cards that follow agy's sign-in, and platforms that cannot switch accounts, keep the signed-in quota. * feat(agy): open all four quota windows from the card's quota column The card keeps one line per group (Gemini, Claude/GPT) to fit, so the 5-hour and weekly windows could not both be seen anywhere on it. On Antigravity cards the quota column is now one dropdown, like the resets dropdown on Codex official cards, listing every window with its own reset time. Refreshing moves to the ↻ button that appears on hover. Other cards keep click-to-refresh. * fix(agy): drop the Antigravity-only note on the usage page No other app shows a pricing note above the usage tables, although Codex, Gemini and the rest also estimate session-log amounts from API prices. Remove the note, its locale key and the test that pinned it. * fix(agy): show Antigravity session-log rows as "Session log" The frontend map of session placeholder names missed "Antigravity (Session)", so the provider column showed the raw English name and truncated it. Add the entry and a test that every placeholder name in usage_stats.rs is translated. * feat(presets): promote EveryAPI to a full sponsor EveryAPI moves from the basic tier (in-app presets and website only) to a full sponsor, placed directly below OpenModel in the sponsor order. - Four-locale README sponsor rows with the new copy and a 1200px banner. - The EveryAPI preset blocks in the seven preset files move below OpenModel so file order matches the README tables; the presets themselves are unchanged. - The in-app promo copy adds "GPT from 15% of the official price" and folds the first top-up bonus and the "ccswitch" code into one perk, since they are the same offer. * chore(presets): remove ZetaAPI sponsor and presets ZetaAPI is no longer a sponsor; its presets go too. - Drop the four-locale README sponsor rows and the banner. - Remove the ZetaAPI presets from Claude Code, Claude Desktop, Codex, Grok Build, OpenCode, OpenClaw, Hermes and Pi. - Remove partnerPromotion.zetaapi from every locale, Turkish included. - Keep the zetaapi icon: existing providers saved from the preset still reference that icon key. * feat(presets): add DIT.ai sponsor presets across nine apps DIT.ai (dit.ai), an AI token exchange routing each request across competing providers, joins as a sponsor, placed directly below GrooRoute Pro in the sponsor order (OpenModel in the Gemini file, which has no GrooRoute preset). - One key on api.dit.ai serves Chat Completions, Responses, Anthropic Messages and native Gemini (protocol docs at dit.ai/docs, catalog at dit.ai/models, checked 2026-10-21). - The gateway has no Haiku 5.5 and only the dated claude-haiku-4-5-20251001, so Claude Code overrides the Haiku tier with it and Claude Desktop maps Sonnet 5.5 / Opus 5.5 / that Haiku. - OpenCode, OpenClaw, Hermes and Pi use Anthropic Messages with Claude Opus 5.5 and Sonnet 5.5; Codex uses Responses with gpt-5.6-sol, Grok Build grok-4.7, Gemini CLI gemini-3.8-flash. - Four-locale README sponsor rows with a banner, four-locale promo copy with the "ccswitch" coupon, and an inline icon from the official logo. - Claude preset rows in the selector test go from 105 to 106. * feat(presets): add hao.ai sponsor presets across eight apps Add hao.ai as a full sponsor placed above Fenno: README rows and banner in all four languages, the partner promotion copy, an inline tile icon, and presets for Claude Code, Claude Desktop, Codex, Grok Build, OpenCode, OpenClaw, Hermes and Pi. hao.ai serves Anthropic Messages under /anthropic, OpenAI-compatible APIs under /v1 and xAI under /x-ai/v1, and its catalog IDs carry a provider prefix. There is no Gemini preset since the catalog has no Gemini models. * fix(codex): hide reset countdown for unused quota windows (farion1231#8631) * fix(sessions): group a Claude session by its last /cd location (farion1231#8643) * fix(terminal): stop waiting for the launched console to close on Windows Opening a terminal on Windows ran `cmd /C start ...` through Command::output(). The console that `start` opens inherits the wrapper's stdout/stderr pipe, so the pipe only reached EOF once the user closed that window, and the command (and the open-terminal dialog) kept spinning until then. Point stdio at NUL and wait only for the wrapper's exit status. Launch failures still surface through the exit code. Same approach as the community PR farion1231#6716 by @umutcankurt, applied to the current code path. Refs farion1231#6712 * fix(apps): give runtime CLI commands the login-shell PATH on Unix When CC Switch is launched from Finder, its PATH is launchd's narrow /usr/bin:/bin:/usr/sbin:/sbin. run_detected_tool_command_with_timeout located the tool through a login shell but then spawned it with only the tool's own directory prepended to that narrow PATH. An npm-installed CLI is a `#!/usr/bin/env node` script, so when node lives in a different directory (codex in /opt/homebrew/bin, node in /usr/local/bin) the "Restart Codex daemon" action failed with `env: node: No such file or directory`. The same helper runs `opencode models`. Merge the login-shell PATH in front of the inherited PATH, as tool install/update already does. login_shell_path now honours the caller's deadline and spawns like resolve_path_default (stdin null, own session); when it fails or times out the previous PATH is used unchanged. Refs farion1231#8653 * fix(app): raise the open-file soft limit at startup on Unix macOS GUI apps started from Finder or a login item inherit launchd's soft RLIMIT_NOFILE of 256. The local router holds a client socket and an upstream connection per in-flight request on top of the log, database and config files, so a burst of parallel requests exhausted the descriptors: accept() failed with EMFILE, upstream connects failed, reading live-state.json failed and SQLite could not open the database. Right after the logger is initialized, lift the soft limit to the hard limit capped at 10240 (macOS OPEN_MAX; setrlimit rejects larger values there). The hard limit is never touched and a limit that is already high enough is left alone; failures are only logged. Refs farion1231#8668 * chore(release): v4.0.18 * docs(release): add v4.0.18 release notes * docs(upstream): record the v4.0.18 sync The ledger now states the v4.0.17 to v4.0.18 merge, the conflict resolution, and the schema 21 migration. Co-authored-by: Chris-Xie369 <Chris-Xie369@users.noreply.github.com> --------- Co-authored-by: funkpopo <funkpopoisme@gmail.com> Co-authored-by: chonngyi <111365239+chonngyi@users.noreply.github.com> Co-authored-by: Chaoqi <136579807+Chaoqi31@users.noreply.github.com> Co-authored-by: Jason <farion1231@gmail.com> Co-authored-by: Owlbay <gzh298255@gmail.com> Co-authored-by: peterLwk888 <lwk1124@outlook.com> Co-authored-by: ajia1206 <491387123@qq.com> Co-authored-by: Thefool <thefoolyuzhe@gmail.com> Co-authored-by: SaladDay <1203511142@qq.com> Co-authored-by: cuiyf5516 <48861298+cuiyf5516@users.noreply.github.com> Co-authored-by: Dopetaiga <120038262+Dopetaiga@users.noreply.github.com> Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Chris-Xie369 <Chris-Xie369@users.noreply.github.com>
Why
Codex 0.153+ writes a
token_usage_recordfor every model response. A remote compaction writes that record and then atoken_countwhoselast_token_usageis all zeros. The importer billed onlytoken_count, so those calls became 0 rows. One local thread had 26,705 input and 491 output imported as nothing.Scope
src-tauri/src/services/session_usage_codex.rstoken_usage_recordis held until the nexttoken_count. If thatlast_token_usagematches the record, thetoken_countrow stays the only row. Iflast_token_usageis present and all zeros, the record is imported ascodex_thread:{root_thread_id}:usage:{response_id}.token_countrows keep theirevent_index. The new suffix does not renumber later requests.Tradeoffs
A
token_usage_recordthat does not match a later non-zerolast_token_usagestays timing-only. Billing every mismatch double-counted a latency fixture whose record was 1 input and 999 output while the followingtoken_countlast was 200 and 700.The record is not billed at end of file. A live session often writes the record before
token_count. Billing at end of file would insert ausage:row, and the next sync would also insert the matchingtoken_countrow.The signal is a zero
last_token_usage, not a zero total. Real files keep a non-zero total and a zero last after compaction.A compaction record copied into a child fork is not covered here. That replay prefix is issue 7917 item 1.
Pre-upgrade compaction usage is not recovered. A one-time reread of already-scanned files would insert detail rows for days that already live in
usage_daily_rollups.Blast Radius
This touches Codex session import. New compaction rows use a separate request id, so existing detail rows are not rewritten. Files whose cursor is already current are not reread.
Verification
Before the change,
remote_compaction_usage_record_is_imported_when_the_following_token_count_is_zeroimported 0 rows and the test expected 1. The sibling case, a record followed by a matchingtoken_count, imported exactly 1 row under the existingevent_index.After dropping the upgrade backfill,
cargo test --manifest-path src-tauri/Cargo.toml --lib session_usage_codex::testspassed 87 tests, failed 0, and ignored 1.cargo clippy --manifest-path src-tauri/Cargo.toml -- -D warningspassed.rustfmt --checkwas applied tosession_usage_codex.rsbefore the test run.Section 4 item 3 of #7917. Issue 7917 stays open.