@synkro-sh/cli 1.3.26 → 1.3.27

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
@@ -1677,7 +1677,7 @@ Commands:
1677
1677
  status - print "running"/"stopped"
1678
1678
  """
1679
1679
 
1680
- import os, sys, json, socket, time, signal, fcntl, re
1680
+ import os, sys, json, socket, time, signal, fcntl, re, select
1681
1681
  import subprocess, threading
1682
1682
  from pathlib import Path
1683
1683
 
@@ -1707,27 +1707,49 @@ def log(msg):
1707
1707
  pass
1708
1708
 
1709
1709
 
1710
+ STALL_TIMEOUT_SEC = int(os.environ.get("SYNKRO_DAEMON_STALL_TIMEOUT", "10"))
1711
+
1710
1712
  def _read_response(proc, timeout=45):
1711
1713
  acc = []
1712
1714
  deadline = time.time() + timeout
1715
+ last_data = time.time()
1716
+ fd = proc.stdout.fileno()
1717
+ buf = ""
1713
1718
  while True:
1714
- if time.time() > deadline:
1719
+ remaining = deadline - time.time()
1720
+ if remaining <= 0:
1715
1721
  log("read timeout")
1716
1722
  return ""
1717
- line = proc.stdout.readline()
1718
- if not line:
1723
+ if time.time() - last_data > STALL_TIMEOUT_SEC:
1724
+ log(f"stall timeout: no data for {STALL_TIMEOUT_SEC}s")
1719
1725
  return ""
1720
- try:
1721
- obj = json.loads(line)
1722
- except json.JSONDecodeError:
1726
+ ready, _, _ = select.select([fd], [], [], min(remaining, 2.0))
1727
+ if not ready:
1728
+ if proc.poll() is not None:
1729
+ log("process exited during read")
1730
+ return ""
1723
1731
  continue
1724
- t = obj.get("type")
1725
- if t == "assistant":
1726
- for c in obj.get("message", {}).get("content", []):
1727
- if c.get("type") == "text":
1728
- acc.append(c["text"])
1729
- elif t == "result":
1730
- return "".join(acc)
1732
+ chunk = os.read(fd, 65536)
1733
+ if not chunk:
1734
+ return ""
1735
+ last_data = time.time()
1736
+ buf += chunk.decode("utf-8", errors="replace")
1737
+ while "\\n" in buf:
1738
+ line, buf = buf.split("\\n", 1)
1739
+ line = line.strip()
1740
+ if not line:
1741
+ continue
1742
+ try:
1743
+ obj = json.loads(line)
1744
+ except json.JSONDecodeError:
1745
+ continue
1746
+ t = obj.get("type")
1747
+ if t == "assistant":
1748
+ for c in obj.get("message", {}).get("content", []):
1749
+ if c.get("type") == "text":
1750
+ acc.append(c["text"])
1751
+ elif t == "result":
1752
+ return "".join(acc)
1731
1753
 
1732
1754
 
1733
1755
  def _send_msg(proc, text):
@@ -1757,6 +1779,7 @@ class WarmGrader:
1757
1779
  self._warm_thread = None
1758
1780
  self._lock = threading.Lock()
1759
1781
  self._total_grades = 0
1782
+ self._prewarm_ok = True
1760
1783
  self._start_prewarm()
1761
1784
 
1762
1785
  def _make_proc(self):
@@ -1786,27 +1809,32 @@ class WarmGrader:
1786
1809
  log("pre-warming process")
1787
1810
  proc = self._make_proc()
1788
1811
  _send_msg(proc, "Ready")
1789
- resp = _read_response(proc, timeout=30)
1812
+ resp = _read_response(proc, timeout=15)
1790
1813
  if resp:
1791
1814
  with self._lock:
1792
1815
  old = self._warm_proc
1793
1816
  self._warm_proc = proc
1817
+ self._prewarm_ok = True
1794
1818
  if old:
1795
1819
  self._kill_proc(old)
1796
1820
  log(f"pre-warm ready ({len(resp)} chars)")
1797
1821
  else:
1798
1822
  log("pre-warm response empty")
1799
1823
  self._kill_proc(proc)
1824
+ self._prewarm_ok = False
1800
1825
  except Exception as e:
1801
1826
  log(f"pre-warm failed: {e}")
1827
+ self._prewarm_ok = False
1802
1828
 
1803
1829
  def _start_prewarm(self):
1804
1830
  self._warm_thread = threading.Thread(target=self._prewarm, daemon=True)
1805
1831
  self._warm_thread.start()
1806
1832
 
1807
1833
  def grade(self, prompt):
1808
- if self._warm_thread:
1809
- self._warm_thread.join(timeout=60)
1834
+ if self._warm_thread and self._prewarm_ok:
1835
+ self._warm_thread.join(timeout=8)
1836
+ elif self._warm_thread and not self._prewarm_ok:
1837
+ log("skipping prewarm join (last prewarm failed)")
1810
1838
 
1811
1839
  with self._lock:
1812
1840
  proc = self._warm_proc
@@ -1817,11 +1845,17 @@ class WarmGrader:
1817
1845
  log("no warm process, cold fallback")
1818
1846
  proc = self._make_proc()
1819
1847
  warm = False
1848
+ elif proc.stdin.closed:
1849
+ log("warm process stdin closed, cold fallback")
1850
+ self._kill_proc(proc)
1851
+ proc = self._make_proc()
1852
+ warm = False
1820
1853
 
1854
+ wall_limit = int(os.environ.get("SYNKRO_DAEMON_WALL_TIMEOUT", "12"))
1821
1855
  t0 = time.time()
1822
1856
  try:
1823
1857
  _send_msg(proc, prompt)
1824
- resp = _read_response(proc, timeout=GRADE_TIMEOUT_SEC)
1858
+ resp = _read_response(proc, timeout=min(GRADE_TIMEOUT_SEC, wall_limit))
1825
1859
  except Exception as e:
1826
1860
  log(f"grade error: {e}")
1827
1861
  resp = ""
@@ -2448,6 +2482,7 @@ async function callApi(method, endpoint, body) {
2448
2482
  throw new Error("API URL not configured. Run `synkro install` first.");
2449
2483
  }
2450
2484
  const url = `${API_URL}${endpoint}`;
2485
+ await ensureValidToken();
2451
2486
  const accessToken = getAccessToken();
2452
2487
  const headers = {
2453
2488
  "Content-Type": "application/json"
@@ -3333,7 +3368,7 @@ function writeConfigEnv(opts) {
3333
3368
  `SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle(credsPath)}`,
3334
3369
  `SYNKRO_TIER=${shellQuoteSingle(safeTier)}`,
3335
3370
  `SYNKRO_INFERENCE=${shellQuoteSingle(safeInference)}`,
3336
- `SYNKRO_VERSION=${shellQuoteSingle("1.3.26")}`
3371
+ `SYNKRO_VERSION=${shellQuoteSingle("1.3.27")}`
3337
3372
  ];
3338
3373
  if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle(safeUserId)}`);
3339
3374
  if (safeOrgId) lines.push(`SYNKRO_ORG_ID=${shellQuoteSingle(safeOrgId)}`);