@tiens.nguyen/gonext-local-worker 1.0.312 → 1.0.314

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
@@ -948,6 +948,11 @@ let cancelRequested = false; // a cancel POST has been sent for the current turn
948
948
  let approvalActive = false;
949
949
  let approvalSel = 0; // 0 = Yes (default highlighted), 1 = No
950
950
  let approvalResolve = null;
951
+ // The active turn's live-status wiper (its local clearStatus), or null between turns. A
952
+ // picker calls this before drawing so the "Almost done…/thought/token" block is gone
953
+ // first — otherwise the 120ms ticker keeps redrawing that block on top of the picker and
954
+ // its relative-cursor math lands on the wrong rows (duplicated/interleaved options).
955
+ let clearActiveStatus = null;
951
956
  function drawApprovalOptions(redraw) {
952
957
  // Two option lines, redrawn in place. On redraw the cursor sits one row below "No";
953
958
  // step up 2 rows, clear+rewrite both, landing back where we started.
@@ -986,6 +991,10 @@ const MAXSTEP_PREFIX = "__MAXSTEP__::";
986
991
  const TIMEOUT_PREFIX = "__TIMEOUT__::";
987
992
  const COMPACT_PREFIX = "__COMPACT__::";
988
993
  async function approvalPrompt(command) {
994
+ // Wipe the live "thinking" status block BEFORE any picker draws, and stop the ticker from
995
+ // redrawing it (the tick early-returns while a picker is active) — otherwise the block
996
+ // reappears under the options and corrupts the layout.
997
+ if (clearActiveStatus) clearActiveStatus();
989
998
  // Context-compaction prompt (task #87 rec#3): the running context got large. Ask whether
990
999
  // to aggressively compact older steps. No answer in 30s = No (keep full detail — the safe
991
1000
  // default; the always-on auto-trim still bounds things).
@@ -1463,10 +1472,11 @@ async function runAgentTurn(history) {
1463
1472
  process.stdout.write(seq);
1464
1473
  statusShown = false;
1465
1474
  };
1475
+ clearActiveStatus = clearStatus; // let pickers wipe this turn's status block before they draw
1466
1476
 
1467
1477
  const tick = () => {
1468
1478
  if (!following || followAborted || !jobRunning) return;
1469
- if (approvalActive) return; // a Yes/No picker owns the screen — don't draw over it
1479
+ if (approvalActive || listPickerActive) return; // a picker owns the screen — don't draw over it
1470
1480
  if (Date.now() - lastContentAt < QUIET_MS) return; // tokens still flowing
1471
1481
  // A plain-reply answer streams onto a line with NO trailing newline until it's fully
1472
1482
  // done — overwriting the ticker there corrupts the visible answer mid-word. Stay
@@ -2009,6 +2019,7 @@ async function runAgentTurn(history) {
2009
2019
  } finally {
2010
2020
  clearInterval(ticker);
2011
2021
  clearStatus();
2022
+ clearActiveStatus = null; // no turn owns the status wiper between turns
2012
2023
  // Task #96: stop mouse-reporting the moment the turn ends (also on cancel/error — this
2013
2024
  // finally always runs), and detach the click parser, so the user's shell selection is
2014
2025
  // never captured between turns.
@@ -4960,6 +4960,7 @@ def run_agent_chat(cfg):
4960
4960
  _log("streamed generate: request issued (stream=True), awaiting first token…")
4961
4961
  stream = self.client.chat.completions.create(**completion_kwargs)
4962
4962
  parts = []
4963
+ rparts = [] # reasoning-channel text (task #111: may hold the <code> block)
4963
4964
  role = "assistant"
4964
4965
  in_tok = out_tok = reasoning_len = 0
4965
4966
  first_token_at = None
@@ -5028,6 +5029,7 @@ def run_agent_chat(cfg):
5028
5029
  if rpiece:
5029
5030
  _mark_first()
5030
5031
  reasoning_len += len(rpiece)
5032
+ rparts.append(rpiece)
5031
5033
  _stream_buf.append(rpiece)
5032
5034
  _stream_chars += len(rpiece)
5033
5035
  _emit({"type": "stream", "text": rpiece})
@@ -5055,10 +5057,63 @@ def run_agent_chat(cfg):
5055
5057
  _emit({"type": "step", "text": "Model output ran away — restarting this step…"})
5056
5058
  _log(f"streamed generate: discarded {len(content)} runaway chars → empty content")
5057
5059
  return "", role, in_tok, out_tok, reasoning_len
5060
+ # Task #111: reasoning models on an OpenAI-compatible API (e.g. Kimi K3 @ Moonshot)
5061
+ # can emit the actionable <code>…</code> into the REASONING channel, leaving only
5062
+ # prose + a stray tag in `content` → smolagents' parser can't find the pair. When
5063
+ # content has no usable code block but the reasoning stream does, rebuild content
5064
+ # from it. Inert when reasoning is empty (Ollama with thinking off) → path untouched.
5065
+ content = self._recover_code_from_channels(content, "".join(rparts))
5058
5066
  _log(f"streamed generate assembled {len(content)} chars "
5059
5067
  f"(in={in_tok} out={out_tok} tokens, reasoning={reasoning_len} chars)")
5060
5068
  return content, role, in_tok, out_tok, reasoning_len
5061
5069
 
5070
+ def _recover_code_from_channels(self, content, reasoning):
5071
+ """Recover a <code>…</code> tool call that a reasoning model placed in the
5072
+ REASONING channel instead of the visible message (task #111). No-op when the
5073
+ visible content already has a usable code block, or when there is no reasoning
5074
+ text (e.g. Ollama with reasoning_effort='none') — so the Ollama path is unchanged.
5075
+
5076
+ Kimi K3 @ Moonshot splits its output two ways when it fails: (a) the full
5077
+ <code>…</code> block lands in reasoning and only prose leaks to content; or
5078
+ (b) the OPENER + code land in reasoning while the CLOSING </code> leaks into
5079
+ content (seen live: content = 'Thought: …contains.</code>'). Handle both."""
5080
+ _pair = r"<code[^>]*>[\s\S]*?</code>"
5081
+ if re.search(_pair, content or "", re.I):
5082
+ return content # content already has a usable block — never override it
5083
+ reasoning = reasoning or ""
5084
+ if not reasoning.strip():
5085
+ return content # nothing to recover from (Ollama / non-reasoning model)
5086
+ code = None
5087
+ # (a) Prefer a COMPLETE block already inside reasoning (the model drafts the
5088
+ # whole tool call there) — take the LAST one, ignoring any stray </code> in content.
5089
+ last = None
5090
+ for last in re.finditer(r"<code[^>]*>([\s\S]*?)</code>", reasoning, re.I):
5091
+ pass
5092
+ if last:
5093
+ code = last.group(1)
5094
+ else:
5095
+ # (b) Opener + code in reasoning, closer leaked to content → take everything
5096
+ # after the LAST opener in reasoning (its </code> is the one now in content).
5097
+ m = None
5098
+ for m in re.finditer(r"<code[^>]*>([\s\S]*)$", reasoning, re.I):
5099
+ pass
5100
+ if m:
5101
+ code = m.group(1)
5102
+ if code is None:
5103
+ return content
5104
+ # A leaked 'Thought:' line (or a stray </code>) can ride along after the code —
5105
+ # cut it so only the executable snippet remains.
5106
+ code = re.split(r"\n\s*Thought\s*:\s", code, maxsplit=1)[0]
5107
+ code = re.sub(r"</?code[^>]*>", "", code, flags=re.I).strip()
5108
+ if not code:
5109
+ return content
5110
+ # Keep the visible Thought prose (strip any stray/unbalanced code tags), then
5111
+ # append the recovered block so smolagents parses a clean Thought → <code> pair.
5112
+ prose = re.sub(r"</?code[^>]*>", "", content or "", flags=re.I).strip()
5113
+ rebuilt = (prose + "\n" if prose else "") + "<code>\n" + code + "\n</code>"
5114
+ _log(f"#111 recovered <code> block from reasoning channel ({len(code)} chars)")
5115
+ return rebuilt
5116
+
5062
5117
  # Injected when a turn produced ONLY reasoning and no message content — a hard,
5063
5118
  # model-agnostic steer to emit the actionable code block instead of more analysis.
5064
5119
  _CODE_NOW_DIRECTIVE = (
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiens.nguyen/gonext-local-worker",
3
- "version": "1.0.312",
3
+ "version": "1.0.314",
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",