@tiens.nguyen/gonext-local-worker 1.0.195 → 1.0.196

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
@@ -36,7 +36,9 @@ 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
38
  const yellow = (s) => `\x1b[33m${s}\x1b[0m`;
39
- const blue = (s) => `\x1b[34m${s}\x1b[0m`;
39
+ // ANSI 90 ("bright black") a reliable neutral grey across terminal themes, unlike
40
+ // code 34 (blue) which many dark-theme palettes render as purple/indigo instead.
41
+ const codeColor = (s) => `\x1b[90m${s}\x1b[0m`;
40
42
  const CLEAR_LINE = "\r\x1b[K"; // carriage-return + erase-to-end-of-line
41
43
 
42
44
  // The worker's "still thinking" heartbeat lines look like "Caffeinating… (90s)" — we
@@ -45,7 +47,7 @@ const CLEAR_LINE = "\r\x1b[K"; // carriage-return + erase-to-end-of-line
45
47
  const isHeartbeatLine = (line) => /…\s?\(\d+s\)\s*$/.test(line);
46
48
  const isFenceLine = (line) => line.trim() === "~~~";
47
49
  // The model's raw Python code blob is delimited by <code>/</code> lines. We don't print
48
- // the tag lines themselves, but the code IN BETWEEN prints in a distinct color (blue)
50
+ // the tag lines themselves, but the code IN BETWEEN prints in a distinct grey (codeColor)
49
51
  // instead of dim prose, so it visually separates from the "Thought:" reasoning.
50
52
  const isCodeOpenLine = (line) => line.trim() === "<code>";
51
53
  const isCodeCloseLine = (line) => line.trim() === "</code>";
@@ -127,16 +129,26 @@ if (!workerKey || !apiBase) {
127
129
  // readline stops managing the line/cursor itself; the OS's own canonical-mode terminal
128
130
  // still echoes keystrokes normally, and 'line' events fire exactly the same.
129
131
  const rl = createInterface({ input: process.stdin, output: process.stdout, terminal: false });
132
+ // Side effect of terminal:false above: readline no longer intercepts arrow keys for
133
+ // command-history recall, so pressing ↑/↓/→/← (or Home/End/Delete) sends the RAW escape
134
+ // bytes straight through as literal input — the OS's own canonical line discipline
135
+ // doesn't understand them either, so they get echoed and end up IN the submitted line
136
+ // once Enter is pressed (seen live: a task sent to the model started with a literal
137
+ // "\x1b[A" from an accidental up-arrow press). Strip CSI escape sequences (ESC '[' …
138
+ // final-byte) from every submitted line so stray arrow-key presses can't corrupt task
139
+ // text sent to the model.
140
+ const stripAnsiEscapes = (s) => String(s ?? "").replace(/\x1b\[[0-9;]*[A-Za-z~]/g, "");
130
141
  const _lineQueue = [];
131
142
  let _lineWaiter = null;
132
143
  let _stdinClosed = false;
133
144
  rl.on("line", (l) => {
145
+ const clean = stripAnsiEscapes(l);
134
146
  if (_lineWaiter) {
135
147
  const w = _lineWaiter;
136
148
  _lineWaiter = null;
137
- w(l);
149
+ w(clean);
138
150
  } else {
139
- _lineQueue.push(l);
151
+ _lineQueue.push(clean);
140
152
  }
141
153
  });
142
154
  rl.on("close", () => {
@@ -359,8 +371,8 @@ async function runAgentTurn(history) {
359
371
  // Split newly-arrived text into lines: heartbeats + ~~~ fences + the post-tool-call step
360
372
  // summary ("tool(...) | → result", redundant with the daemon's own terminal) are
361
373
  // swallowed; the model's <code>/</code> TAG lines are hidden but the code body between
362
- // them prints in blue (not dim, so it stands out from "Thought:" prose); blank lines
363
- // keep spacing; everything else commits dim to scrollback.
374
+ // them prints in a distinct grey (not dim, so it stands out from "Thought:" prose);
375
+ // blank lines keep spacing; everything else commits dim to scrollback.
364
376
  let inCode = false;
365
377
  let lastWasBlank = true; // suppresses a leading blank line and collapses repeats
366
378
  // Chars of the CURRENT trailing (newline-less) `carry` line already flushed live — lets
@@ -400,7 +412,7 @@ async function runAgentTurn(history) {
400
412
  if (isCodeCloseLine(line)) { inCode = false; lastContentAt = Date.now(); continue; }
401
413
  if (line.trim() === "") { lastContentAt = Date.now(); continue; } // skip blank lines in code
402
414
  onContent();
403
- process.stdout.write(blue(line) + "\n");
415
+ process.stdout.write(codeColor(line) + "\n");
404
416
  lastWasBlank = false;
405
417
  continue;
406
418
  }
@@ -297,6 +297,15 @@ _EMAIL_CONFIRM = re.compile(
297
297
  re.IGNORECASE,
298
298
  )
299
299
 
300
+ # A bare "continue"-style nudge — matched WHOLE-MESSAGE (anchored) so it only fires on a
301
+ # short, unambiguous continuation cue, never a longer sentence that happens to contain
302
+ # one of these words. See _continuation_pending().
303
+ _CONTINUE_CUE = re.compile(
304
+ r"^(please\s+)?(continue|keep\s+going|go\s+on|resume|carry\s+on|try\s+again)"
305
+ r"[\s.!?]*$",
306
+ re.IGNORECASE,
307
+ )
308
+
300
309
 
301
310
  def _email_allowed(addr, allow):
302
311
  """True if addr matches an allow-list entry — an exact address or a bare domain."""
@@ -2220,6 +2229,29 @@ def run_agent_chat(cfg):
2220
2229
  return bool(_EMAIL_AVAILABLE and _EMAIL_CONFIRM.search(latest_user_text or "")
2221
2230
  and _prior_email_preview())
2222
2231
 
2232
+ def _prior_turn_was_interrupted() -> bool:
2233
+ """True if the immediately preceding assistant turn is one of OUR OWN
2234
+ interrupted-investigation markers (the honest degrade-fallback note, or the
2235
+ max-steps 'ran out of steps' note) — i.e. a genuine tool-use attempt started but
2236
+ didn't finish, as opposed to a normal completed answer."""
2237
+ for m in reversed(messages[:last_user_idx]):
2238
+ if m.get("role") == "assistant":
2239
+ c = m.get("content") or ""
2240
+ return ("ran out of steps before finishing" in c
2241
+ or "couldn't finish the full investigation" in c)
2242
+ return False
2243
+
2244
+ def _continuation_pending() -> bool:
2245
+ """A bare 'continue'/'keep going' after an interrupted investigation must reach
2246
+ the AGENT, not plain chat — plain chat has zero tool access and can never
2247
+ actually continue a stalled grep_repo/read_file_lines investigation, which is
2248
+ exactly the reported bug: 'continue' silently degraded to a tool-less reply that
2249
+ could only apologize, never resume. Gated tightly (short continuation phrase +
2250
+ genuinely-interrupted prior turn) so it doesn't misfire on an unrelated 'continue'
2251
+ after a normal, already-complete answer."""
2252
+ return bool(_CONTINUE_CUE.match((latest_user_text or "").strip())
2253
+ and _prior_turn_was_interrupted())
2254
+
2223
2255
  # Route: does this task need HTTP tool use? A trivial greeting/thank-you/"who are you"
2224
2256
  # is caught LOCALLY (no model classifier call, no tool preamble) → straight to a fast
2225
2257
  # small-prompt reply. Otherwise ask the model classifier.
@@ -2236,6 +2268,13 @@ def run_agent_chat(cfg):
2236
2268
  _log("router → YES (email confirm pending)")
2237
2269
  _emit({"type": "step", "text": "→ Agent mode (email confirm)"})
2238
2270
  needs_agent = True
2271
+ # A bare "continue" after WE reported an interrupted investigation must reach the
2272
+ # agent too — plain chat has zero tool access, so it can only apologize again, never
2273
+ # actually resume the grep_repo/read_file_lines work (the reported bug).
2274
+ if not needs_agent and _continuation_pending():
2275
+ _log("router → YES (continuation of an interrupted investigation)")
2276
+ _emit({"type": "step", "text": "→ Agent mode (continuing)"})
2277
+ needs_agent = True
2239
2278
 
2240
2279
  if not needs_agent:
2241
2280
  _log("router: plain chat (no HTTP needed)")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiens.nguyen/gonext-local-worker",
3
- "version": "1.0.195",
3
+ "version": "1.0.196",
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",