@tiens.nguyen/gonext-local-worker 1.0.179 → 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
  }
@@ -1481,10 +1481,16 @@ _WS_ACTIVE: str = ""
1481
1481
  def _rag_read_allowed(path: str) -> str:
1482
1482
  """Resolve `path` and ensure it stays within a safe root (worker cwd, temp dir,
1483
1483
  ~/.gonext, or a registered workspace) — so the agent's file tools can't read
1484
- 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"."""
1485
1487
  import os
1486
1488
  import tempfile
1487
- 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)
1488
1494
  roots = [
1489
1495
  os.path.realpath(os.getcwd()),
1490
1496
  os.path.realpath(tempfile.gettempdir()),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiens.nguyen/gonext-local-worker",
3
- "version": "1.0.179",
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",