@synkro-sh/cli 1.1.6 → 1.1.7

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
@@ -1335,7 +1335,7 @@ returns the assistant's response text. ONE CC startup (~3.5s) amortizes
1335
1335
  across N gradings.
1336
1336
 
1337
1337
  Session bloat is bounded: the daemon rotates its claude subprocess every
1338
- ROTATION_CALLS (default 30) gradings or ROTATION_AGE_SEC (default 1h),
1338
+ ROTATION_CALLS (default 10) gradings or ROTATION_AGE_SEC (default 1h),
1339
1339
  whichever comes first. Each rotation eats a one-time ~5s primer cost; calls
1340
1340
  in between target ~2-3s steady-state.
1341
1341
 
@@ -1365,9 +1365,9 @@ def mode_paths(mode):
1365
1365
  MODE = "edit"
1366
1366
  PID_FILE, SOCK_PATH, LOG_FILE = mode_paths(MODE)
1367
1367
 
1368
- ROTATION_CALLS = int(os.environ.get("SYNKRO_DAEMON_ROTATE_CALLS", "30"))
1368
+ ROTATION_CALLS = int(os.environ.get("SYNKRO_DAEMON_ROTATE_CALLS", "10"))
1369
1369
  ROTATION_AGE_SEC = int(os.environ.get("SYNKRO_DAEMON_ROTATE_AGE", "3600"))
1370
- GRADE_TIMEOUT_SEC = 60
1370
+ GRADE_TIMEOUT_SEC = int(os.environ.get("SYNKRO_DAEMON_GRADE_TIMEOUT", "10"))
1371
1371
  DEFAULT_MODEL = os.environ.get("SYNKRO_DAEMON_MODEL", "claude-sonnet-4-6")
1372
1372
  MAX_PROMPT_BYTES = 4 * 1024 * 1024
1373
1373
 
@@ -1380,6 +1380,8 @@ def log(msg):
1380
1380
  pass
1381
1381
 
1382
1382
 
1383
+ PREWARM_HEADROOM = 4
1384
+
1383
1385
  class GraderDaemon:
1384
1386
  def __init__(self, primer):
1385
1387
  self.primer = primer or ""
@@ -1387,14 +1389,12 @@ class GraderDaemon:
1387
1389
  self.calls = 0
1388
1390
  self.start_time = 0.0
1389
1391
  self.lock = threading.Lock()
1392
+ self._next_proc = None
1393
+ self._prewarm_thread = None
1390
1394
  self._spawn()
1391
1395
 
1392
- def _spawn(self):
1393
- if self.proc and self.proc.poll() is None:
1394
- try: self.proc.terminate(); self.proc.wait(timeout=3)
1395
- except Exception: self.proc.kill()
1396
- log("spawning claude subprocess")
1397
- self.proc = subprocess.Popen(
1396
+ def _make_proc(self):
1397
+ return subprocess.Popen(
1398
1398
  [
1399
1399
  "claude", "--print", "--model", DEFAULT_MODEL,
1400
1400
  "--input-format=stream-json",
@@ -1405,41 +1405,26 @@ class GraderDaemon:
1405
1405
  stdin=subprocess.PIPE, stdout=subprocess.PIPE,
1406
1406
  stderr=subprocess.DEVNULL, text=True, bufsize=1,
1407
1407
  )
1408
- if self.primer:
1409
- self._send(self.primer)
1410
- primer_resp = self._recv()
1411
- log(f"primer ack: {primer_resp[:80]!r}")
1412
- self.calls = 0
1413
- self.start_time = time.time()
1414
1408
 
1415
- def _send(self, text):
1409
+ def _send_to(self, proc, text):
1416
1410
  msg = json.dumps({
1417
1411
  "type": "user",
1418
1412
  "message": {"role": "user", "content": [{"type": "text", "text": text}]},
1419
1413
  "parent_tool_use_id": None,
1420
1414
  "session_id": "",
1421
1415
  })
1422
- try:
1423
- self.proc.stdin.write(msg + "\\n")
1424
- self.proc.stdin.flush()
1425
- except (BrokenPipeError, OSError) as e:
1426
- log(f"send broke: {e}; respawn")
1427
- self._spawn()
1428
- self.proc.stdin.write(msg + "\\n")
1429
- self.proc.stdin.flush()
1416
+ proc.stdin.write(msg + "\\n")
1417
+ proc.stdin.flush()
1430
1418
 
1431
- def _recv(self):
1419
+ def _recv_from(self, proc):
1432
1420
  acc = []
1433
1421
  deadline = time.time() + GRADE_TIMEOUT_SEC
1434
1422
  while True:
1435
1423
  if time.time() > deadline:
1436
- log("recv timeout; respawn")
1437
- self._spawn()
1424
+ log("recv timeout")
1438
1425
  return ""
1439
- line = self.proc.stdout.readline()
1426
+ line = proc.stdout.readline()
1440
1427
  if not line:
1441
- log("subprocess closed stdout; respawn")
1442
- self._spawn()
1443
1428
  return ""
1444
1429
  try:
1445
1430
  obj = json.loads(line)
@@ -1453,18 +1438,87 @@ class GraderDaemon:
1453
1438
  elif t == "result":
1454
1439
  return "".join(acc)
1455
1440
 
1441
+ def _spawn(self):
1442
+ if self.proc and self.proc.poll() is None:
1443
+ try: self.proc.terminate(); self.proc.wait(timeout=3)
1444
+ except Exception: self.proc.kill()
1445
+ log("spawning claude subprocess")
1446
+ self.proc = self._make_proc()
1447
+ if self.primer:
1448
+ self._send_to(self.proc, self.primer)
1449
+ primer_resp = self._recv_from(self.proc)
1450
+ log(f"primer ack: {primer_resp[:80]!r}")
1451
+ self.calls = 0
1452
+ self.start_time = time.time()
1453
+ self._next_proc = None
1454
+
1455
+ def _prewarm_worker(self):
1456
+ try:
1457
+ log("pre-warming next subprocess")
1458
+ proc = self._make_proc()
1459
+ if self.primer:
1460
+ self._send_to(proc, self.primer)
1461
+ resp = self._recv_from(proc)
1462
+ log(f"pre-warm ack: {resp[:80]!r}")
1463
+ self._next_proc = proc
1464
+ log("pre-warm ready")
1465
+ except Exception as e:
1466
+ log(f"pre-warm failed: {e}")
1467
+ self._next_proc = None
1468
+
1469
+ def _maybe_prewarm(self):
1470
+ if self._prewarm_thread and self._prewarm_thread.is_alive():
1471
+ return
1472
+ if self._next_proc is not None:
1473
+ return
1474
+ if self.calls >= ROTATION_CALLS - PREWARM_HEADROOM:
1475
+ self._prewarm_thread = threading.Thread(target=self._prewarm_worker, daemon=True)
1476
+ self._prewarm_thread.start()
1477
+
1478
+ def _try_rotate(self):
1479
+ if self._next_proc and self._next_proc.poll() is None:
1480
+ log("hot-swapping to pre-warmed subprocess")
1481
+ old = self.proc
1482
+ self.proc = self._next_proc
1483
+ self._next_proc = None
1484
+ self._prewarm_thread = None
1485
+ self.calls = 0
1486
+ self.start_time = time.time()
1487
+ if old and old.poll() is None:
1488
+ threading.Thread(target=lambda: (old.terminate(), old.wait()), daemon=True).start()
1489
+ return True
1490
+ log("pre-warm not ready, deferring rotation")
1491
+ return False
1492
+
1493
+ def _send(self, text):
1494
+ try:
1495
+ self._send_to(self.proc, text)
1496
+ except (BrokenPipeError, OSError) as e:
1497
+ log(f"send broke: {e}; respawn")
1498
+ self._spawn()
1499
+ self._send_to(self.proc, text)
1500
+
1501
+ def _recv(self):
1502
+ resp = self._recv_from(self.proc)
1503
+ if not resp and (not self.proc or self.proc.poll() is not None):
1504
+ log("subprocess died; respawn")
1505
+ self._spawn()
1506
+ return resp
1507
+
1456
1508
  def grade(self, prompt):
1457
1509
  with self.lock:
1458
- age = time.time() - self.start_time
1459
- if self.calls >= ROTATION_CALLS or age >= ROTATION_AGE_SEC:
1460
- log(f"rotating: calls={self.calls} age={age:.0f}s")
1461
- self._spawn()
1462
1510
  t0 = time.time()
1463
1511
  self._send(prompt)
1464
1512
  resp = self._recv()
1465
1513
  elapsed = (time.time() - t0) * 1000
1466
1514
  self.calls += 1
1467
1515
  log(f"grade #{self.calls} elapsed={elapsed:.0f}ms resp_chars={len(resp)}")
1516
+ age = time.time() - self.start_time
1517
+ if self.calls >= ROTATION_CALLS or age >= ROTATION_AGE_SEC:
1518
+ if not self._try_rotate():
1519
+ self._maybe_prewarm()
1520
+ else:
1521
+ self._maybe_prewarm()
1468
1522
  return resp
1469
1523
 
1470
1524
 
@@ -2090,7 +2144,7 @@ function writeConfigEnv(opts) {
2090
2144
  `SYNKRO_GATEWAY_URL=${shellQuoteSingle(safeGateway)}`,
2091
2145
  `SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle(credsPath)}`,
2092
2146
  `SYNKRO_TIER=${shellQuoteSingle(safeTier)}`,
2093
- `SYNKRO_VERSION=${shellQuoteSingle("1.1.6")}`
2147
+ `SYNKRO_VERSION=${shellQuoteSingle("1.1.7")}`
2094
2148
  ];
2095
2149
  if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle(safeUserId)}`);
2096
2150
  if (safeOrgId) lines.push(`SYNKRO_ORG_ID=${shellQuoteSingle(safeOrgId)}`);