@synkro-sh/cli 1.3.35 → 1.3.36

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bootstrap.js CHANGED
@@ -1986,25 +1986,73 @@ def _read_jwt():
1986
1986
  except Exception:
1987
1987
  return ""
1988
1988
 
1989
+ def _refresh_jwt():
1990
+ """Refresh the access token using the saved refresh_token. Writes new
1991
+ tokens back to credentials.json. Returns the new access_token or ''."""
1992
+ try:
1993
+ with open(CREDS_PATH) as f:
1994
+ creds = json.load(f)
1995
+ rt = creds.get("refresh_token", "")
1996
+ if not rt:
1997
+ return ""
1998
+ req = urllib.request.Request(
1999
+ f"{GATEWAY_URL}/api/auth/refresh",
2000
+ data=json.dumps({"refresh_token": rt}).encode("utf-8"),
2001
+ headers={
2002
+ "Content-Type": "application/json",
2003
+ "User-Agent": "synkro-cli-grader-daemon/1",
2004
+ },
2005
+ )
2006
+ with urllib.request.urlopen(req, timeout=5) as resp:
2007
+ data = json.loads(resp.read().decode("utf-8"))
2008
+ new_at = data.get("access_token", "")
2009
+ new_rt = data.get("refresh_token", "") or rt
2010
+ if not new_at:
2011
+ return ""
2012
+ creds["access_token"] = new_at
2013
+ creds["refresh_token"] = new_rt
2014
+ tmp = str(CREDS_PATH) + ".synkro.tmp"
2015
+ with open(tmp, "w") as f:
2016
+ json.dump(creds, f)
2017
+ os.replace(tmp, str(CREDS_PATH))
2018
+ return new_at
2019
+ except Exception:
2020
+ return ""
2021
+
1989
2022
  def fetch_primer(mode):
1990
- """Fetch primer text for {bash,edit} from /cli/judge-prompts. In-memory only."""
2023
+ """Fetch primer text for {bash,edit} from /cli/judge-prompts. In-memory only.
2024
+ Auto-refreshes JWT on 401 and retries once."""
1991
2025
  _load_gateway_url()
1992
- jwt = _read_jwt()
1993
- if not jwt:
1994
- return ""
1995
2026
  field = "grader_primer_bash" if mode == "bash" else "grader_primer_edit"
1996
- try:
2027
+
2028
+ def _do_fetch(jwt):
1997
2029
  req = urllib.request.Request(
1998
2030
  f"{GATEWAY_URL}/api/v1/cli/judge-prompts",
1999
2031
  headers={
2000
2032
  "Authorization": f"Bearer {jwt}",
2001
- # CF rejects requests without UA \u2014 without this we get 403.
2002
2033
  "User-Agent": "synkro-cli-grader-daemon/1",
2003
2034
  },
2004
2035
  )
2005
2036
  with urllib.request.urlopen(req, timeout=5) as resp:
2006
2037
  data = json.loads(resp.read().decode("utf-8"))
2007
2038
  return data.get(field, "") or ""
2039
+
2040
+ jwt = _read_jwt()
2041
+ if not jwt:
2042
+ return ""
2043
+ try:
2044
+ return _do_fetch(jwt)
2045
+ except urllib.error.HTTPError as e:
2046
+ if e.code != 401:
2047
+ return ""
2048
+ # Token expired \u2014 refresh and retry once.
2049
+ new_jwt = _refresh_jwt()
2050
+ if not new_jwt:
2051
+ return ""
2052
+ try:
2053
+ return _do_fetch(new_jwt)
2054
+ except Exception:
2055
+ return ""
2008
2056
  except Exception:
2009
2057
  return ""
2010
2058
 
@@ -2111,6 +2159,7 @@ class WarmGrader:
2111
2159
  def __init__(self, primer):
2112
2160
  self.primer = primer or ""
2113
2161
  self._warm_proc = None
2162
+ self._warm_ready_at = 0.0
2114
2163
  self._warm_thread = None
2115
2164
  self._lock = threading.Lock()
2116
2165
  self._total_grades = 0
@@ -2149,6 +2198,7 @@ class WarmGrader:
2149
2198
  with self._lock:
2150
2199
  old = self._warm_proc
2151
2200
  self._warm_proc = proc
2201
+ self._warm_ready_at = time.time()
2152
2202
  self._prewarm_ok = True
2153
2203
  if old:
2154
2204
  self._kill_proc(old)
@@ -2173,8 +2223,16 @@ class WarmGrader:
2173
2223
 
2174
2224
  with self._lock:
2175
2225
  proc = self._warm_proc
2226
+ ready_at = self._warm_ready_at
2176
2227
  self._warm_proc = None
2177
-
2228
+ self._warm_ready_at = 0.0
2229
+
2230
+ # Warm-process TTL: claude --print holds an HTTP/2 conn to Anthropic
2231
+ # that goes stale after ~20s idle. The subprocess doesn't notice the
2232
+ # broken pipe and just hangs forever when given a prompt. Force cold
2233
+ # if the warm has been sitting idle too long \u2014 still faster than
2234
+ # eating a 10s stall timeout + cold fallback.
2235
+ WARM_TTL_SEC = int(os.environ.get("SYNKRO_DAEMON_WARM_TTL", "15"))
2178
2236
  warm = True
2179
2237
  if not proc or proc.poll() is not None:
2180
2238
  log("no warm process, cold fallback")
@@ -2185,6 +2243,12 @@ class WarmGrader:
2185
2243
  self._kill_proc(proc)
2186
2244
  proc = self._make_proc()
2187
2245
  warm = False
2246
+ elif ready_at and (time.time() - ready_at) > WARM_TTL_SEC:
2247
+ age = time.time() - ready_at
2248
+ log(f"warm process stale ({age:.0f}s > {WARM_TTL_SEC}s), cold fallback")
2249
+ self._kill_proc(proc)
2250
+ proc = self._make_proc()
2251
+ warm = False
2188
2252
 
2189
2253
  wall_limit = int(os.environ.get("SYNKRO_DAEMON_WALL_TIMEOUT", "12"))
2190
2254
  t0 = time.time()
@@ -3651,7 +3715,7 @@ function writeConfigEnv(opts) {
3651
3715
  `SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle(credsPath)}`,
3652
3716
  `SYNKRO_TIER=${shellQuoteSingle(safeTier)}`,
3653
3717
  `SYNKRO_INFERENCE=${shellQuoteSingle(safeInference)}`,
3654
- `SYNKRO_VERSION=${shellQuoteSingle("1.3.35")}`
3718
+ `SYNKRO_VERSION=${shellQuoteSingle("1.3.36")}`
3655
3719
  ];
3656
3720
  if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle(safeUserId)}`);
3657
3721
  if (safeOrgId) lines.push(`SYNKRO_ORG_ID=${shellQuoteSingle(safeOrgId)}`);