From 1ec1aa8f1d8fe125e3455124dbfe3530818e04e8 Mon Sep 17 00:00:00 2001 From: autonomic-bot Date: Mon, 13 Jul 2026 17:11:26 +0000 Subject: [PATCH] =?UTF-8?q?feat(upgrader):=20LOOP=5FTIER=3Dgo|zen=20config?= =?UTF-8?q?=20=E2=80=94=20switch=20weekly=20upgrade=20to=20OpenCode=20ZEN?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GO subscription hit a monthly usage limit (resets in 9 days). Add LOOP_TIER env (default 'zen') to launch-upgrader.py + launch-report.py so the opencode backend can use either subscription: zen→opencode/glm-5.2, go→opencode-go/glm-5.2. The tier selects the default model, the usage-limit probe endpoint, and the auth.json key. The systemd timer reads LOOP_TIER from /srv/cc-ci/upgrader.env. --- cc-ci-plan/launch-report.py | 17 ++++++---- cc-ci-plan/launch-upgrader.py | 61 ++++++++++++++++++++++------------- 2 files changed, 49 insertions(+), 29 deletions(-) diff --git a/cc-ci-plan/launch-report.py b/cc-ci-plan/launch-report.py index 902a90f..145b1bc 100755 --- a/cc-ci-plan/launch-report.py +++ b/cc-ci-plan/launch-report.py @@ -2,8 +2,8 @@ """cc-ci recipe-report launcher — one-shot agent that runs /recipe-report after the weekly upgrade. The report agent's model is configured SEPARATELY from the upgrader (REPORT_BACKEND/REPORT_MODEL), -but defaults to the same OpenCode Go subscription on glm-5.2. The model default tracks the backend -(opencode→opencode-go/glm-5.2, claude→opus). +but defaults to the same OpenCode subscription on glm-5.2 as the upgrader. The model default tracks +the backend+tier (opencode+zen→opencode/glm-5.2, opencode+go→opencode-go/glm-5.2, claude→opus). Usage: launch-report.py start [DATE] use-or-create the session; runs /recipe-report [DATE] @@ -11,7 +11,8 @@ Usage: launch-report.py stop kill the session launch-report.py status show session state -Env: REPORT_BACKEND (default opencode), REPORT_MODEL (default opencode-go/glm-5.2 | opus for claude), +Env: REPORT_BACKEND (default opencode), REPORT_MODEL (default tracks tier), + LOOP_TIER / REPORT_TIER (default 'zen'; 'go' or 'zen'; only for opencode), OPENCODE_SHARE (1=attach web server + public --share link), REPORT_SESSION, REPORT_DIR. """ import os, subprocess, sys, time @@ -22,7 +23,9 @@ SESSION = os.environ.get("REPORT_SESSION", "cc-ci-report") WORKDIR = os.environ.get("REPORT_DIR", "/srv/cc-ci") LOG_DIR = os.environ.get("LOG_DIR", "/srv/cc-ci/.cc-ci-logs") BACKEND = os.environ.get("REPORT_BACKEND", "opencode") -_DEFAULT_MODEL = "opencode-go/glm-5.2" if BACKEND == "opencode" else "opus" +TIER = os.environ.get("LOOP_TIER", os.environ.get("REPORT_TIER", "zen")) +_TIER_MODEL = {"go": "opencode-go/glm-5.2", "zen": "opencode/glm-5.2"} +_DEFAULT_MODEL = _TIER_MODEL.get(TIER, _TIER_MODEL["zen"]) if BACKEND == "opencode" else "opus" MODEL = os.environ.get("REPORT_MODEL", _DEFAULT_MODEL) CLAUDE_BIN = os.environ.get("CLAUDE_BIN", "claude") CLAUDE_FLAGS = os.environ.get("CLAUDE_FLAGS", "--dangerously-skip-permissions") @@ -84,7 +87,7 @@ def start(mode, date): ) else: die(f"unknown REPORT_BACKEND '{BACKEND}'") - log(f"starting {SESSION} (backend={BACKEND}, model={MODEL}, date={date or 'today'})") + log(f"starting {SESSION} (backend={BACKEND}, tier={TIER}, model={MODEL}, date={date or 'today'})") subprocess.run(["tmux", "new-session", "-d", "-s", SESSION, "-c", cwd, cmd]) subprocess.run(["tmux", "pipe-pane", "-o", "-t", SESSION, f"cat >> '{LOG_DIR}/{SESSION}.log'"]) if BACKEND == "opencode": @@ -99,13 +102,13 @@ def start(mode, date): LU = "/srv/cc-ci/cc-ci-plan/launch-upgrader.py" rf = Path(LOG_DIR) / f".kickoff-{SESSION}-resume.txt" rf.write_text( - "The opencode-go usage limit has reset (or the report stalled). CONTINUE generating and " + "The opencode usage limit has reset (or the report stalled). CONTINUE generating and " "publishing this week's public Recipe Report per /recipe-report: survey → write the spec " "JSON → render with recipe-report.py → publish. Do NOT hand-write HTML (render() owns all " "formatting). When the page is live, print 'RECIPE REPORT COMPLETE' and go idle.") wenv = {"HOME": os.environ.get("HOME") or "/home/loops", "UPGRADER_SESSION": SESSION, "UPGRADER_DIR": cwd, "LOG_DIR": LOG_DIR, - "UPGRADER_BACKEND": "opencode", "UPGRADER_MODEL": MODEL, + "UPGRADER_BACKEND": "opencode", "UPGRADER_MODEL": MODEL, "LOOP_TIER": TIER, "OPENCODE_BIN": OPENCODE_BIN, "OPENCODE_SERVER": OPENCODE_SERVER, "OPENCODE_SHARE": "1" if OPENCODE_SHARE else "0", "UPGRADER_DONE_MARKER": "RECIPE REPORT COMPLETE", diff --git a/cc-ci-plan/launch-upgrader.py b/cc-ci-plan/launch-upgrader.py index 2ae63b6..8f95d04 100644 --- a/cc-ci-plan/launch-upgrader.py +++ b/cc-ci-plan/launch-upgrader.py @@ -15,10 +15,14 @@ Usage: Env: LOOP_BACKEND opencode (default) | claude — also accepts UPGRADER_BACKEND - LOOP_MODEL model flag (overrides UPGRADER_MODEL); default tracks backend — - opencode→opencode-go/glm-5.2, claude→sonnet - UPGRADER_MODEL provider/model for opencode, e.g. opencode-go/glm-5.2 (OpenCode Go - subscription) or tinfoil/deepseek-v4-pro; sonnet etc. for claude + LOOP_TIER opencode subscription tier: "zen" (OpenCode ZEN, default) or "go" + (OpenCode Go). Selects the default model + the usage-limit probe + endpoint/key. Go hit a monthly limit; ZEN is the working alternative. + Only affects the opencode backend (ignored for claude). + LOOP_MODEL model flag (overrides UPGRADER_MODEL); default tracks backend+tier — + opencode+zen→opencode/glm-5.2, opencode+go→opencode-go/glm-5.2, claude→sonnet + UPGRADER_MODEL provider/model for opencode, e.g. opencode/glm-5.2 (OpenCode ZEN), + opencode-go/glm-5.2 (OpenCode Go), or tinfoil/deepseek-v4-pro; sonnet for claude UPGRADER_ARGS extra args passed to /upgrade-all (e.g. "n8n ghost", "--dry-run") claude backend: @@ -38,11 +42,22 @@ WORKDIR = os.environ.get("UPGRADER_DIR", "/srv/cc-ci") LOG_DIR = os.environ.get("LOG_DIR", "/srv/cc-ci/.cc-ci-logs") # LOOP_BACKEND / LOOP_MODEL take precedence (unified control from the operator). -# Default backend+model is the OpenCode Go subscription on glm-5.2; override either via env. -# The model default tracks the backend so an explicit `LOOP_BACKEND=claude` (without a model) -# still gets a sensible claude model rather than a glm id. +# LOOP_TIER selects the OpenCode subscription: "zen" (default) or "go". Go hit a monthly +# usage limit; ZEN is the working alternative. Set LOOP_TIER=go when the Go limit resets. +# Only affects the opencode backend; claude ignores the tier. BACKEND = os.environ.get("LOOP_BACKEND", os.environ.get("UPGRADER_BACKEND", "opencode")) -_DEFAULT_MODEL = "opencode-go/glm-5.2" if BACKEND == "opencode" else "sonnet" +TIER = os.environ.get("LOOP_TIER", os.environ.get("UPGRADER_TIER", "zen")) +_TIER_CFG = { + "go": {"model": "opencode-go/glm-5.2", "endpoint": "https://opencode.ai/zen/go/v1/chat/completions", + "auth": "opencode-go", "label": "OpenCode Go"}, + "zen": {"model": "opencode/glm-5.2", "endpoint": "https://opencode.ai/zen/v1/chat/completions", + "auth": "opencode", "label": "OpenCode ZEN"}, +} +if BACKEND == "opencode" and TIER not in _TIER_CFG: + print(f"[upgrader] ERROR: unknown LOOP_TIER '{TIER}' — use 'go' or 'zen'", flush=True) + sys.exit(1) +_tier = _TIER_CFG.get(TIER, _TIER_CFG["zen"]) +_DEFAULT_MODEL = _tier["model"] if BACKEND == "opencode" else "sonnet" MODEL = os.environ.get("LOOP_MODEL", os.environ.get("UPGRADER_MODEL", _DEFAULT_MODEL)) CLAUDE_BIN = os.environ.get("CLAUDE_BIN", "claude") @@ -173,7 +188,8 @@ def start(mode="use-or-create"): kf.write_text(build_kickoff()) model_flag = f"--model '{MODEL}'" if MODEL else "" - log(f"starting {SESSION} (backend={BACKEND}, model={MODEL}, args='{UPGRADER_ARGS or ''}')") + tier_note = f", tier={TIER} ({_tier['label']})" if BACKEND == "opencode" else "" + log(f"starting {SESSION} (backend={BACKEND}{tier_note}, model={MODEL}, args='{UPGRADER_ARGS or ''}')") if BACKEND == "claude": if not shutil.which(CLAUDE_BIN): @@ -201,13 +217,13 @@ def start(mode="use-or-create"): subprocess.run(["tmux", "pipe-pane", "-o", "-t", SESSION, f"cat >> '{LOG_DIR}/{SESSION}.log'"]) log(f"started. attach: tmux attach -t {SESSION} log: {LOG_DIR}/{SESSION}.log") - # For the opencode backend, spawn a watchdog that auto-resumes the run if the opencode-go + # For the opencode backend, spawn a watchdog that auto-resumes the run if the opencode # usage-limit (429) stalls it mid-run (it does NOT self-resume). See watchdog(). if BACKEND == "opencode" and os.environ.get("UPGRADER_WATCHDOG", "1") == "1": _spawn_watchdog() # ── opencode stall-detect + auto-resume watchdog ──────────────────────────────── -# The opencode-go subscription enforces a rolling usage-limit (HTTP 429 + retry-after). When it +# The opencode subscription (Go or ZEN tier) enforces a rolling usage-limit (HTTP 429 + retry-after). When it # trips mid-run, the `opencode run` agent loop ENDS and does NOT self-resume. This watchdog detects # the stall (the session log stops growing), waits out the limit, and resumes the SAME session — # context preserved — via `opencode run -s --continue`. Standalone: launch-upgrader.py {resume|watchdog}. @@ -219,7 +235,7 @@ CHECK_EVERY = int(os.environ.get("UPGRADER_CHECK_SEC", "180")) # watchdog pol # cc-ci-report session with its own marker + resume prompt via these env vars). DONE_MARKER = os.environ.get("UPGRADER_DONE_MARKER", "UPGRADE RUN COMPLETE") RESUME_FILE = os.environ.get("UPGRADER_RESUME_FILE") # optional path to a custom resume prompt -GO_ENDPOINT = "https://opencode.ai/zen/go/v1/chat/completions" +LIMIT_ENDPOINT = _tier["endpoint"] # tier-aware: zen or go AUTH_JSON = os.path.expanduser("~/.local/share/opencode/auth.json") LOG_FILE = f"{LOG_DIR}/{SESSION}.log" @@ -279,20 +295,20 @@ def _session_idle_min(): # Back-compat alias (older callers / the report watchdog import this name). _log_idle_min = _session_idle_min -def _go_key(): +def _limit_key(): try: - return (_json.load(open(AUTH_JSON)).get("opencode-go") or {}).get("key") + return (_json.load(open(AUTH_JSON)).get(_tier["auth"]) or {}).get("key") except Exception: return None def _limit_retry_after(): - """0 if the opencode-go endpoint is available (HTTP 200); else the 429 retry-after seconds.""" - key = _go_key() + """0 if the tier's opencode endpoint is available (HTTP 200); else the 429 retry-after seconds.""" + key = _limit_key() if not key: return 0 body = _json.dumps({"model": (MODEL or "").split("/")[-1] or "glm-5.2", "max_tokens": 8, "messages": [{"role": "user", "content": "hi"}]}).encode() - req = _ureq.Request(GO_ENDPOINT, data=body, method="POST", + req = _ureq.Request(LIMIT_ENDPOINT, data=body, method="POST", headers={"Authorization": "Bearer " + key, "content-type": "application/json"}) try: _ureq.urlopen(req, timeout=20).read(); return 0 @@ -369,7 +385,7 @@ def resume(reason="manual"): kf.write_text(open(RESUME_FILE).read()) # caller-supplied (e.g. the report job) else: kf.write_text( - "The opencode-go usage limit has reset (or the run stalled). You were mid-way through the weekly " + "The opencode usage limit has reset (or the run stalled). You were mid-way through the weekly " "cc-ci /upgrade-all run. CONTINUE from where you left off — do NOT start over. Process the enrolled " "recipes not yet done this week, alphabetically; SKIP ones already done (their PRs exist — extend, " "never duplicate). Per recipe: run /recipe-upgrade in DEFAULT mode via a subagent, verify with " @@ -400,8 +416,8 @@ def _spawn_watchdog(): wsess = f"{SESSION}-watchdog" wlog = f"{LOG_DIR}/{SESSION}-watchdog.log" env = {"HOME": os.environ.get("HOME") or os.path.expanduser("~"), - "UPGRADER_SESSION": SESSION, "UPGRADER_DIR": WORKDIR, "LOG_DIR": LOG_DIR, - "UPGRADER_BACKEND": "opencode", "UPGRADER_MODEL": MODEL, + "UPGRADER_SESSION": SESSION, "UPGRADER_DIR": WORKDIR, "LOG_DIR": LOG_DIR, + "UPGRADER_BACKEND": "opencode", "UPGRADER_MODEL": MODEL, "LOOP_TIER": TIER, "OPENCODE_BIN": OPENCODE_BIN, "OPENCODE_SERVER": OPENCODE_SERVER, "OPENCODE_SHARE": "1" if OPENCODE_SHARE else "0"} for k in ("UPGRADER_RESUME_FILE", "UPGRADER_DONE_MARKER", "UPGRADER_STALL_MIN", "UPGRADER_CHECK_SEC"): @@ -420,7 +436,7 @@ def watchdog(): also freezes, so log-idle alone would take 15min to notice a run that died in 5. We detect it directly: no live `opencode run` proc for the session + not completed ⇒ resume promptly. (b) LOG-IDLE — a proc is alive but wedged (no output > STALL_MIN); resume after confirming. - Either way, wait out an opencode-go usage-limit (429) first rather than hammering. Exits when the + Either way, wait out an opencode usage-limit (429) first rather than hammering. Exits when the model prints DONE_MARKER, or after MAX_RESUMES consecutive resumes fail to get a live proc going (truly broken — hand back to the hourly supervisor / operator). Spawned by start()/resume().""" MAX_RESUMES = int(os.environ.get("UPGRADER_MAX_RESUMES", "20")) @@ -485,7 +501,7 @@ def main(): shell=True) else: log(f"{SESSION}: stopped") - log(f"backend: {BACKEND} model: {MODEL} args: '{UPGRADER_ARGS or ''}'") + log(f"backend: {BACKEND} tier: {TIER} model: {MODEL} args: '{UPGRADER_ARGS or ''}'") elif cmd == "attach": os.execvp("tmux", ["tmux", "attach", "-t", SESSION]) elif cmd == "resume": @@ -504,6 +520,7 @@ def main(): launch-upgrader.py watchdog watch + auto-resume the opencode run across usage-limit (429) stalls Backend: {BACKEND} (LOOP_BACKEND or UPGRADER_BACKEND env var) +Tier: {TIER} (LOOP_TIER or UPGRADER_TIER env var; 'go' or 'zen'; only for opencode) Model: {MODEL} (LOOP_MODEL or UPGRADER_MODEL env var) Args: {UPGRADER_ARGS or ''} (UPGRADER_ARGS env var, passed to /upgrade-all)