@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 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
- // Don't stream the final delta dim the answer prints bright right after.
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
- process.stdout.write(dim(stripThinkTags(text.slice(shownChars))));
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
  }
@@ -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
- rp = os.path.realpath(path)
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 (the `gonext` REPL's launch folder) download_file /
1786
- # unzip_file put their output here. Only honoured if it's a registered workspace
1787
- # root, so it can't redirect writes to an arbitrary folder.
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
- try:
1790
- import os as _os
1791
- aw = _os.path.realpath(_os.path.expanduser(str(cfg.get("activeWorkspace") or "")))
1792
- if aw and _os.path.isdir(aw) and any(
1793
- aw == w["path"] or aw.startswith(w["path"] + _os.sep) for w in _WS_ROOTS
1794
- ):
1795
- _WS_ACTIVE = aw
1796
- _log(f"active workspace: {aw} (download/unzip output → here)")
1797
- except Exception: # noqa: BLE001
1798
- _WS_ACTIVE = ""
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"Model is still working… ({waited}s — a large or "
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.178",
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