@tiens.nguyen/gonext-local-worker 1.0.134 → 1.0.136
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/gonext-local-worker.mjs +8 -6
- package/gonext_agent_chat.py +59 -17
- package/package.json +1 -1
package/gonext-local-worker.mjs
CHANGED
|
@@ -1526,12 +1526,14 @@ async function runAgentChatJob(job) {
|
|
|
1526
1526
|
emailFrom: payload?.emailFrom ?? "",
|
|
1527
1527
|
emailAllowList: payload?.emailAllowList ?? "",
|
|
1528
1528
|
});
|
|
1529
|
-
//
|
|
1530
|
-
//
|
|
1531
|
-
// client's hard WS deadline (
|
|
1532
|
-
// instead of a generic WS timeout. On expiry the python child is
|
|
1533
|
-
// also shows up as a BrokenPipeError in the
|
|
1534
|
-
|
|
1529
|
+
// 10 min max for an agent run: 5 ReAct steps on a 14B with a cold prompt cache —
|
|
1530
|
+
// or a remote Ollama box cold-loading a big model — can exceed the old caps. Must
|
|
1531
|
+
// stay BELOW the web client's hard WS deadline (660s) so our clean timeout error
|
|
1532
|
+
// reaches the user instead of a generic WS timeout. On expiry the python child is
|
|
1533
|
+
// SIGKILLed, which also shows up as a BrokenPipeError in the model server log
|
|
1534
|
+
// (expected). Liveness during long model calls comes from the python heartbeat
|
|
1535
|
+
// (a keepalive step chunk every 45s), not from this cap.
|
|
1536
|
+
const timeoutMs = 600_000;
|
|
1535
1537
|
|
|
1536
1538
|
let inThink = false;
|
|
1537
1539
|
let finalText = "";
|
package/gonext_agent_chat.py
CHANGED
|
@@ -23,6 +23,7 @@ import contextlib
|
|
|
23
23
|
import json
|
|
24
24
|
import re
|
|
25
25
|
import sys
|
|
26
|
+
import threading
|
|
26
27
|
import time
|
|
27
28
|
import traceback
|
|
28
29
|
import urllib.request
|
|
@@ -388,9 +389,16 @@ def _detect_model_id(base_url, api_key=""):
|
|
|
388
389
|
return ""
|
|
389
390
|
data = payload.get("data") if isinstance(payload, dict) else None
|
|
390
391
|
if isinstance(data, list) and data:
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
392
|
+
ids = [d["id"].strip() for d in data
|
|
393
|
+
if isinstance(d, dict) and isinstance(d.get("id"), str) and d["id"].strip()]
|
|
394
|
+
if ids:
|
|
395
|
+
if len(ids) > 1:
|
|
396
|
+
# Ollama servers list every pulled model; "first" is arbitrary and may
|
|
397
|
+
# be a huge/slow one (seen live: picked gemma4:31b over qwen3:14b).
|
|
398
|
+
_log(f"model detect: server hosts {len(ids)} models {ids} — using "
|
|
399
|
+
f"{ids[0]!r}. Set the coding model NAME in Settings to choose "
|
|
400
|
+
"a specific one.")
|
|
401
|
+
return ids[0]
|
|
394
402
|
return ""
|
|
395
403
|
|
|
396
404
|
|
|
@@ -1750,18 +1758,38 @@ def run_agent_chat(cfg):
|
|
|
1750
1758
|
# A transient "Connection error" to the local MLX server (seen mid-loop when the
|
|
1751
1759
|
# context grows large) otherwise aborts the whole run — retry a couple of times
|
|
1752
1760
|
# with a short backoff before giving up.
|
|
1761
|
+
# Heartbeat: while a model call is in flight the agent emits NOTHING, and
|
|
1762
|
+
# the web's no-progress watchdog kills the chat. A big/cold model (e.g. a
|
|
1763
|
+
# 31B on a remote Ollama box) can take minutes on prompt-eval before the
|
|
1764
|
+
# first byte — emit a keepalive step every 45s until the call returns.
|
|
1765
|
+
hb_stop = threading.Event()
|
|
1766
|
+
|
|
1767
|
+
def _heartbeat():
|
|
1768
|
+
waited = 0
|
|
1769
|
+
while not hb_stop.wait(45):
|
|
1770
|
+
waited += 45
|
|
1771
|
+
_emit({"type": "step",
|
|
1772
|
+
"text": f"Model is still working… ({waited}s — a large or "
|
|
1773
|
+
"cold-loading model can take a few minutes)"})
|
|
1774
|
+
_log(f"heartbeat: model call in flight {waited}s")
|
|
1775
|
+
|
|
1776
|
+
hb = threading.Thread(target=_heartbeat, daemon=True)
|
|
1777
|
+
hb.start()
|
|
1753
1778
|
last_err = None
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1779
|
+
try:
|
|
1780
|
+
for attempt in range(3):
|
|
1781
|
+
try:
|
|
1782
|
+
msg = super().generate(*args, **kwargs)
|
|
1783
|
+
break
|
|
1784
|
+
except Exception as e: # noqa: BLE001
|
|
1785
|
+
last_err = e
|
|
1786
|
+
if attempt < 2:
|
|
1787
|
+
_log(f"generate transient error (attempt {attempt + 1}/3): {e} — retrying")
|
|
1788
|
+
time.sleep(1.5 * (attempt + 1))
|
|
1789
|
+
else:
|
|
1790
|
+
raise last_err
|
|
1791
|
+
finally:
|
|
1792
|
+
hb_stop.set()
|
|
1765
1793
|
try:
|
|
1766
1794
|
content = getattr(msg, "content", None)
|
|
1767
1795
|
if content is None:
|
|
@@ -1896,6 +1924,12 @@ def run_agent_chat(cfg):
|
|
|
1896
1924
|
model_id=coding_model_id,
|
|
1897
1925
|
api_base=coding_base_url,
|
|
1898
1926
|
api_key=agent_api_key,
|
|
1927
|
+
# Cap each HTTP attempt at 240s so one hung request (remote server
|
|
1928
|
+
# cold-loading a big model) fails into OUR retry loop instead of eating
|
|
1929
|
+
# the whole worker budget; by the retry the model is usually loaded.
|
|
1930
|
+
# max_retries=0: the openai client's own silent retries would otherwise
|
|
1931
|
+
# stack multiplicatively with ours (3 × 3 attempts).
|
|
1932
|
+
client_kwargs={"timeout": 240.0, "max_retries": 0},
|
|
1899
1933
|
)
|
|
1900
1934
|
agent_tools = [http_request, web_search, fetch_url, calculate,
|
|
1901
1935
|
get_current_datetime, create_pdf]
|
|
@@ -1945,10 +1979,18 @@ def _log(text: str):
|
|
|
1945
1979
|
_emit({"type": "log", "text": text})
|
|
1946
1980
|
|
|
1947
1981
|
|
|
1982
|
+
_EMIT_LOCK = threading.Lock()
|
|
1983
|
+
|
|
1984
|
+
|
|
1948
1985
|
def _emit(obj):
|
|
1949
|
-
"""Write one NDJSON line to the real stdout and flush immediately.
|
|
1950
|
-
|
|
1951
|
-
|
|
1986
|
+
"""Write one NDJSON line to the real stdout and flush immediately.
|
|
1987
|
+
|
|
1988
|
+
Lock-guarded: the model-call heartbeat thread emits concurrently with the
|
|
1989
|
+
main loop, and interleaved writes would corrupt the NDJSON stream.
|
|
1990
|
+
"""
|
|
1991
|
+
with _EMIT_LOCK:
|
|
1992
|
+
_REAL_STDOUT.write(json.dumps(obj) + "\n")
|
|
1993
|
+
_REAL_STDOUT.flush()
|
|
1952
1994
|
|
|
1953
1995
|
|
|
1954
1996
|
def main():
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tiens.nguyen/gonext-local-worker",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.136",
|
|
4
4
|
"description": "Polls GoNext cloud API for async local LLM jobs and runs them against Ollama/OpenAI-compatible servers on this Mac",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|