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

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.
@@ -1800,6 +1800,41 @@ def _ws_for_path(path: str):
1800
1800
  return None
1801
1801
 
1802
1802
 
1803
+ def _workspace_overview(roots, max_entries: int = 40) -> str:
1804
+ """Cheap, LOCAL top-level listing of each registered workspace root — a plain
1805
+ os.listdir(), no recursion, no network call, no model call. Used two ways: (1)
1806
+ injected into the task prompt so the agent has upfront grounding that the workspace
1807
+ is non-empty WITHOUT spending its first (possibly failing) tool-call round-trip just
1808
+ discovering that; (2) appended to the degrade-to-plain-reply message so even a TOTAL
1809
+ coding-model outage still shows the user their files are actually there — this is
1810
+ the one piece of workspace context that stays available no matter how badly the
1811
+ model-calling side of the agent is failing, since it never touches a model at all."""
1812
+ import os
1813
+ parts = []
1814
+ for w in roots:
1815
+ root = w["path"]
1816
+ try:
1817
+ entries = sorted(
1818
+ e for e in os.listdir(root) if e not in _RAG_SKIP_DIRS
1819
+ )
1820
+ except OSError as e:
1821
+ parts.append(f"{w['name']} ({root}): could not list ({e})")
1822
+ continue
1823
+ if not entries:
1824
+ parts.append(f"{w['name']} ({root}): EMPTY — no files or folders found.")
1825
+ continue
1826
+ shown = entries[:max_entries]
1827
+ labeled = [
1828
+ f"{e}/" if os.path.isdir(os.path.join(root, e)) else e for e in shown
1829
+ ]
1830
+ more = f" …(+{len(entries) - max_entries} more)" if len(entries) > max_entries else ""
1831
+ parts.append(
1832
+ f"{w['name']} ({root}) — {len(entries)} top-level item(s): "
1833
+ + ", ".join(labeled) + more
1834
+ )
1835
+ return "\n".join(parts)
1836
+
1837
+
1803
1838
  def _ws_write_allowed(path: str) -> str:
1804
1839
  """Resolve `path` for WRITING: must be inside a registered workspace and never
1805
1840
  inside its .git/ internals (agents must not corrupt version control)."""
@@ -2347,8 +2382,15 @@ def run_agent_chat(cfg):
2347
2382
  "If the user adds information, rag_add(source_url=url, text=...).\n"
2348
2383
  ) if _RAG_AVAILABLE else ""
2349
2384
  _ws_names = ", ".join(f"{w['name']} = {w['path']}" for w in _WS_ROOTS)
2385
+ # Upfront, ZERO-COST (no tool call, no model round-trip) top-level listing — so the
2386
+ # agent already knows the workspace is non-empty and roughly what's in it before its
2387
+ # first step, instead of having to spend (and risk losing to a timeout) a whole
2388
+ # round-trip just calling list_dir to discover that.
2389
+ _ws_overview = _workspace_overview(_WS_ROOTS) if _WS_AVAILABLE else ""
2350
2390
  _ws_tool_block = (
2351
2391
  f" WORKSPACES (local code folders you may READ and EDIT): {_ws_names}\n"
2392
+ f" Current contents:\n"
2393
+ + "\n".join(f" {line}" for line in _ws_overview.splitlines()) + "\n"
2352
2394
  " - list_dir(path) / read_text_file(path) — browse and read files.\n"
2353
2395
  " - grep_repo(pattern, path='', glob='') — search code, returns file:line hits. "
2354
2396
  "Use this to find the EXACT lines to change.\n"
@@ -3929,7 +3971,24 @@ def run_agent_chat(cfg):
3929
3971
  # a fabricated "final" answer here instead would poison every future turn's
3930
3972
  # prompt with raw exception text (this is exactly what caused task #35).
3931
3973
  raise e
3932
- _emit({"type": "final", "text": fallback})
3974
+ # _plain_reply has NO tool/file/HTTP access whatsoever — that's by design (it's a
3975
+ # bare chat completion). Without a note here, its honest "I don't have access to
3976
+ # your workspace/that page/etc." reads to the user as a PERMISSIONS bug, when the
3977
+ # real cause is that the tool-capable agent loop failed upstream (e.g. the coding
3978
+ # model timed out) and we silently swapped in a capability-limited fallback. Make
3979
+ # that swap visible so the user can tell "genuinely no access" apart from
3980
+ # "the real attempt never got to run" (seen live: workspace summarize request →
3981
+ # coding model timed out 3x on a slow/unreachable remote Ollama box → silent
3982
+ # fallback claimed "no access to your workspace").
3983
+ note = f"⚠️ I couldn't finish the full investigation ({_clip(str(e), 120)}) — here's what I can say without it:\n\n{fallback}"
3984
+ # Append the workspace's ACTUAL top-level contents (computed locally, no model
3985
+ # call — see _workspace_overview) so the user sees their files are really there
3986
+ # even during a total coding-model outage, instead of trusting a plain-chat
3987
+ # model's guess that "there's no code or project" (seen live — false: the
3988
+ # workspace had real content the whole time, the model just never reached it).
3989
+ if _WS_AVAILABLE:
3990
+ note += f"\n\n(Your registered workspace:\n{_workspace_overview(_WS_ROOTS)})"
3991
+ _emit({"type": "final", "text": note})
3933
3992
 
3934
3993
 
3935
3994
  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.193",
3
+ "version": "1.0.195",
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",