@tiens.nguyen/gonext-local-worker 1.0.192 → 1.0.194

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.
Files changed (2) hide show
  1. package/gonext_agent_chat.py +173 -57
  2. package/package.json +1 -1
@@ -3288,6 +3288,14 @@ def run_agent_chat(cfg):
3288
3288
  text = (_last_obs.get("text") or "").strip()
3289
3289
  if text:
3290
3290
  _log(f"max-steps fallback (last tool obs) → {text[:80]}")
3291
+ # Without this, `text` (a tool's RAW return value — e.g. a bare grep_repo
3292
+ # dump of file:line hits) becomes the entire chat reply with zero framing,
3293
+ # reading as a bizarre non-answer. A short honest note makes clear this is
3294
+ # a partial result from an interrupted run, not the actual answer/edit.
3295
+ text = (
3296
+ "I ran out of steps before finishing this. Here's the last thing I "
3297
+ "found — reply to have me continue from here:\n\n" + text
3298
+ )
3291
3299
  else:
3292
3300
  # No usable tool output (e.g. every step's code failed to parse).
3293
3301
  # Don't dead-end on a canned apology — answer from the conversation.
@@ -3344,12 +3352,18 @@ def run_agent_chat(cfg):
3344
3352
  path, cached = _rag_download(url, (dest_path or "").strip())
3345
3353
  if cached:
3346
3354
  _emit({"type": "step", "text": "Already downloaded — reusing cached file"})
3347
- return (f"Already downloaded earlier — reusing {path} "
3348
- f"({os.path.getsize(path)} bytes). Next: unzip_file(zip_path='{path}').")
3355
+ out = (f"Already downloaded earlier — reusing {path} "
3356
+ f"({os.path.getsize(path)} bytes). Next: unzip_file(zip_path='{path}').")
3357
+ _last_obs["text"] = out
3358
+ return out
3349
3359
  _emit({"type": "step", "text": f"Downloading → {url[:80]}"})
3350
- return f"Downloaded to {path} ({os.path.getsize(path)} bytes). Next: unzip_file(zip_path='{path}') if it is a zip."
3360
+ out = f"Downloaded to {path} ({os.path.getsize(path)} bytes). Next: unzip_file(zip_path='{path}') if it is a zip."
3361
+ _last_obs["text"] = out
3362
+ return out
3351
3363
  except Exception as e: # noqa: BLE001
3352
- return f"Error: download failed: {type(e).__name__}: {e}"
3364
+ msg = f"Error: download failed: {type(e).__name__}: {e}"
3365
+ _last_obs["text"] = msg
3366
+ return msg
3353
3367
 
3354
3368
  @tool
3355
3369
  def unzip_file(zip_path: str, dest_dir: str = "") -> str:
@@ -3365,12 +3379,18 @@ def run_agent_chat(cfg):
3365
3379
  more = f" …(+{len(names) - 15} more)" if len(names) > 15 else ""
3366
3380
  if cached:
3367
3381
  _emit({"type": "step", "text": "Already unzipped — reusing extracted files"})
3368
- return (f"Already unzipped earlier — {len(names)} files at {d}. "
3369
- f"Files: {preview}{more}. Use list_dir/read_text_file or rag_index on it.")
3382
+ out = (f"Already unzipped earlier — {len(names)} files at {d}. "
3383
+ f"Files: {preview}{more}. Use list_dir/read_text_file or rag_index on it.")
3384
+ _last_obs["text"] = out
3385
+ return out
3370
3386
  _emit({"type": "step", "text": "Unzipping…"})
3371
- return f"Unzipped {len(names)} files to {d}. Files: {preview}{more}. Next: rag_index(path='{d}', source_url=<the original url>)."
3387
+ out = f"Unzipped {len(names)} files to {d}. Files: {preview}{more}. Next: rag_index(path='{d}', source_url=<the original url>)."
3388
+ _last_obs["text"] = out
3389
+ return out
3372
3390
  except Exception as e: # noqa: BLE001
3373
- return f"Error: unzip failed: {type(e).__name__}: {e}"
3391
+ msg = f"Error: unzip failed: {type(e).__name__}: {e}"
3392
+ _last_obs["text"] = msg
3393
+ return msg
3374
3394
 
3375
3395
  @tool
3376
3396
  def list_dir(path: str) -> str:
@@ -3383,7 +3403,9 @@ def run_agent_chat(cfg):
3383
3403
  import os
3384
3404
  root = _rag_read_allowed((path or ".").strip())
3385
3405
  if os.path.isfile(root):
3386
- return f"{root} is a file ({os.path.getsize(root)} bytes)."
3406
+ out = f"{root} is a file ({os.path.getsize(root)} bytes)."
3407
+ _last_obs["text"] = out
3408
+ return out
3387
3409
  entries = []
3388
3410
  for dirpath, dirnames, filenames in os.walk(root):
3389
3411
  dirnames[:] = [d for d in dirnames if d not in _RAG_SKIP_DIRS]
@@ -3398,9 +3420,13 @@ def run_agent_chat(cfg):
3398
3420
  break
3399
3421
  entries.sort()
3400
3422
  more = " …(truncated)" if len(entries) >= 300 else ""
3401
- return f"{len(entries)} files under {root}:\n" + "\n".join(entries) + more
3423
+ out = f"{len(entries)} files under {root}:\n" + "\n".join(entries) + more
3424
+ _last_obs["text"] = out
3425
+ return out
3402
3426
  except Exception as e: # noqa: BLE001
3403
- return f"Error: list_dir failed: {type(e).__name__}: {e}"
3427
+ msg = f"Error: list_dir failed: {type(e).__name__}: {e}"
3428
+ _last_obs["text"] = msg
3429
+ return msg
3404
3430
 
3405
3431
  @tool
3406
3432
  def read_text_file(path: str, max_chars: int = 20000) -> str:
@@ -3414,14 +3440,22 @@ def run_agent_chat(cfg):
3414
3440
  import os
3415
3441
  rp = _rag_read_allowed((path or "").strip())
3416
3442
  if not os.path.isfile(rp):
3417
- return f"Error: not a file: {path}"
3443
+ msg = f"Error: not a file: {path}"
3444
+ _last_obs["text"] = msg
3445
+ return msg
3418
3446
  if os.path.getsize(rp) > 5 * 1024 * 1024:
3419
- return "Error: file too large to read (> 5 MB)."
3447
+ msg = "Error: file too large to read (> 5 MB)."
3448
+ _last_obs["text"] = msg
3449
+ return msg
3420
3450
  with open(rp, "r", encoding="utf-8", errors="replace") as fh:
3421
3451
  data = fh.read(max(500, min(int(max_chars or 20000), 100000)))
3422
- return data or "(empty file)"
3452
+ out = data or "(empty file)"
3453
+ _last_obs["text"] = out
3454
+ return out
3423
3455
  except Exception as e: # noqa: BLE001
3424
- return f"Error: read_text_file failed: {type(e).__name__}: {e}"
3456
+ msg = f"Error: read_text_file failed: {type(e).__name__}: {e}"
3457
+ _last_obs["text"] = msg
3458
+ return msg
3425
3459
 
3426
3460
  @tool
3427
3461
  def rag_index(path: str, source_url: str) -> str:
@@ -3432,7 +3466,9 @@ def run_agent_chat(cfg):
3432
3466
  source_url: the original URL the user gave — used as the knowledge-base key.
3433
3467
  """
3434
3468
  if not _RAG_AVAILABLE:
3435
- return "Error: RAG is not configured (enable it + add AWS credentials in Settings)."
3469
+ msg = "Error: RAG is not configured (enable it + add AWS credentials in Settings)."
3470
+ _last_obs["text"] = msg
3471
+ return msg
3436
3472
  _emit({"type": "step", "text": f"RAG source → {source_url}"})
3437
3473
  _emit({"type": "step", "text": "Indexing files for RAG…"})
3438
3474
  try:
@@ -3448,7 +3484,9 @@ def run_agent_chat(cfg):
3448
3484
  files += 1
3449
3485
  records.extend(_rag_chunk_text(text, rel, ext))
3450
3486
  if not records:
3451
- return "No indexable text files found at that path."
3487
+ msg = "No indexable text files found at that path."
3488
+ _last_obs["text"] = msg
3489
+ return msg
3452
3490
  for i in range(0, len(records), 64):
3453
3491
  batch = records[i:i + 64]
3454
3492
  vecs = _embed(rag_embed_base, rag_embed_model, [r["text"] for r in batch])
@@ -3469,10 +3507,14 @@ def run_agent_chat(cfg):
3469
3507
  client.put_object(Bucket=bucket, Key=f"{base}/manifest.json",
3470
3508
  Body=json.dumps(manifest).encode("utf-8"),
3471
3509
  ContentType="application/json")
3472
- return (f"Indexed {files} files into {len(records)} chunks for {source_url}. "
3473
- f"Use rag_search(source_url='{source_url}', query=...) to retrieve, then answer the user.")
3510
+ out = (f"Indexed {files} files into {len(records)} chunks for {source_url}. "
3511
+ f"Use rag_search(source_url='{source_url}', query=...) to retrieve, then answer the user.")
3512
+ _last_obs["text"] = out
3513
+ return out
3474
3514
  except Exception as e: # noqa: BLE001
3475
- return f"Error: indexing failed: {type(e).__name__}: {e}"
3515
+ msg = f"Error: indexing failed: {type(e).__name__}: {e}"
3516
+ _last_obs["text"] = msg
3517
+ return msg
3476
3518
 
3477
3519
  @tool
3478
3520
  def rag_add(source_url: str, text: str) -> str:
@@ -3483,9 +3525,13 @@ def run_agent_chat(cfg):
3483
3525
  text: the new information to embed and store.
3484
3526
  """
3485
3527
  if not _RAG_AVAILABLE:
3486
- return "Error: RAG is not configured."
3528
+ msg = "Error: RAG is not configured."
3529
+ _last_obs["text"] = msg
3530
+ return msg
3487
3531
  if not (text or "").strip():
3488
- return "Error: no text to add."
3532
+ msg = "Error: no text to add."
3533
+ _last_obs["text"] = msg
3534
+ return msg
3489
3535
  _emit({"type": "step", "text": f"RAG source → {source_url}"})
3490
3536
  _emit({"type": "step", "text": "Adding info to RAG…"})
3491
3537
  try:
@@ -3499,9 +3545,13 @@ def run_agent_chat(cfg):
3499
3545
  shard = f"{base}/chunks-add-{int(_t.time())}.jsonl"
3500
3546
  body = "\n".join(json.dumps(r, ensure_ascii=False) for r in records).encode("utf-8")
3501
3547
  client.put_object(Bucket=bucket, Key=shard, Body=body, ContentType="application/x-ndjson")
3502
- return f"Added {len(records)} chunk(s) to the knowledge base for {source_url}."
3548
+ out = f"Added {len(records)} chunk(s) to the knowledge base for {source_url}."
3549
+ _last_obs["text"] = out
3550
+ return out
3503
3551
  except Exception as e: # noqa: BLE001
3504
- return f"Error: rag_add failed: {type(e).__name__}: {e}"
3552
+ msg = f"Error: rag_add failed: {type(e).__name__}: {e}"
3553
+ _last_obs["text"] = msg
3554
+ return msg
3505
3555
 
3506
3556
  @tool
3507
3557
  def rag_search(source_url: str, query: str, k: int = 0) -> str:
@@ -3513,7 +3563,9 @@ def run_agent_chat(cfg):
3513
3563
  k: number of chunks to return (0 = configured default).
3514
3564
  """
3515
3565
  if not _RAG_AVAILABLE:
3516
- return "Error: RAG is not configured."
3566
+ msg = "Error: RAG is not configured."
3567
+ _last_obs["text"] = msg
3568
+ return msg
3517
3569
  _emit({"type": "step", "text": f"RAG source → {source_url}"})
3518
3570
  _emit({"type": "step", "text": f"Searching RAG → {query[:60]}"})
3519
3571
  try:
@@ -3521,7 +3573,9 @@ def run_agent_chat(cfg):
3521
3573
  base = _rag_prefix_for(source_url, prefix)
3522
3574
  chunks = _rag_load_chunks(client, bucket, base)
3523
3575
  if not chunks:
3524
- return f"No knowledge base found for {source_url}. Run rag_index first."
3576
+ msg = f"No knowledge base found for {source_url}. Run rag_index first."
3577
+ _last_obs["text"] = msg
3578
+ return msg
3525
3579
  qvec = _embed(rag_embed_base, rag_embed_model, [query])[0]
3526
3580
  scored = sorted(
3527
3581
  ((_cosine(qvec, c.get("embedding") or []), c) for c in chunks),
@@ -3530,9 +3584,13 @@ def run_agent_chat(cfg):
3530
3584
  topk = scored[: (k if k and k > 0 else rag_top_k)]
3531
3585
  parts = [f"[{c.get('file', '?')}] (score {score:.2f})\n{c.get('text', '')}"
3532
3586
  for score, c in topk]
3533
- return "\n\n---\n\n".join(parts) if parts else "No matches."
3587
+ out = "\n\n---\n\n".join(parts) if parts else "No matches."
3588
+ _last_obs["text"] = out
3589
+ return out
3534
3590
  except Exception as e: # noqa: BLE001
3535
- return f"Error: rag_search failed: {type(e).__name__}: {e}"
3591
+ msg = f"Error: rag_search failed: {type(e).__name__}: {e}"
3592
+ _last_obs["text"] = msg
3593
+ return msg
3536
3594
 
3537
3595
  @tool
3538
3596
  def grep_repo(pattern: str, path: str = "", glob: str = "") -> str:
@@ -3573,11 +3631,17 @@ def run_agent_chat(cfg):
3573
3631
  break
3574
3632
  _emit({"type": "step", "text": f"Searching code → {pattern[:60]} ({len(hits)} hits)"})
3575
3633
  if not hits:
3576
- return f"No matches for {pattern!r} under {root}."
3634
+ msg = f"No matches for {pattern!r} under {root}."
3635
+ _last_obs["text"] = msg
3636
+ return msg
3577
3637
  more = "\n…(capped at 100 hits)" if len(hits) >= 100 else ""
3578
- return f"Matches under {root}:\n" + "\n".join(hits) + more
3638
+ out = f"Matches under {root}:\n" + "\n".join(hits) + more
3639
+ _last_obs["text"] = out
3640
+ return out
3579
3641
  except Exception as e: # noqa: BLE001
3580
- return f"Error: grep_repo failed: {type(e).__name__}: {e}"
3642
+ msg = f"Error: grep_repo failed: {type(e).__name__}: {e}"
3643
+ _last_obs["text"] = msg
3644
+ return msg
3581
3645
 
3582
3646
  @tool
3583
3647
  def read_file_lines(path: str, start_line: int = 1, end_line: int = 0) -> str:
@@ -3596,9 +3660,13 @@ def run_agent_chat(cfg):
3596
3660
  e = int(end_line or 0) or len(lines)
3597
3661
  e = min(e, len(lines), s + 399)
3598
3662
  body = "".join(f"{i}: {lines[i - 1]}" for i in range(s, e + 1))
3599
- return f"{rp} (lines {s}-{e} of {len(lines)}):\n{body}"
3663
+ out = f"{rp} (lines {s}-{e} of {len(lines)}):\n{body}"
3664
+ _last_obs["text"] = out
3665
+ return out
3600
3666
  except Exception as ex: # noqa: BLE001
3601
- return f"Error: read_file_lines failed: {type(ex).__name__}: {ex}"
3667
+ msg = f"Error: read_file_lines failed: {type(ex).__name__}: {ex}"
3668
+ _last_obs["text"] = msg
3669
+ return msg
3602
3670
 
3603
3671
  @tool
3604
3672
  def edit_lines(path: str, start_line: int, end_line: int, new_content: str) -> str:
@@ -3614,14 +3682,18 @@ def run_agent_chat(cfg):
3614
3682
  rp = _ws_write_allowed((path or "").strip())
3615
3683
  import os
3616
3684
  if not os.path.isfile(rp):
3617
- return f"Error: not a file: {path}"
3685
+ msg = f"Error: not a file: {path}"
3686
+ _last_obs["text"] = msg
3687
+ return msg
3618
3688
  backup = _ws_snapshot(rp)
3619
3689
  with open(rp, "r", encoding="utf-8", errors="replace") as fh:
3620
3690
  lines = fh.readlines()
3621
3691
  s, e = int(start_line), int(end_line)
3622
3692
  if not (1 <= s <= e <= len(lines)):
3623
- return (f"Error: line range {s}-{e} is out of bounds "
3624
- f"(file has {len(lines)} lines). Re-check with read_file_lines.")
3693
+ msg = (f"Error: line range {s}-{e} is out of bounds "
3694
+ f"(file has {len(lines)} lines). Re-check with read_file_lines.")
3695
+ _last_obs["text"] = msg
3696
+ return msg
3625
3697
  new_lines = new_content.splitlines(keepends=True)
3626
3698
  if new_lines and not new_lines[-1].endswith("\n"):
3627
3699
  new_lines[-1] += "\n"
@@ -3632,10 +3704,14 @@ def run_agent_chat(cfg):
3632
3704
  _emit({"type": "step", "text": f"Edited {os.path.basename(rp)} lines {s}-{e}"})
3633
3705
  shown = "".join(f"{i}: {lines[i - 1]}" for i in
3634
3706
  range(max(1, s - 2), min(len(lines), s + len(new_lines) + 1) + 1))
3635
- return (f"Replaced lines {s}-{e} of {rp} with {len(new_lines)} line(s). "
3636
- f"Backup saved. Updated region:\n{shown}")
3707
+ out = (f"Replaced lines {s}-{e} of {rp} with {len(new_lines)} line(s). "
3708
+ f"Backup saved. Updated region:\n{shown}")
3709
+ _last_obs["text"] = out
3710
+ return out
3637
3711
  except Exception as ex: # noqa: BLE001
3638
- return f"Error: edit_lines failed: {type(ex).__name__}: {ex}"
3712
+ msg = f"Error: edit_lines failed: {type(ex).__name__}: {ex}"
3713
+ _last_obs["text"] = msg
3714
+ return msg
3639
3715
 
3640
3716
  @tool
3641
3717
  def edit_file(path: str, old_string: str, new_string: str) -> str:
@@ -3650,23 +3726,33 @@ def run_agent_chat(cfg):
3650
3726
  rp = _ws_write_allowed((path or "").strip())
3651
3727
  import os
3652
3728
  if not os.path.isfile(rp):
3653
- return f"Error: not a file: {path}"
3729
+ msg = f"Error: not a file: {path}"
3730
+ _last_obs["text"] = msg
3731
+ return msg
3654
3732
  with open(rp, "r", encoding="utf-8", errors="replace") as fh:
3655
3733
  text = fh.read()
3656
3734
  n = text.count(old_string)
3657
3735
  if n == 0:
3658
- return ("Error: old_string not found — it must match EXACTLY "
3659
- "(check whitespace with read_file_lines, or use edit_lines).")
3736
+ msg = ("Error: old_string not found — it must match EXACTLY "
3737
+ "(check whitespace with read_file_lines, or use edit_lines).")
3738
+ _last_obs["text"] = msg
3739
+ return msg
3660
3740
  if n > 1:
3661
- return f"Error: old_string occurs {n} times — add surrounding context to make it unique, or use edit_lines."
3741
+ msg = f"Error: old_string occurs {n} times — add surrounding context to make it unique, or use edit_lines."
3742
+ _last_obs["text"] = msg
3743
+ return msg
3662
3744
  backup = _ws_snapshot(rp)
3663
3745
  with open(rp, "w", encoding="utf-8") as fh:
3664
3746
  fh.write(text.replace(old_string, new_string, 1))
3665
3747
  _ws_record_change("edit", rp, backup)
3666
3748
  _emit({"type": "step", "text": f"Edited {os.path.basename(rp)}"})
3667
- return f"Replaced 1 occurrence in {rp}. Backup saved."
3749
+ out = f"Replaced 1 occurrence in {rp}. Backup saved."
3750
+ _last_obs["text"] = out
3751
+ return out
3668
3752
  except Exception as ex: # noqa: BLE001
3669
- return f"Error: edit_file failed: {type(ex).__name__}: {ex}"
3753
+ msg = f"Error: edit_file failed: {type(ex).__name__}: {ex}"
3754
+ _last_obs["text"] = msg
3755
+ return msg
3670
3756
 
3671
3757
  @tool
3672
3758
  def create_file(path: str, content: str) -> str:
@@ -3680,15 +3766,21 @@ def run_agent_chat(cfg):
3680
3766
  rp = _ws_write_allowed((path or "").strip())
3681
3767
  import os
3682
3768
  if os.path.exists(rp):
3683
- return f"Error: {path} already exists — use edit_lines/edit_file to change it."
3769
+ msg = f"Error: {path} already exists — use edit_lines/edit_file to change it."
3770
+ _last_obs["text"] = msg
3771
+ return msg
3684
3772
  os.makedirs(os.path.dirname(rp) or ".", exist_ok=True)
3685
3773
  with open(rp, "w", encoding="utf-8") as fh:
3686
3774
  fh.write(content)
3687
3775
  _ws_record_change("create", rp, None)
3688
3776
  _emit({"type": "step", "text": f"Created {os.path.basename(rp)}"})
3689
- return f"Created {rp} ({len(content)} chars)."
3777
+ out = f"Created {rp} ({len(content)} chars)."
3778
+ _last_obs["text"] = out
3779
+ return out
3690
3780
  except Exception as ex: # noqa: BLE001
3691
- return f"Error: create_file failed: {type(ex).__name__}: {ex}"
3781
+ msg = f"Error: create_file failed: {type(ex).__name__}: {ex}"
3782
+ _last_obs["text"] = msg
3783
+ return msg
3692
3784
 
3693
3785
  @tool
3694
3786
  def run_command(command: str, workdir: str = "", timeout_seconds: int = 180) -> str:
@@ -3706,14 +3798,20 @@ def run_agent_chat(cfg):
3706
3798
  wd = _rag_read_allowed((workdir or "").strip() or (_WS_ROOTS[0]["path"] if _WS_ROOTS else "."))
3707
3799
  w = _ws_for_path(wd)
3708
3800
  if w is None or not w.get("allowRun"):
3709
- return ("Error: running commands is not enabled for this workspace. "
3710
- "Re-register it with: gonext-local-worker workspace add <path> --allow-run")
3801
+ msg = ("Error: running commands is not enabled for this workspace. "
3802
+ "Re-register it with: gonext-local-worker workspace add <path> --allow-run")
3803
+ _last_obs["text"] = msg
3804
+ return msg
3711
3805
  argv = shlex.split(command or "")
3712
3806
  if not argv:
3713
- return "Error: empty command."
3807
+ msg = "Error: empty command."
3808
+ _last_obs["text"] = msg
3809
+ return msg
3714
3810
  if argv[0] not in _WS_RUN_ALLOWED:
3715
- return (f"Error: '{argv[0]}' is not an allowed runner. Allowed: "
3716
- + ", ".join(sorted(_WS_RUN_ALLOWED)))
3811
+ msg = (f"Error: '{argv[0]}' is not an allowed runner. Allowed: "
3812
+ + ", ".join(sorted(_WS_RUN_ALLOWED)))
3813
+ _last_obs["text"] = msg
3814
+ return msg
3717
3815
  _emit({"type": "step", "text": f"Running → {command[:70]}"})
3718
3816
  t = max(5, min(int(timeout_seconds or 180), 600))
3719
3817
  # Scrubbed env: repo test scripts must never see worker secrets.
@@ -3724,11 +3822,17 @@ def run_agent_chat(cfg):
3724
3822
  out = _ws_distill_output(proc.stdout or "")
3725
3823
  status = "PASSED (exit 0)" if proc.returncode == 0 else f"FAILED (exit {proc.returncode})"
3726
3824
  _emit({"type": "step", "text": f"Command {status.split()[0].lower()} → {command[:50]}"})
3727
- return f"{status}\n{out}" if out.strip() else status
3825
+ result = f"{status}\n{out}" if out.strip() else status
3826
+ _last_obs["text"] = result
3827
+ return result
3728
3828
  except subprocess.TimeoutExpired:
3729
- return f"Error: command timed out after {timeout_seconds}s."
3829
+ msg = f"Error: command timed out after {timeout_seconds}s."
3830
+ _last_obs["text"] = msg
3831
+ return msg
3730
3832
  except Exception as ex: # noqa: BLE001
3731
- return f"Error: run_command failed: {type(ex).__name__}: {ex}"
3833
+ msg = f"Error: run_command failed: {type(ex).__name__}: {ex}"
3834
+ _last_obs["text"] = msg
3835
+ return msg
3732
3836
 
3733
3837
  agent_tools = [http_request, web_search, fetch_url, calculate,
3734
3838
  get_current_datetime, create_pdf]
@@ -3825,7 +3929,19 @@ def run_agent_chat(cfg):
3825
3929
  # a fabricated "final" answer here instead would poison every future turn's
3826
3930
  # prompt with raw exception text (this is exactly what caused task #35).
3827
3931
  raise e
3828
- _emit({"type": "final", "text": fallback})
3932
+ # _plain_reply has NO tool/file/HTTP access whatsoever — that's by design (it's a
3933
+ # bare chat completion). Without a note here, its honest "I don't have access to
3934
+ # your workspace/that page/etc." reads to the user as a PERMISSIONS bug, when the
3935
+ # real cause is that the tool-capable agent loop failed upstream (e.g. the coding
3936
+ # model timed out) and we silently swapped in a capability-limited fallback. Make
3937
+ # that swap visible so the user can tell "genuinely no access" apart from
3938
+ # "the real attempt never got to run" (seen live: workspace summarize request →
3939
+ # coding model timed out 3x on a slow/unreachable remote Ollama box → silent
3940
+ # fallback claimed "no access to your workspace").
3941
+ _emit({"type": "final", "text": (
3942
+ f"⚠️ I couldn't finish the full investigation ({_clip(str(e), 120)}) — here's "
3943
+ f"what I can say without it:\n\n{fallback}"
3944
+ )})
3829
3945
 
3830
3946
 
3831
3947
  def _log(text: str):
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiens.nguyen/gonext-local-worker",
3
- "version": "1.0.192",
3
+ "version": "1.0.194",
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",