@noobdemon/noob-cli 1.9.1 → 1.9.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@noobdemon/noob-cli",
3
- "version": "1.9.1",
3
+ "version": "1.9.3",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
package/src/agent.js CHANGED
@@ -43,6 +43,7 @@ Context is finite. Don't slurp the whole repo up front. Discover information pro
43
43
  • A server/service the USER wants to keep using (they asked to "run the app/server") → leave it running, and tell the user it is up: its id and the URL/port, and that they can ask you to stop it when done (you will kill_bg it). It also stops automatically when noob exits.
44
44
  • If bg_output shows it exited with a non-zero code or an error, treat it like a failed command: read the output and fix, don't silently move on.
45
45
  - Keep prose tight. Explain what you did and why, not how to use a tool.
46
+ - Do NOT call the same tool with the same input repeatedly. The runtime detects loops and will force a step change. If you need to re-read a file, use the data already in your history.
46
47
  - JSON in the tool block must be valid: escape newlines as \\n inside string values.
47
48
  - LANGUAGE: Always write your prose answers to the user in Vietnamese (tiếng Việt), unless the user explicitly writes in another language. Keep code, file paths, commands, and tool JSON unchanged.
48
49
 
@@ -77,6 +78,11 @@ Follow this pattern exactly. Your very first response to a task that needs the f
77
78
  // Số bước tool tối đa cho một lượt. Đặt rất cao theo yêu cầu người dùng: task
78
79
  // dài cứ chạy, đừng tự dừng. Người dùng vẫn có thể Ctrl+C bất cứ lúc nào.
79
80
  const MAX_STEPS = 10000;
81
+
82
+ // Loop detection: nếu model gọi cùng 1 tool với input giống nhau liên tiếp
83
+ // quá ngưỡng này → coi là bị kẹt, inject thông báo để model chuyển bước.
84
+ const LOOP_DETECT_WINDOW = 3;
85
+ const LOOP_DETECT_THRESHOLD = 2;
80
86
  const MAX_PROMPT_CHARS = 80000; // ngân sách ký tự cho phần hội thoại gửi lên model
81
87
  // Khi history vượt ngưỡng này, gọi model phụ tóm tắt các lượt cũ thay vì cắt cụt
82
88
  // → giữ được "trí nhớ dài hạn" trong phiên mà không nổ context.
@@ -408,6 +414,7 @@ export async function runAgent({ history, model, signal, onTool, onStatus, onDel
408
414
  // [GỠ BUDGET 2026-06-06] Không còn token budget enforcement. Agent/loop/sub-agent
409
415
  // chạy không giới hạn token. Dừng theo: GOAL đạt, <<LOOP_DONE>>, <<ULTRA_DONE>>,
410
416
  // model tự kết thúc reply không có tool block, hoặc user Ctrl+C.
417
+ const recentCalls = []; // {name, inputStr} — theo dõi vòng lặp
411
418
  for (let step = 0; step < MAX_STEPS; step++) {
412
419
  // Mỗi 100 bước log một mốc để người dùng biết noob vẫn đang chạy (task dài).
413
420
  if (step > 0 && step % 100 === 0) onStatus?.(`đã chạy ${step} bước…`);
@@ -460,6 +467,27 @@ export async function runAgent({ history, model, signal, onTool, onStatus, onDel
460
467
  name: call.name,
461
468
  content: allow ? result : t.toolDenied,
462
469
  });
470
+
471
+ // ── Loop detection ──────────────────────────────────────────────────
472
+ // Theo dõi N tool call gần nhất. Nếu cùng tên + input giống nhau liên tiếp
473
+ // quá ngưỡng → model bị kẹt. Inject thông báo nhắc model chuyển bước,
474
+ // KHÔNG ngắt task (vẫn có thể Ctrl+C thủ công).
475
+ const inputStr = JSON.stringify(call.input || {});
476
+ recentCalls.push({ name: call.name, inputStr });
477
+ if (recentCalls.length > LOOP_DETECT_WINDOW) recentCalls.shift();
478
+ if (recentCalls.length >= LOOP_DETECT_THRESHOLD + 1) {
479
+ const lastN = recentCalls.slice(-LOOP_DETECT_THRESHOLD);
480
+ const allSame = lastN.every((c) => c.name === lastN[0].name && c.inputStr === lastN[0].inputStr);
481
+ if (allSame) {
482
+ // Đã lặp — inject cảnh báo, xoá calls để tránh trigger liên tục
483
+ recentCalls.length = 0;
484
+ history.push({
485
+ role: "tool",
486
+ name: "loop_detection",
487
+ content: `[LOOP DETECTED] Bạn vừa gọi ${lastN[0].name} ${LOOP_DETECT_THRESHOLD} lần liên tiếp với cùng input — có vẻ bạn đang bị kẹt trong vòng lặp. HÃY CHUYỂN BƯỚC: gọi tool khác, hoặc trả lời Markdown nếu đã xong. KHÔNG được gọi cùng tool cùng input lần nữa.`,
488
+ });
489
+ }
490
+ }
463
491
  }
464
492
  return t.maxSteps;
465
493
  }
@@ -494,7 +522,7 @@ async function streamWithRetry({ model, message, system, signal, tokenMeter, onD
494
522
  lastErr = err;
495
523
  if (attempt >= MAX_RETRIES) break;
496
524
  const backoff = Math.min(30000, 1000 * Math.pow(2, attempt));
497
- onStatus?.(`mạng lỗi (${err.message}) — thử lại sau ${(backoff/1000)|0}s [${attempt+1}/${MAX_RETRIES}]`);
525
+ onStatus?.(`Lỗi kết nối — thử lại sau ${(backoff/1000)|0}s [${attempt+1}/${MAX_RETRIES}]…`);
498
526
  await sleep(backoff, signal);
499
527
  }
500
528
  }
package/src/api.js CHANGED
@@ -116,7 +116,7 @@ function hasUnclosedToolBlock(text) {
116
116
  *
117
117
  * @returns {Promise<{text:string, reasoning:string}>}
118
118
  */
119
- export async function stream({ mode = "chat", message, model, system, conversation, effort, signal, onDelta, onReasoning, onStatus, idleMs = 25000, maxContinues = Infinity }) {
119
+ export async function stream({ mode = "chat", message, model, system, conversation, effort, signal, onDelta, onReasoning, onStatus, idleMs = 45000, maxContinues = Infinity }) {
120
120
  const endpoint = mode === "search" ? "/api/search" : mode === "merge" ? "/api/merge" : "/api/chat";
121
121
 
122
122
  let fullText = "";
@@ -209,9 +209,9 @@ async function streamOnce({ endpoint, mode, message, model, system, conversation
209
209
  let warnTimer = null;
210
210
  let probeTimer = null;
211
211
  let probeInFlight = false;
212
- const WARN_MS = Math.min(8000, idleMs / 3);
213
- const PROBE_MS = Math.min(12000, (idleMs * 2) / 3);
214
- const WIRE_MS = idleMs * 2; // socket dead-cứng (mất cả heartbeat) — ngưỡng rộng
212
+ const WARN_MS = Math.min(15000, idleMs / 3);
213
+ const PROBE_MS = Math.min(25000, (idleMs * 2) / 3);
214
+ const WIRE_MS = idleMs * 3; // socket dead-cứng (mất cả heartbeat) — ngưỡng rất rộng
215
215
  const clearContentTimers = () => {
216
216
  clearTimeout(warnTimer); warnTimer = null;
217
217
  clearTimeout(probeTimer); probeTimer = null;
@@ -220,7 +220,7 @@ async function streamOnce({ endpoint, mode, message, model, system, conversation
220
220
  const armContent = () => {
221
221
  clearContentTimers();
222
222
  warnTimer = setTimeout(() => {
223
- if (onStatus) onStatus("Đang chờ proxy phản hồi…");
223
+ if (onStatus) onStatus("Model đang suy nghĩ… (đợi thêm nếu task phức tạp)");
224
224
  }, WARN_MS);
225
225
  probeTimer = setTimeout(async () => {
226
226
  if (probeInFlight || ctrl.signal.aborted) return;
@@ -234,7 +234,7 @@ async function streamOnce({ endpoint, mode, message, model, system, conversation
234
234
  } catch {
235
235
  if (!ctrl.signal.aborted) {
236
236
  timedOut = true;
237
- if (onStatus) onStatus("Proxy không phản hồi — đang gọi lại model…");
237
+ if (onStatus) onStatus("Model không phản hồi — đang kết nối lại…");
238
238
  ctrl.abort();
239
239
  }
240
240
  } finally {
@@ -328,7 +328,7 @@ async function streamOnce({ endpoint, mode, message, model, system, conversation
328
328
  return { text, reasoning, truncated };
329
329
  } catch (err) {
330
330
  if (signal?.aborted) throw err; // người dùng bấm Ctrl+C → huỷ thật, không nối tiếp
331
- if (timedOut) throw new ApiError("Kết nối tới máy chủ quá thời gian chờ (treo).", { code: "timeout" });
331
+ if (timedOut) throw new ApiError("Kết nối tới máy chủ quá thời gian chờ (treo). Đang thử lại…", { code: "timeout" });
332
332
  // Rớt mạng giữa chừng (không phải huỷ, không phải treo): với chat, nếu đã có
333
333
  // chữ thì trả phần đã nhận + cờ truncated để lớp trên nối tiếp.
334
334
  if (mode === "chat" && text) return { text, reasoning, truncated: true };