@synkro-sh/cli 1.3.34 → 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,21 +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
- headers={"Authorization": f"Bearer {jwt}"},
2031
+ headers={
2032
+ "Authorization": f"Bearer {jwt}",
2033
+ "User-Agent": "synkro-cli-grader-daemon/1",
2034
+ },
2000
2035
  )
2001
2036
  with urllib.request.urlopen(req, timeout=5) as resp:
2002
2037
  data = json.loads(resp.read().decode("utf-8"))
2003
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 ""
2004
2056
  except Exception:
2005
2057
  return ""
2006
2058
 
@@ -2107,6 +2159,7 @@ class WarmGrader:
2107
2159
  def __init__(self, primer):
2108
2160
  self.primer = primer or ""
2109
2161
  self._warm_proc = None
2162
+ self._warm_ready_at = 0.0
2110
2163
  self._warm_thread = None
2111
2164
  self._lock = threading.Lock()
2112
2165
  self._total_grades = 0
@@ -2145,6 +2198,7 @@ class WarmGrader:
2145
2198
  with self._lock:
2146
2199
  old = self._warm_proc
2147
2200
  self._warm_proc = proc
2201
+ self._warm_ready_at = time.time()
2148
2202
  self._prewarm_ok = True
2149
2203
  if old:
2150
2204
  self._kill_proc(old)
@@ -2169,8 +2223,16 @@ class WarmGrader:
2169
2223
 
2170
2224
  with self._lock:
2171
2225
  proc = self._warm_proc
2226
+ ready_at = self._warm_ready_at
2172
2227
  self._warm_proc = None
2173
-
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"))
2174
2236
  warm = True
2175
2237
  if not proc or proc.poll() is not None:
2176
2238
  log("no warm process, cold fallback")
@@ -2181,6 +2243,12 @@ class WarmGrader:
2181
2243
  self._kill_proc(proc)
2182
2244
  proc = self._make_proc()
2183
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
2184
2252
 
2185
2253
  wall_limit = int(os.environ.get("SYNKRO_DAEMON_WALL_TIMEOUT", "12"))
2186
2254
  t0 = time.time()
@@ -3647,7 +3715,7 @@ function writeConfigEnv(opts) {
3647
3715
  `SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle(credsPath)}`,
3648
3716
  `SYNKRO_TIER=${shellQuoteSingle(safeTier)}`,
3649
3717
  `SYNKRO_INFERENCE=${shellQuoteSingle(safeInference)}`,
3650
- `SYNKRO_VERSION=${shellQuoteSingle("1.3.34")}`
3718
+ `SYNKRO_VERSION=${shellQuoteSingle("1.3.36")}`
3651
3719
  ];
3652
3720
  if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle(safeUserId)}`);
3653
3721
  if (safeOrgId) lines.push(`SYNKRO_ORG_ID=${shellQuoteSingle(safeOrgId)}`);