The Codex migration left /usage returning 501 and no quota signal for the
governor. Codex does expose one after all — it just isn't an HTTP endpoint.
Probe: `codex app-server` is a JSON-RPC-over-stdio surface whose
`account/rateLimits/read` returns the same snapshot the interactive TUI
shows. Handshake is initialize -> `initialized` NOTIFICATION -> read; without
the notification the read never answers. adolf-llm's /usage now drives that
and normalises the result.
Shape change, and why the consumers had to be rewritten rather than repointed:
Kimi reported fixed buckets (window_5h / weekly / window_7d). Codex reports up
to two plan-defined windows, `primary` (long) and `secondary` (shorter burst,
often null), so the payload is now {plan, pct, primary, secondary,
limit_reached} with each row as {pct, window_mins, window_label, resets}. `pct`
is the max across live windows — the single number a gate can read without
knowing which window binds.
Probing spawns a codex process (~2s), so results are cached in memory and on
the workspace volume with a 5min TTL, concurrent probes are de-duped, and a
failed refresh serves the last good reading tagged stale/as_of/age_s rather
than nothing. ?force=1 bypasses the TTL.
kimi-quota-footer-plugin -> codex-quota-footer-plugin (id, mount path and the
openclaw.json entry key all renamed together — they must agree or the plugin
silently fails to load). It now renders whatever windows the plan actually
has, shortest first, and flags limit_reached and stale readings. quota-command
updated for the same payload.
Verified: /usage returns live data (30d 4%, plan free), warm cache serves in
17ms vs ~2s cold, the gateway reaches the route, adolf loads
codex-quota-footer, and the formatter degrades to no footer on empty/null
payloads instead of breaking the reply.
Note: the account reports planType "free", not a paid ChatGPT plan.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Y5QPagv4iun1ghpwM96Ff
Adolf — OpenClaw gateway deployment
Adolf is the self-hosted OpenClaw fork that runs as the adolf container
(Matrix-first personal assistant). This directory holds its version-controlled
gateway configuration.
- Source tree (the OpenClaw fork being built):
/home/alvis/adolf - Compose service
adolflives in:openai/docker-compose.yml - This config directory lives at the repo root (
agap_git/adolf/), not nested insideopenai/, since it is shared config rather than part of theopenaicompose project's own tree. - Model backend:
adolf-llmcontainer (Kimi-CLI wrapper) on:8010
Memory
Adolf's long-term memory is being migrated from Cognee to Hindsight (a single
self-hosted container, :8888 REST + built-in MCP, :9999 UI). It stays wired in
the same two ways: as a tool (Hindsight's built-in MCP in openclaw.json
mcp.servers.hindsight) and as forced hooks (the hindsight-memory OpenClaw
plugin: before_prompt_build→recall inject, agent_end→retain). Authoritative
plan and target architecture: HINDSIGHT-MIGRATION.md
(kanboard Adolf tasks H1–H5). Until those land, the running stack is still Cognee.
Config source of truth
The gateway config is openclaw.json in this directory. It is bind-mounted
read-only over the adolf-state volume:
volumes:
- adolf-state:/home/node/.openclaw # runtime state only
- ../adolf/openclaw.json:/home/node/.openclaw/openclaw.json:ro # tracked config
Previously this file was a hand-edited copy inside the adolf-state Docker
volume (edited via docker cp into the running container). It is now tracked
in git and seeded into the container by the mount, so git is the single
source of truth.
- The file is JSONC (comments + unquoted keys allowed).
- No secrets live here. Every credential is a
${VAR}reference resolved from the container's environment, which is sourced fromopenai/.env(gitignored, never committed):OPENCLAW_GATEWAY_TOKEN,ADOLF_KEY,MATRIX_*,MARKETPLACE_MCP_TOKEN. - The gateway reads this file and writes its own
openclaw.json.last-goodandopenclaw.json.rejected.*snapshots into the volume dir (writable). It does not rewrite this file, so the read-only mount is safe.
Changing the config
- Edit
agap_git/adolf/openclaw.json. - Restart the container:
env -u HTTPS_PROXY -u HTTP_PROXY -u ALL_PROXY -u https_proxy -u http_proxy -u all_proxy \ docker compose -f /home/alvis/agap_git/openai/docker-compose.yml up -d adolf - Verify it came up healthy and the config was accepted:
A fresh
env -u HTTPS_PROXY -u HTTP_PROXY -u ALL_PROXY -u https_proxy -u http_proxy -u all_proxy \ docker ps --filter name=adolf --format '{{.Names}} {{.Status}}' env -u HTTPS_PROXY -u HTTP_PROXY -u ALL_PROXY -u https_proxy -u http_proxy -u all_proxy \ docker logs --tail 40 adolfopenclaw.json.rejected.*file in/home/node/.openclawmeans the edit failed validation and the previous.last-goodis still in use.
Because the mount is read-only, editing config through the gateway UI/API is intentionally disabled — all changes go through git.
Granting a Matrix user access to Adolf
Adolf only responds to Matrix users on an allow-list. This is the
channels.matrix.dm block in openclaw.json:
channels: {
matrix: {
enabled: true,
encryption: true,
dm: {
policy: "allowlist",
allowFrom: [
"@admin:mtx.alogins.net",
"@elizaveta:mtx.alogins.net",
],
},
groupPolicy: "disabled", // no group-room handling yet
autoJoin: "always",
},
}
dm.policy: "allowlist"— only users whose full Matrix ID appears inallowFromcan DM the bot. Everyone else is ignored. (policy: "pairing"is the alternative: the owner must approve each unknown sender interactively.allowlistis stricter and declarative.)dm.allowFrom— the list of authorized Matrix user IDs.groupPolicy: "disabled"— Adolf does not act in group rooms; DM only.
To grant a new user access
- Get the user's full Matrix ID, e.g.
@ivan:mtx.alogins.net. - Add it to
allowFrominagap_git/adolf/openclaw.json:allowFrom: [ "@admin:mtx.alogins.net", "@elizaveta:mtx.alogins.net", "@ivan:mtx.alogins.net", ], - Restart adolf (see "Changing the config" above).
- The user can now start a DM with Adolf's Matrix account
(
MATRIX_USER_IDinopenai/.env).autoJoin: "always"means Adolf auto-accepts the DM invite; conversation is end-to-end encrypted (encryption: true).
To revoke access, remove the ID from allowFrom and restart.
Matrix accounts themselves are created on the Synapse homeserver (
mtx.alogins.net) — see the AgapHost wiki Matrix page. The allow-list here only controls which existing Matrix users Adolf will talk to.
Matrix device identity (kb#67)
Adolf's Matrix login used password auth with no pinned device_id
(MATRIX_PASSWORD in openai/.env). Every time OpenClaw's own credential
cache (in the adolf-state volume) was missing — first boot, a lost/rebuilt
volume — a fresh password login minted a brand-new Matrix device with no
cross-signing, leaving dead ghost devices behind and risking new encrypted
DMs getting keys shared to a device that no longer exists.
Fix: openai/.env now also pins MATRIX_ACCESS_TOKEN + MATRIX_DEVICE_ID to
Adolf's current live device (TIANDTKUZJ, cross-signed; token in Vaultwarden
as MATRIX_ADOLF_GATEWAY_TOKEN). OpenClaw's matrix extension prefers a
configured access token over password login
(extensions/matrix/src/matrix/client/config.ts resolveMatrixAuth), so as
long as that token stays valid, restarts — even after a volume loss — reuse
the same device instead of minting a new one. MATRIX_PASSWORD stays set as
a manual-recovery fallback only (unset MATRIX_ACCESS_TOKEN to force a fresh
password login if the token is ever revoked).
Cross-signing for @bot is already bootstrapped automatically by OpenClaw's
matrix extension (extensions/matrix/src/matrix/sdk/crypto-bootstrap.ts) —
no separate setup needed.
If the pinned token is ever revoked/rotated, get a fresh one bound to the
same device by logging in with device_id explicitly set to TIANDTKUZJ
(Synapse reuses an existing device when its id is given in /login, instead
of creating a new one), then update MATRIX_ACCESS_TOKEN in openai/.env
and Vaultwarden's MATRIX_ADOLF_GATEWAY_TOKEN.