Add claude-profile-router: route tasks by Claude account headroom - #1
Open
bborn wants to merge 2 commits into
Open
Add claude-profile-router: route tasks by Claude account headroom#1bborn wants to merge 2 commits into
bborn wants to merge 2 commits into
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.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
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 aRouted to Claude profile …line in its log.jqorpython3on 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 usagecommand 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.shdoes that work here instead, so a fix is agit pull.It reads the profile's OAuth token (macOS Keychain, namespaced per config dir by a hash of its path;
.credentials.jsonelsewhere), 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.shis usable on its own:./usage.sh percent ~/.claude-work→12.How it decides
TY_CLAUDE_MAX_PERCENT(default 90) are skipped.HOLD=1, which leaves the task queued (not blocked), so it starts by itself once limits reset.Failure is always silent
No credentials, an expired login,
tymissing from the daemon'sPATH, 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
usage.shreads one profile's token and reports its used percent;route.shasks it about each profile and prints the winner.TY_CLAUDE_JSONforces 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).config.env, so you can try a threshold withTY_CLAUDE_MAX_PERCENT=50 ./route.shwithout editing the file.<plugins dir>/plugins/claude-profile-router, asty plugins addproduces it):ty plugins listdiscovers the hook and action, thestatusaction runs, androute.shemits the right decision — routing away from a 99%-spent account to a 12% one and skipping a profile with an expired login.shellcheckclean.task.routehook picks the account for every task, and that this plugin reads stored Claude credentials to check usage.🤖 Generated with Claude Code