Skip to content

Resolve Codex through mise which instead of running the lazy launcher - #13109

Closed
surim0n wants to merge 2 commits into
omacom:quattrofrom
surim0n:fix/codex-launcher-probe
Closed

surim0n wants to merge 2 commits into
omacom:quattrofrom
surim0n:fix/codex-launcher-probe

Conversation

@surim0n

@surim0n surim0n commented Sep 24, 2026

Copy link
Copy Markdown
Contributor

Summary

omarchy-agent-usage-codex resolved codex with shutil.which, which finds ~/.local/bin/codex first — the lazy launcher written by omarchy-mise-install, whose first step is mise use -g. On a machine without Codex installed, the Agents widget's read-only refresh probe started a full install; when the app-server handshake timed out, the orphaned mise kept installing.

  • Scan PATH for a real codex binary, skipping lazy launchers (a regular file that invokes mise use/mise x, and anything under mise's shims directory
  • Fall back to , which only prints a binary that is already installed — it can never trigger an install
  • A symlink at the launcher path is treated as the user's own binary and probed directly, without invoking
  • If nothing real resolves, the record reports as before

Fixes #12939

Test plan

  • New tests in : a launcher-only PATH never executes the launcher and reports Codex unavailable; a result is probed; a user symlink at is probed without invoking mise
  • Verified all three scenarios against stub / binaries with a sanitized PATH

Generated with Devin
EOF
)

The Agents widget runs this collector on every refresh. The probe used
shutil.which, which finds ~/.local/bin/codex first: a mise wrapper whose
first step is `mise use -g`. On a machine without Codex, the read-only
probe started a full install, and the orphaned mise kept going after the
handshake timeout killed the launcher.

Scan PATH for a real binary, skipping anything that looks like a lazy
launcher (a regular file invoking mise, or a mise shim), then fall back
to `mise which codex`, which only prints a binary that is already
installed. A symlink at the launcher path is the user's own binary and
is still probed directly.

Fixes omacom#12939
@llstrk

llstrk commented Sep 25, 2026

Copy link
Copy Markdown

Automated AI review

Community review: Independent automated community review, unaffiliated with the Omarchy team, intended to help prepare PRs for their review.

Verified: on a machine where Codex was never installed, the usage collector no longer runs the ~/.local/bin/codex launcher. It skips the launcher, mise which codex finds nothing, and the record reports "Codex unavailable". A Codex from an npm global install is also no longer shadowed by the launcher. One gap remains: mise's own shims are not caught by the new check, and in some mise states a shim still reaches an installing path.

Setup (reviewed head vs base) Base Head
Launcher only, Codex never installed runs launcher (mise use -g) skips launcher, reports unavailable
npm global Codex plus launcher launcher shadows npm Codex probes npm Codex
Codex from pacman (/usr/bin/codex) probed probed (unchanged)
User binary or symlink at ~/.local/bin/codex probed probed (unchanged)
mise-installed Codex, shim first on session PATH shim executed shim executed (unchanged)

mise shims bypass the lazy-launcher check

On Linux, mise creates each shim as a symlink to the mise binary (src/shims.rs add_shim, v2026.9.9). is_lazy_launcher() returns False for every symlink before it reaches the shims-directory comparison (bin/omarchy-agent-usage-codex#L98). So the "anything under mise's shims directory" rule never applies to a real shim, and the collector executes shims/codex whenever it exists. In a sandboxed run with a synthetic shim symlinked to a stub mise, the head collector executed the shim in both the Omarchy session PATH order and the collector's own appended order.

Executing a shim is not always read-only. Traced from mise 2026.9.9 source (not run against a real mise):

collector -> shims/codex (symlink, not treated as lazy)
  -> mise shim dispatch, no active Codex version in config
       -> not_found_system_fallback (default true) searches PATH
            -> ~/.local/bin/codex (Omarchy launcher)
                 -> mise use -g codex   (install + global config write)
  -> or: a configured Codex version is missing
       -> not_found_auto_install (default true) may install it

Impact: in the default flow (Codex installed through the launcher and active in the global config) the shim runs the installed Codex, the same as base. The install path is reached when mise has a Codex install or shim but no active Codex version in config (for example after mise install codex without mise use, or after the global entry is removed), or when a configured version is missing. In the session PATH order base behaves the same way, so this is not a regression, but the PR's stated guarantee that shims are skipped does not hold.

Suggested change: detect shims before the symlink exemption, for example by treating a candidate whose os.path.realpath() equals the realpath of the resolved mise binary as lazy, and let mise which codex return the installed binary instead. Optional approaches: move the directory check above islink and derive the shims directory from MISE_DATA_DIR/XDG_DATA_HOME, or set MISE_NOT_FOUND_SYSTEM_FALLBACK=false and MISE_NOT_FOUND_AUTO_INSTALL=false when spawning a shim. A test with shims/codex as a symlink to the mise stub would cover this; the current symlink case points at the Codex stub and passes on base too (confirmed by running it against the base collector).

Test isolation: the new cases use SAFE_PATH="$(dirname "$(command -v jq)"):/usr/bin:/bin". find_codex_binary() scans PATH in order, and Arch's openai-codex package installs /usr/bin/codex, so on such a host all three new cases pick the real Codex and start codex ... app-server with the scratch HOME. A synthetic tripwire codex placed in a SAFE_PATH directory was invoked three times and all three cases failed. A private bin directory holding only the needed tools (or skipping when PATH=$SAFE_PATH command -v codex succeeds) would keep the test hermetic.

Optional: mise which can resolve a configured latest version over the network when some tool has no installed version. Setting MISE_OFFLINE=1 for that call would keep the refresh probe offline.


Review information

Test scope: Source review of the reviewed head d34d2c9 against base 28ceaae, including bin/omarchy-mise-install, Omarchy session PATH setup and mise 2026.9.9 source (the version Arch ships). The existing and new test files and the new cases against the base collector ran in a network-isolated sandbox with stub mise and codex binaries; no real mise, npm or Codex was executed and no desktop session was used. The shim fallback and auto-install paths are traced from source only.

AI process: Opus 5.5 Medium coordination and synthesis, independent Opus 5.5 Xhigh and GPT 6 Sol Xhigh technical assessments with targeted follow-up questions, Opus 5.5 Medium editorial check.

Opt out: To stop receiving these reviews, reply to this comment saying so.

@surim0n

surim0n commented Sep 25, 2026

Copy link
Copy Markdown
Contributor Author

Addressed in af251f2:

  • Shim detection now runs before the symlink exemption. A shim is recognised either by living in the mise shims dir (derived from MISE_DATA_DIR/XDG_DATA_HOME) or by resolving via realpath to the mise binary itself. A symlink elsewhere is still treated as the user's own binary.
  • mise which is called with MISE_OFFLINE=1 so the probe cannot resolve a configured latest over the network.
  • The new tests use a private tools dir instead of /usr/bin:/bin, so a packaged /usr/bin/codex on the host can no longer leak into the probe. Added a case with shims/codex symlinked to the mise stub.

@llstrk

llstrk commented Sep 27, 2026

Copy link
Copy Markdown

Automated AI review

Community review: Independent automated community review, unaffiliated with the Omarchy team, intended to help prepare PRs for their review.

Follow-up on the earlier review and the author's reply, checked at af251f2.

Verified: all three earlier points are resolved. The new commit adds one narrow regression, described below: when mise itself is a symlink, a mise-installed Codex is reported as unavailable.

Earlier point Outcome at af251f2
mise shims bypass the lazy-launcher check Resolved. A shim symlinked to a stub mise is no longer executed. The installed Codex from mise which codex is probed instead, in both the session PATH order and the collector's appended order.
Test isolation (SAFE_PATH included /usr/bin:/bin) Resolved. With a tripwire codex in the sandbox directory that the test's command -v resolves python3, jq and rg to, the new test file passed 25/25 and never invoked it. The previous test file invoked it and failed its first new case.
Optional: MISE_OFFLINE=1 for mise which Resolved. Set only for the mise which call; the Codex process does not inherit it.

The two shim checks cover different cases. The directory check alone catches a shim under a custom MISE_DATA_DIR that points at a different mise. The realpath check alone catches a shim in the collector's appended ~/.local/share/mise/shims when XDG_DATA_HOME points elsewhere, and a custom MISE_SHIMS_DIR. The other rows of the earlier table are unchanged.

A symlinked mise is classified as a lazy launcher

find_codex_binary() calls mise which only when not is_lazy_launcher(mise). When the mise found on PATH is itself a symlink (for example Homebrew on Linux, a Nix profile, or a hand-made ~/.local/bin/mise link), the new realpath branch compares that path with find_command("mise"), which is the same path, so it returns True (bin/omarchy-agent-usage-codex#L111-L118). mise which codex is then never called.

Symlinked mise, Codex installed through mise Base d34d2c9 af251f2
Launcher at ~/.local/bin/codex, no shim launcher run (mise use -g) installed Codex probed "Codex unavailable", no mise call
Shim first on the session PATH shim executed shim executed "Codex unavailable", no mise call

Impact: on these setups the widget shows "Codex unavailable" instead of the limits. Nothing is executed or installed, so this fails safe. Omarchy installs mise-bin, whose AUR PKGBUILD installs a regular /usr/bin/mise, so default installs are not affected.

Suggested change: skip the realpath comparison when the candidate is the mise binary itself:

if os.path.islink(path):
  mise = find_command("mise")
  if mise and os.path.abspath(path) != os.path.abspath(mise):
    ...  # existing realpath comparison
  return False

With this change, both rows above probe the installed Codex (with MISE_OFFLINE=1 on the which call), and the PR's test file still passes 25/25. A test case with bin/mise as a symlink to a stub that prints a Codex path fails at af251f2 and passes with the change.

Optional test improvement: the private tools dir holds only python3 and rg, but the Codex stub the tests probe calls jq. In the "mise-resolved binary" and "user-owned symlink" cases the stub cannot answer initialize, so the collector waits out its 8 s timeout (timed at 8.08 s for the resolved-binary case) and records "Codex limits unavailable". Both cases still pass, because they check only the args file and the mise call log, not the probe's result. Adding jq (for tool in python3 rg jq) brought the file from 18.2 s to 2.27 s, still 25/25.

Test interaction with #13106 (test-only): that open PR adds a no-credentials guard that returns before the app-server starts unless CODEX_ACCESS_TOKEN, a non-file credential store or CODEX_HOME/auth.json exists. This PR's "mise-resolved binary" and "user-owned symlink" test cases create none of these but require codex-args. In a sandbox merge of the two PRs (the collector merges cleanly; the test files conflict only at the end, where both append), the "mise-resolved binary" case fails with "Codex limits unavailable". Adding an auth.json to the CODEX_HOME of those two cases, as #13106 does for the existing fixtures, makes all 29 checks pass.


Review information

Test scope: Source review of af251f2 against the previously reviewed d34d2c9 and base 28ceaae, with mise 2026.9.9 (Arch extra) and 2026.9.14 (AUR mise-bin) source. The collector at all three commits, variants of it with one or both shim checks removed, and the current test file against each of them ran in a network-isolated sandbox with synthetic homes and stub mise and codex binaries. The combined test file was also run on a local merge with #13106. No real mise, npm or Codex was executed. Real shim dispatch and MISE_OFFLINE handling are traced from mise source only.

AI process: Opus 5.5 Medium coordination and synthesis, Opus 5.5 Xhigh technical review and final fact check, GPT 6 Sol Xhigh search for related issues, Opus 5.5 Medium editorial check.

Opt out: To stop receiving these reviews, reply to this comment saying so.

@dhh

dhh commented Oct 2, 2026

Copy link
Copy Markdown
Contributor

Thank you! Brought in with your authorship in #14049.

@dhh dhh closed this Oct 2, 2026
dhh added a commit that referenced this pull request Oct 3, 2026
…ommunity PRs (#14049)

* Read Codex app-server replies from the raw fd (#13703)

* Resolve Codex through mise which instead of running the lazy launcher (#13109)

* Skip the Codex app-server probe when there are no credentials (#13106)

Adapted: credentials are checked in the home being probed rather than in
the CODEX_HOME environment variable, since each registered account is
probed in its own home, so a signed-out secondary account isn't hidden
behind the primary's login. A home without credentials reports "Waiting
for auth" like any other signed-out home. The credentials store setting is
read with tomllib, so a single-quoted value counts too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Show the Codex CLI's own error when its app-server dies (#8977)

Detect an app-server that exits or stops answering, and report the end of
its stderr instead of a bare RPC method name. Rebased onto the raw-fd
reply reader; the switch from "-a on-request" to "-a never" is left out,
keeping the current approval flags.

* Count pi sessions when HOME is a git checkout (#13209)

* Count only OpenAI-backed native sessions as Codex usage (#12032)

* Deduplicate Pi usage across forked sessions (#8602)

* Skip unchanged native Codex token snapshots (#10531)

* Count omp and pi profile sessions in the agent usage collectors (#9546)

`omp --profile=<name>` (and pi's equivalent) relocates the whole agent
tree under <base>/profiles/<name>/. The Claude and Codex collectors only
ever scanned <base>/agent/sessions, so a subscription driven entirely
through a profile was invisible to the agents panel: no tokens by day, no
tokens by model, no prompt or session counts.

Discover the profile roots alongside the default one. Sessions are keyed
by file path, so a profile adds sessions instead of double-counting the
default root, and a missing or unreadable profiles directory leaves the
existing behavior untouched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014zFbJcDEEpV5BAmsH6kAB3

* Skip unrelated Codex session lines before JSON parsing (#12803)

Adapted: session_meta lines also pass the pre-filter, since the provider
filter from #12032 reads them to skip rollouts served by a non-OpenAI
provider.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Read only the Codex session files that changed since the last scan (#12595)

Native Codex rollouts keep per-file totals between runs, replayed while a
file's mtime and size are unchanged. Rebased onto the session_meta
provider filter, snapshot dedup, and line pre-filter, which now live in
the per-file reader. pi and omp sessions are left out of the per-file
cache: a forked pi session repeats its parent's messages, so they are
deduplicated across the whole tree on every scan.

* Count streamed Claude messages by their highest-output usage line (#10606)

Claude Code writes a streamed assistant response as several transcript
lines that share one message id, one per content block. Each line
carries a usage object. The first line's output_tokens is a placeholder,
often 1, and the last line has the real count. Input and cache fields
usually match across the lines.

The scanner dedupes by message id and keeps the first line it sees, so
it under-counts output tokens. On a machine with 2,577 transcripts it
reported 39.0M output tokens against 60.1M used, a 35% shortfall. Input
and both cache fields differed by under 0.01%.

Keep the line with the highest output count, with the last one scanned
winning a tie. The whole line is kept because a response can fall back
to another model mid-stream. Those lines are separate snapshots with
different cache figures and a different model, and taking a maximum per
field across them over-counts cache tokens and credits the wrong model.

The zero-usage check now runs before dedup, so a zero-usage first line
no longer claims a message id and hides a later line with real usage.

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: GPT-6 Astra <noreply@openai.com>

* Index Claude transcripts so the agents refresh reads only what was appended (#8313)

omarchy-agent-usage-claude re-parsed every line of every transcript under
~/.claude/projects on each refresh: no mtime cutoff, no memory of the last
pass. The agents widget is on by default and ticks every 15 minutes, so the
cost grew for the life of the machine. After one month here that was 803
files, 640 MB, 127k lines and 57k JSON parses per tick, about 1 core-second,
pushed through the page cache every quarter hour forever.

Keep a per-file index next to the scan cache: the unique usage records
already parsed out of each transcript and the byte offset they end at. A
file whose size and mtime match is not opened; a file that grew is read
from the stored offset; a file that shrank or was rewritten is read from
the start. --force drops the index and rescans from scratch.

The summary is built from the indexed records in the same directory order
the walk always used. That matters: when a resumed session carries earlier
messages, the same message id appears in two files with different usage,
and the first file visited wins. 91 ids differed on this machine; sorting
the walk moved one model's output total by 25k tokens. Output is now
byte-identical to the previous scan on a frozen copy of the corpus, cold,
warm, and after an append.

Warm refresh: 1.0 s -> 0.10 s of CPU, of which the scan itself is 70 ms;
the index for this corpus is 2.9 MB.

Adapted:
- Rebased onto #10606: the highest-output rule for streamed messages now
  lives where the index parses records, and decides between files too.
- The index records the timezone it was written in, and a change rereads
  every transcript, since its records hold local days.
- A file only counts as appended to when its inode and the hash of what
  was already read still match, so a transcript replaced by a larger one,
  or rewritten in place, is read from the start.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Label a Claude Team seat by its subscription, not its rate-limit tier (#11109)

The collector built the plan label from the OAuth rateLimitTier first, so a
Team premium seat, which runs on default_claude_max_5x, showed in the agents
panel as "Max 5x". Lead with subscriptionType and keep the multiplier as its
qualifier: Max still reads "Max 5x", a Team seat reads "Team 5x".

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* Label the Claude plan from the profile the CLI refreshes (#7225)

Adapted: the profile is found the same way current_account_id() finds it,
now shared as profile_path(): ~/.claude.json for the default home, the
home's own .claude.json otherwise. The original fell back to ~/.claude.json
for any home without CLAUDE_CONFIG_DIR set, so a secondary account read the
primary's tier. The profile's tier also keeps the subscription in the label,
so a Team seat stays "Team" (#11109).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Call a lapsed Claude access token paused, not signed out (#8093)

* Refresh Claude usage after the clock moves backwards (#9956)

* Bound unreadable Claude transcript warnings (#12414)

* Count Claude usage from opencode v2 sessions (#13894)

* Reload agent usage records when an inotify watch fails to rearm (#10067)

* Reload agent usage records after each update run instead of on a timer

Rather than #10067's two-minute timer per record, reload every record when
the omarchy-agent-usage-update process exits, the moment its files can have
been replaced. A reload that finds a file unchanged keeps its record, so the
panel isn't stirred up by identical data. The grep test now runs the QML
functions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Show the agent status when the trouble line has no help text (#8497)

* Clear stale agent login guidance after a successful probe (#8892)

* Clear the Grok login hint after a successful probe

#8892 cleared the default login hint after a successful probe in the
Claude and Codex collectors; Grok's collector had the same stale hint.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Read Fireworks credentials from pi's auth.json (#7455)

The Fireworks collector skipped pi, Omarchy's default agent, when
walking its credential ladder, so a machine signed in to Fireworks only
through pi (/login fireworks) never showed the tab. Insert the key pi
stores in $PI_CODING_AGENT_DIR/auth.json (default ~/.pi/agent) between
the firectl auth.ini and the opencode fallback.

pi keys can be literals, $ENV_VAR/${ENV_VAR} references, or !command
shell lookups. The collector resolves the first two; command lookups
stay pi-only and are skipped rather than sent to the API verbatim.

* Call a lapsed Grok access token paused, not signed out

Grok's access token lives six hours and Grok mints a new one from its
refresh token whenever it starts, so a lapsed one is routine. Reporting
it as an expired sign-in made the panel offer Sign-in required several
times a day, sending people through grok login for nothing. With a
refresh token present it now reads as paused, like Claude's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Keep showing Grok's last limits while it sits idle

While Grok hasn't run, nothing on the machine has spent its allowance,
so with a refresh token on hand the last numbers still stand: they show
as current rather than dimmed under a status line. A weekly window that
reset in the meantime starts over at 0%, a whole number of weeks on.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Ask for a Grok sign-in once its refresh token is past 30 days

A refresh token older than Grok's 30-day sign-in can't renew anything,
so the panel offers Sign-in required again instead of showing the last
limits as current. With nothing cached yet it says to start Grok, rather
than showing an empty section without a word.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Check both ends of what the Claude index read before resuming a transcript

A transcript rewritten in place could grow and change only after its
first kilobytes, and the index took it for an append. It now compares
the last kilobytes before the resume point too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Simplify the agent usage collectors

- Codex: pass the forced-scan choice down instead of a module global, make
  the per-file reader's cache arguments required, shrink the cache record
  check, and drop guards for shapes that can't occur: an empty launcher
  path, realpath raising, mise itself being a lazy launcher, multi-line
  `mise which` output, and probing without a temp file for stderr.
- Claude: decide an append by the digest of both ends of what was read
  alone; the inode and mtime checks it made redundant are gone.
- Snapshot: the device id falls back to the hostname, which always exists.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Share fixture setup in the agent usage scanner tests

Every fixture home lives under one scratch directory with a single cleanup
trap, instead of a trap rewritten with a longer list for each new home, and
the Codex test builds its signed-in homes with one helper.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Treat a replaced Claude transcript as new even when its ends match

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Probe Codex without its error text when there's no temporary space

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: tossbaws <17258053+tossbaws@users.noreply.github.com>
Co-authored-by: surim0n <suritech@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: anonwurcod <anonwurcod@proton.me>
Co-authored-by: Kevin Rajan <7121943+kvnloo@users.noreply.github.com>
Co-authored-by: Nate Ashby <nate.ashby11@gmail.com>
Co-authored-by: Aris Gysel <aris.gysel@me.com>
Co-authored-by: Brams <76213579+Brams-s@users.noreply.github.com>
Co-authored-by: This_Is_NPC <gabrielfollone27@gmail.com>
Co-authored-by: sanjyay <102979855+sanjyay@users.noreply.github.com>
Co-authored-by: PapistProtocol <12738904+PapistProtocol@users.noreply.github.com>
Co-authored-by: steez <stevedimakos97@gmail.com>
Co-authored-by: GPT-6 Astra <noreply@openai.com>
Co-authored-by: Ryan Yogan <ryanyogan@gmail.com>
Co-authored-by: Oli Denton <41393837+omdenton@users.noreply.github.com>
Co-authored-by: Igor Kramar <i@ikramar.ru>
Co-authored-by: Martin Eidensten <martin@meibe.se>
Co-authored-by: Romain Perron <rdj.perron@gmail.com>
Co-authored-by: Omarchy Contributor <contributor@users.noreply.github.com>
Co-authored-by: manuaudio <manu@arimaka.com>
Co-authored-by: Tyler South <tsouth2@gmail.com>
Co-authored-by: whathek <Hek846@users.noreply.github.com>
Co-authored-by: Ty Richards <me@tyrichards.com>
sgruendel pushed a commit to sgruendel/omarchy that referenced this pull request Oct 3, 2026
…ommunity PRs (omacom#14049)

* Read Codex app-server replies from the raw fd (omacom#13703)

* Resolve Codex through mise which instead of running the lazy launcher (omacom#13109)

* Skip the Codex app-server probe when there are no credentials (omacom#13106)

Adapted: credentials are checked in the home being probed rather than in
the CODEX_HOME environment variable, since each registered account is
probed in its own home, so a signed-out secondary account isn't hidden
behind the primary's login. A home without credentials reports "Waiting
for auth" like any other signed-out home. The credentials store setting is
read with tomllib, so a single-quoted value counts too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Show the Codex CLI's own error when its app-server dies (omacom#8977)

Detect an app-server that exits or stops answering, and report the end of
its stderr instead of a bare RPC method name. Rebased onto the raw-fd
reply reader; the switch from "-a on-request" to "-a never" is left out,
keeping the current approval flags.

* Count pi sessions when HOME is a git checkout (omacom#13209)

* Count only OpenAI-backed native sessions as Codex usage (omacom#12032)

* Deduplicate Pi usage across forked sessions (omacom#8602)

* Skip unchanged native Codex token snapshots (omacom#10531)

* Count omp and pi profile sessions in the agent usage collectors (omacom#9546)

`omp --profile=<name>` (and pi's equivalent) relocates the whole agent
tree under <base>/profiles/<name>/. The Claude and Codex collectors only
ever scanned <base>/agent/sessions, so a subscription driven entirely
through a profile was invisible to the agents panel: no tokens by day, no
tokens by model, no prompt or session counts.

Discover the profile roots alongside the default one. Sessions are keyed
by file path, so a profile adds sessions instead of double-counting the
default root, and a missing or unreadable profiles directory leaves the
existing behavior untouched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014zFbJcDEEpV5BAmsH6kAB3

* Skip unrelated Codex session lines before JSON parsing (omacom#12803)

Adapted: session_meta lines also pass the pre-filter, since the provider
filter from omacom#12032 reads them to skip rollouts served by a non-OpenAI
provider.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Read only the Codex session files that changed since the last scan (omacom#12595)

Native Codex rollouts keep per-file totals between runs, replayed while a
file's mtime and size are unchanged. Rebased onto the session_meta
provider filter, snapshot dedup, and line pre-filter, which now live in
the per-file reader. pi and omp sessions are left out of the per-file
cache: a forked pi session repeats its parent's messages, so they are
deduplicated across the whole tree on every scan.

* Count streamed Claude messages by their highest-output usage line (omacom#10606)

Claude Code writes a streamed assistant response as several transcript
lines that share one message id, one per content block. Each line
carries a usage object. The first line's output_tokens is a placeholder,
often 1, and the last line has the real count. Input and cache fields
usually match across the lines.

The scanner dedupes by message id and keeps the first line it sees, so
it under-counts output tokens. On a machine with 2,577 transcripts it
reported 39.0M output tokens against 60.1M used, a 35% shortfall. Input
and both cache fields differed by under 0.01%.

Keep the line with the highest output count, with the last one scanned
winning a tie. The whole line is kept because a response can fall back
to another model mid-stream. Those lines are separate snapshots with
different cache figures and a different model, and taking a maximum per
field across them over-counts cache tokens and credits the wrong model.

The zero-usage check now runs before dedup, so a zero-usage first line
no longer claims a message id and hides a later line with real usage.

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: GPT-6 Astra <noreply@openai.com>

* Index Claude transcripts so the agents refresh reads only what was appended (omacom#8313)

omarchy-agent-usage-claude re-parsed every line of every transcript under
~/.claude/projects on each refresh: no mtime cutoff, no memory of the last
pass. The agents widget is on by default and ticks every 15 minutes, so the
cost grew for the life of the machine. After one month here that was 803
files, 640 MB, 127k lines and 57k JSON parses per tick, about 1 core-second,
pushed through the page cache every quarter hour forever.

Keep a per-file index next to the scan cache: the unique usage records
already parsed out of each transcript and the byte offset they end at. A
file whose size and mtime match is not opened; a file that grew is read
from the stored offset; a file that shrank or was rewritten is read from
the start. --force drops the index and rescans from scratch.

The summary is built from the indexed records in the same directory order
the walk always used. That matters: when a resumed session carries earlier
messages, the same message id appears in two files with different usage,
and the first file visited wins. 91 ids differed on this machine; sorting
the walk moved one model's output total by 25k tokens. Output is now
byte-identical to the previous scan on a frozen copy of the corpus, cold,
warm, and after an append.

Warm refresh: 1.0 s -> 0.10 s of CPU, of which the scan itself is 70 ms;
the index for this corpus is 2.9 MB.

Adapted:
- Rebased onto omacom#10606: the highest-output rule for streamed messages now
  lives where the index parses records, and decides between files too.
- The index records the timezone it was written in, and a change rereads
  every transcript, since its records hold local days.
- A file only counts as appended to when its inode and the hash of what
  was already read still match, so a transcript replaced by a larger one,
  or rewritten in place, is read from the start.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Label a Claude Team seat by its subscription, not its rate-limit tier (omacom#11109)

The collector built the plan label from the OAuth rateLimitTier first, so a
Team premium seat, which runs on default_claude_max_5x, showed in the agents
panel as "Max 5x". Lead with subscriptionType and keep the multiplier as its
qualifier: Max still reads "Max 5x", a Team seat reads "Team 5x".

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* Label the Claude plan from the profile the CLI refreshes (omacom#7225)

Adapted: the profile is found the same way current_account_id() finds it,
now shared as profile_path(): ~/.claude.json for the default home, the
home's own .claude.json otherwise. The original fell back to ~/.claude.json
for any home without CLAUDE_CONFIG_DIR set, so a secondary account read the
primary's tier. The profile's tier also keeps the subscription in the label,
so a Team seat stays "Team" (omacom#11109).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Call a lapsed Claude access token paused, not signed out (omacom#8093)

* Refresh Claude usage after the clock moves backwards (omacom#9956)

* Bound unreadable Claude transcript warnings (omacom#12414)

* Count Claude usage from opencode v2 sessions (omacom#13894)

* Reload agent usage records when an inotify watch fails to rearm (omacom#10067)

* Reload agent usage records after each update run instead of on a timer

Rather than omacom#10067's two-minute timer per record, reload every record when
the omarchy-agent-usage-update process exits, the moment its files can have
been replaced. A reload that finds a file unchanged keeps its record, so the
panel isn't stirred up by identical data. The grep test now runs the QML
functions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Show the agent status when the trouble line has no help text (omacom#8497)

* Clear stale agent login guidance after a successful probe (omacom#8892)

* Clear the Grok login hint after a successful probe

omacom#8892 cleared the default login hint after a successful probe in the
Claude and Codex collectors; Grok's collector had the same stale hint.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Read Fireworks credentials from pi's auth.json (omacom#7455)

The Fireworks collector skipped pi, Omarchy's default agent, when
walking its credential ladder, so a machine signed in to Fireworks only
through pi (/login fireworks) never showed the tab. Insert the key pi
stores in $PI_CODING_AGENT_DIR/auth.json (default ~/.pi/agent) between
the firectl auth.ini and the opencode fallback.

pi keys can be literals, $ENV_VAR/${ENV_VAR} references, or !command
shell lookups. The collector resolves the first two; command lookups
stay pi-only and are skipped rather than sent to the API verbatim.

* Call a lapsed Grok access token paused, not signed out

Grok's access token lives six hours and Grok mints a new one from its
refresh token whenever it starts, so a lapsed one is routine. Reporting
it as an expired sign-in made the panel offer Sign-in required several
times a day, sending people through grok login for nothing. With a
refresh token present it now reads as paused, like Claude's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Keep showing Grok's last limits while it sits idle

While Grok hasn't run, nothing on the machine has spent its allowance,
so with a refresh token on hand the last numbers still stand: they show
as current rather than dimmed under a status line. A weekly window that
reset in the meantime starts over at 0%, a whole number of weeks on.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Ask for a Grok sign-in once its refresh token is past 30 days

A refresh token older than Grok's 30-day sign-in can't renew anything,
so the panel offers Sign-in required again instead of showing the last
limits as current. With nothing cached yet it says to start Grok, rather
than showing an empty section without a word.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Check both ends of what the Claude index read before resuming a transcript

A transcript rewritten in place could grow and change only after its
first kilobytes, and the index took it for an append. It now compares
the last kilobytes before the resume point too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Simplify the agent usage collectors

- Codex: pass the forced-scan choice down instead of a module global, make
  the per-file reader's cache arguments required, shrink the cache record
  check, and drop guards for shapes that can't occur: an empty launcher
  path, realpath raising, mise itself being a lazy launcher, multi-line
  `mise which` output, and probing without a temp file for stderr.
- Claude: decide an append by the digest of both ends of what was read
  alone; the inode and mtime checks it made redundant are gone.
- Snapshot: the device id falls back to the hostname, which always exists.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Share fixture setup in the agent usage scanner tests

Every fixture home lives under one scratch directory with a single cleanup
trap, instead of a trap rewritten with a longer list for each new home, and
the Codex test builds its signed-in homes with one helper.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Treat a replaced Claude transcript as new even when its ends match

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Probe Codex without its error text when there's no temporary space

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: tossbaws <17258053+tossbaws@users.noreply.github.com>
Co-authored-by: surim0n <suritech@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: anonwurcod <anonwurcod@proton.me>
Co-authored-by: Kevin Rajan <7121943+kvnloo@users.noreply.github.com>
Co-authored-by: Nate Ashby <nate.ashby11@gmail.com>
Co-authored-by: Aris Gysel <aris.gysel@me.com>
Co-authored-by: Brams <76213579+Brams-s@users.noreply.github.com>
Co-authored-by: This_Is_NPC <gabrielfollone27@gmail.com>
Co-authored-by: sanjyay <102979855+sanjyay@users.noreply.github.com>
Co-authored-by: PapistProtocol <12738904+PapistProtocol@users.noreply.github.com>
Co-authored-by: steez <stevedimakos97@gmail.com>
Co-authored-by: GPT-6 Astra <noreply@openai.com>
Co-authored-by: Ryan Yogan <ryanyogan@gmail.com>
Co-authored-by: Oli Denton <41393837+omdenton@users.noreply.github.com>
Co-authored-by: Igor Kramar <i@ikramar.ru>
Co-authored-by: Martin Eidensten <martin@meibe.se>
Co-authored-by: Romain Perron <rdj.perron@gmail.com>
Co-authored-by: Omarchy Contributor <contributor@users.noreply.github.com>
Co-authored-by: manuaudio <manu@arimaka.com>
Co-authored-by: Tyler South <tsouth2@gmail.com>
Co-authored-by: whathek <Hek846@users.noreply.github.com>
Co-authored-by: Ty Richards <me@tyrichards.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants