@synkro-sh/cli 1.1.5 → 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 +98 -37
- package/dist/bootstrap.js.map +1 -1
- package/package.json +1 -1
package/dist/bootstrap.js
CHANGED
|
@@ -83,11 +83,18 @@ function writeSettingsAtomic(path, settings) {
|
|
|
83
83
|
writeFileSync(tmpPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
|
|
84
84
|
renameSync(tmpPath, path);
|
|
85
85
|
}
|
|
86
|
+
function isSynkroEntry(entry) {
|
|
87
|
+
if (entry?.[SYNKRO_MARKER]) return true;
|
|
88
|
+
const hooks = Array.isArray(entry?.hooks) ? entry.hooks : [];
|
|
89
|
+
return hooks.some(
|
|
90
|
+
(h) => typeof h?.command === "string" && h.command.includes("/.synkro/hooks/")
|
|
91
|
+
);
|
|
92
|
+
}
|
|
86
93
|
function removeSynkroEntries(events, eventName) {
|
|
87
94
|
if (!events) return;
|
|
88
95
|
const arr = events[eventName];
|
|
89
96
|
if (!Array.isArray(arr)) return;
|
|
90
|
-
events[eventName] = arr.filter((entry) => !entry
|
|
97
|
+
events[eventName] = arr.filter((entry) => !isSynkroEntry(entry));
|
|
91
98
|
}
|
|
92
99
|
function installCCHooks(settingsPath, config) {
|
|
93
100
|
const settings = readSettings(settingsPath);
|
|
@@ -118,7 +125,7 @@ function installCCHooks(settingsPath, config) {
|
|
|
118
125
|
{
|
|
119
126
|
type: "command",
|
|
120
127
|
command: config.editPrecheckScriptPath,
|
|
121
|
-
timeout:
|
|
128
|
+
timeout: 15
|
|
122
129
|
}
|
|
123
130
|
],
|
|
124
131
|
[SYNKRO_MARKER]: true
|
|
@@ -1328,7 +1335,7 @@ returns the assistant's response text. ONE CC startup (~3.5s) amortizes
|
|
|
1328
1335
|
across N gradings.
|
|
1329
1336
|
|
|
1330
1337
|
Session bloat is bounded: the daemon rotates its claude subprocess every
|
|
1331
|
-
ROTATION_CALLS (default
|
|
1338
|
+
ROTATION_CALLS (default 10) gradings or ROTATION_AGE_SEC (default 1h),
|
|
1332
1339
|
whichever comes first. Each rotation eats a one-time ~5s primer cost; calls
|
|
1333
1340
|
in between target ~2-3s steady-state.
|
|
1334
1341
|
|
|
@@ -1358,9 +1365,9 @@ def mode_paths(mode):
|
|
|
1358
1365
|
MODE = "edit"
|
|
1359
1366
|
PID_FILE, SOCK_PATH, LOG_FILE = mode_paths(MODE)
|
|
1360
1367
|
|
|
1361
|
-
ROTATION_CALLS = int(os.environ.get("SYNKRO_DAEMON_ROTATE_CALLS", "
|
|
1368
|
+
ROTATION_CALLS = int(os.environ.get("SYNKRO_DAEMON_ROTATE_CALLS", "10"))
|
|
1362
1369
|
ROTATION_AGE_SEC = int(os.environ.get("SYNKRO_DAEMON_ROTATE_AGE", "3600"))
|
|
1363
|
-
GRADE_TIMEOUT_SEC =
|
|
1370
|
+
GRADE_TIMEOUT_SEC = int(os.environ.get("SYNKRO_DAEMON_GRADE_TIMEOUT", "10"))
|
|
1364
1371
|
DEFAULT_MODEL = os.environ.get("SYNKRO_DAEMON_MODEL", "claude-sonnet-4-6")
|
|
1365
1372
|
MAX_PROMPT_BYTES = 4 * 1024 * 1024
|
|
1366
1373
|
|
|
@@ -1373,6 +1380,8 @@ def log(msg):
|
|
|
1373
1380
|
pass
|
|
1374
1381
|
|
|
1375
1382
|
|
|
1383
|
+
PREWARM_HEADROOM = 4
|
|
1384
|
+
|
|
1376
1385
|
class GraderDaemon:
|
|
1377
1386
|
def __init__(self, primer):
|
|
1378
1387
|
self.primer = primer or ""
|
|
@@ -1380,14 +1389,12 @@ class GraderDaemon:
|
|
|
1380
1389
|
self.calls = 0
|
|
1381
1390
|
self.start_time = 0.0
|
|
1382
1391
|
self.lock = threading.Lock()
|
|
1392
|
+
self._next_proc = None
|
|
1393
|
+
self._prewarm_thread = None
|
|
1383
1394
|
self._spawn()
|
|
1384
1395
|
|
|
1385
|
-
def
|
|
1386
|
-
|
|
1387
|
-
try: self.proc.terminate(); self.proc.wait(timeout=3)
|
|
1388
|
-
except Exception: self.proc.kill()
|
|
1389
|
-
log("spawning claude subprocess")
|
|
1390
|
-
self.proc = subprocess.Popen(
|
|
1396
|
+
def _make_proc(self):
|
|
1397
|
+
return subprocess.Popen(
|
|
1391
1398
|
[
|
|
1392
1399
|
"claude", "--print", "--model", DEFAULT_MODEL,
|
|
1393
1400
|
"--input-format=stream-json",
|
|
@@ -1398,41 +1405,26 @@ class GraderDaemon:
|
|
|
1398
1405
|
stdin=subprocess.PIPE, stdout=subprocess.PIPE,
|
|
1399
1406
|
stderr=subprocess.DEVNULL, text=True, bufsize=1,
|
|
1400
1407
|
)
|
|
1401
|
-
if self.primer:
|
|
1402
|
-
self._send(self.primer)
|
|
1403
|
-
primer_resp = self._recv()
|
|
1404
|
-
log(f"primer ack: {primer_resp[:80]!r}")
|
|
1405
|
-
self.calls = 0
|
|
1406
|
-
self.start_time = time.time()
|
|
1407
1408
|
|
|
1408
|
-
def
|
|
1409
|
+
def _send_to(self, proc, text):
|
|
1409
1410
|
msg = json.dumps({
|
|
1410
1411
|
"type": "user",
|
|
1411
1412
|
"message": {"role": "user", "content": [{"type": "text", "text": text}]},
|
|
1412
1413
|
"parent_tool_use_id": None,
|
|
1413
1414
|
"session_id": "",
|
|
1414
1415
|
})
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
self.proc.stdin.flush()
|
|
1418
|
-
except (BrokenPipeError, OSError) as e:
|
|
1419
|
-
log(f"send broke: {e}; respawn")
|
|
1420
|
-
self._spawn()
|
|
1421
|
-
self.proc.stdin.write(msg + "\\n")
|
|
1422
|
-
self.proc.stdin.flush()
|
|
1416
|
+
proc.stdin.write(msg + "\\n")
|
|
1417
|
+
proc.stdin.flush()
|
|
1423
1418
|
|
|
1424
|
-
def
|
|
1419
|
+
def _recv_from(self, proc):
|
|
1425
1420
|
acc = []
|
|
1426
1421
|
deadline = time.time() + GRADE_TIMEOUT_SEC
|
|
1427
1422
|
while True:
|
|
1428
1423
|
if time.time() > deadline:
|
|
1429
|
-
log("recv timeout
|
|
1430
|
-
self._spawn()
|
|
1424
|
+
log("recv timeout")
|
|
1431
1425
|
return ""
|
|
1432
|
-
line =
|
|
1426
|
+
line = proc.stdout.readline()
|
|
1433
1427
|
if not line:
|
|
1434
|
-
log("subprocess closed stdout; respawn")
|
|
1435
|
-
self._spawn()
|
|
1436
1428
|
return ""
|
|
1437
1429
|
try:
|
|
1438
1430
|
obj = json.loads(line)
|
|
@@ -1446,18 +1438,87 @@ class GraderDaemon:
|
|
|
1446
1438
|
elif t == "result":
|
|
1447
1439
|
return "".join(acc)
|
|
1448
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
|
+
|
|
1449
1508
|
def grade(self, prompt):
|
|
1450
1509
|
with self.lock:
|
|
1451
|
-
age = time.time() - self.start_time
|
|
1452
|
-
if self.calls >= ROTATION_CALLS or age >= ROTATION_AGE_SEC:
|
|
1453
|
-
log(f"rotating: calls={self.calls} age={age:.0f}s")
|
|
1454
|
-
self._spawn()
|
|
1455
1510
|
t0 = time.time()
|
|
1456
1511
|
self._send(prompt)
|
|
1457
1512
|
resp = self._recv()
|
|
1458
1513
|
elapsed = (time.time() - t0) * 1000
|
|
1459
1514
|
self.calls += 1
|
|
1460
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()
|
|
1461
1522
|
return resp
|
|
1462
1523
|
|
|
1463
1524
|
|
|
@@ -2083,7 +2144,7 @@ function writeConfigEnv(opts) {
|
|
|
2083
2144
|
`SYNKRO_GATEWAY_URL=${shellQuoteSingle(safeGateway)}`,
|
|
2084
2145
|
`SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle(credsPath)}`,
|
|
2085
2146
|
`SYNKRO_TIER=${shellQuoteSingle(safeTier)}`,
|
|
2086
|
-
`SYNKRO_VERSION=${shellQuoteSingle("1.1.
|
|
2147
|
+
`SYNKRO_VERSION=${shellQuoteSingle("1.1.7")}`
|
|
2087
2148
|
];
|
|
2088
2149
|
if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle(safeUserId)}`);
|
|
2089
2150
|
if (safeOrgId) lines.push(`SYNKRO_ORG_ID=${shellQuoteSingle(safeOrgId)}`);
|