Your coding agent keeps your own login. Its subagents run on SWE-2.
Claude Code and Codex both stay signed in as you. The work they delegate goes to Cognition's SWE-2 — Medium, High or Max — through one local service that routes on the model name and nothing else.
One port, no external binary, nothing written into your agent's config.
devinx --or devinx --cx --or
└─ Claude Code └─ Codex
claude.ai login ChatGPT login
│ │
└──────────► devinx.py on :8316 ◄──────────┘
│
routed on the model name
│
┌───────────────────────┼───────────────────────┐
claude-* swe-2-* gpt-*
api.anthropic.com server.codeium.com chatgpt.com/backend-api
relayed SWE-2 relayed
The subagents are real subagents of the client you launched: same tools, same harness, same transcripts. Nothing spawns a second process, and the credential for each upstream is the one its own client sent — devinx holds none of them.
This talks to Cognition's private API, not a public one, and it gets there by looking like something it is not:
- it identifies itself as the Windsurf IDE (
IDE_NAME,IDE_VERSION,EXT_VERSIONand a matching user-agent indevinx.py); _SYS_REWRITESindevinx.pyrewrites parts of the client's system prompt for the specific purpose of getting past Cognition's input classifier, which otherwise rejects them. That is circumvention of a check the provider put there, not a compatibility shim;descriptors/*.fdpare protobuf definitions of that private API, extracted from a published npm package.
It runs on your own Devin/Windsurf subscription and spends your own quota. Using it plausibly breaches Cognition's terms of service. That is your call and your risk, not the authors'.
Not affiliated with, endorsed by, or supported by Anthropic, OpenAI or Cognition.
Requires Python 3.10+, a Devin CLI login for the SWE-2 side, and at least one of the Claude Code CLI or the Codex CLI on PATH.
python3 install.pyIt creates a virtualenv, installs two dependencies, drops a devinx launcher in
~/.local/bin, and runs a smoke test that ends with a real SWE-2 call.
If you have not logged into Devin on this machine yet, the installer prints the exact command — the login is interactive and cannot be automated:
XDG_DATA_HOME="<data dir printed by the installer>" devin auth loginUseful flags: --force (overwrite an existing launcher and virtualenv),
--port N, --bin DIR, --no-smoke, --global-agents.
Almost nothing is installed into your agent's configuration, because a tool that only works when you ask for it should also be invisible when you don't.
The Claude Code subagents are not written into ~/.claude/agents: the
launcher injects them per session with --agents, so a plain session shows no
swe2-* agent at all. Installing them globally would list agents that fail when
invoked outside devin mode, since nothing routes swe-2 models there. Pass
--global-agents for the old behaviour. The orchestrator skill rides along the
same way, as a session-scoped --plugin-dir.
Codex is configured entirely through per-session -c overrides, so your
config.toml, plugins and agents are untouched. The one exception is the
orchestrator skill, which Codex can only load as an installed plugin: the
installer writes a devinx.config.toml profile next to your config and
materialises the plugin cache. A profile does nothing until it is named, and the
launcher names it only for --codex --or.
devinx # plain Claude Code, exactly as `claude`
devinx --devin # with the SWE-2 layer (--d is a shorthand)
devinx --or # ... and the orchestrator skill; implies --devin
devinx --d --resume # composes with any claude flag
devinx --resume x -p "..." # flags and prompts pass through untouchedWithout a flag, devinx is a plain passthrough: nothing is started, nothing is
injected, no proxy.
devinx --codex # Codex through devinx (--cx is a shorthand)
devinx --cx --or # ... with the SWE-2 roles and the orchestrator skill
devinx --cx exec "..." # any codex subcommand and flag passes through--codex implies the service, the same way --devin does. Your root model is
whatever your Codex config already says; only what it delegates changes.
| flag | effect |
|---|---|
--devin, --d |
route Claude Code through devinx; inject the swe2-* agents and claude-reviewer |
--codex, --cx |
route Codex through devinx; pin the SWE-2 roles |
--or |
add the orchestrator skill; implies the client flag it is used with |
--plain |
cancel DEVINX_ALWAYS=1 for this one run (an explicit --devin/--or/--codex still wins) |
-- |
everything after it passes through verbatim |
--status, --doctor, --explain |
local diagnostics (see stability controls); recognised in the first position or after devinx's own flags (devinx --d --status), as long as the position is unambiguous |
| variable | effect |
|---|---|
DEVINX_ALWAYS=1 |
make the SWE-2 layer the default |
DEVINX_ORCHESTRATOR=1 |
make the orchestrator the default (implies DEVINX_ALWAYS) |
DEVINX_SUBAGENT_MCP=1 |
give subagents their MCP tools back |
DEVINX_PORT |
change the port for one run |
DEVINX_CONTEXT_TOKENS |
override the context window devinx declares (default 262144) |
DEVINX_ALLOW_BROWSER=1 |
accept browser-originated requests (see below) |
DEVINX_DATA |
override the data directory (logs, lock file, Devin credentials); defaults to the platform data dir |
DEVINX_LOG |
override the log file path; defaults to devinx.log in the data directory |
DEVINX_API_KEYS / DEVINX_API_KEY |
comma-separated Devin credentials (or a single one), instead of every credentials.toml under the data directory |
DEVINX_OWNED_PATHS |
os.pathsep-separated paths/globs the --or hooks enforce as owned files when a subagent's brief declares no <owned-paths> block, and for the main thread (see hooks below); unset means unenforced |
DEVINX_DESCRIPTORS |
an extra directory of protobuf *.fdp descriptors to load, ahead of the packaged ones |
--or is separate from the client flags on purpose: the SWE-2 layer changes
which model does the work, the orchestrator changes who decides what the work
is. A session that just wants cheap subagents does not want a skill telling it
to delegate, so --devin alone leaves the skill out entirely.
-d is left alone — it is Claude Code's own --debug. Everything after a bare
-- is passed through verbatim, so a prompt containing --devin is never
mistaken for the flag.
devinx never adds --dangerously-skip-permissions. Choosing a model and
choosing to run tools unattended are separate decisions; pass the flag yourself
if you want it. The launcher is deliberately not called cc: on Unix that is
the C compiler, which make, autoconf and cgo invoke by name.
Already have your own cc wrapper? Add a branch to it rather than using
devinx, and keep whatever flags you already pass:
--devin|--d) exec ~/devinx/.venv/bin/python ~/devinx/launcher.py --devin "$@" ;;
--or) exec ~/devinx/.venv/bin/python ~/devinx/launcher.py --or "$@" ;;The service starts on demand the first time and stays up afterwards.
--or turns the session around: your own model stops writing the code and
starts running the agents that do.
The trade it is built on is that the two sides are scarce in different ways.
Your own context is the thing that runs out fastest; SWE-2 is far cheaper but
not free — it enforces a token budget over a rolling window, and a burst of
parallel agents empties it, after which calls are refused for one to twelve
minutes. devinx reports that refusal as a rate_limit_error with the wait the
upstream named, so the client backs off instead of killing the agent.
So execution belongs downstream and judgment stays up top — architecture, decomposition, what the diff actually says, what to tell you.
| agent | for | writes files |
|---|---|---|
swe2-explorer |
mapping code, tracing flow, locating symbols and tests | no |
swe2-worker |
bounded implementation, targeted fixes, mechanical edits | yes |
swe2-tester |
running tests, reproducing failures, adding tests | yes |
swe2-researcher |
external docs, version-specific API behaviour | no |
swe2-reviewer |
independent read of a finished change | no |
claude-reviewer (Claude Code only) |
the same, on your own model, for high-stakes changes | no |
Every SWE role is pinned to swe-2-max, because it is the strongest — not
because it is free. Under rate limiting a weaker tier is a real lever: the same
mechanical edit costs less of the shared budget on swe2-medium.
On Claude Code the read-only roles are denied Edit, Write and
NotebookEdit outright. Bash can still write, so their prompts say it too —
this closes the accidental path, not a determined one.
claude-reviewer is the one agent that costs your own quota: it has no pinned
model and inherits the session's own. It is for changes that are expensive to
get wrong — security, auth, migrations, data integrity, concurrency — and the
skill says so rather than leaving the choice to habit.
What the skill actually spends its length on is the asymmetry. A strong peer can be handed an outcome; these agents need the approach, the file list, acceptance criteria a command can settle, and tests declared off-limits unless changing them is the job.
And the root reads the real diff before believing any of it: a subagent reporting success is a hypothesis, and the summary is the part that lies. Retry policy is two strikes — narrow the brief once, then take the task back and say that you did.
It is written as defaults with stated escape hatches, not as hard MUST-gates:
review is chosen per change, and delegating nothing is a legitimate answer for a
two-line fix you have already located.
your own model
root — decides and briefs
|
+-------------------+-------------------+
| | |
explorer worker researcher
swe-2-max swe-2-max swe-2-max
| |
+---------+---------+
|
tester
swe-2-max
|
reviewer
swe-2-max, or claude-reviewer
when it is expensive to
get this wrong
|
v
your own model
reads the real diff, verifies, reports
The root appears twice on purpose. It is the first thing in the chain and the last, and the bottom half is the half people skip.
On Claude Code the skill is /devinx:swe-orchestrator; on Codex it is
swe-orchestrator. Both are also picked up on their own when a task matches.
/devinx:swe-orchestrator
Add rate limiting to the public API.
Map the current middleware chain before touching anything.
One worker per endpoint group, a tester for the limit boundaries
themselves, and review this one with claude-reviewer — a limiter that
is wrong in the permissive direction is not going to announce itself.
Naming the roles is optional; the skill picks them on its own. Saying it is worth doing when you already know the shape of the work, or when you want the expensive reviewer on something that looks routine and is not.
--or also loads two Claude Code hooks (plugin/hooks/), the same
session-scoped way it loads the skill: --devin alone does not, and neither
does Codex, which has no equivalent hook mechanism.
edit_loop_guard.pyrefuses an Edit/Write/NotebookEdit identical to one that has already run and failed three times — the same failing call retried forever instead of the agent reading the file and adjusting. Only real failures count (a call denied by a prompt or another hook never ran); a successful edit, or re-reading the file, forgives them; a new session starts clean.ownership_guard.pyrefuses an Edit/Write/NotebookEdit outside the files a subagent owns — the wall behind the skill's file ownership, since a prompt alone is a request an agent can talk itself out of. Ownership is declared per agent, in its brief, as an<owned-paths>block (one path or glob per line); the hook reads it back from that agent's own transcript, so parallel workers each get their own boundary.DEVINX_OWNED_PATHSis the session-wide fallback for the main thread and for briefs that declare none.
Both fail open on anything they cannot parse: a hook erroring out must never be the reason a legitimate edit is blocked.
hooks.json's commands resolve python3, then python, then py at
invocation time using command -v, which needs a POSIX shell. Untested
on Windows: if Claude Code runs hook commands through Git Bash there, this
resolves the same way it does on Linux/macOS; if it uses cmd.exe instead,
command -v is not a command it understands and the hooks do not run at
all. Either way, a missing or unusable interpreter means the hooks silently
do not load — not that anything is refused wrongly.
Useful without the orchestrator, on Claude Code.
/model offers a single swe-2 entry. It has no tier of its own: the effort
slider beside it picks one, which is what makes that slider meaningful.
| effort | tier |
|---|---|
| low, medium | swe-2-medium |
| high (default) | swe-2-high |
| xhigh, max | swe-2-max |
The three tiers are also selectable by name (--model swe-2-high). Asked for
explicitly they are never retiered by effort — otherwise a subagent pinned to a
tier would follow whatever its parent session was set to. The response reports
the tier that actually ran, not the alias.
For a subagent, delegate as usual and pick swe2-medium, swe2-high or
swe2-max directly.
Subagents are given disallowedTools: ["mcp__*"]. MCP tool schemas are re-sent
in full on every request and there are usually many of them: measured here, a
subagent turn drops from ~240KB to ~49KB and a cold first turn from ~58s to a
few seconds. It is a glob rather than an allow-list on purpose — every built-in
tool stays available, including ones a future release adds, so the agents keep
inheriting the native tool set.
Only the subagents are affected; your main session keeps its connectors. Set
DEVINX_SUBAGENT_MCP=1 to give them back, or pass --strict-mcp-config to drop
MCP for the whole session instead.
devinx.py listens on 127.0.0.1:8316 and routes on the model name. That single
decision is what makes the whole thing possible, and the Codex section below is
the story of why.
claude-* and gpt-* are relayed to api.anthropic.com and
chatgpt.com/backend-api/codex respectively, untouched, with the credential the
client sent. The service holds no credential of its own for either; with none in
the request it answers 401 rather than inventing one.
swe-2-* is translated to Cognition's Connect-RPC GetChatMessage. No
client header is forwarded there at all: the request is built from scratch with
its own header set, so your claude.ai or ChatGPT token cannot reach it.
Speaking the client's own protocol on both sides, rather than translating through a third one in the middle, is what keeps usage accounting exact — input, cache read and cache write map 1:1 — and lets reasoning survive across turns: the client keeps the thinking blocks it receives and replays them, and nothing has to be stored server-side.
Each subagent gets its own conversation key, derived from the session id plus its system prompt and first task. Sharing the parent's key collapses them into a single upstream cascade and costs the prefix cache on every continuation turn.
Codex speaks the Responses API rather than Messages, which is the easy half. The rest was only findable by watching the wire, and three things had to be true at once.
A provider is chosen per session, never per agent. A role's config layer
honours model and silently ignores model_provider, so the obvious design —
point the subagents at a different provider — cannot work. Routing on the model
name instead means one global provider is all Codex needs.
Codex has two multi-agent surfaces. On V2 a spawned agent's task is handed
over as ciphertext only OpenAI can open: an executor on any other model receives
the envelope and none of the letter. On V1 the same task arrives in the clear.
The surface is chosen from the model catalog, so /v1/models serves one pinned
to V1, and the launcher disables the V2 feature flag, which resolves ahead of
the catalog. That catalog parser is strict and rejects the whole catalog over a
single missing key — taking plugins, apps and MCP down with it rather than just
the offending row — so every entry spells out every default it wants.
The classifier reads tool descriptions too. Codex's code-mode exec tool
ships 16KB of API documentation and is refused whole, while its first ~6KB
passes. Since that text changes with every release, the permission_denied
retry shrinks tool descriptions rather than replaying the same body. The one
other block that had to go is <model_switch>, which Codex injects whenever a
turn changes model and which contains the other model's entire system prompt.
With those in place, agents.default_subagent_model puts every spawned agent on
swe-2-max, and the roles are the same five the Claude Code side uses.
One difference remains. Codex has no per-agent prompt: everything under
[agents] is parsed as a role except a short list of recognised scalars, and an
unrecognised one fails config loading outright. An executor brief therefore has
nowhere to hang; the roles carry the tier (each -c agents.<role>.description=
in launcher.py's CODEX_ROLES), and the orchestrator doctrine reaches the
root through the Codex skill in codex/marketplace/, kept in step with the
Claude Code one in plugin/skills/.
gpt-* sent by Claude Code (on /v1/messages) goes to a local
claude-code-proxy sidecar on
127.0.0.1:18765 (DEVINX_GPT_UPSTREAM to change it), which holds its own
ChatGPT login and translates Messages to Codex Responses. Claude Code's
Anthropic credential is stripped before the hop. Codex's own gpt-* traffic
still goes straight to chatgpt.com on /v1/responses.
claude-code-proxy codex auth login # once; separate from ~/.codex/auth.json
systemctl --user enable --now claude-code-proxy/model lists Sol (gpt-6.1-sol), Astra (gpt-6-astra) and Luna
(gpt-6-luna), with the effort slider (low to max): the launcher passes a
session-scoped --settings whose modelPicker rows use behavesAs: claude-opus-5-5 for the client-side handling, and the sidecar turns
output_config.effort into Codex's reasoning effort. Medium is the floor:
a low turn on the gpt-* route is raised to medium.
Subagents: gpt-{sol,astra,luna}-{medium,high,xhigh,max}, generated by the
launcher and injected with the swe2-* ones. The Agent tool takes no effort,
so the subagent chosen is the effort chosen (an agent's effort: frontmatter
pins it; without one a subagent runs at the session's). Codex's own subagent
effort for Sol and Astra is xhigh. A user --settings
wins, and then the rows are not added.
Compaction copies Codex (ModelInfo::auto_compact_token_limit in
codex-rs): compact at 90% of the 272k window, 244800 tokens, measured on the
counts the server reports.
- Main session: Claude Code compacts it, with
modelSettings.<model>. autoCompactWindowat 277800 so that its fixed 33k buffer lands on 244800; behind it the sidecar's Codex-native compaction (CCP_CODEX_SERVER_COMPACTION) keeps the earlier history in model-native form. - Subagents (
x-claude-code-agent-id), which Claude Code never compacts: devinx does, with the same machinery as SWE-2 but a summariser that runs on the agent's own GPT model through the sidecar, so nothing goes to Cognition. The trigger is the server's last reported usage plus an estimate of what was added since, as in Codex. A summary that takes long keeps the stream alive with SSE comments; a context refusal is retried once, compacted harder, instead of ending the agent.DEVINX_GPT_COMPACT_AToverrides 244800.
The advisor on GPT (off by default). Claude Code declares advisor as a server
tool, which on the Anthropic API the server runs and on a GPT model nobody
would (the sidecar turns it into a plain function, GPT calls it, and the client
answers No such tool available: advisor). Claude Code offers the tool to a
GPT model when it treats it as an Opus (the launcher's picker rows say
behavesAs: claude-opus-5-5) or when CLAUDE_CODE_ENABLE_EXPERIMENTAL_ADVISOR_TOOL=1
is set, in a GPT main session and in a gpt-* subagent of a Claude one alike.
Then devinx answers the call itself: the tool call is held back, a GPT model (gpt-6.1-sol) is given the
task and the transcript and asked for advice, and the turn continues upstream
with that advice as the tool's result, as one continuous message. The client
sees the server_tool_use / advisor_tool_result pair Claude's own advisor
leaves, so the exchange is stored and replayed with the conversation; on the
way back to the sidecar the pair is turned into an ordinary tool call and
result (so a conversation that began on Claude, whose advice is encrypted, also
continues on GPT, told that the advice cannot be read). Tool calls made beside
the advisor are not run and the model is told so. A failed advisor is an error
result, never a failed turn, and a slow one keeps the stream alive with SSE
comments. Only streamed requests are handled; for any other the tool is taken
out. Requests that do not declare the advisor are relayed byte for byte.
The advisor's own calls travel under a cache key derived from the session
(<session>:advisor[:<agent>], never the conversation's own, which the sidecar
keeps state for): successive advice read ~97% of their transcript from the
cache instead of paying for all of it each time. The advisor tool stays in the
request until a call is refused, because the tools head the prompt and changing
them re-reads the whole history. DEVINX_ADVISOR_MODEL (default gpt-6.1-sol), DEVINX_ADVISOR_EFFORT
(high), DEVINX_ADVISOR_MAX_CALLS (0 by default, which turns the feature off and
takes the tool out of every gpt-* request; set 3, say, to turn it on, after
which that many advice per client request are given and the tool is withdrawn), DEVINX_ADVISOR_TIMEOUT (300s).
Report files. Claude Code tells every subagent "Do NOT Write
report/summary/findings/analysis .md files", and a GPT model obeys it even when
the task asks for exactly such a file under a path it names (two subagents
answered that a generic rule forbade it and sent their findings as messages).
On the gpt-* route (subagents only), devinx narrows that sentence to
unrequested reports: a file the launching agent asks for, or a path it names,
is the deliverable and gets written. The gpt-* subagents' own prompt says the
same, for a client that bypasses the rewrite. Claude's own route is relayed
untouched, and so is swe-2-* (its subagents were measured writing a requested
report with the sentence in place).
Switching a conversation from gpt-* back to claude-* works: GPT-signed thinking
(ccp: signatures) is dropped from a claude-* history, and kept unsigned on
swe-2-*.
The sidecar is a patched build: upstream claude-code-proxy (0.1.43) accepts
browser-originated requests and any Host, so a page could spend the ChatGPT
quota. The patch in sidecar/origin-guard.patch refuses any Origin and any
non-loopback Host. Upstream's install.sh and brew install the unpatched
binary, so build it with devinx instead (needs Rust, via rustup):
python3 install.py --gpt # or, on its own: python3 sidecar/build.py
~/.local/bin/claude-code-proxy codex auth login # once, the sidecar's own login
sidecar/build.py checks out the pinned upstream commit, applies the patch,
runs cargo build --release --locked, installs the binary in
~/.local/bin (an unpatched one found there is kept as .unpatched) and, on
Linux, installs and starts the systemd user unit sidecar/claude-code-proxy.service
(port 18765, Codex-native compaction). A sidecar already running is not
restarted, since that cuts its GPT turns: pass --restart, or restart it when
idle. A unit edited by hand is left alone unless --force.
It rejects any request carrying an Origin header, or a Host that is not
loopback. That route spends your quota with a credential the service holds, so
it cannot make the caller prove anything — and a page you merely visit could
otherwise POST to it, since a JSON body sent as text/plain needs no CORS
preflight and the attacker never has to read the reply. Browsers send Origin
on such a request and API clients do not.
Set DEVINX_ALLOW_BROWSER=1 if you are deliberately driving it from a local web
UI. This is not isolation between users of the machine: anything running as you
can still reach the port.
http://127.0.0.1:8316/dashboard
Publishing it — a tailnet mount, a funnel, any reverse proxy — means pointing
that forwarder at DEVINX_DASHBOARD_PORT (8317 by default), never at the API
port. The forwarder publishes every route that answers on the port it is given,
/v1/messages included, and it rewrites Host to 127.0.0.1 on the way
through, so the guard that refuses a non-loopback Host sees a local call and
waves it past. No header survives that. The dashboard port answers only
/dashboard, /api/stats and /api/hello, and opens only when a token exists
— from DEVINX_DASHBOARD_TOKEN, or from dashboard-token in the data
directory so it survives a restart the launcher does.
Set DEVINX_DASHBOARD_TOKEN and the dashboard becomes …/dashboard?k=<token>, for every
caller including loopback: judging by the Host header stops a rebound browser
and nothing else, since any forwarder — socat, ssh -L, a tailnet mount —
delivers its connections from 127.0.0.1 with whatever Host the client chose.
A browser pays the token once; the reply sets an HttpOnly cookie and the page
scrubs the token back out of the address bar. Without a token set, the dashboard
stays loopback-only.
Served by the service itself, so it reads the live log rather than a snapshot:
turns, tokens and what the cache saved, latency by percentile, the two rate-limit
counters and what each credential is carrying, how far compaction is reducing a
conversation before it goes upstream, and the incidents worth watching — with the
last non-routine log lines streaming underneath. GET /api/stats returns the
same figures as JSON. The log is re-read on demand and cached for a few seconds,
so several open tabs cost one pass.
devinx --status, devinx --doctor --json, and devinx --explain --cx --or
inspect the running service without starting it or spending inference quota.
The dashboard also shows current requests and their phase. See
stability controls and validation for the new resource
bounds, opt-in strict compaction, shutdown behaviour, test coverage and limits.
Logs are in devinx.log, in the data directory printed by the installer
(%LOCALAPPDATA%\devinx, ~/Library/Application Support/devinx, or
$XDG_DATA_HOME/devinx). Each request logs a line: route=swe, route=claude
or route=codex, with the model and the status.
SWE-2 calls fail, the rest still works. Expected when the Devin credential is missing or expired — the service starts anyway so the main session is unaffected. Log in again with the command above; the running service drops its cached credential when the upstream rejects it, so it picks the new one up without a restart.
The launcher reports the service failed to start. Read devinx.log. A port
conflict on 8316 is the usual cause; --port at install time changes it.
After a pull, is the running service the new code? It is checked for you.
The service outlives the sessions that use it on purpose — several share it, and
closing one should not restart it for the others — so a pull otherwise leaves
last week's code answering today's launcher, silently. Each start now stamps a
fingerprint of its own source, and the launcher compares it against the file on
disk: a stale service is replaced when idle, and reported rather than cut in
half when a turn is in flight. curl 127.0.0.1:8316/api/hello shows the build,
pid and how many requests are running right now.
An agent stops with a rate-limit error. It should no longer get that far. SWE-2 enforces a token budget over a rolling window, and a burst of parallel agents empties it — measured on one machine, 1107 refusals against 8598 successful turns, in bursts of up to 133 in a row, each naming a wait of one to twelve minutes.
devinx now holds the turn instead of handing the refusal back: it waits the delay the upstream named, with jitter so a fan-out of agents does not all return together and empty the window again, then retries. The client sees a request that took longer; the agent never stops, and the orchestrator is never told anything happened. Measured: a subagent refused with "reset in 1 minute" waited 65 seconds inside one request and finished its task, its parent reporting no error at all.
More than one Devin account. Both of Cognition's limits are per credential — a short one that recycles in well under a minute, and a longer window that can lock for twelve minutes — so a second account is a switch rather than a wait. Log in again with the data directory pointed somewhere else and devinx finds it:
XDG_DATA_HOME="<data dir>/account2" devin auth loginEvery credentials.toml under the data directory is loaded, and turns are
spread across them round robin — always starting at the same one keeps that one
permanently at its ceiling while the others idle, which is the difference
between switching on every burst and not being limited at all. On a rate limit
the turn moves to another credential immediately, and only waits when every one
of them is spent. DEVINX_API_KEYS takes a
comma-separated list instead. Each request logs the account that served it, so
which credential a limit belongs to is answerable from the log. A login done while the service is running is picked up without a restart, within 30 seconds (DEVINX_RESCAN); deleting a credential's file removes it the same way.
DEVINX_RATE_WAIT bounds the total wait (1800s by default) — the client has its
own timeout, and an answer that never comes is worse than one that says to try
later. Past that budget the refusal does go back, as a 429 rate_limit_error
carrying retry-after.
A SWE-2 turn ends with permission_denied. Cognition's input classifier
refused the payload. The log says which attempt failed and what was capped; the
retry shrinks tool descriptions on its own.
A subagent dies with prompt is too long instead of compacting. Three
things have to be right, and the one that actually killed agents was none of the
obvious two.
The client enforces the declared window itself for a model it does not
recognise, and for a subagent that enforcement is fatal rather than corrective:
the agent ends with "Agent terminated early due to an API error: Prompt is too
long (error type invalid_request)" and devinx never receives a request at all —
refused on the client's own estimate, without compacting and without asking. The
launcher therefore sets CLAUDE_CODE_DISABLE_UNKNOWN_MODEL_WINDOW_ENFORCEMENT,
measured: the same subagent then runs to completion, its turns reaching the
upstream that was always willing to take them. DEVINX_ENFORCE_WINDOW=1 puts
the local cap back.
The other two are the window and the wording.
And past those, the proxy compacts. A client compacts its main session but not an agent: measured, an agent at its limit drops a single message — 30412 tokens to 30284 — and then ends with that error, having summarised nothing. So devinx does it instead. When a turn would not fit, the first turn is kept (it is the task), the recent turns are kept verbatim, and everything between is replaced by a summary written with Claude Code's own compaction prompt, read out of its binary rather than reinvented. The client's transcript is untouched; only what goes upstream is reduced, which the agent experiences as a turn that took a few seconds longer. Measured end to end: a subagent whose context reached 158k against an 80k threshold compacted to 18k and finished its task.
Compaction is lossy by nature — the agent keeps the summary, not the detail —
and a threshold set below the size of what the agent is actually working on will
make it re-read the same files forever. DEVINX_COMPACT_AT sets it; the default
leaves roughly 96k of recent turns verbatim. devinx declares 262144 —
the real one — through CLAUDE_CODE_MAX_CONTEXT_TOKENS, which is the only lever
that works: a context_window on the model catalog is ignored for a model the
client does not recognise. DEVINX_CONTEXT_TOKENS overrides it, and erring low
costs only an earlier compaction.
The other is the wording of the refusal. A client recovers from an overflow by
parsing two numbers out of the message, and Cognition's has none in it — it says
"The prompt is too long for this model" and nothing more, so the recovery never
fires. devinx rewrites it as prompt is too long: N tokens > M maximum, with N
estimated locally and M the declared window. Measured on one machine before that
change: 48 overflows in a day, on requests of 1.7MB and 200 messages, while
every turn that did compact stopped at 257k — the window was being honoured on
one path and ignored on another.
Checking what a Codex session will actually see, without spending a request:
devinx --cx --or debug prompt-inputEverything is broken. claude and codex on their own do not go through
devinx at all and are always available as a fallback.
descriptors/*.fdp are Cognition's message definitions, shipped with the
package. They only need refreshing if the upstream wire format changes:
python3 extract_fdps.py descriptorsIt resolves the latest published catalog, so it can pull a version newer than
the one this release was tested against.