@tiens.nguyen/gonext-local-worker 1.0.178 → 1.0.180
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-repl.mjs +47 -2
- package/gonext_agent_chat.py +67 -17
- package/package.json +2 -1
- package/thinking_words.txt +106 -0
package/gonext-repl.mjs
CHANGED
|
@@ -35,6 +35,14 @@ const dim = (s) => `\x1b[2m${s}\x1b[0m`;
|
|
|
35
35
|
const cyan = (s) => `\x1b[36m${s}\x1b[0m`;
|
|
36
36
|
const green = (s) => `\x1b[32m${s}\x1b[0m`;
|
|
37
37
|
const red = (s) => `\x1b[31m${s}\x1b[0m`;
|
|
38
|
+
const yellow = (s) => `\x1b[33m${s}\x1b[0m`;
|
|
39
|
+
const CLEAR_LINE = "\r\x1b[K"; // carriage-return + erase-to-end-of-line
|
|
40
|
+
|
|
41
|
+
// The worker's playful "still thinking" heartbeat lines look like "Caffeinating… (90s)"
|
|
42
|
+
// (a word/phrase, an ellipsis, then the elapsed seconds). They're ephemeral progress —
|
|
43
|
+
// we render them on a single in-place status line (yellow) that the next one overwrites
|
|
44
|
+
// and that gets erased once real output arrives, so they never pile up in the scrollback.
|
|
45
|
+
const isHeartbeatLine = (line) => /…\s?\(\d+s\)\s*$/.test(line);
|
|
38
46
|
|
|
39
47
|
// ---------- flags ----------
|
|
40
48
|
const argv = process.argv.slice(2);
|
|
@@ -219,10 +227,42 @@ async function runAgentTurn(history) {
|
|
|
219
227
|
followAborted = false;
|
|
220
228
|
const startedAt = Date.now();
|
|
221
229
|
let shownChars = 0;
|
|
230
|
+
let carry = ""; // trailing partial line held until its newline arrives
|
|
231
|
+
let statusShown = false; // a transient heartbeat status line is on screen
|
|
222
232
|
let warnedPending = false;
|
|
233
|
+
|
|
234
|
+
// Erase the transient heartbeat status line (if any) before printing real output.
|
|
235
|
+
const clearStatus = () => {
|
|
236
|
+
if (statusShown) {
|
|
237
|
+
process.stdout.write(CLEAR_LINE);
|
|
238
|
+
statusShown = false;
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
// Feed newly-arrived stream text through the line splitter: heartbeat lines become an
|
|
242
|
+
// in-place status line (never committed to scrollback); everything else commits dim.
|
|
243
|
+
const consume = (chunk) => {
|
|
244
|
+
carry += stripThinkTags(chunk);
|
|
245
|
+
let nl;
|
|
246
|
+
while ((nl = carry.indexOf("\n")) >= 0) {
|
|
247
|
+
const line = carry.slice(0, nl);
|
|
248
|
+
carry = carry.slice(nl + 1);
|
|
249
|
+
if (isHeartbeatLine(line)) {
|
|
250
|
+
// Overwrite the previous heartbeat in place — no newline, so it stays transient.
|
|
251
|
+
process.stdout.write(CLEAR_LINE + yellow(line));
|
|
252
|
+
statusShown = true;
|
|
253
|
+
} else if (line.trim() === "") {
|
|
254
|
+
// Swallow the blank line the worker appends after a heartbeat; keep real spacing.
|
|
255
|
+
if (!statusShown) process.stdout.write("\n");
|
|
256
|
+
} else {
|
|
257
|
+
clearStatus();
|
|
258
|
+
process.stdout.write(dim(line) + "\n");
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
};
|
|
223
262
|
try {
|
|
224
263
|
for (;;) {
|
|
225
264
|
if (followAborted) {
|
|
265
|
+
clearStatus();
|
|
226
266
|
process.stdout.write(
|
|
227
267
|
dim("\n(stopped following — the job keeps running on the worker)\n")
|
|
228
268
|
);
|
|
@@ -235,14 +275,17 @@ async function runAgentTurn(history) {
|
|
|
235
275
|
if (!jr.ok) throw new Error(job?.error || `job poll failed (HTTP ${jr.status})`);
|
|
236
276
|
const text = typeof job.resultText === "string" ? job.resultText : "";
|
|
237
277
|
if (job.jobStatus === "completed") {
|
|
238
|
-
//
|
|
278
|
+
// Drop the transient heartbeat line and any partial reasoning tail; the bright
|
|
279
|
+
// answer prints right after via the caller.
|
|
280
|
+
clearStatus();
|
|
239
281
|
return answerFrom(text);
|
|
240
282
|
}
|
|
241
283
|
if (text.length > shownChars) {
|
|
242
|
-
|
|
284
|
+
consume(text.slice(shownChars));
|
|
243
285
|
shownChars = text.length;
|
|
244
286
|
}
|
|
245
287
|
if (job.jobStatus === "failed" || job.jobStatus === "cancelled") {
|
|
288
|
+
clearStatus();
|
|
246
289
|
throw new Error(job.errorMessage || `job ${job.jobStatus}`);
|
|
247
290
|
}
|
|
248
291
|
if (
|
|
@@ -251,6 +294,7 @@ async function runAgentTurn(history) {
|
|
|
251
294
|
Date.now() - startedAt > 6000
|
|
252
295
|
) {
|
|
253
296
|
warnedPending = true;
|
|
297
|
+
clearStatus();
|
|
254
298
|
process.stdout.write(
|
|
255
299
|
red("\n⚠ no worker has claimed the job — is `gonext-local-worker` running?\n")
|
|
256
300
|
);
|
|
@@ -258,6 +302,7 @@ async function runAgentTurn(history) {
|
|
|
258
302
|
await sleep(700);
|
|
259
303
|
}
|
|
260
304
|
} finally {
|
|
305
|
+
clearStatus();
|
|
261
306
|
following = false;
|
|
262
307
|
}
|
|
263
308
|
}
|
package/gonext_agent_chat.py
CHANGED
|
@@ -37,6 +37,33 @@ import urllib.error
|
|
|
37
37
|
_REAL_STDOUT = sys.stdout
|
|
38
38
|
|
|
39
39
|
|
|
40
|
+
# Playful "still thinking" status words shown on the heartbeat while a model call is
|
|
41
|
+
# in flight (see _heartbeat). Loaded once from thinking_words.txt next to this file;
|
|
42
|
+
# a random one is picked per tick so the wait feels alive instead of a fixed string.
|
|
43
|
+
_THINKING_WORDS: list = []
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _thinking_word() -> str:
|
|
47
|
+
"""A random playful status word (e.g. 'Caffeinating'). Falls back to a small
|
|
48
|
+
built-in list if thinking_words.txt is missing or empty."""
|
|
49
|
+
import os
|
|
50
|
+
import random
|
|
51
|
+
global _THINKING_WORDS
|
|
52
|
+
if not _THINKING_WORDS:
|
|
53
|
+
try:
|
|
54
|
+
path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "thinking_words.txt")
|
|
55
|
+
with open(path, "r", encoding="utf-8") as fh:
|
|
56
|
+
_THINKING_WORDS = [
|
|
57
|
+
line.strip() for line in fh
|
|
58
|
+
if line.strip() and not line.lstrip().startswith("#")
|
|
59
|
+
]
|
|
60
|
+
except OSError:
|
|
61
|
+
_THINKING_WORDS = []
|
|
62
|
+
if not _THINKING_WORDS:
|
|
63
|
+
_THINKING_WORDS = ["Thinking", "Percolating", "Caffeinating", "Cogitating"]
|
|
64
|
+
return random.choice(_THINKING_WORDS)
|
|
65
|
+
|
|
66
|
+
|
|
40
67
|
def _ssl_context():
|
|
41
68
|
import ssl
|
|
42
69
|
# Disable cert verification — this agent runs locally against dev tunnels
|
|
@@ -1454,15 +1481,25 @@ _WS_ACTIVE: str = ""
|
|
|
1454
1481
|
def _rag_read_allowed(path: str) -> str:
|
|
1455
1482
|
"""Resolve `path` and ensure it stays within a safe root (worker cwd, temp dir,
|
|
1456
1483
|
~/.gonext, or a registered workspace) — so the agent's file tools can't read
|
|
1457
|
-
arbitrary files like ~/.ssh.
|
|
1484
|
+
arbitrary files like ~/.ssh. Relative paths anchor to the terminal workspace (where
|
|
1485
|
+
downloads/unzips land), NOT the worker's process cwd (the home dir), because the
|
|
1486
|
+
model routinely passes bare names like "tools-hook" or "tools-hook/README.md"."""
|
|
1458
1487
|
import os
|
|
1459
1488
|
import tempfile
|
|
1460
|
-
|
|
1489
|
+
expanded = os.path.expanduser((path or "").strip())
|
|
1490
|
+
if not os.path.isabs(expanded):
|
|
1491
|
+
base = _WS_ACTIVE or (_WS_ROOTS[0]["path"] if _WS_ROOTS else os.getcwd())
|
|
1492
|
+
expanded = os.path.join(base, expanded)
|
|
1493
|
+
rp = os.path.realpath(expanded)
|
|
1461
1494
|
roots = [
|
|
1462
1495
|
os.path.realpath(os.getcwd()),
|
|
1463
1496
|
os.path.realpath(tempfile.gettempdir()),
|
|
1464
1497
|
os.path.realpath(os.path.join(os.path.expanduser("~"), ".gonext")),
|
|
1465
1498
|
] + [w["path"] for w in _WS_ROOTS]
|
|
1499
|
+
# The active terminal folder (download/unzip target) — allow reading what we just
|
|
1500
|
+
# extracted there, even when it isn't a registered workspace.
|
|
1501
|
+
if _WS_ACTIVE:
|
|
1502
|
+
roots.append(_WS_ACTIVE)
|
|
1466
1503
|
if any(rp == r or rp.startswith(r + os.sep) for r in roots):
|
|
1467
1504
|
return rp
|
|
1468
1505
|
raise ValueError(
|
|
@@ -1782,20 +1819,31 @@ def run_agent_chat(cfg):
|
|
|
1782
1819
|
except Exception: # noqa: BLE001
|
|
1783
1820
|
continue
|
|
1784
1821
|
_WS_AVAILABLE = bool(_WS_ROOTS)
|
|
1785
|
-
# Active terminal workspace
|
|
1786
|
-
#
|
|
1787
|
-
#
|
|
1822
|
+
# Active terminal workspace: the folder the `gonext` REPL was launched from (sent as
|
|
1823
|
+
# payload.activeWorkspace = the terminal's cwd). download_file / unzip_file put their
|
|
1824
|
+
# output HERE instead of the shared ~/.gonext/rag-work cache, so files land where the
|
|
1825
|
+
# user opened the terminal. Honoured for any real directory the user explicitly opened
|
|
1826
|
+
# the terminal in — these tools already write outside registered workspaces (to
|
|
1827
|
+
# ~/.gonext), so this is not a new write boundary; the code-EDIT tools stay gated on
|
|
1828
|
+
# _WS_ROOTS (registered workspaces) via _ws_write_allowed, unchanged.
|
|
1788
1829
|
_WS_ACTIVE = ""
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
aw
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1830
|
+
_aw_raw = str(cfg.get("activeWorkspace") or "").strip()
|
|
1831
|
+
if _aw_raw:
|
|
1832
|
+
try:
|
|
1833
|
+
import os as _os
|
|
1834
|
+
aw = _os.path.realpath(_os.path.expanduser(_aw_raw))
|
|
1835
|
+
if _os.path.isdir(aw):
|
|
1836
|
+
_WS_ACTIVE = aw
|
|
1837
|
+
registered = any(
|
|
1838
|
+
aw == w["path"] or aw.startswith(w["path"] + _os.sep) for w in _WS_ROOTS
|
|
1839
|
+
)
|
|
1840
|
+
_log(f"active workspace: {aw} (download/unzip output → here"
|
|
1841
|
+
f"{'' if registered else '; not a registered workspace — reads/writes here limited to download/unzip'})")
|
|
1842
|
+
else:
|
|
1843
|
+
_log(f"active workspace ignored (not a directory): {aw}")
|
|
1844
|
+
except Exception as _e: # noqa: BLE001
|
|
1845
|
+
_WS_ACTIVE = ""
|
|
1846
|
+
_log(f"active workspace ignored (error resolving '{_aw_raw}'): {_e}")
|
|
1799
1847
|
if _WS_AVAILABLE:
|
|
1800
1848
|
_log(f"workspaces: {[(w['name'], w['path'], w['allowRun']) for w in _WS_ROOTS]}")
|
|
1801
1849
|
# Coding tasks are edit→test→fix loops — 5 steps is too tight. Bump the default
|
|
@@ -2766,9 +2814,11 @@ def run_agent_chat(cfg):
|
|
|
2766
2814
|
waited = 0
|
|
2767
2815
|
while not hb_stop.wait(45):
|
|
2768
2816
|
waited += 45
|
|
2817
|
+
# A random playful word each tick (e.g. "Caffeinating… (90s)") instead
|
|
2818
|
+
# of a fixed "still working" line — a large or cold-loading model can
|
|
2819
|
+
# legitimately take a few minutes on prompt-eval.
|
|
2769
2820
|
_emit({"type": "step",
|
|
2770
|
-
"text": f"
|
|
2771
|
-
"cold-loading model can take a few minutes)"})
|
|
2821
|
+
"text": f"{_thinking_word()}… ({waited}s)"})
|
|
2772
2822
|
_log(f"heartbeat: model call in flight {waited}s")
|
|
2773
2823
|
|
|
2774
2824
|
hb = threading.Thread(target=_heartbeat, daemon=True)
|
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.180",
|
|
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",
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
"gonext_agent_chat.py",
|
|
29
29
|
"gonext_transcribe.py",
|
|
30
30
|
"gonext_mlx_embed.py",
|
|
31
|
+
"thinking_words.txt",
|
|
31
32
|
"README.md",
|
|
32
33
|
"launchd/"
|
|
33
34
|
],
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# Playful "still thinking / still working" status words for the agent heartbeat.
|
|
2
|
+
# One word/phrase per line; blank lines and lines starting with # are ignored.
|
|
3
|
+
# The worker appends "…" and the elapsed seconds, e.g. "Caffeinating… (90s)".
|
|
4
|
+
Collating
|
|
5
|
+
Caffeinating
|
|
6
|
+
Percolating
|
|
7
|
+
Marinating
|
|
8
|
+
Ruminating
|
|
9
|
+
Cogitating
|
|
10
|
+
Noodling
|
|
11
|
+
Pondering
|
|
12
|
+
Contemplating
|
|
13
|
+
Mulling
|
|
14
|
+
Brewing
|
|
15
|
+
Simmering
|
|
16
|
+
Steeping
|
|
17
|
+
Distilling
|
|
18
|
+
Fermenting
|
|
19
|
+
Incubating
|
|
20
|
+
Marshalling
|
|
21
|
+
Wrangling
|
|
22
|
+
Herding electrons
|
|
23
|
+
Untangling
|
|
24
|
+
Unknotting
|
|
25
|
+
Defragmenting
|
|
26
|
+
Reticulating splines
|
|
27
|
+
Compiling thoughts
|
|
28
|
+
Buffering brilliance
|
|
29
|
+
Warming up the neurons
|
|
30
|
+
Spinning up
|
|
31
|
+
Booting the big brain
|
|
32
|
+
Consulting the oracle
|
|
33
|
+
Summoning tokens
|
|
34
|
+
Conjuring
|
|
35
|
+
Divining
|
|
36
|
+
Calibrating
|
|
37
|
+
Recalibrating
|
|
38
|
+
Triangulating
|
|
39
|
+
Extrapolating
|
|
40
|
+
Interpolating
|
|
41
|
+
Synthesizing
|
|
42
|
+
Crystallizing
|
|
43
|
+
Coalescing
|
|
44
|
+
Assembling
|
|
45
|
+
Stitching
|
|
46
|
+
Weaving
|
|
47
|
+
Knitting
|
|
48
|
+
Threading the needle
|
|
49
|
+
Connecting the dots
|
|
50
|
+
Chasing the thread
|
|
51
|
+
Following the breadcrumbs
|
|
52
|
+
Sifting
|
|
53
|
+
Sieving
|
|
54
|
+
Panning for gold
|
|
55
|
+
Mining
|
|
56
|
+
Excavating
|
|
57
|
+
Spelunking
|
|
58
|
+
Deep-diving
|
|
59
|
+
Surfacing insights
|
|
60
|
+
Percolating harder
|
|
61
|
+
Steeping the tea
|
|
62
|
+
Kneading the dough
|
|
63
|
+
Proofing the loaf
|
|
64
|
+
Letting it rise
|
|
65
|
+
Whisking
|
|
66
|
+
Folding in the facts
|
|
67
|
+
Reducing the sauce
|
|
68
|
+
Plating up
|
|
69
|
+
Garnishing
|
|
70
|
+
Sharpening the pencils
|
|
71
|
+
Oiling the gears
|
|
72
|
+
Greasing the cogs
|
|
73
|
+
Winding the springs
|
|
74
|
+
Charging the flux capacitor
|
|
75
|
+
Aligning the stars
|
|
76
|
+
Consulting the tea leaves
|
|
77
|
+
Reading the room
|
|
78
|
+
Gathering wool
|
|
79
|
+
Chewing it over
|
|
80
|
+
Sleeping on it
|
|
81
|
+
Doing the math
|
|
82
|
+
Carrying the one
|
|
83
|
+
Crunching numbers
|
|
84
|
+
Tallying
|
|
85
|
+
Counting sheep
|
|
86
|
+
Herding cats
|
|
87
|
+
Nudging pixels
|
|
88
|
+
Polishing
|
|
89
|
+
Buffing
|
|
90
|
+
Tuning the antenna
|
|
91
|
+
Finding the signal
|
|
92
|
+
Clearing the static
|
|
93
|
+
Untwisting
|
|
94
|
+
Loosening the knot
|
|
95
|
+
Priming the pump
|
|
96
|
+
Stoking the furnace
|
|
97
|
+
Kindling
|
|
98
|
+
Fanning the embers
|
|
99
|
+
Gathering steam
|
|
100
|
+
Building momentum
|
|
101
|
+
Hatching a plan
|
|
102
|
+
Plotting a course
|
|
103
|
+
Charting the map
|
|
104
|
+
Threading the maze
|
|
105
|
+
Almost there
|
|
106
|
+
Nearly cooked
|