8000 GitHub - am-a/aiq: Quota-aware router for pooled Claude Code and Codex subscriptions: PATH shims, rollover scoring, long-session handoff, status daemon · GitHub
[go: up one dir, main page]

Skip to content
am-aPublic
forked from orlenko/aiq

About

Quota-aware router for pooled Claude Code and Codex subscriptions: PATH shims, rollover scoring, long-session handoff, status daemon

Resources

Stars

0 stars

Watchers

0 watching

Forks

 
 

Repository files navigation

aiq

aiq pools several paid Claude Code and Codex subscriptions, plus an Antigravity CLI (agy) login, and routes every claude, codex and agy invocation to the account that is about to waste the most quota. Interactive sessions, claude -p / codex exec / agy -p workers, and the workers those agents spawn in turn all go through it. Nobody has to know which subscription they are on.

$ claude                      # interactive: sticky per workspace
aiq: using claude/work — 5h 43%, weekly 8%

$ codex exec "review the diff" # worker: scored per call, silent on success

A small daemon keeps telemetry fresh and serves a status page on http://127.0.0.1:7379/. When you come back to a directory, aiq resume lists the sessions that ran there, shows what each turn asked and answered, and resumes the one you pick. Works on macOS and Linux.

How it works

Accounts are overlay homes. Each Claude account is a CLAUDE_CONFIG_DIR under ~/.local/share/aiq/claude/<name>/; each Codex account is a CODEX_HOME under ~/.local/share/aiq/codex/<name>/. Every entry of the real ~/.claude or ~/.codex is symlinked into the overlay (settings, plugins, projects, sessions, history, sqlite state), so all accounts share one configuration and --resume works on any of them. Only the credential is private: the Keychain entry Claude Code keys by config dir on macOS (.credentials.json on Linux), and auth.json for Codex. The overlay is re-synced before every launch under a per-account lock; a file the CLI created or rewrote in the overlay is moved back into the real home and re-linked, a directory two sessions created independently is merged, and a displaced copy is kept as a rotated .aiq-bak next to the real file. Two things stay private per account: the credential, and .claude.json (Claude Code rewrites it with a rename on every change, so sharing it would lose writes). It is seeded from your ~/.claude.json when the account is created, which means project trust and user-scoped MCP servers are per account.

The account matching the current ~/.claude login is registered as native and launches without CLAUDE_CONFIG_DIR at all.

Shims. aiq shim install writes claude and codex scripts into ~/.local/share/aiq/shims/; put that directory first on PATH. Each script runs aiq run <provider> -- args, which picks an account, syncs its overlay, records a lease, and execs the real CLI. Nested calls carry AIQ_DEPTH, AIQ_PARENT_ACCOUNT and a root lease id; max_depth stops runaway recursion. Wrappers other tools put on PATH are handled by an exec chain: a wrapper that bounces back into the shim with the same pid (or as the direct child of a shell holding that pid) is recognised and the next candidate on PATH is exec'd instead. Only entries after the shim directory are candidates: a wrapper ahead of the shims has already run and delegated to them, and running it again would apply its flags twice.

Two kinds of launch.

  • Interactive (a terminal, no -p/exec): sticky per workspace, i.e. a git checkout keeps its account until a binding window crosses switch_pct. The shim execs the CLI, so the lease follows its pid.
  • Worker (claude -p, codex exec, or a non-terminal stdout): scored per call, kept off accounts that carry an interactive session when an alternative exists, capped at max_workers_per_account, and skipped on accounts whose weekly remaining is at or below weekly_reserve_pct. The worker runs as a child with its output tee'd; if the provider rejects it with a usage-limit message early (little output, or within seconds) and within retry_max_seconds, the account is benched and the same command reruns on the next account. The bench lasts until the reset of a window telemetry shows near its cap, otherwise one hour.

Telemetry. aiq polls every account itself, every interval_seconds and right after any limit event. Claude usage comes from Anthropic's OAuth usage endpoint with a token aiq mints once per account through the same PKCE flow Claude Code uses (aiq account authorize); Claude Code's own credential is never read. Codex usage comes from the official codex app-server rate-limits RPC, run against the account's home, so Codex keeps refreshing its own auth.json. Claude's status line adds live updates for the account in use through a multiplexer that chains to whatever status line you had. That status line may itself run aiq claude-statusline to append aiq's quota segment: the multiplexer marks the chained command with AIQ_STATUSLINE_CHAINED=1, and the inner call prints aiq's line instead of chaining again. Shims never depend on the daemon: state lives in SQLite, so with the daemon down they route on the last poll.

Long-running sessions

A QC loop or an orchestration overseer can run for days and burn a week of quota. aiq long claude / aiq long codex starts such a session in a tmux session named after the workspace and supervises it:

  • Hooks, not a startup instruction. The session is launched with aiq's SessionStart, UserPromptSubmit and Stop hooks (per launch, via --settings for Claude and -c hooks.… for Codex); ordinary sessions are untouched. When the account's tightest binding window has drain_pct or less remaining, the daemon flags the lease and the next hook tells the agent to keep working, write a handoff note at the next natural break and keep it current. The session moves at its next turn end if another account has headroom, or when the account runs out, whichever comes first. The successor must have more than drain_pct left; while none does, the agent spends what is left instead of moving to another nearly spent account, and once the current account is blocked, any positive headroom is enough.
  • Recovered quota cancels the move. A drain aiq started is called off when the account has more than twice drain_pct left again: a reset credit spent by aiq, by another host, or in the CLI (/limit-reset), or a window that rolled over. aiq re-polls the account right before every move, so a reading taken before the credit was spent cannot move a refilled session. A drain asked for with aiq long drain always goes through.
  • Supervised for as long as it runs. A long session's lease lives as long as its process. A pane whose lease is gone anyway (aiq before this fix dropped every lease 36 hours after it started) is re-adopted on the next check: the daemon reads AIQ_LEASE from the CLI's environment and rebuilds the lease under that id from the launch that started it.
  • Launches start above the floor. aiq long ranks accounts at or below drain_pct after every other account and ignores the reset-credit "drain first" preference, since the session would be moved off such an account before its credit could redeem. With nothing above the floor it still starts on the best of what is left.
  • Idle sessions do not strand quota. An agent can stop on its own above drain_pct (a quota floor, nothing it thinks it can afford), and then the account never runs out and nothing moves it. A long session that has been idle for idle_rotate_minutes, with nothing written to its transcript or its subagents' in that time, on an account with idle_rotate_pct or less left, moves to an account with more than that.
  • Resume, not summarize. The overlay shares transcripts, so the same provider's successor gets --resume <session-id> on the next account and keeps the whole conversation; the note is a supplement. A different provider (every Claude out, a Codex takes over, or the reverse) starts fresh from the note plus the working tree. The permission bypass carries over in the successor's spelling (--dangerously-skip-permissions ↔ --dangerously-bypass-approvals-and-sandbox), and so do a tier model (--model opus ↔ --model gpt-6.1-sol, see the tier table below) and an effort level (--effort high ↔ -c model_reasoning_effort=high). Every other flag is provider-specific and dropped. Fallback order is long.fallback, less any provider with no model of the session's tier.
  • Keep the session name. Claude and Codex handoffs carry the latest saved display name, including a name changed with /rename. Fresh Claude successors receive --name; fresh Codex successors start from a named, empty thread, so the handoff prompt does not become the thread's title. Ordinary same-provider resumes keep their existing name.
  • Takeover in place. The daemon respawn-panes the same tmux pane with the successor, copies the project's trust entry between Claude accounts so no dialog blocks the restart, and nudges the successor with a first prompt. An idle session is moved without waiting for a wrap-up turn; a session on an account that is already blocked is moved at once; when no account has headroom the session waits and is moved when a window resets.
aiq long claude [--account <name>] [--model-tier N] [--effort N] [-- claude args...]
                                    start or attach
aiq long auto [--model-tier 0] [--providers claude,codex]
                                    start on whichever pool auto picks, or attach
aiq long auto resume [--account <name>] [<id>]
                                    pick a session here and resume it as a long one
aiq long resume [--account <name>] [<id>]
                                    the same, as it ran: no forced bypass or tier
aiq long list                       leases, pane, drain state, idle/busy
aiq long drain .                    move this workspace's session now
aiq long attach                     re-attach
aiq long stop .                     end it

Choose the initial account with aiq long codex --account acme -- --yolo (or --account=acme). This also works with Claude and named launchers. Put --account before CLI arguments; -- ends aiq's options. The daemon can still move the session to another account later. If a long session already exists for the workspace, the command attaches to it instead of changing its account.

aiq long auto chooses the first account the way aiq run auto does: across both pools, at the requested tier (default 1), with YOLO. Its takeover order is the chosen provider first, so a successor resumes the same transcript while that pool has headroom, then the other provider. It takes no prompt, because a takeover replays the launch arguments; type the task in the session.

aiq long auto resume opens the aiq resume browser for this directory; r on a session reopens it in a long session instead of the current terminal (or give the id: aiq long auto resume 3f2a). The transcript fixes the provider; the account is routed unless --account names one (a bare name, --account claude3, or an id, --account claude/claude3, which must belong to the session's provider), the model tier and effort apply as for auto, and the bypass is on. As with aiq resume, a session that ran under a launcher resumes through it (b or --bare skips it), and then gets the bypass only if its earlier launch passed it. A session already running in aiq long is attached. The pane starts in the current directory, so the CLI finds the transcript.

aiq run auto resume is the same command for this terminal: the same browser, flags and defaults (bypass on, tier 1, --account on the session's provider), but the session reopens here instead of in tmux. aiq long resume is the other way round, the long twin of aiq resume: the session reopens in tmux as it ran, with the bypass only if it had one and the CLI's own model unless --model-tier or --effort say otherwise. Each start has both forms:

this terminal tmux, supervised
new, on any pool aiq run auto aiq long auto
resume, as auto starts aiq run auto resume aiq long auto resume
resume, as it ran aiq resume aiq long resume

Handoff notes live in ~/.local/share/aiq/handoff/, in a directory only you can read. A note quotes prompts and tool output, and the next session reads it, so its contents can reach another account or vendor when a session moves.

  • aiq replaces values matching known secret formats with [redacted ...]: invite strings, common API keys and tokens, JWTs, private keys, credentials in URLs, and password=-style values. It does this when it rebuilds a note, and when it offers or archives a note the session wrote.
  • It matches formats only. A password in plain prose or a token in an unknown format stays in the note. Session transcripts, which a rebuilt note names, and worker prompts in state.db are not redacted.
  • Archived notes are deleted 30 days after they were archived. A workspace's current note stays until its next takeover.
  • A note is a record of earlier work, not an instruction from you. The takeover prompt tells the next session to leave anything beyond the task, and to list what it left in its next message and in any note 8000 it writes. This wording is advice to the model, and its effect is untested. A permission you gave during a session may not carry over to a fresh successor, so you may need to repeat it. aiq convo shows what a session said it left. A session that cannot tell what the task was is told to say so instead of guessing, and nothing alerts you if it then waits. Where hard links do not work, aiq writes no rebuilt note and says so in the daemon log. A rebuilt note quotes what it copies, may name earlier archived notes, and gives its transcript's path only to say where it came from. The transcript is not redacted, and the note does not ask the successor to read it. In a partial clone it leaves out the log, stashes and worktrees and says so, and the successor can run git for itself.
  • aiq handoff redact lists what would change in the notes already on disk, by kind and count. aiq handoff redact --apply rewrites them, keeping no backup.

Fresh CLIs without stopping work. Once a day the daemon checks whether an npm-installed Codex or Claude Code has a newer release and, if so, installs that exact version with the installation's own npm (npm install -g --prefix <its prefix> <package>@<version>), then runs <cli> --version; a release that will not start is replaced by the version that was there. Running sessions keep the binary they started with; the next start, by hand or by a takeover, gets the new one. While a reinstall runs, a marker in ~/.local/share/aiq/updating/ holds every launch (up to five minutes) so nothing starts a half-installed CLI. Claude Code from the native installer updates itself and is left alone. [updates] enabled = false turns this off.

Antigravity

agy, Google's Antigravity CLI, joins the pool as a third provider with one difference: it runs on your real login. The CLI reads no variable for another home (only HOME moves ~/.gemini), and it keeps its token in the OS keyring, so in this release aiq registers the one ~/.gemini login as a native account and does not build overlay homes for it:

aiq account add agy main       # registers ~/.gemini; opens the sign-in if it has none
aiq run auto -p "fix the test" # may now land on agy/main when it has the most to spare
aiq long agy                   # supervised like the others; moves to Claude or Codex when dry

Quota polls use the read-only usage command. With agy 1.1.11 or newer, agy -p "/usage" --output-format json returns the quota buckets (gemini-5h, gemini-weekly, 3p-* for third-party models, and the separate Argon weekly bucket) without starting a model turn or spending tokens. Polling needs no custom statusline; agy 1.4.7 stopped running those in print mode. Failed polls retain the last good reading.

aiq account add agy also points ~/.gemini/antigravity-cli/settings.json at aiq agy-statusline for live interactive quota, identity, and plan telemetry. A command of your own keeps rendering behind it; with none, aiq's line is stacked under the built-in one. aiq statusline uninstall agy removes this interactive integration; quota polls continue to work.

Long sessions use global hooks. The CLI takes no per-launch hook flag, so aiq long agy installs an aiq-long entry in ~/.gemini/config/hooks.json (PreInvocation and Stop) that does nothing unless the session carries an aiq long lease, and adds the workspace to the CLI's trusted list so an unattended pane is not stopped at the trust prompt. A same-provider takeover resumes with --conversation <id>.

Not yet: several agy accounts (the keyring, not the home, holds the login), and a live "open right now" marker for aiq resume.

Picking up where the agents left off

After a day of sessions in one directory, several of them run by aiq long, aiq resume shows what each agent did there before you start the next one.

aiq resume                  browse this directory's sessions
aiq resume --all            include workers (claude -p, codex exec)
aiq resume <id>             resume one directly (a unique prefix is enough)
aiq resume --print [<id>]   the same lists as plain text, for scripts and agents
aiq resume --bare <id>      resume without the launcher the session ran under

The first screen lists every Claude and Codex session whose working directory is the current one, newest first, with the model, turn and tool counts, and the session's name or first prompt. Enter opens a session's turns: each prompt with the agent's final reply for that turn. Enter on a turn shows it in full, and ←/→ move between turns. r resumes the selected session on an account the pool picks, with claude --resume <id> or codex resume <id>.

  • A session is resumed through the launcher it ran under, so a safe-claude session goes back into the same nono sandbox. aiq records every launch (account, launcher, directory, arguments) in state.db before the CLI starts. For a new Claude session it chooses the id itself (--session-id), so the match is exact. Codex cannot be given an id, so a new Codex thread is matched to the latest aiq launch in the same directory before it; the list marks such a guess with ? ([via safe-codex?]). Once a session has run under a launcher, it keeps resuming there: b in the browser, or --bare, resumes it without one, for that time only. --launcher <name> picks another launcher. Sessions from before this record existed, and anything started without aiq, resume bare.
  • A bare resume adds the permission bypass if the transcript shows the session ran with it. Through a launcher, aiq adds it only if you passed it to that launcher yourself: the safe-* wrappers add it on their own, and Codex refuses the flag twice. Anything after -- is added to the CLI's arguments.
  • A session still running in aiq long is attached, not started a second time. For a Claude session that is open in another terminal (● in the list), r asks once more before it opens a second copy.
  • Subagent transcripts are not listed. Their work appears in the parent session's turns as tool calls.

Reading a running session

A long session buries the conversation under tool calls. aiq convo prints only what was typed into the session and what the agent answered, in the order it happened: each prompt, anything typed while the agent worked, and the agent's final reply to each turn, headed with how long the turn took and how many tools it used. The agent's first message after something you typed is its reply to it and shows in full (one message after several of your messages answers them all); a reply said on the way is headed with the time it came, while one that is an answer shows under that answer's own heading. What else it said on the way (Codex commentary, Claude's text before a tool call) shows as one dim line each, and a line that turns out to be an answer shows as that answer instead. Answers it gave before going on (after a background task or another agent's message) show as earlier. Prompts that aiq, another agent or a notifier typed in show as one dim line (an Agent Orchestra inbox notice as orchestra inbox · <time>); what the agent did with one shows by the same rules as your own turns, since its answers report work you want to know about. Agent-team idle notifications are left out.

aiq convo                   the agent in this tmux pane, else the one live session here
aiq convo --pane %5         the agent in tmux pane %5
aiq convo <id>              any session of this directory (a unique prefix is enough)
aiq convo --last 3          only the last three prompts and what followed
aiq convo --follow          keep printing as the conversation goes on
aiq convo --theme light     colours for a light background (auto|light|dark)
aiq convo --hide-markup     style the Markdown without its **, backticks and marks

On a terminal the conversation opens at its end in $PAGER (else less -R +G), with the agent's Markdown styled: bold as bold, code in its own colour. The Markdown stays in the text (**, backticks, #, list and quote marks, code fences, pipe tables), so a copy from the terminal pastes into Slack or a document as Markdown. --hide-markup draws it the way Claude Code does instead: the marks hidden, pipe tables as boxes that fit the terminal's width. What you typed sits on a grey band across the terminal, so it stands out from the answers. Anywhere else it prints plain text, Markdown as written. --follow never redraws: new prompts and replies are appended, a status line at the bottom says how long the agent has been working, and each prompt carries a mark that tmux copy mode's previous-prompt / next-prompt jump to. When the pane moves to a new transcript (/clear, Codex /new, a takeover by another provider), --follow prints a divider and goes on with the new one. It is read-only: what you type into it is swallowed, not echoed, and does not reach the session or the shell afterwards; Ctrl-C quits.

The colours suit a dark background unless the terminal says it is light. --theme auto (the default) asks the terminal for its background colour (OSC 11), else asks tmux which theme the terminals showing the pane reported (#{client_theme}, tmux 3.7; $AIQ_TMUX_CLIENT picks one client when they disagree), else reads $COLORFGBG, else takes dark; --theme light|dark, or AIQ_THEME, settles it (the flag wins). tmux answers that query in a pane when it knows the colour (from the outer terminal, or a window-style background) but not in a popup; there the client theme usually settles it, and otherwise set-environment -g AIQ_THEME light or --theme light in the binding does.

A pane is matched through the agent's own records: Claude Code's sessions/<pid>.json, else the pane's aiq long lease. Either counts only while its process is alive and runs in that pane, since tmux reuses pane ids after a restart. A claude -p worker the agent starts is never taken for the agent. Outside the agent's pane, aiq convo looks for the session running in this directory or one holding it: an open Claude session or a session aiq launched. A short Codex or Antigravity session reports no transcript, so its lease takes the newest interactive session of that CLI in its directory written since it started. A Codex session started without aiq is found by id only. --follow does not support Antigravity sessions yet.

Two tmux bindings make it a keystroke from any agent pane. aiq never installs them; aiq convo --help prints them with the path of your aiq binary. Use the absolute path: tmux runs them with its own PATH.

# prefix a: this pane's conversation in a popup; q closes it, and an error stays up until Escape
bind-key a run-shell "tmux display-popup -c '#{client_name}' -w 90% -h 90% -EE '/opt/homebrew/bin/aiq convo --pane #{pane_id}' || true"
# prefix A: the same, live, in a split beside the agent (focus stays on the agent)
bind-key A run-shell "tmux split-window -h -d -l 40% -t '#{pane_id}' '/opt/homebrew/bin/aiq convo --follow --pane #{pane_id}'"

Both go through run-shell because it is where tmux expands #{pane_id}: display-popup and split-window pass their command on unexpanded, and inside a popup $TMUX_PANE is empty.

Automatic provider and model selection

aiq run auto -p "do this thing"
aiq run auto --model-tier 0 --effort 6 -p "analyze this design"
aiq run auto --model-tier 2 --effort 2 -p "fix the failing test"
aiq run auto --model-tier 1             # interactive session
aiq run auto --providers claude,codex   # only these pools

auto ranks eligible accounts of every provider together (Claude, Codex, Antigravity, Copilot) using the quota score, worker reserve and concurrency limits, after the cost order (plan quota, then Claude extra usage, then the last-resort providers; see Cost order). It skips providers whose CLI is missing and providers with no model at the requested tier. --providers claude,codex limits one launch to those pools; auto.providers in config.toml sets the default (empty means every provider), and the flag overrides it. A worker's retry and a long session's takeover stay inside the same set. Selection uses stored telemetry, as ordinary routing does; unknown or stale telemetry cannot guarantee remaining quota. An early worker quota rejection can retry on another account, including the other provider, under the existing worker retry policy. Interactive sessions choose once at launch.

-p translates to Claude's print mode or Codex's exec command. The prompt is preserved as one literal argument. Without -p, auto opens an interactive session; add -- "initial prompt" to start it with a prompt. Auto always enables permission bypass: --dangerously-skip-permissions for Claude or --yolo for Codex, including workers and cross-provider retries. An explicit --yolo is still accepted but is redundant. Native provider options require aiq run claude or aiq run codex; auto does not translate arbitrary flags, resume sessions (use aiq run auto resume, aiq resume, or aiq long auto resume), named launchers, or forced accounts. --wait and --mode remain available, with worker mode requiring -p. For a supervised session that moves between accounts, use aiq long auto (see Long-running sessions).

Tier Claude Codex Antigravity Copilot
0 Fable 5.1 (fable) Astra (gpt-6-astra) — Astra (gpt-6-astra)
1 (default for auto) Opus 5.5 (opus) Sol 6.1 (gpt-6.1-sol) Gemini 4 Argon (gemini-4-argon-high) Sol 5.6 (gpt-5.6-sol)
2 Sonnet 5.5 (sonnet) Terra (gpt-5.6-terra) Claude Sonnet 5.5 (claude-sonnet-5-5-high) Terra (gpt-5.6-terra)
3 Haiku 5.5 (haiku) Luna 6 (gpt-6-luna) Gemini 3.8 Flash (gemini-3.8-flash-high) Luna (gpt-5.6-luna)

A tier is a class of model, the same on every provider, not a provider's own ranking of its models. Claude's aliases follow the newest model of each family; the other names are versions, current as of 2026-10-10 (Antigravity's Argon as of 2026-11-01), kept in internal/tier/tier.go. A model the table used to name (gpt-5.6-sol, claude-opus-5-5-high) keeps its tier, and a long session started on one is restarted on the tier's current model at its next same-provider takeover. Antigravity's Claude Opus stays available with an explicit --model, but a long session started that way moves to Argon at that takeover. Antigravity has no frontier model, so it has no tier 0: aiq run auto --model-tier 0 skips it, aiq run agy --model-tier 0 is an error, and a long session on a tier-0 model never moves to it. Copilot serves the Codex models, but no newer than the 5.6 generation below Astra; which of them an account may pick depends on its Copilot plan (on a Copilot Free login every named model was rejected as not available).

The requested tier stays fixed during retries; aiq never silently downgrades it. Model-scoped quota windows bind only the selected model, so an exhausted Fable cap does not disqualify an otherwise eligible Opus account. On Antigravity the Gemini models share the account's main windows, including the Argon weekly one; Claude models there draw on separate "3p" windows, which bind only tier 2.

Effort Claude Codex Antigravity Copilot
1 low low low low
2 medium medium medium medium
3 high high high high
4 xhigh xhigh high xhigh
5 max max high max
6 ultracode ultra high max

Effort defaults to high (3) for aiq run auto and for a model a takeover translates; with an explicit provider, omitting it preserves the CLI default. Claude's sixth setting means xhigh plus dynamic workflows, rather than a sixth model reasoning level; it requires Claude Code 2.1.203 or later. See the Claude CLI reference. Model and client support still apply to each setting. Antigravity spells the level as the model name's suffix (gemini-3.8-flash-low), so a tier and an effort become one --model; the Pro models have no medium and take high.

Both shared options also work with an explicit provider, before --:

aiq run claude --model-tier 0 --effort 6 -- -p "review this design"
aiq run codex --model-tier 2 --effort 3 -- exec "fix the failing test"

Explicit providers keep their configured model when no tier is supplied. Do not combine a tier with --model-scope or a native model override; the tier sets both the model and its quota scope. Pass native named effort values after --, for example aiq run claude -- --effort high -p "review this".

Recipes

Habits that make the pool disappear from day-to-day work.

Start from the menu

aiq

With no arguments on a terminal, aiq opens a start menu instead of the command list:

 aiq 0.7.13 · ~/personal/aiq

 › Session       New  Resume
   Agent         Any  Claude  Codex  boxed
   Length        Short  Long
   More          model tier, effort, account, permissions

   Start a new conversation in this directory.

 $ aiq run auto

Three rows cover most starts: a new session or one of this directory's to resume, any agent or a named one (your launchers included), and short (this terminal) or long (supervised in tmux). m opens the rest: model tier, effort, a specific account, and the permission bypass. Rows that do not apply to the current choice dim and say why, and the line under the rows explains the focused choice, including which account a routed start would take right now.

The bottom line is the ordinary aiq command the choices add up to. Enter prints it and runs it, so the menu doubles as a way to learn the commands. The menu remembers the last start, so Enter alone repeats it. When a long session already runs in the workspace, a attaches to it. ? prints the full command list, which aiq help also prints; piped or scripted, bare aiq prints it too.

Start a session without choosing anything

aiq run auto

When the question is "can I finish this session" rather than "which model do I want", let aiq decide. It picks the provider and account with the most quota about to expire, opens an interactive session on tier 1 (Opus or Sol) with permission bypass on, and stays there for the session. Pin the tier for the session with --model-tier:

aiq run aut
8BD0
o --model-tier 0              # the strongest model that has quota
aiq run auto --model-tier 2 --effort 2   # cheap and quick

Add -- "initial prompt" to open the session with a first message already sent.

Leave a long job running

aiq long auto

For work that will outlast one account's quota (an overnight refactor, a QC loop, an overseer that babysits PRs), start it as a long session instead. aiq picks the account as aiq run auto does and opens the CLI in a tmux session named after the repository. Type the task there, then detach (tmux prefix, then d; Ctrl-b d by default). The session keeps running after you close the terminal or drop an SSH connection. When the account runs low, the daemon has the agent write a handoff note and restarts it on another account in the same pane.

aiq long list      # which accounts your long sessions are on, busy or idle
aiq long attach    # from anywhere in the repository: back into its session
aiq long drain .   # move it to another account now
aiq long stop .    # end it

aiq long auto --model-tier 0 keeps the strongest model, on whichever provider it moves to; providers with no tier-0 model are passed over. Run aiq long auto again in the same repository and it attaches to the running session rather than starting a second one.

Turn a session into a long one

A session started with aiq run auto sometimes turns into hours of work. Exit it (/exit), then:

aiq long auto resume

The browser from aiq resume opens on this directory's sessions. Pick the one you left and press r: the same conversation reopens in a supervised tmux session, on an account aiq picks. It continues on other accounts, as in the previous recipe. If you already know the id, skip the browser: aiq long auto resume 3f2a.

aip: ask from the shell prompt

A one-shot question should cost one line, with no quoting. This shell function turns everything after aip into a single aiq run auto -p prompt, and a line widget quotes the rest of the line at Enter so apostrophes, question marks, globs and ! history references pass through untouched:

# ~/.zshrc
aip() {
  aiq run auto -p "$*"
}

# Shorthands whose arguments get passed verbatim when typed at the prompt.
verbatim_cmds=(aip)

_verbatim_accept_line() {
  local name rest
  for name in $verbatim_cmds; do
    [[ $BUFFER == "$name "* ]] || continue
    rest=${BUFFER#"$name "}
    # Skip lines already quoted, e.g. recalled from history.
    [[ $rest == "${(qq)${(Q)rest}}" ]] || BUFFER="$name ${(qq)rest}"
    break
  done
  zle .accept-line
}
zle -N accept-line _verbatim_accept_line

Then:

$ aip how do I run a command on a remote machine over ssh without a tty?
$ aip what's the git command to drop the last commit but keep its changes?
$ aip why does !ls fail on this box?

Each call is a worker: scored per call, kept off accounts that carry an interactive session when another is available, and retried on the next account if the provider rejects it with a usage limit. The widget rewrites the buffer before it is accepted, so the quoting is visible on the line as it runs, history keeps the quoted form, and recalling a line does not quote it twice. Add other functions to verbatim_cmds to give them the same treatment.

Scoring: spend what is about to vanish

For every binding window w of an account (the 5h and weekly windows, plus a model-scoped weekly cap such as "Fable" when that model is your default):

remaining_w = 100 − used_w
hours_w     = time until w resets, floored at min_hours
perish_w    = min(remaining_w, remaining_weekly) / hours_w
credits     = reset_credits × 100 / hours_to_credit_expiry × weekly_weight
score       = (Σ perish_w × weight_w + credits)  ÷  (1 + worker leases on the account)

perish_w is the number of percentage points that vanish per hour if the account sits idle. weight_w is 1 for the session window and weekly_weight (default 5) for weekly windows, because one weekly point is several session points' worth of tokens: an account whose week rolls over in ten hours with 40% unspent gets the work all afternoon, not only in the last hour. Highest score normally wins. Exhausted accounts (a binding window at 100%, or a limit hit), disabled accounts, accounts without a login, and Codex accounts whose latest poll definitively rejected their authentication are filtered first; workers additionally respect the weekly reserve and the per-account cap. Telemetry older than stale_after_seconds falls back to a neutral prior. aiq status --explain prints the ranking with every term.

Reset credits expire, and each one is a full weekly window. Codex earns them; Anthropic grants Claude accounts limit resets (Claude Code's /limit-reset, clearing the session and weekly windows). An account holding them scores as if that quota perished at the soonest credit's expiry (a week out when the expiry is unknown). Once its weekly allowance crosses switch_pct, it takes priority over a fresh account and stays sticky until the remaining allowance is gone; this makes the credit redeemable instead of leaving it stranded behind the final few percent. Workers may drain such an account past the weekly reserve, and take it even while it carries an interactive session. When it then hits its cap, the poll that sees the block redeems a credit on its own — always for a weekly block, and for a shorter window only when the credit would expire first or within a day. Set auto_reset_credits = false to leave spending to aiq reset codex/<name>.

Cost order: plan, extra usage, last resort

Ranking goes by cost class before score:

  1. Plan quota. Every account with subscription quota left, on any provider, Antigravity included (it serves Claude Opus 5.5).
  2. Extra usage. A Claude account whose plan is spent, or nearly spent, but which can run on prepaid usage credits. Where one exists, plan quota that is nearly spent (at or below drain_pct for a long session, past switch_pct for an interactive pick) ranks after it: a few points of plan buy a short stretch and another move, the credits run on. A long session on such an account is not drained when its plan runs low. Claude Code switches to them on its own once the plan's windows run out ("Extra usage is now covering your requests"). aiq polls the balance and the monthly spend limit, and uses such an account only when selection.extra_usage = true, extra usage is on, the prepaid balance is above zero, auto-reload is off (so no card is charged), and the monthly limit is not reached. Workers may spend it past the weekly reserve. A worker rejected with "You're out of extra usage" or "You've hit your monthly spend limit" benches the account for an hour.
  3. Last-resort providers, selection.last_resort in order (default Copilot), for auto and long-session takeovers once neither of the above is left. A provider still has to be allowed (auto.providers, --providers, long.fallback), and a long session still never moves to a provider with no model of its tier.

aiq reads the balance and the auto-reload setting together; a reading older than 15 minutes, or an auto-reload setting it cannot read as off, stops the spending. The switch only governs where aiq routes: a session already running on an account with extra usage turned on spills into it by itself when the plan runs out, whatever extra_usage says.

A sticky interactive session on extra usage moves back to plan quota at its next launch, and keeps its account (one balance, not each in turn) while no plan quota exists. A long session on extra usage moves once an account has plan quota above drain_pct; a nearly spent one would only bounce it back. A long session on a last-resort provider moves back to such an account after idle_rotate_minutes of quiet. aiq status shows the balance and marks accounts running on extra usage extra and last-resort ones last.

Install

For a first install on macOS or Ubuntu, install Git and Go (the version in go.mod or newer), then clone and run:

git clone https://github.com/orlenko/aiq.git
cd aiq
./install-or-update.sh

For updates, run the same script from your existing checkout, from any directory:

~/code/aiq/install-or-update.sh       # use your checkout's actual path

The script fast-forwards the checkout's tracking branch, builds and atomically installs ~/.local/bin/aiq, refreshes provider and launcher shims, and installs or restarts the current user's daemon (launchd on macOS, systemd on Ubuntu). Run it as your regular user, without sudo. It refuses to pull over local changes. Use --no-pull to install the current working tree, or --no-daemon to install only the CLI and shims. On Ubuntu, daemon installation needs a running systemd user manager. Add the printed shim directory and ~/.local/bin to your shell's PATH; the script does not edit shell startup files.

Alternatively, install manually:

go install github.com/orlenko/aiq/cmd/aiq@latest   # or: go build -o ~/.local/bin/aiq ./cmd/aiq
aiq shim install                 # then add the printed PATH line to your shell rc
aiq daemon install               # launchd (macOS) or systemd --user (Linux)

After either installation method, add your first accounts:

aiq account add claude work      # Claude Code login + quota poll grant (two browser steps)
aiq account add claude home
aiq account add codex work       # codex login (one browser step)
aiq doctor

On a headless machine (an SSH session, or Linux with no DISPLAY or WAYLAND_DISPLAY), the Codex login uses codex login --device-auth: open the printed link on any device and enter the code. The browser flow needs a callback to localhost:1455 on the machine running aiq, which a browser elsewhere cannot reach. Pass --device-auth or --browser to aiq account add or aiq account login to override the detection, which reads the environment. A tmux pane keeps what it started with, so a pane created over SSH and later used at the machine still counts as headless, and a pane created at the machine and later attached over SSH does not; use the flags in those panes. ChatGPT may require device code authorization to be enabled in its security settings first. If a login fails during aiq account add, the account is already saved, so retry with aiq account login codex/<name>.

A definitive Codex authentication rejection from the periodic quota poll automatically removes that account from routing. aiq account login codex/<name> verifies the replacement login with a fresh poll and restores it when successful. For a manual pause, use aiq account disable codex/<name> and later aiq account enable codex/<name>.

Sign in as the intended account at each browser step: sign out of claude.ai or chatgpt.com first, or use a private window, so the browser does not hand back the account it is already signed in as. aiq warns when two accounts turn out to be the same login.

If you run aiquota on the same machine, aiq account import adopts its accounts instead: Codex auth.json files and Claude poll grants move into the aiq homes and aiquota is re-pointed at them, labels and grid order come along, and the Claude account matching the current ~/.claude login is registered as native. aiquota is not needed for anything else.

Commands

aiq                                  start menu (on a terminal)
aiq claude [args...]                 same as the shim: route and launch
aiq codex  [args...]
aiq run <provider> [--account <name>] [--next] [--mode interactive|worker] -- [args...]

aiq status [--json] [--refresh] [--explain] [--events N]
aiq top                              live console view

aiq account list
aiq account add <provider> <name> [--device-auth|--browser]
                                     new overlay home + login (+ poll grant for Claude)
aiq account login <provider>/<name> [--device-auth|--browser]
                                     redo the CLI login
aiq account authorize claude/<name>  redo the quota poll grant
aiq account poll [<id>...]           poll now
aiq account label <id> <text>        label shown on the status page
aiq account order [<id>...]          display order within each provider column
aiq account rename <id> <new-name> [--keep-home]
                                     rename everywhere; moves the home and its login
aiq account enable|disable|remove <provider>/<name> [--purge]
                                     disable temporarily removes it from routing
aiq account use <provider>/<name>    pin this workspace
aiq account next <provider>          rotate this workspace
aiq account import [id[=name]...]    adopt aiquota accounts, if you have aiquota

aiq mark <provider>/<name> exhausted [--until 14:42|+2h|RFC3339]
aiq mark <provider>/<name> ready
aiq reset <provider>/<name>          consume a Codex or Claude reset credit

aiq resume [--all] [--print] [--launcher <name> | --bare] [<id>] [-- args]
                                     this directory's sessions: browse turns, resume one
aiq convo [--pane <id>] [--follow] [--last N] [--theme auto|light|dark] [--hide-markup] [<id>]
                                     only the prompts and replies of a running session

aiq shim install|uninstall|path
aiq statusline install|uninstall|status   Claude status-line multiplexer
aiq daemon run|install|uninstall|status
aiq doctor [-v] [--device-auth|--browser]
                                     failed checks and how to fix them

aiq doctor prints only the checks that failed (-v prints every one), then groups them into an ordered list of fixes, one per account, so a Claude account that needs a new login and a new poll grant is one step with both sign-ins. On a terminal it offers to go through the list: for each step it says which account to sign in as and what to paste, runs the fix when you press Enter (s skips, q stops), then checks everything again.

aiq account rename claude/claude4 fourth carries the account's usage, leases, workspace pins, launch history, label and display order to the new name. An overlay home at the default path moves with it, along with the macOS keychain login Claude Code keeps for that path and any aiquota credential path inside it, so no new login is needed. --keep-home renames the account and leaves the home where it is; a native home never moves. A running session holds the old name in its environment, so the rename waits until no session is using the account.

Launch flags for orchestrators (also as AIQ_MODEL_SCOPE, AIQ_WAIT, AIQ_MODE in the environment of a child):

  • --model-scope <name> binds the named model-scoped window for this launch (fable skips accounts whose Fable weekly cap is dry; opus or none ignores it). Default comes from providers.<p>.model_scope.
  • --wait <duration> (600, 600s, 10m) makes a worker poll for a free slot when every eligible account is at max_workers_per_account, instead of refusing; it still refuses at once when the pool is exhausted, since waiting cannot fix that. A value that does not parse is bad-flags. [worker] wait_for_slot_seconds sets a default.

Escape hatches: AIQ_BYPASS=1 claude ... runs the real CLI on your real home with no routing; claude auth ..., claude setup-token, claude update, codex login and codex logout always pass through.

Machine interface

Two things are stable for other tools: exit codes and the status JSON.

Exit codes. A child's own exit code passes through unchanged. aiq's own refusals exit with a distinct code and print one machine-readable line, aiq: refused: <token>, before the human-readable message:

code tokens meaning
75 pool-exhausted, at-worker-cap, wait-timeout nothing available right now; try later or elsewhere
78 no-accounts, no-binary, bad-flags, max-depth, account-not-found nothing to route to; a human has to change something

bad-flags covers aiq's own flags only (--mode, --wait). An unknown flag before -- is not aiq's: it is forwarded to the CLI, which fails in its own way with its own exit code.

Status JSON. aiq status --json and GET http://127.0.0.1:7379/api/state (under view) return the same document, except that since v0.7.15 the API leaves out events and each lease's args, and GET /api/events is gone. Launch arguments and the audit log can hold prompts, and any local user can reach this port. To read them, use aiq status --json, which runs as you and reads your files. It shows the last 15 events, and --events N shows more; the daemon trims the log to about 2000 rows. args is a string: the arguments joined by spaces and shortened (80 characters each, about 300 in all), or for a long session a JSON-encoded array in full.

schema_version is 1 from the command and 2 from the API. Additions do not bump it; a field that changes meaning or goes away does. Stable fields:

schema_version, generated_at, hostname
worker_capacity{<provider>: n}        free worker slots per provider, summed over eligible accounts
worker_capacity_by_scope{<provider>: {none: n, <scope>: n, …}}
                                      same, with that model-scoped window binding ("none" = ignored)
accounts[]:
  id, provider, name, enabled, native, identity, plan
  has_credential, eligible, ineligible, exhausted, reason, cooldown_until
  score, rank, terms[]                worker ranking (see Scoring)
  worker_slots                        free slots on this account (0 when ineligible)
  workers, interactive                live leases by kind
  windows[]: key, label, kind, scope, binding, used_pct, resets_at, window_seconds, source, observed_at
  leases[]: pid, mode, cwd, depth, started_at, drain, pane, workspace
rankings{"<provider>/<mode>": [{id, score, eligible, reason, terms}]}
events[]: ts, provider, account, type, detail     aiq status --json only

Size a batch fleet from worker_capacity_by_scope[provider][scope]; pick a provider per model tier from the same map.

Example: a review fleet spawning Codex seats. An orchestrator that runs inside a routed interactive claude session (depth 0) launches one process per seat, which spawns codex by bare name with the prompt on stdin and the result in a file:

env:  AIQ_MODEL_SCOPE=<tier scope>  AIQ_WAIT=600     (inherited by the spawn)
argv: codex exec --ephemeral -C <repo> -s read-only -m <model> \
        -c model_reasoning_effort=<effort> --color never -o <result.md> -
stdio: stdin ← prompt (written once, then closed); stdout ignored; stderr tail kept

What aiq does with it: the shim classifies it as a worker (exec first, stdout a pipe), routes it to an eligible account under that scope, forwards stdin unchanged, tees stderr and stdout for the usage-limit patterns, and reruns the same command on the next account if the provider rejected it early. Six seats launched at once on a one-account pool run three at a time with AIQ_WAIT set; without it, seats four to six exit 75 at-worker-cap immediately. A timeout that signals the process group reaches both aiq and its child; aiq also forwards SIGTERM/SIGHUP to the child. Depth is 1, well under max_depth.

Storage

What Where
Overlay homes (credential, poll grant, .claude.json private; everything else symlinked) ~/.local/share/aiq/claude/<name>/, ~/.local/share/aiq/codex/<name>/
Antigravity: the native account is ~/.gemini; quota polls run in ~/.local/share/aiq/agy-probe/
Shims ~/.local/share/aiq/shims/
State (accounts, windows, leases, affinity, launches, events; no credentials) ~/.local/share/aiq/state.db
Daemon log ~/.local/share/aiq/log/daemon.log
Config ~/.config/aiq/config.toml
Session transcripts aiq resume reads (never writes) ~/.claude/projects/, ~/.codex/sessions/, plus any unsynced copies in the overlay homes
Service ~/Library/LaunchAgents/dev.aiq.daemon.plist or ~/.config/systemd/user/aiq.service

AIQ_DATA_DIR and AIQ_CONFIG relocate the data directory and the config file.

Configuration

Everything has defaults; the file is optional.

version = 2

[providers.claude]
binary = ""              # discovered on PATH (outside the shim dir) when unset
model_scope = "auto"     # scoped weekly cap that binds; "auto" reads settings.json model
wrapper = ""             # launcher for interactive and long sessions (see below)

[providers.codex]
binary = ""
model_scope = "auto"     # reads config.toml model
wrapper = ""

[selection]
interactive_policy = "sticky"   # sticky | score
switch_pct = 95.0
stale_after_seconds = 900
weekly_reserve_pct = 5.0
max_workers_per_account = 3
max_depth = 3
min_hours = 0.25
weekly_weight = 5.0
auto_reset_credits = true       # redeem a Codex or Claude reset credit when a holder is blocked
extra_usage = false             # spend Claude prepaid extra usage once plan quota is gone everywhere
last_resort = ["copilot"]       # providers used only after plan quota and extra usage

[worker]
retry = true
retry_max_seconds = 60.0
wait_for_slot_seconds = 0       # workers poll this long for a free slot at the cap
# limit_patterns = ["(?i)hit your (usage|session|weekly) limit", ...]

[poll]
interval_seconds = 300
timeout_seconds = 60

[display]                       # status page and `aiq status`
order = ["claude/work", "claude/home", "codex/work"]
[display.labels]
"claude/work" = "Claude · work"

[daemon]
listen = "127.0.0.1:7379"       # 127.0.0.1 or [::1] only; see below

[auto]
providers = ["claude", "codex"]  # pools auto may pick; empty = every provider

[long]
drain_pct = 4.0                 # ask for a handoff note at this much remaining
fallback = ["claude", "codex", "agy"]  # takeover order after the session's own provider
check_interval_seconds = 30
idle_grace_seconds = 20
idle_rotate_pct = 15.0          # move a quiet session off an account this low (0 = off)
idle_rotate_minutes = 15        # ...once it has been quiet this long
tmux_prefix = "aiq"

[updates]
enabled = true                  # keep npm-installed codex/claude current
interval_hours = 24

[telemetry]
claude_statusline = true

daemon.listen must be 127.0.0.1 or [::1] with a plain port. With anything else, including localhost or 0.0.0.0, aiq daemon install refuses, and a daemon that is already installed keeps polling and supervising long sessions but serves no status page or API, so scripts calling its URL get connection refused. To read the page from another machine, use a tunnel with a local port of its own: ssh -L 17379:127.0.0.1:7379 host, then open http://127.0.0.1:17379/. A running daemon keeps the address it started with until it restarts. A daemon from before 0.7.13 on the same port also stops a newer one starting, which then logs why and takes over once the older one stops. aiq doctor and aiq daemon status say what they can observe, and lsof -nP -a -c aiq -iTCP -sTCP:LISTEN lists aiq's listeners. With a refused address, the install script replaces the binary, then stops at the daemon step with the same advice.

Launchers

A launcher is a named program that starts the CLI in an environment of its own: a sandbox, a recorder, a profiler. Routing happens first, so the launcher inherits a session that already has an account.

Nothing is ever wrapped unless its name is the word you typed. Plain claude and codex keep exec'ing the CLI directly.

aiq launcher add boxed --provider claude --credential file \
  --env BOX_ALLOW='$CLAUDE_CONFIG_DIR' -- /usr/local/bin/boxed --profile strict

which registers

[launchers.boxed]
provider   = "claude"
command    = "/usr/local/bin/boxed"
args       = ["--profile", "strict"]
credential = "file"
fallback   = ["boxed-codex"]
[launchers.boxed.env]
BOX_ALLOW = "$CLAUDE_CONFIG_DIR"

and writes ~/.local/share/aiq/shims/boxed, so all three of these start it:

boxed [args...]            the shim
aiq boxed [args...]        the same, spelled out
aiq long boxed [args...]   supervised, with drain and takeover

The contract:

  • The launcher receives the CLI's arguments, after any args of its own, and starts the CLI itself.
  • CLAUDE_CONFIG_DIR (or CODEX_HOME) already names the chosen account's overlay home. A launcher that confines the CLI has to let it reach that directory, and the real ~/.claude / ~/.codex the overlay links into. An env value may name those variables, as BOX_ALLOW does above; they expand when the launcher runs.
  • aiq drops its own shim directory from PATH first, so a launcher that starts claude or codex by bare name reaches the real CLI instead of routing a second time. A launcher may therefore be named after the very command it wraps. AIQ_BINARY holds the resolved path, AIQ_LAUNCHER the name in use.
  • credential = "file" writes the account's credential into the overlay home before the launch, for a launcher that cuts the CLI off from the system credential store. Without it such a launcher gets a login prompt. A token the CLI then refreshes stays in that file; aiq doctor says so when the copy in the store has fallen behind.
  • Workers, logins, passthrough commands and telemetry probes never use a launcher. They are not sessions a person is sitting in front of, and some have no terminal at all.

A long session started under a launcher keeps it across a takeover. fallback names the launchers it may move to, in order; the providers of those launchers are the only ones it moves between, so a session never quietly loses the launcher it was started with. aiq long <name> prints the order it will use.

Scope

Not in this version: agy (Antigravity) accounts.

A note on the Claude quota endpoint

Codex quota comes from the documented codex app-server RPC. Claude has no documented read path for subscription usage, so aiq uses the same OAuth usage endpoint Claude Code's own /usage calls, with a grant minted through Claude Code's OAuth client. That endpoint is not a public API and may change or be restricted without notice; if it stops answering, routing degrades to the status-line feed (live for the account in use) and worker rejections, and aiq doctor says so.

License

MIT

Contributing

See AGENTS.md and the quality gates.

About

Quota-aware router for pooled Claude Code and Codex subscriptions: PATH shims, rollover scoring, long-session handoff, status daemon

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

0