Skip to content

Staging (Week of 06-07-22) - #235

Merged
hamishfagg merged 5 commits into
mainfrom
staging
Jul 13, 2026
Merged

Staging (Week of 06-07-22)#235
hamishfagg merged 5 commits into
mainfrom
staging

Conversation

@lucas-koontz

Copy link
Copy Markdown
Contributor

No description provided.

lucas-koontz and others added 5 commits July 6, 2026 08:54
…self-update loop (ENG-655) (#240)

* fix(cli): restore Anton CLI for MindsHub users — minds-cloud crash + self-update loop (ENG-655)

Two verified failures that made the standalone `anton` CLI unusable for MindsHub
users (desktop app + webapp are unaffected — they build the LLM client via
cowork-server's own build_llm_client and never call this updater).

1. Crash: `ValueError: Unknown planning provider: minds-cloud`.
   The CLI's LLMClient registry only has anthropic/openai/openai-compatible;
   MindsHub speaks the OpenAI-compatible API but the desktop/consolidated config
   uses the first-class name "minds-cloud", which the CLI never mapped. Add an
   AntonSettings field-validator that normalizes minds-cloud (and the underscore
   spelling) → openai-compatible, so model_post_init derives the creds/base from
   the minds_* fields and the existing provider handles it. Reproduced the crash
   and the fix in a test.

2. Perpetual failed self-update that corrupts installs (reported on Windows).
   `__version__` was hardcoded in anton/__init__.py and drifted from the release
   tags (stuck at 2.26.6.30.1 across several releases) because the release
   workflow only tags (pyproject is hatch-vcs / version-from-tag). The updater
   read that stale constant as local_ver, so remote(tag) > local was always true
   → it force-reinstalled the running tool on every launch → on Windows that
   partial reinstall bricked the env (missing rich._emoji_codes, typer rich_utils
   ImportError, or "No module named 'anton'").
   - Derive __version__ from installed package metadata (importlib.metadata) so it
     always matches what's installed — kills the drift class at the root.
   - Anti-thrash guard in updater.py: record a tag whose reinstall didn't take and
     don't force-reinstall that same tag again until a newer one appears.

3. Escape hatch + dep cap: add a discoverable `--no-update` CLI flag (surfacing
   the existing ANTON_DISABLE_AUTOUPDATES setting), and cap `rich<15` (rich 14 +
   typer 0.24 verified compatible; the reported errors were reinstall corruption,
   not a version range).

Tests: 5 for minds-cloud normalization incl. the from_settings crash repro; 3 for
the updater anti-thrash marker. Full suite passes (the 2 scratchpad failures are
pre-existing/environmental — they fail on clean staging too; subprocess can't
spawn in the sandbox).

NOTE for reviewer: the updater changes (dynamic version + anti-thrash) resolve the
loop deterministically in unit tests, but the uv-tool-install / hatch-vcs
interaction should be sanity-checked with a real `uv tool install git+...@<tag>`
on macOS + Windows before relying on it in the wild.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(updater): read anton-agent's version specifically, not a leftover legacy anton tool (ENG-655)

Local uv-flow testing surfaced an additional root cause of the update loop.
`_read_installed_anton_version()` matched `re.search(r"anton\s+(\S+)")`, but a
real machine can have BOTH a leftover legacy `anton` tool (pre-rename) and the
current `anton-agent` tool — `uv tool list` shows `anton` first, so the bare
regex read the legacy tool's version (2.26.5.13.1) instead of anton-agent's
(2.26.7.6.2). That made installed_ver != remote_ver → "Update skipped" forever,
even after a correct install, and combined with the new anti-thrash marker would
permanently block updates.

Anchor the match to `anton-agent` (line-start, optional v prefix). Verified
against the real `uv tool list` output. Two tests: dual-tool ordering, and the
absent-anton-agent case (returns None → caller bails safely).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* review(cli): dual-dist version fallback + case-tolerant minds-cloud validator (ENG-655)

Adversarial review of PR #240 surfaced two real gaps:

1. __version__ resolved only the "anton-agent" distribution → on a legacy install
   under the old "anton" dist name it raised PackageNotFoundError and fell back
   to "0.0.0.dev0", which makes the updater see local < remote and force-reinstall
   once per release — reintroducing the exact ENG-655 loop for that cohort. Now
   try "anton-agent" then "anton" before the dev fallback. Also wrap the whole
   block defensively: __init__ runs on every `import anton` (incl. the desktop's
   cowork-server), so it must never raise.

2. The minds-cloud → openai-compatible validator was case-sensitive; "MINDS-CLOUD"
   or padded values slipped through and re-hit the crash. Now lower()/strip()
   tolerant.

Cross-surface review confirmed no app/webapp/instance regressions (validate_assignment
is off so the harness setattr path is unaffected; cowork-server reads
importlib.metadata directly, not anton.__version__; rich 14.3.3 satisfies <15;
updater is CLI-only).

Tests: added case/whitespace normalization case; suite 1148 passed (2 pre-existing
scratchpad-subprocess failures unrelated, fail on clean staging too).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
)

* feat(llm): shared status-error mapper + typed model-403s (ENG-598)

A gateway 403 for a tier-locked/disabled model surfaced in chat as
"An unexpected error occurred: Server returned 403 — the LLM endpoint may be
temporarily unavailable. Try again in a moment.. Please try again or rephrase
your request." — wrong cause, useless advice, double-wrapped. Two layers:

1) openai.py mapped every non-401/429 APIStatusError to the generic
   "temporarily unavailable" copy, in four copy-pasted blocks (complete /
   stream / _complete_via_responses / _stream_via_responses) that had already
   drifted in wording.
2) session.py's plan_stream fallback swallowed the exception and yielded it
   as assistant TEXT, so the turn "succeeded" and cowork-server's turn-error
   mapping (the error-card machinery) never ran.

Fix:
- One shared _raise_for_status_error(exc, model) used by all four call paths
  so the mapping can't drift. 401 and 429 behavior unchanged (same copy the
  downstream provider_auth / token_limit detections key on; the "or to top
  up" wording drift normalized).
- New ModelUnavailableError(ConnectionError) in provider.py carrying
  {code, model} for the gateway's structured 403s: model_access_denied →
  plan copy with upgrade/switch guidance; model_disabled → hedged copy
  (can be tier lock or admin kill switch until ENG-596's config lands).
  Detection is code-exact on error.code — BYOK OpenAI 403s, Anthropic
  permission errors, and Cloudflare HTML 403s carry no such code and fall
  through to the generic message, never the plan copy. Subclassing
  ConnectionError keeps every legacy call site working unchanged.
- session.py: TokenLimitExceeded / ModelUnavailableError now PROPAGATE from
  the plan_stream fallback instead of being wrapped into prose, so the turn
  fails and the server can render an actionable card. All other exceptions
  keep the existing text fallback.

Tests: 11 new (401 copy pin, 429 quota + precedence over model-403, both
gateway codes incl. attrs/copy, ConnectionError compatibility, BYOK/HTML/
codeless 403 fall-through, 500 generic). Suite: 1147 passed; the 2
test_chat_scratchpad failures pre-exist on clean staging (env-dependent).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* review: don't auto-retry ModelUnavailableError — deterministic like TokenLimitExceeded

A model-gate 403 is deterministic: the recovery loop would re-send the same
doomed request twice (burning the retry budget + user-visible delay) before
the fallback path finally propagated it. Short-circuit at the outer catch,
exactly like TokenLimitExceeded.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* review(598): steer CLI default to setup for ModelUnavailableError; mapper doc/type nits

Addresses lucas-koontz's review on PR #236 (all non-blocking; PR approved):

- CLI turn handler (chat.py) defaulted to "retry" for any ConnectionError, and
  ModelUnavailableError subclasses ConnectionError — so a deterministic plan
  gate highlighted "retry" as the default, re-sending the same doomed request
  (contradicting the session.py comment). Now defaults to "setup" (switch
  model/provider) for ModelUnavailableError; transient ConnectionErrors keep
  retry.
- Fixed the grammatically-broken 429 docstring bullet in _raise_for_status_error.
- _raise_for_status_error typed -> NoReturn (it always raises); documents the
  contract the four except-sites rely on.

Mapper + session tests: 11 passed. (Pre-existing F541s elsewhere in chat.py
left untouched — not in scope.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@hamishfagg
hamishfagg enabled auto-merge July 13, 2026 04:15
@hamishfagg
hamishfagg disabled auto-merge July 13, 2026 04:30
@hamishfagg
hamishfagg enabled auto-merge July 13, 2026 04:30
@hamishfagg
hamishfagg added this pull request to the merge queue Jul 13, 2026
Merged via the queue into main with commit ab995d9 Jul 13, 2026
8 checks passed
@github-actions github-actions Bot locked and limited conversation to collaborators Jul 13, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants