Skip to content

Add claude-profile-router: route tasks by Claude account headroom - #1

Open
bborn wants to merge 2 commits into
mainfrom
add-claude-profile-router
Open

Add claude-profile-router: route tasks by Claude account headroom#1
bborn wants to merge 2 commits into
mainfrom
add-claude-profile-router

Conversation

@bborn

@bborn bborn commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

What

claude-profile-router — if you have two Claude logins, each task goes to whichever account has the most rate-limit headroom left, and waits in the queue when both are spent.

status

decision

Real output from a two-account setup: one profile is at 99% of its weekly limit, the other at 12%, and the router skips the spent one.

Requirements

  • A ty that emits task.route (taskyou#690). This is the collection's first hook plugin rather than a workflow, so unlike the others it needs support in ty itself. Merge and release that first — until then this plugin loads and does nothing, because the event never fires. To confirm it's live, start a task and look for a Routed to Claude profile … line in its log.
  • jq or python3 on the daemon's PATH, to read the usage API's JSON.

I added a line to the collection README's Anatomy section noting that workflow plugins run on any ty while hook plugins need one new enough to emit their event.

Self-contained on purpose

The first draft of this leaned on a ty usage command that taskyou#690 would have shipped. That put ~1250 lines of knowledge about someone else's endpoint into a released binary, where a change on Anthropic's side needs a ty release to fix. usage.sh does that work here instead, so a fix is a git pull.

It reads the profile's OAuth token (macOS Keychain, namespaced per config dir by a hash of its path; .credentials.json elsewhere), calls the usage endpoint, and reports the binding window — the worst of session / weekly / per-model, since that's the one that stops work first. Readings are cached for a minute under ~/.cache/ty/claude-usage (the endpoint rate-limits and routing probes it every spawn), with a 30-minute stale window that rescues a failed read rather than leaving the router blind.

usage.sh is usable on its own: ./usage.sh percent ~/.claude-work12.

How it decides

  • A profile's binding limit is the worst of its windows (5-hour session, weekly, per-model weekly) — a session window at 98% blocks the next task even when the weekly one is untouched.
  • Lowest binding percent wins; profiles at or above TY_CLAUDE_MAX_PERCENT (default 90) are skipped.
  • Every profile over the threshold → HOLD=1, which leaves the task queued (not blocked), so it starts by itself once limits reset.
  • A task that already names a config dir is left alone, and a routed task stays on its profile — its Claude session lives in that config dir, so resuming elsewhere would silently start a fresh conversation.

Failure is always silent

No credentials, an expired login, ty missing from the daemon's PATH, an unparseable response — every one of them prints nothing and the task spawns exactly as it would have without the plugin. The one case that deliberately does not hold tasks is "every probe failed": that's the plugin being broken, not the accounts being spent, and parking the whole board behind a broken credential lookup would be the worst possible response.

Notes for review

  • Worth reading before installing, given it decides which account your tasks spend: usage.sh reads one profile's token and reports its used percent; route.sh asks it about each profile and prints the winner.
  • JSON needs a real parser, so it's jq or python3. TY_CLAUDE_JSON forces one — without that a machine with both installed would only ever exercise the jq branch and the python3 dialect could rot unnoticed. I verified both agree with each other on real accounts (and, while it still existed, with the Go implementation).
  • Environment beats config.env, so you can try a threshold with TY_CLAUDE_MAX_PERCENT=50 ./route.sh without editing the file.
  • Verified end-to-end from the real collection layout (<plugins dir>/plugins/claude-profile-router, as ty plugins add produces it): ty plugins list discovers the hook and action, the status action runs, and route.sh emits the right decision — routing away from a 99%-spent account to a 12% one and skipping a profile with an expired login.
  • Exercised the failure paths directly: expired credentials, no credentials, cache hit with the API unreachable, stale-cache rescue, and cache too old to rescue.
  • shellcheck clean.
  • I extended the Trust section to mention that a task.route hook picks the account for every task, and that this plugin reads stored Claude credentials to check usage.

🤖 Generated with Claude Code

Two Claude logins means a choice on every task — which account should this one
spend? Made by hand it is uninformed, so one gets hammered into a 429 while the
other sits idle.

This plugin answers ty's task.route hook, which fires just before a task spawns
and reads the script's stdout back as a decision. It asks `ty usage` how much of
each profile's limits are gone, sends the task to the one with the most headroom,
and holds it in the queue when every account is spent.

First hook plugin in the collection rather than a workflow, so it needs a ty new
enough to emit task.route (taskyou#690) — the README says so and `ty usage`
is the check.

Failure is always silent: no credentials, an expired login, ty missing from the
daemon's PATH, or any other surprise means the plugin prints nothing and the
task spawns exactly as it would have without it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The first draft had ty carry the usage reader (a `ty usage` command backed by a
Go package) and this plugin just call it. That put ~1250 lines of knowledge about
someone else's endpoint into a released binary, where a change on Anthropic's
side needs a ty release to fix. Here it's a git pull.

usage.sh does what that package did: read the profile's OAuth token (macOS
Keychain, namespaced per config dir by a hash of its path, or
.credentials.json elsewhere), call the usage endpoint, and report the binding
window — the worst of session/weekly/per-model, since that's the one that stops
work first. The endpoint rate-limits and routing probes it on every spawn, so
readings are cached for a minute, with a 30-minute stale window that rescues a
failed read rather than leaving the router blind.

JSON needs a real parser, so it's jq or python3. TY_CLAUDE_JSON forces one:
without it a machine with both would only ever exercise the jq branch and the
python3 dialect could rot unnoticed. Verified both agree, on real accounts.

Nothing shells out to ty any more, which also retires the "ty isn't on the
daemon's PATH" failure mode.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
bborn added a commit to bborn/taskyou that referenced this pull request Aug 15, 2026
`ty usage` and internal/claudeusage were ~79% of this branch, and none of it had
to be here — the plugin can read the keychain and call the endpoint itself, as it
now does.

What's left is the part a plugin genuinely cannot do. Plugins see four events,
all from OnStatusChange, and the earliest of them (task.started) fires after the
spawn command is already built. The alternative — a service polling for queued
tasks — races a 2s tick and would have to write claude_config_dir straight into
SQLite, since the HTTP API exposes that field on projects only. So ty has to
offer the moment; it does not have to offer the data.

Moving it also puts knowledge of someone else's endpoint where it can be fixed
with a git pull instead of a ty release, which matters for something this likely
to drift.

Core keeps: the task.route hook, its executor wiring, and
UpdateTaskClaudeConfigDir. Data and policy both live in
taskyou/plugins#1 now.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant