@tiens.nguyen/gonext-local-worker 1.0.235 → 1.0.236

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.
@@ -2043,7 +2043,13 @@ async function runAgentChatJob(job) {
2043
2043
  method: "PATCH",
2044
2044
  body: JSON.stringify({
2045
2045
  jobStatus: "completed",
2046
- resultText: finalText || fullText,
2046
+ // When the answer streamed live (answer_stream), completed resultText must stay
2047
+ // an EXACT EXTENSION of the chunk accumulation: the REPL renders the final poll
2048
+ // by flushing resultText.slice(<chars already shown>), so replacing the stream
2049
+ // with the shorter bare answer here made short plain replies render cut
2050
+ // mid-word ("Hello! How"). Only when nothing streamed is the deduped finalText
2051
+ // the whole story.
2052
+ resultText: answerStreamed ? fullText : finalText || fullText,
2047
2053
  tokenCount: Math.max(1, Math.ceil((finalText || fullText).length / 4)),
2048
2054
  totalTimeSeconds,
2049
2055
  }),
package/gonext-repl.mjs CHANGED
@@ -994,6 +994,14 @@ async function runAgentTurn(history) {
994
994
  // (see carryFlushedLen below); the agent path's code/tool-call lines must stay buffered
995
995
  // until a full line is known, or we'd risk showing part of something we meant to hide.
996
996
  let plainReplyFlow = false;
997
+ // Exact answer characters printed live in the plain-reply flow — lets the completed
998
+ // poll figure out how much of the FINAL answer is still unshown even when resultText
999
+ // was replaced (not extended) on completion. See the completed-branch recovery below.
1000
+ let liveAnswer = "";
1001
+ // First few KB of the raw stream as consumed — a cheap fingerprint to verify the
1002
+ // completed resultText really EXTENDS what we streamed (an old worker replaces it
1003
+ // with the shorter bare answer, which must not be sliced by shownChars).
1004
+ let seenRaw = "";
997
1005
 
998
1006
  // --- Live in-progress ("blinking") bullet -------------------------------------------
999
1007
  // The agent's raw Thought/code stream is SUPPRESSED (see the ~~~-fence handling in
@@ -1235,6 +1243,7 @@ async function runAgentTurn(history) {
1235
1243
  // Plain-reply content IS the answer — print it in the default color and remember
1236
1244
  // it's visible so the caller doesn't reprint it once the turn completes.
1237
1245
  process.stdout.write(line.slice(alreadyFlushed) + "\n");
1246
+ liveAnswer += line.slice(alreadyFlushed) + "\n";
1238
1247
  answerShownLive = true;
1239
1248
  } else {
1240
1249
  // The remaining out-of-fence ACTION events ("Command passed → …", "Searching code
@@ -1251,6 +1260,7 @@ async function runAgentTurn(history) {
1251
1260
  if (plainReplyFlow && !inCode && carry.length > carryFlushedLen) {
1252
1261
  if (carryFlushedLen === 0) onContent();
1253
1262
  process.stdout.write(carry.slice(carryFlushedLen)); // default color — this IS the answer
1263
+ liveAnswer += carry.slice(carryFlushedLen);
1254
1264
  carryFlushedLen = carry.length;
1255
1265
  answerShownLive = true;
1256
1266
  lastWasBlank = false;
@@ -1323,14 +1333,38 @@ async function runAgentTurn(history) {
1323
1333
  // caller (shown=false), so a live flush there is unnecessary (and would compound
1324
1334
  // the separate agent double-print). This runs for the completed poll only; the
1325
1335
  // running-branch consume below handles every earlier poll.
1326
- if (plainReplyFlow && text.length > shownChars) {
1336
+ //
1337
+ // The suffix math is only valid when the completed resultText EXTENDS the
1338
+ // streamed accumulation. An older worker REPLACES it with the bare deduped
1339
+ // answer — a shorter, different string — so slicing by shownChars either shows
1340
+ // nothing (the reported "Hello! How" cut) or a garbage tail. Verify with the
1341
+ // seenRaw fingerprint; on a mismatch, recover by answer-prefix instead: print
1342
+ // whatever of the final answer wasn't already shown live.
1343
+ const extendsStream = text.length >= shownChars && text.startsWith(seenRaw);
1344
+ if (plainReplyFlow && extendsStream && text.length > shownChars) {
1327
1345
  consume(text.slice(shownChars));
1328
1346
  shownChars = text.length;
1347
+ } else if (plainReplyFlow && !extendsStream && answerShownLive) {
1348
+ const ans = answerFrom(text);
1349
+ let rest = ans.startsWith(liveAnswer) ? ans.slice(liveAnswer.length) : null;
1350
+ if (rest === null) {
1351
+ // The live text may carry a trailing newline the trimmed answer lacks.
1352
+ const printed = liveAnswer.replace(/\s+$/, "");
1353
+ rest = ans.startsWith(printed) ? ans.slice(printed.length) : null;
1354
+ }
1355
+ if (rest) {
1356
+ onContent();
1357
+ process.stdout.write(rest);
1358
+ lastWasBlank = false;
1359
+ }
1329
1360
  }
1330
1361
  clearStatus();
1331
1362
  return { text: answerFrom(text), shown: answerShownLive };
1332
1363
  }
1333
1364
  if (text.length > shownChars) {
1365
+ if (seenRaw.length < 4096) {
1366
+ seenRaw = (seenRaw + text.slice(shownChars)).slice(0, 4096);
1367
+ }
1334
1368
  consume(text.slice(shownChars));
1335
1369
  shownChars = text.length;
1336
1370
  }
@@ -1470,6 +1504,11 @@ async function main() {
1470
1504
  process.on("SIGINT", onInterrupt);
1471
1505
  rl.on("SIGINT", onInterrupt);
1472
1506
 
1507
+ // The task from the most recent Ctrl+C-cancelled turn — so a bare "continue" right after
1508
+ // resumes it. A cancelled turn is popped from history (nothing to continue otherwise), so
1509
+ // without this "continue" reaches the agent with no task and it replies as generic chat.
1510
+ let lastCancelledTask = "";
1511
+
1473
1512
  for (;;) {
1474
1513
  rl.prompt();
1475
1514
  // Absorb blank Enter-presses SILENTLY under the same prompt — without this, lines
@@ -1536,7 +1575,22 @@ async function main() {
1536
1575
  console.log(dim(`unknown command: ${cmd} — type /help (or / then Tab) to list commands\n`));
1537
1576
  continue;
1538
1577
  }
1539
- history.push({ role: "user", content: line });
1578
+ // Resume a cancelled task: a BARE "continue" right after a Ctrl+C re-runs the ORIGINAL
1579
+ // request. Only the immediately-next input may resume (consumed here) and only a bare
1580
+ // continue-word — a message with real instructions is a new task. Slash commands above
1581
+ // used `continue` and never reach here, so `/model` then "continue" still resumes.
1582
+ const pendingResume = lastCancelledTask;
1583
+ lastCancelledTask = "";
1584
+ let taskLine = line;
1585
+ if (
1586
+ pendingResume &&
1587
+ /^(continue|resume|go on|keep going|carry on|proceed)\.?$/i.test(line)
1588
+ ) {
1589
+ taskLine = pendingResume;
1590
+ const shown = pendingResume.length > 80 ? pendingResume.slice(0, 80) + "…" : pendingResume;
1591
+ console.log(dim(`↻ resuming the cancelled task: ${shown}\n`));
1592
+ }
1593
+ history.push({ role: "user", content: taskLine });
1540
1594
  try {
1541
1595
  const { text: answer, shown, cancelled } = await runAgentTurn(history);
1542
1596
  if (answer) {
@@ -1552,7 +1606,12 @@ async function main() {
1552
1606
  history.pop(); // aborted/failed/cancelled turn — don't poison the next request
1553
1607
  // Ctrl+C cancel already printed its own "(cancelled)" note in runAgentTurn — a
1554
1608
  // second "(no answer)" line here would be redundant/misleading.
1555
- if (!cancelled) {
1609
+ if (cancelled) {
1610
+ // Remember the ACTUAL task that ran (taskLine, not a bare "continue") so the
1611
+ // next bare "continue" resumes it — even if it was cancelled during step 1,
1612
+ // before any worker-side checkpoint step was written.
1613
+ lastCancelledTask = taskLine;
1614
+ } else {
1556
1615
  console.log(red("\n(no answer — run aborted or failed)\n"));
1557
1616
  }
1558
1617
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiens.nguyen/gonext-local-worker",
3
- "version": "1.0.235",
3
+ "version": "1.0.236",
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",