Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 65 additions & 3 deletions agent/model_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -2347,6 +2347,51 @@ def _query_anthropic_context_length(model: str, base_url: str, api_key: str) ->
"gpt-5": 272_000,
}

# Codex OAuth advertises 272K via /backend-api/codex/models for these
# families, but the backend actually ACCEPTS more (verified live Aug 16 2026
# against chatgpt.com/backend-api/codex/responses: ~371K input tokens
# completed OK for gpt-5.6-sol/terra/luna and gpt-5.4; ~382K+ rejected with
# ``context_length_exceeded``; gpt-5.5 rejected 360K, so its 272K
# advertisement is real and it is NOT listed). 350K keeps ~22K margin under
# the observed ~372K enforcement.
#
# Applied ONLY when the resolved value (live probe or fallback table) is
# exactly the known-stale 272,000 advertisement — if OpenAI moves the
# advertised number in either direction (the gpt-5.6 family shifted
# 272K → 372K → 272K during July 2026), the catalog is trusted again and
# this table is inert. ``gpt-5.6`` is a FAMILY PREFIX (sol/terra/luna and
# dated snapshots; ``-pro`` slugs are not routable on Codex OAuth — the
# backend 400s them — so over-matching there is moot). ``gpt-5.4`` is EXACT:
# gpt-5.4-mini was probed and genuinely enforces 272K (rejected 360K), so
# prefix-matching the 5.4 family would over-report for mini.
_CODEX_OAUTH_VERIFIED_ABOVE_ADVERTISED_PREFIXES: Dict[str, int] = {
"gpt-5.6": 350_000, # sol / terra / luna — all three verified live
}
_CODEX_OAUTH_VERIFIED_ABOVE_ADVERTISED_EXACT: Dict[str, int] = {
"gpt-5.4": 350_000, # verified live; gpt-5.4-mini rejected 360K — excluded
}

# The advertised value the verified-above table is allowed to override.
_CODEX_OAUTH_STALE_ADVERTISED_CTX = 272_000


def _verified_codex_ctx_for_slug(model_bare: str) -> Optional[int]:
"""Return the live-verified Codex cap for a slug, or ``None``.

Exact slugs first, then family prefixes (``<key>``, ``<key>-``,
``<key>.``) so dated snapshots of a verified family inherit the bump.
"""
slug = (model_bare or "").strip().lower()
if not slug:
return None
exact = _CODEX_OAUTH_VERIFIED_ABOVE_ADVERTISED_EXACT.get(slug)
if exact is not None:
return exact
for key, ctx in _CODEX_OAUTH_VERIFIED_ABOVE_ADVERTISED_PREFIXES.items():
if slug == key or slug.startswith(key + "-") or slug.startswith(key + "."):
return ctx
return None


_codex_oauth_context_cache: Dict[str, Tuple[Dict[str, int], float]] = {}
_CODEX_OAUTH_CONTEXT_CACHE_TTL = 3600 # 1 hour
Expand Down Expand Up @@ -2474,24 +2519,41 @@ def _resolve_codex_oauth_context_length_with_source(
if not model_bare:
return None, ""

def _apply_verified_bump(ctx: int, source: str) -> Tuple[int, str]:
"""Lift a known-stale 272K advertisement to the live-verified cap.

Only fires when the resolved value is EXACTLY the stale 272,000
advertisement for a slug we have probed above it (see
``_verified_codex_ctx_for_slug``). Any other advertised value —
higher or lower — is trusted as a real server-side change.
"""
bumped = _verified_codex_ctx_for_slug(model_bare)
if bumped is not None and ctx == _CODEX_OAUTH_STALE_ADVERTISED_CTX:
logger.debug(
"Codex OAuth context for %s: advertised %d raised to "
"live-verified %d", model_bare, ctx, bumped,
)
return bumped, source
return ctx, source

if access_token:
live, fresh_probe = _fetch_codex_oauth_context_lengths_with_source(access_token)
live_source = "live" if fresh_probe else "memory"
if model_bare in live:
return live[model_bare], live_source
return _apply_verified_bump(live[model_bare], live_source)
# Case-insensitive match in case casing drifts
model_lower = model_bare.lower()
for slug, ctx in live.items():
if slug.lower() == model_lower:
return ctx, live_source
return _apply_verified_bump(ctx, live_source)

# Fallback: longest-key-first substring match over hardcoded defaults.
model_lower = model_bare.lower()
for slug, ctx in sorted(
_CODEX_OAUTH_CONTEXT_FALLBACK.items(), key=lambda x: len(x[0]), reverse=True
):
if slug in model_lower:
return ctx, "fallback"
return _apply_verified_bump(ctx, "fallback")

return None, ""

Expand Down
72 changes: 72 additions & 0 deletions docs/atm/FORK-MAINTENANCE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# Fork Maintenance — how randlee/hermes-agent tracks the upstream firehose

Upstream (`NousResearch/hermes-agent`) lands 200–700 commits/day. This fork exists to
carry ONE thing on top of it: the ATM injection patch stack (see
`PATCH-REQUIREMENTS.md` in this directory — read it first; it is the contract).

## Repo roles

| Thing | Role |
|---|---|
| `main` | upstream/main + the ATM patch stack, advanced only by reviewed PR |
| `atm/stack` branch | the patch stack (3 code commits + this docs commit), rebased onto upstream daily; force-push allowed HERE, never on main |
| `sync/candidate-YYYYMMDD` branches | daily PR candidates produced by the cron |
| `runtime-*` tags | sources of built runtimes (see `~/.hermes/RUNTIME-PLAN.md` on the gateway host) |
| `~/Documents/forks/hermes-agent` (host) | integration WORKSPACE only — nothing executes from it (enforced by runtime-audit) |
| `~/.hermes/runtime/<tag>/` (host) | immutable runtime installs; gateways run `runtime/current` |

## Daily pipeline (2-level cron)

**Level 1 — mechanical (no agent judgment):** `fork-sync-v2.sh` in a scratch clone:
1. fetch upstream; branch `sync/candidate-YYYYMMDD` from `upstream/main`
2. rebase `atm/stack` onto it (`git rebase`); a clean rebase proceeds, ANY conflict
→ level 2
3. fresh venv: `uv sync --frozen --no-dev --extra messaging`; run the seam contract
tests (`tests/gateway/test_inject_internal_message.py`, 26 expected) + hooks tests
4. green → pre-resolve the merge into `main`: because main and each candidate
carry different rebased copies of the stack, a raw candidate→main PR always
conflicts. The script builds the merge commit itself with the sanctioned
resolution — **candidate tree wins** (first established by loki in PR #7) —
verifies tree-hash equality with the candidate, pushes both branches, and
opens the PR from the pre-resolved merge branch (`sync/candidate-*-merge`).
The PR therefore arrives conflict-free; reviewers judge the candidate via
`git diff upstream/main..sync/candidate-*` (must be exactly the stack)
5. review chain: **contessa** (local qwen, free — does the context-intensive
work) reviews the diff-vs-upstream and test output — the diff must be exactly
the known patch stack, nothing more; then **alpha-prime** (qwen 3.7) approves
and merges routine PRs and signs off smoke tests, then advances the stack pointer.
AUTH NOTE: all agents share the `randlee` account, which also authors the PRs —
formal `gh pr review --approve` is therefore impossible (GitHub forbids
self-approval). The sanctioned path (established by loki, PR #7): post the
review verdict as a PR comment, then merge via owner bypass
(`gh pr merge --merge --admin`; enforce_admins is off). The 1-approval branch
protection stays as a guard against accidental non-admin pushes, not as a
working review gate
(`git push origin +<candidate>:atm/stack`). **Loki** (frontier, expensive) is
NOT in the routine path — non-trivial PRs, reviewer disagreement, or anything
unexpected → level 2.

**Level 2 — escalation (agent judgment):** triggered by rebase conflict, test
failure, or reviewer rejection. The escalation agent is **loki** (hermes-agent-atm
maintainer, frontier model; workspace `hendrix/loki/`, reachable via
`atm send loki`). Loki receives `PATCH-REQUIREMENTS.md`
and follows its "How to update the patch" procedure. Its output is an updated
`atm/stack` + a PR — never a direct push to main, never a force-push of main, never
a branch-protection change. If the contract can't be met, it stops and reports to
Rand with analysis.

## Promotion (deliberate, not automatic)

Merged main ≠ deployed. To deploy: tag, then on the gateway host
`make-runtime.sh --repo <fork> --ref <tag> --name <runtime-N> --hermes-atm <ver>
--atm-graft <ver>`, canary one profile, flip `runtime/current`, rolling restart.
Rollback = flip the symlink back. Full procedure: `~/.hermes/RUNTIME-PLAN.md`.

## History / lessons already learned

- Merge-based daily syncs (the pre-2026-08-16 pipeline) accumulated conflict debt
and once ended with an agent force-pushing main and loosening branch protection.
Rebase-the-stack + PR + protected main is the replacement. Do not regress to it.
- The patch's only recurring conflict is the `gateway/run.py` import block (trivial).
- Goal state is patch size ZERO: if upstream ever ships a public injection API,
adapt hermes-atm to it and retire this stack.
98 changes: 98 additions & 0 deletions docs/atm/PATCH-REQUIREMENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# ATM Patch Requirements — the contract an updated patch MUST satisfy

This document is the knowledge base for the escalation path of the fork-maintenance
pipeline: when the mechanical rebase of the ATM patch stack onto upstream fails (or
tests fail after it), an agent is given this document and expected to produce an
updated patch. It states WHAT the patch must provide and WHY, so the patch can be
re-derived even if upstream refactors everything it currently touches.

## What the patch is

`NousResearch/hermes-agent` has no public way for an external process to inject a
message into a running gateway session. The ATM patch adds exactly one seam:

**`GatewayRunner.inject_internal_message(...)`** — a public, keyword-only async API
that the `hermes-atm` package (pip; source in atm-core `crates/hermes-atm`) calls to
deliver agent-team-mail nudges into a profile's chat session. Everything else in the
stack exists to support or test that seam.

Current stack shape (3 commits + this doc commit):
1. `feat: expose public gateway injection seam` — the API + hook exposure + tests
2. `fix(gateway): notify visible internal-message notices` — notice delivery hardening
3. `test(gateway): cover soft visible-notice failure`

## The contract (every item is load-bearing)

1. **Public API surface** — `GatewayRunner` must expose:
```python
async def inject_internal_message(
*, profile: str, platform: Platform, chat_id: str, text: str,
notice_text: Optional[str] = None,
mode: Literal["queue", "steer"] = "queue",
) -> None
```
plus `InjectInternalMessageError(code, chat_id, detail)`. hermes-atm validates at
install time that `inject_internal_message` is callable on the runner and calls it
with exactly these keywords. Changing names/signature breaks every deployed
hermes-atm wheel — do not.

2. **Hook exposure** — the `gateway:startup` hook context dict must contain
`"gateway_runner": self`. This is how hermes-atm's installed hook obtains the
runner without private imports. (Upstream may rename the emit site; the key in the
context dict must survive.)

3. **Profile resolution, fail-closed** — resolve the target profile via the runner's
profile-adapter map (or the active profile). Unknown profile / empty adapter map
MUST raise `InjectInternalMessageError` — never fall through to a default chat.
Misrouted injection = message delivered to the wrong Telegram chat. Fail closed.

4. **Modes** — `"queue"`: enqueue via the platform adapter's normal inbound-message
path (fire-and-forget). `"steer"`: if the profile's agent is mid-turn and exposes
a steer capability, inject into the running turn; otherwise fall back to queue.
Queue is the default and the only mode hermes-atm currently uses in production.

5. **Visible notice is soft-fail** — when `notice_text` is provided, send it via
`adapter.send(chat_id, notice_text, metadata={"notify": True})` BEFORE routing the
event; check the result's `success` and log a warning (with the result error) on
failure; catch exceptions and log. A notice failure must NEVER prevent the main
event from routing. (This is commits 2–3 of the stack.)

6. **Tests are the contract's enforcement** —
`tests/gateway/test_inject_internal_message.py` (26 tests: queue, steer,
isolation, error cases, notice soft-fail). The updated patch must keep all 26
green. Only change a test when upstream semantics genuinely force it, and then
re-verify hermes-atm compatibility (its hook + runtime must still work — see
"Definition of done").

## Known conflict hotspots

- `gateway/run.py` import block (upstream churns `datetime`/`typing` imports; ours
adds `Literal`, `Optional`). Union the imports — this is the most common conflict
and is always trivially resolvable.
- The `inject_internal_message` method body sits in `GatewayRunner` (a huge class
upstream refactors freely). If upstream moves adapter access or the event-routing
entry point (`adapter.handle_message` today), re-wire the seam to the new
internals while keeping the public surface identical.
- The `gateway:startup` emit site (search: `hooks.emit("gateway:startup"`).

## How to update the patch (escalation procedure)

1. Work in a scratch clone. NEVER in `~/Documents/forks/hermes-agent` directly,
never force-push `main`, never touch branch protection.
2. `git fetch upstream main`; start from `upstream/main`; attempt
`git rebase upstream/main` of the `atm/stack` branch.
3. Resolve conflicts per the contract above — the contract, not the old diff, is the
spec. If upstream now provides an equivalent public injection API, prefer
adapting hermes-atm to it and SHRINKING the patch (goal state: patch size zero).
4. Verify (all with Python 3.11, deps frozen from the repo's own `uv.lock`):
`uv sync --frozen --no-dev --extra messaging` then
`python -m pytest tests/gateway/test_inject_internal_message.py` → 26 passed,
plus `tests/gateway/test_hooks.py` and any test file the conflict touched.
5. Definition of done: tests green AND a live check that
`hermes_atm.HermesAtmRuntime.from_gateway_runner` accepts the runner (import
`hermes_atm`, construct against a stub runner exposing the API — the seam tests
include this shape) — then push the branch and open a PR to `main`; the review
pipeline (contessa → qwen reviewer) takes it from there.
6. If the contract itself cannot be satisfied (upstream removed a capability the
seam needs), STOP and escalate to Rand with a written analysis — do not ship a
behavioral compromise.
Loading
Loading