clouds-coder 2026.3.25__tar.gz → 2026.3.27__tar.gz

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.
@@ -27,6 +27,7 @@ import shlex
27
27
  import socket
28
28
  import subprocess
29
29
  import sys
30
+ import tarfile
30
31
  import threading
31
32
  import time
32
33
  import traceback
@@ -103,7 +104,7 @@ CODE_LIBRARY_DIRNAME = "Code_Library"
103
104
  CODE_ADMIN_PORT_OFFSET = 3
104
105
  RAG_CHUNK_CHARS = 1200
105
106
  RAG_CHUNK_OVERLAP = 180
106
- RAG_MAX_CHUNKS_PER_DOC = 200
107
+ RAG_MAX_CHUNKS_PER_DOC = 500
107
108
  CODE_CHUNK_CHARS = 1800
108
109
  CODE_CHUNK_OVERLAP = 120
109
110
  CODE_MAX_CHUNKS_PER_DOC = 260
@@ -152,7 +153,7 @@ MIN_AGENT_ROUNDS = 8
152
153
  MAX_AGENT_ROUNDS_CAP = 400
153
154
  REPEATED_TOOL_LOOP_THRESHOLD = 2
154
155
  BASH_READ_LOOP_THRESHOLD = 3
155
- HARD_BREAK_TOOL_ERROR_THRESHOLD = 3
156
+ HARD_BREAK_TOOL_ERROR_THRESHOLD = 20
156
157
  HARD_BREAK_RECOVERY_ROUND_THRESHOLD = 3
157
158
  FUSED_FAULT_BREAK_THRESHOLD = 3
158
159
  STALL_SEVERITY_ESCALATION_THRESHOLD = 5
@@ -974,11 +975,214 @@ OFFLINE_JS_LIB_CATALOG: list[dict[str, object]] = [
974
975
  ],
975
976
  "match_tokens": ["cdn.tailwindcss.com", "@tailwindcss/browser", "tailwindcss"],
976
977
  },
978
+ {
979
+ "id": "mathjax",
980
+ "filename": "tex-mml-chtml.js",
981
+ "relative_path": "mathjax/es5/tex-mml-chtml.js",
982
+ "package_urls": [
983
+ "https://registry.npmjs.org/mathjax/-/mathjax-3.2.2.tgz",
984
+ ],
985
+ "package_install_dir": "mathjax",
986
+ "package_required_paths": [
987
+ "package.json",
988
+ "es5/core.js",
989
+ "es5/loader.js",
990
+ "es5/startup.js",
991
+ "es5/tex-mml-chtml.js",
992
+ ],
993
+ "match_tokens": ["mathjax", "tex-mml-chtml.js", "/mathjax@"],
994
+ },
995
+ {
996
+ "id": "katex",
997
+ "filename": "katex.min.js",
998
+ "relative_path": "katex/dist/katex.min.js",
999
+ "package_urls": [
1000
+ "https://registry.npmjs.org/katex/-/katex-0.16.11.tgz",
1001
+ ],
1002
+ "package_install_dir": "katex",
1003
+ "package_required_paths": [
1004
+ "package.json",
1005
+ "dist/katex.min.js",
1006
+ "dist/katex.min.css",
1007
+ "dist/contrib/auto-render.min.js",
1008
+ ],
1009
+ "match_tokens": ["katex", "katex.min.js", "/katex@"],
1010
+ },
1011
+ {
1012
+ "id": "katex_auto_render",
1013
+ "filename": "auto-render.min.js",
1014
+ "relative_path": "katex/dist/contrib/auto-render.min.js",
1015
+ "match_tokens": ["auto-render.min.js", "katex/contrib/auto-render", "katex-auto-render"],
1016
+ },
1017
+ {
1018
+ "id": "html2canvas",
1019
+ "filename": "html2canvas.min.js",
1020
+ "urls": [
1021
+ "https://cdn.jsdelivr.net/npm/html2canvas@1.4.1/dist/html2canvas.min.js",
1022
+ "https://unpkg.com/html2canvas@1.4.1/dist/html2canvas.min.js",
1023
+ ],
1024
+ "match_tokens": ["html2canvas", "html2canvas.min.js"],
1025
+ },
1026
+ {
1027
+ "id": "jspdf",
1028
+ "filename": "jspdf.umd.min.js",
1029
+ "urls": [
1030
+ "https://cdn.jsdelivr.net/npm/jspdf@2.5.1/dist/jspdf.umd.min.js",
1031
+ "https://unpkg.com/jspdf@2.5.1/dist/jspdf.umd.min.js",
1032
+ ],
1033
+ "match_tokens": ["jspdf", "jspdf.umd.min.js"],
1034
+ },
1035
+ {
1036
+ "id": "xlsx",
1037
+ "filename": "xlsx.full.min.js",
1038
+ "urls": [
1039
+ "https://cdn.jsdelivr.net/npm/xlsx@0.18.5/dist/xlsx.full.min.js",
1040
+ "https://unpkg.com/xlsx@0.18.5/dist/xlsx.full.min.js",
1041
+ ],
1042
+ "match_tokens": ["xlsx", "xlsx.full.min.js", "sheetjs"],
1043
+ },
1044
+ {
1045
+ "id": "jszip",
1046
+ "filename": "jszip.min.js",
1047
+ "relative_path": "node_modules/jszip/dist/jszip.min.js",
1048
+ "package_urls": [
1049
+ "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz",
1050
+ ],
1051
+ "package_install_dir": "node_modules/jszip",
1052
+ "package_required_paths": [
1053
+ "package.json",
1054
+ "dist/jszip.min.js",
1055
+ ],
1056
+ "package_postprocess": "jszip-main-to-dist",
1057
+ "match_tokens": ["jszip", "jszip.min.js"],
1058
+ },
1059
+ {
1060
+ "id": "pptxgenjs",
1061
+ "filename": "pptxgen.bundle.js",
1062
+ "relative_path": "pptxgenjs/dist/pptxgen.bundle.js",
1063
+ "package_urls": [
1064
+ "https://registry.npmjs.org/pptxgenjs/-/pptxgenjs-4.0.1.tgz",
1065
+ ],
1066
+ "package_install_dir": "pptxgenjs",
1067
+ "package_required_paths": [
1068
+ "package.json",
1069
+ "dist/pptxgen.bundle.js",
1070
+ "dist/pptxgen.cjs.js",
1071
+ "dist/pptxgen.es.js",
1072
+ "dist/pptxgen.min.js",
1073
+ ],
1074
+ "match_tokens": ["pptxgenjs", "pptxgen.bundle.js", "pptxgen.cjs.js", "pptxgen.es.js", "pptxgen.min.js", "jszip.min.js"],
1075
+ },
1076
+ {
1077
+ "id": "pptxgenjs_bundle",
1078
+ "filename": "pptxgen.bundle.js",
1079
+ "relative_path": "pptxgenjs/dist/pptxgen.bundle.js",
1080
+ "match_tokens": ["pptxgenjs", "pptxgen.bundle.js", "pptxgen.bundle"],
1081
+ },
1082
+ {
1083
+ "id": "pptxgenjs_cjs",
1084
+ "filename": "pptxgen.cjs.js",
1085
+ "relative_path": "pptxgenjs/dist/pptxgen.cjs.js",
1086
+ "match_tokens": ["pptxgenjs", "pptxgen.cjs.js", "pptxgen.cjs"],
1087
+ },
1088
+ {
1089
+ "id": "pptxgenjs_es",
1090
+ "filename": "pptxgen.es.js",
1091
+ "relative_path": "pptxgenjs/dist/pptxgen.es.js",
1092
+ "match_tokens": ["pptxgenjs", "pptxgen.es.js", "pptxgen.es"],
1093
+ },
1094
+ {
1095
+ "id": "pptxgenjs_min",
1096
+ "filename": "pptxgen.min.js",
1097
+ "relative_path": "pptxgenjs/dist/pptxgen.min.js",
1098
+ "match_tokens": ["pptxgenjs", "pptxgen.min.js", "pptxgen.min"],
1099
+ },
977
1100
  ]
978
1101
  OFFLINE_JS_LIB_INDEX_FILE = "index.json"
979
1102
  OFFLINE_JS_LIB_README_FILE = "README.md"
980
1103
 
981
1104
 
1105
+ def _normalize_js_lib_asset_ref(value: str) -> str:
1106
+ raw = str(value or "").replace("\\", "/").strip()
1107
+ if not raw:
1108
+ return ""
1109
+ raw = raw.lstrip("/")
1110
+ pure = PurePosixPath(raw)
1111
+ parts: list[str] = []
1112
+ for part in pure.parts:
1113
+ if part in {"", "."}:
1114
+ continue
1115
+ if part == "..":
1116
+ return ""
1117
+ parts.append(part)
1118
+ return "/".join(parts)
1119
+
1120
+
1121
+ def _resolve_js_lib_asset_path(js_root: Path, asset_ref: str) -> Path | None:
1122
+ rel = _normalize_js_lib_asset_ref(asset_ref)
1123
+ if not rel:
1124
+ return None
1125
+ exact = (js_root / rel).resolve()
1126
+ try:
1127
+ if exact.is_file() and exact.is_relative_to(js_root):
1128
+ return exact
1129
+ except Exception:
1130
+ pass
1131
+ basename = _safe_js_filename(Path(rel).name, "lib.js")
1132
+ candidates: list[Path] = []
1133
+ try:
1134
+ for fp in js_root.rglob(basename):
1135
+ try:
1136
+ resolved = fp.resolve()
1137
+ except Exception:
1138
+ continue
1139
+ if resolved.is_file():
1140
+ try:
1141
+ if resolved.is_relative_to(js_root):
1142
+ candidates.append(resolved)
1143
+ except Exception:
1144
+ continue
1145
+ except Exception:
1146
+ return None
1147
+ if not candidates:
1148
+ return None
1149
+ candidates.sort(key=lambda p: (len(p.relative_to(js_root).parts), p.relative_to(js_root).as_posix()))
1150
+ return candidates[0]
1151
+
1152
+
1153
+ def _discover_extra_js_lib_files(js_root: Path, known_relative_paths: set[str]) -> list[dict]:
1154
+ rows: list[dict] = []
1155
+ if not js_root.exists():
1156
+ return rows
1157
+ seen: set[str] = set()
1158
+ for fp in sorted(js_root.rglob("*")):
1159
+ if not fp.is_file() or fp.name in {".DS_Store", OFFLINE_JS_LIB_INDEX_FILE, OFFLINE_JS_LIB_README_FILE}:
1160
+ continue
1161
+ if fp.suffix.lower() not in {".js", ".mjs", ".cjs"}:
1162
+ continue
1163
+ rel = fp.relative_to(js_root).as_posix()
1164
+ if rel in known_relative_paths or rel in seen:
1165
+ continue
1166
+ seen.add(rel)
1167
+ stem = fp.stem.replace(".min", "").replace(".umd", "").replace(".bundle", "")
1168
+ rows.append(
1169
+ {
1170
+ "id": f"local:{stem or fp.name}",
1171
+ "filename": fp.name,
1172
+ "relative_path": rel,
1173
+ "available": True,
1174
+ "size": int(fp.stat().st_size),
1175
+ "sha256": _sha256_file(fp),
1176
+ "source": "existing-local",
1177
+ "error": "",
1178
+ "match_tokens": [fp.name.lower(), stem.lower(), rel.lower()],
1179
+ "urls": [],
1180
+ "catalog": False,
1181
+ }
1182
+ )
1183
+ return rows
1184
+
1185
+
982
1186
  def normalize_ui_language(raw: str | None) -> str:
983
1187
  key = str(raw or "").strip()
984
1188
  if key in UI_LANGUAGE_LABELS:
@@ -1105,6 +1309,7 @@ def _detect_os_shell_instruction() -> str:
1105
1309
  "Package manager is 'brew'. "
1106
1310
  "Environment variables WORKSPACE_ROOT, SESSION_ROOT, and SKILLS_ROOT are available. "
1107
1311
  "Virtual aliases '/workspace/...' and '/skills/...' are supported in shell commands and rewritten to real paths before execution. "
1312
+ "Virtual alias '/js_lib/...' maps to the offline JS libraries root ($JS_LIB_ROOT) and is also supported in file tools. "
1108
1313
  "Do NOT assume Linux-specific paths like /proc or /etc/os-release exist. "
1109
1314
  "IMPORTANT: The workspace path contains spaces. Always use relative paths "
1110
1315
  "(e.g., 'ls uploaded/' not 'ls /full/absolute/path/uploaded/'). "
@@ -1253,6 +1458,28 @@ def extract_ui_style_setting(raw: object) -> str | None:
1253
1458
  return None
1254
1459
 
1255
1460
 
1461
+ def extract_js_lib_download_setting(raw: object) -> bool | None:
1462
+ """Read download_js_lib flag from config dict.
1463
+ Keys accepted: download_js_lib / js_lib_download / enable_js_lib_download
1464
+ Sections searched: top-level, then 'startup' / 'offline' / 'web_ui'.
1465
+ Returns True/False, or None if key absent (caller uses default=True).
1466
+ """
1467
+ if not isinstance(raw, dict):
1468
+ return None
1469
+ keys = ("download_js_lib", "js_lib_download", "enable_js_lib_download", "offline_js_download")
1470
+ for key in keys:
1471
+ if key in raw:
1472
+ return _to_bool_like(raw.get(key), default=True)
1473
+ for section_key in ("startup", "offline", "web_ui", "ui"):
1474
+ section = raw.get(section_key)
1475
+ if not isinstance(section, dict):
1476
+ continue
1477
+ for key in keys:
1478
+ if key in section:
1479
+ return _to_bool_like(section.get(key), default=True)
1480
+ return None
1481
+
1482
+
1256
1483
  def default_multimodal_capabilities() -> dict[str, bool]:
1257
1484
  return {
1258
1485
  "input_image": False,
@@ -1618,20 +1845,194 @@ def _download_http_bytes(url: str, timeout: float = 25.0) -> tuple[bytes, str]:
1618
1845
  def offline_js_lib_root(workdir: Path = WORKDIR) -> Path:
1619
1846
  return (workdir / "js_lib").resolve()
1620
1847
 
1848
+ def _offline_js_entry_relative_path(entry: dict[str, object], fallback_name: str) -> str:
1849
+ rel = _normalize_js_lib_asset_ref(str(entry.get("relative_path", "") or ""))
1850
+ if rel:
1851
+ return rel
1852
+ return _safe_js_filename(fallback_name, fallback_name)
1853
+
1854
+ def _archive_member_relative_path(name: str) -> str:
1855
+ raw = _normalize_js_lib_asset_ref(name)
1856
+ if not raw:
1857
+ return ""
1858
+ parts = [part for part in PurePosixPath(raw).parts if part not in {"", "."}]
1859
+ while parts and parts[0].lower() == "package":
1860
+ parts = parts[1:]
1861
+ if not parts:
1862
+ return ""
1863
+ return "/".join(parts)
1864
+
1865
+ def _path_size_bytes(target: Path) -> int:
1866
+ try:
1867
+ if target.is_file():
1868
+ return int(target.stat().st_size)
1869
+ if not target.exists():
1870
+ return 0
1871
+ total = 0
1872
+ for fp in target.rglob("*"):
1873
+ try:
1874
+ if fp.is_file():
1875
+ total += int(fp.stat().st_size)
1876
+ except Exception:
1877
+ continue
1878
+ return total
1879
+ except Exception:
1880
+ return 0
1881
+
1882
+ def _extract_archive_to_dir(raw: bytes, install_root: Path) -> list[str]:
1883
+ install_root.mkdir(parents=True, exist_ok=True)
1884
+ install_root_resolved = install_root.resolve()
1885
+ written: list[str] = []
1886
+
1887
+ def _write_bytes(rel: str, data: bytes):
1888
+ target = (install_root / rel).resolve()
1889
+ if not target.is_relative_to(install_root_resolved):
1890
+ raise ValueError(f"archive member escapes target dir: {rel}")
1891
+ target.parent.mkdir(parents=True, exist_ok=True)
1892
+ target.write_bytes(data)
1893
+ written.append(rel)
1894
+
1895
+ bio = io.BytesIO(raw)
1896
+ try:
1897
+ if zipfile.is_zipfile(bio):
1898
+ bio.seek(0)
1899
+ with zipfile.ZipFile(bio, "r") as zf:
1900
+ for info in zf.infolist():
1901
+ if info.is_dir():
1902
+ continue
1903
+ rel = _archive_member_relative_path(info.filename)
1904
+ if not rel:
1905
+ continue
1906
+ _write_bytes(rel, zf.read(info))
1907
+ return written
1908
+ except Exception:
1909
+ pass
1910
+
1911
+ with tarfile.open(fileobj=io.BytesIO(raw), mode="r:*") as tf:
1912
+ for member in tf.getmembers():
1913
+ if not member.isfile():
1914
+ continue
1915
+ rel = _archive_member_relative_path(member.name)
1916
+ if not rel:
1917
+ continue
1918
+ extracted = tf.extractfile(member)
1919
+ if extracted is None:
1920
+ continue
1921
+ _write_bytes(rel, extracted.read())
1922
+ return written
1923
+
1924
+ def _package_required_paths(entry: dict[str, object]) -> list[str]:
1925
+ rows: list[str] = []
1926
+ for item in entry.get("package_required_paths") or []:
1927
+ rel = _normalize_js_lib_asset_ref(str(item or ""))
1928
+ if rel:
1929
+ rows.append(rel)
1930
+ return rows
1931
+
1932
+ def _package_install_ready(install_root: Path, required_paths: list[str]) -> bool:
1933
+ if not install_root.exists():
1934
+ return False
1935
+ if required_paths:
1936
+ return all((install_root / rel).is_file() for rel in required_paths)
1937
+ try:
1938
+ return any(fp.is_file() for fp in install_root.rglob("*"))
1939
+ except Exception:
1940
+ return False
1941
+
1942
+ def _postprocess_offline_js_package(entry: dict[str, object], install_root: Path):
1943
+ action = str(entry.get("package_postprocess", "") or "").strip().lower()
1944
+ if action != "jszip-main-to-dist":
1945
+ return
1946
+ pkg_json = (install_root / "package.json").resolve()
1947
+ dist_fp = (install_root / "dist" / "jszip.min.js").resolve()
1948
+ if not pkg_json.exists():
1949
+ if not dist_fp.exists():
1950
+ return
1951
+ obj: dict[str, object] = {
1952
+ "name": "jszip",
1953
+ "version": "offline-local",
1954
+ "main": "./dist/jszip.min.js",
1955
+ "exports": {".": "./dist/jszip.min.js"},
1956
+ }
1957
+ else:
1958
+ try:
1959
+ raw = pkg_json.read_text(encoding="utf-8")
1960
+ loaded = json.loads(raw)
1961
+ if not isinstance(loaded, dict):
1962
+ return
1963
+ obj = loaded
1964
+ except Exception:
1965
+ return
1966
+ changed = False
1967
+ if obj.get("main") != "./dist/jszip.min.js":
1968
+ obj["main"] = "./dist/jszip.min.js"
1969
+ changed = True
1970
+ exports = obj.get("exports")
1971
+ desired_exports = {".": "./dist/jszip.min.js"}
1972
+ if exports != desired_exports:
1973
+ obj["exports"] = desired_exports
1974
+ changed = True
1975
+ if changed or (not pkg_json.exists()):
1976
+ pkg_json.parent.mkdir(parents=True, exist_ok=True)
1977
+ pkg_json.write_text(json_dumps(obj, indent=2), encoding="utf-8")
1978
+
1979
+ def _ensure_offline_js_package(root: Path, entry: dict[str, object], force: bool = False) -> tuple[bool, str, str, str]:
1980
+ package_urls = [str(x).strip() for x in (entry.get("package_urls") or []) if str(x).strip()]
1981
+ if not package_urls:
1982
+ return True, "", "", ""
1983
+ install_dir = _normalize_js_lib_asset_ref(str(entry.get("package_install_dir", "") or ""))
1984
+ if not install_dir:
1985
+ install_dir = _normalize_js_lib_asset_ref(str(entry.get("id", "") or "package"))
1986
+ if not install_dir:
1987
+ return False, "missing", "package install dir is empty", ""
1988
+ install_root = (root / install_dir).resolve()
1989
+ if not install_root.is_relative_to(root):
1990
+ return False, "missing", f"package install dir escapes js_lib: {install_dir}", install_dir
1991
+ required_paths = _package_required_paths(entry)
1992
+ if not force and install_root.exists():
1993
+ _postprocess_offline_js_package(entry, install_root)
1994
+ if _package_install_ready(install_root, required_paths):
1995
+ return True, "existing-package", "", install_dir
1996
+ try:
1997
+ if any(fp.is_file() for fp in install_root.rglob("*")):
1998
+ return True, "existing-package", "", install_dir
1999
+ except Exception:
2000
+ pass
2001
+ error = ""
2002
+ source = "missing"
2003
+ for url in package_urls:
2004
+ try:
2005
+ data, _ = _download_http_bytes(url, timeout=90.0)
2006
+ if len(data) < 200:
2007
+ error = f"archive too small from {url}"
2008
+ continue
2009
+ _extract_archive_to_dir(data, install_root)
2010
+ _postprocess_offline_js_package(entry, install_root)
2011
+ if _package_install_ready(install_root, required_paths):
2012
+ return True, url, "", install_dir
2013
+ error = f"package install incomplete from {url}"
2014
+ source = url
2015
+ except Exception as exc:
2016
+ error = trim(str(exc), 220)
2017
+ source = url
2018
+ return False, source, error, install_dir
2019
+
1621
2020
  def _render_offline_js_catalog_md() -> str:
1622
2021
  rows = [
1623
2022
  "# Offline JS Library Catalog",
1624
2023
  "",
1625
2024
  "Pre-fetched common JS libraries for offline HTML deliverables.",
2025
+ "Additional local `.js/.mjs/.cjs` files placed anywhere under `js_lib/` are auto-indexed in `index.json` and can be served by relative path.",
1626
2026
  "",
1627
- "| id | filename | source urls |",
1628
- "|---|---|---|",
2027
+ "| id | relative path | file urls | package urls |",
2028
+ "|---|---|---|---|",
1629
2029
  ]
1630
2030
  for row in OFFLINE_JS_LIB_CATALOG:
1631
2031
  lib_id = str(row.get("id", "") or "").strip()
1632
- filename = str(row.get("filename", "") or "").strip()
2032
+ filename = _offline_js_entry_relative_path(row, str(row.get("filename", "") or lib_id or "lib.js"))
1633
2033
  urls = [str(x).strip() for x in (row.get("urls") or []) if str(x).strip()]
1634
- rows.append(f"| `{lib_id}` | `{filename}` | {'<br>'.join(urls)} |")
2034
+ package_urls = [str(x).strip() for x in (row.get("package_urls") or []) if str(x).strip()]
2035
+ rows.append(f"| `{lib_id}` | `{filename}` | {'<br>'.join(urls)} | {'<br>'.join(package_urls)} |")
1635
2036
  return "\n".join(rows) + "\n"
1636
2037
 
1637
2038
  def load_offline_js_lib_index(js_root: Path) -> dict:
@@ -1645,26 +2046,78 @@ def load_offline_js_lib_index(js_root: Path) -> dict:
1645
2046
  except Exception:
1646
2047
  return {}
1647
2048
 
1648
- def ensure_offline_js_libs(workdir: Path = WORKDIR, force: bool = False) -> dict:
2049
+ def ensure_offline_js_libs(
2050
+ workdir: Path = WORKDIR,
2051
+ force: bool = False,
2052
+ verbose: bool = False,
2053
+ no_connection_deadline: float = 60.0,
2054
+ ) -> dict:
1649
2055
  root = offline_js_lib_root(workdir)
1650
2056
  root.mkdir(parents=True, exist_ok=True)
1651
2057
  fetched = 0
1652
2058
  available = 0
1653
2059
  missing = 0
2060
+ _dl_start = time.time()
2061
+ _deadline_hit = False
2062
+ total_catalog = len(OFFLINE_JS_LIB_CATALOG)
2063
+ if verbose:
2064
+ print(f"[js_lib] Starting JS library download ({total_catalog} files)...", flush=True)
1654
2065
  rows: list[dict] = []
2066
+ known_relative_paths: set[str] = set()
1655
2067
  for entry in OFFLINE_JS_LIB_CATALOG:
1656
2068
  lib_id = str(entry.get("id", "") or "").strip() or "lib"
1657
2069
  filename = _safe_js_filename(str(entry.get("filename", "") or f"{lib_id}.js"), f"{lib_id}.js")
2070
+ relative_path = _offline_js_entry_relative_path(entry, filename)
1658
2071
  urls = [str(x).strip() for x in (entry.get("urls") or []) if str(x).strip()]
2072
+ package_urls = [str(x).strip() for x in (entry.get("package_urls") or []) if str(x).strip()]
1659
2073
  match_tokens = [str(x).strip().lower() for x in (entry.get("match_tokens") or []) if str(x).strip()]
1660
- target = root / filename
2074
+ target = (root / relative_path).resolve()
2075
+ if not target.is_relative_to(root):
2076
+ rows.append(
2077
+ {
2078
+ "id": lib_id,
2079
+ "filename": filename,
2080
+ "available": False,
2081
+ "size": 0,
2082
+ "sha256": "",
2083
+ "source": "missing",
2084
+ "error": f"relative path escapes js_lib: {relative_path}",
2085
+ "match_tokens": match_tokens,
2086
+ "urls": urls,
2087
+ "package_urls": package_urls,
2088
+ "relative_path": relative_path,
2089
+ "package_install_dir": "",
2090
+ "package_required_paths": [],
2091
+ "package_available": not package_urls,
2092
+ "package_size": 0,
2093
+ "catalog": True,
2094
+ }
2095
+ )
2096
+ missing += 1
2097
+ continue
2098
+ known_relative_paths.add(relative_path)
1661
2099
  source = "existing"
1662
2100
  error = ""
1663
- ok = bool(target.exists() and target.is_file() and target.stat().st_size > 40 and (not force))
1664
- if not ok:
2101
+ effective_target = target
2102
+ file_ok = bool(target.exists() and target.is_file() and target.stat().st_size > 40 and (not force))
2103
+ if (not file_ok) and (not force):
2104
+ resolved_existing = _resolve_js_lib_asset_path(root, relative_path)
2105
+ if resolved_existing and resolved_existing.exists() and resolved_existing.is_file() and resolved_existing.stat().st_size > 40:
2106
+ effective_target = resolved_existing
2107
+ file_ok = True
2108
+ source = "existing-local"
2109
+ if not file_ok and urls:
2110
+ if fetched == 0 and (time.time() - _dl_start) > no_connection_deadline:
2111
+ _deadline_hit = True
2112
+ if verbose:
2113
+ print(f"[js_lib] No connection after {no_connection_deadline:.0f}s — skipping remaining downloads.", flush=True)
2114
+ break
2115
+ if verbose:
2116
+ print(f"[js_lib] Downloading {filename}...", flush=True)
1665
2117
  for url in urls:
1666
2118
  try:
1667
- data, _ = _download_http_bytes(url, timeout=35.0)
2119
+ _timeout = 12.0 if fetched == 0 else 35.0
2120
+ data, _ = _download_http_bytes(url, timeout=_timeout)
1668
2121
  if len(data) < 40:
1669
2122
  error = f"download too small from {url}"
1670
2123
  continue
@@ -1672,39 +2125,67 @@ def ensure_offline_js_libs(workdir: Path = WORKDIR, force: bool = False) -> dict
1672
2125
  if "<html" in probe and "<script" not in probe and "tailwind" not in probe:
1673
2126
  error = f"unexpected html payload from {url}"
1674
2127
  continue
2128
+ target.parent.mkdir(parents=True, exist_ok=True)
1675
2129
  target.write_bytes(data)
1676
- ok = True
2130
+ effective_target = target
2131
+ file_ok = True
1677
2132
  source = url
1678
2133
  fetched += 1
1679
2134
  break
1680
2135
  except Exception as exc:
1681
2136
  error = trim(str(exc), 220)
1682
- if not ok:
2137
+ if not file_ok:
1683
2138
  source = "missing"
2139
+ package_ok, package_source, package_error, package_install_dir = _ensure_offline_js_package(root, entry, force=force)
2140
+ if package_urls and package_source and package_source not in {"existing-package", "missing"}:
2141
+ fetched += 1
2142
+ if package_error:
2143
+ error = package_error if not error else f"{error}; {package_error}"
2144
+ ok = bool((file_ok or (not urls)) and package_ok and effective_target.exists() and effective_target.is_file() and effective_target.stat().st_size > 40)
1684
2145
  if ok:
1685
2146
  available += 1
1686
2147
  else:
1687
2148
  missing += 1
2149
+ if package_urls and package_source == "missing" and not source.strip():
2150
+ source = "missing"
2151
+ package_root = (root / package_install_dir).resolve() if package_install_dir else None
1688
2152
  rows.append(
1689
2153
  {
1690
2154
  "id": lib_id,
1691
2155
  "filename": filename,
1692
2156
  "available": ok,
1693
- "size": int(target.stat().st_size) if target.exists() else 0,
1694
- "sha256": _sha256_file(target) if target.exists() else "",
1695
- "source": source,
2157
+ "size": int(effective_target.stat().st_size) if effective_target.exists() else 0,
2158
+ "sha256": _sha256_file(effective_target) if effective_target.exists() else "",
2159
+ "source": source if source != "existing" or file_ok else package_source or source,
1696
2160
  "error": error,
1697
2161
  "match_tokens": match_tokens,
1698
2162
  "urls": urls,
2163
+ "package_urls": package_urls,
2164
+ "relative_path": relative_path,
2165
+ "resolved_path": effective_target.relative_to(root).as_posix() if effective_target.exists() else "",
2166
+ "package_install_dir": package_install_dir,
2167
+ "package_required_paths": _package_required_paths(entry),
2168
+ "package_available": package_ok,
2169
+ "package_size": _path_size_bytes(package_root) if package_root else 0,
2170
+ "package_source": package_source,
2171
+ "catalog": True,
1699
2172
  }
1700
2173
  )
2174
+ extra_rows = _discover_extra_js_lib_files(root, known_relative_paths)
2175
+ rows.extend(extra_rows)
2176
+ if verbose:
2177
+ _status = "skipped (no connection)" if _deadline_hit else f"{fetched} downloaded"
2178
+ print(f"[js_lib] Done: {available}/{len(rows)} available, {_status}.", flush=True)
1701
2179
  payload = {
1702
2180
  "generated_at": int(now_ts()),
1703
2181
  "js_lib_root": str(root),
1704
- "total": len(OFFLINE_JS_LIB_CATALOG),
2182
+ "total": len(rows),
1705
2183
  "available": available,
1706
2184
  "missing": missing,
1707
2185
  "fetched": fetched,
2186
+ "catalog_total": len(OFFLINE_JS_LIB_CATALOG),
2187
+ "catalog_missing": missing,
2188
+ "extra_local": len(extra_rows),
1708
2189
  "libs": rows,
1709
2190
  }
1710
2191
  (root / OFFLINE_JS_LIB_INDEX_FILE).write_text(json_dumps(payload, indent=2), encoding="utf-8")
@@ -9043,7 +9524,12 @@ TOOLS = [
9043
9524
  ),
9044
9525
  tool_def(
9045
9526
  "query_knowledge_library",
9046
- "Read current global knowledge-library status or query the TF-Graph_IDF RAG library for grounded document references.",
9527
+ (
9528
+ "Query the RAG knowledge library for grounded document references and background knowledge. "
9529
+ "Call this BEFORE answering questions that require domain expertise, factual grounding, "
9530
+ "or synthesis from imported documents. "
9531
+ "Pass an empty query to check library status only."
9532
+ ),
9047
9533
  {
9048
9534
  "query": {"type": "string"},
9049
9535
  "top_k": {"type": "integer"},
@@ -10780,6 +11266,32 @@ class SessionState:
10780
11266
  self.runtime_code_reference_meta = {}
10781
11267
  return removed_hints
10782
11268
 
11269
+ def _reset_blackboard_plan_state_locked(self) -> None:
11270
+ """Clear plan/todo/skills state from a completed run so the next run starts fresh.
11271
+
11272
+ Called from submit_user_message when a new user request arrives after a
11273
+ previous run finished (running=False, not awaiting plan choice).
11274
+ Prevents the manager from seeing status=COMPLETED + all todos done and
11275
+ immediately routing to 'finish' again on the very first round.
11276
+ """
11277
+ bb = self._ensure_blackboard()
11278
+ bb["project_todos"] = []
11279
+ bb["plan_steps"] = []
11280
+ bb["plan_step_cursor"] = 0
11281
+ bb["plan_step_total"] = 0
11282
+ bb["status"] = ""
11283
+ bb["approval"] = ""
11284
+ bb["plan_findings"] = ""
11285
+ bb["plan_proposal"] = ""
11286
+ bb["plan_risks"] = ""
11287
+ bb["loaded_skills"] = {}
11288
+ bb["loaded_skills_goal_sig"] = ""
11289
+ self.blackboard = bb
11290
+ try:
11291
+ self.todo.items = []
11292
+ except Exception:
11293
+ pass
11294
+
10783
11295
  def _event_payload_with_agent_role(self, kind: str, data: dict | None) -> dict:
10784
11296
  payload = dict(data or {})
10785
11297
  if self._sanitize_agent_bubble_role(payload.get("agent_role", "")):
@@ -11133,7 +11645,10 @@ class SessionState:
11133
11645
  skill_desc = str(skill_row.get("description", "-")).strip()
11134
11646
  inject_msg = (
11135
11647
  f"<loaded-skill name=\"{skill_key}\">\n"
11136
- f"A skill has been loaded. Follow its instructions precisely.\n"
11648
+ f"A skill has been loaded. IMPORTANT: This skill's workflow, tools, and commands "
11649
+ f"OVERRIDE the plan's implementation approach for any step where it applies. "
11650
+ f"Read the full instructions below and follow them exactly — do NOT substitute a "
11651
+ f"different tool, library, or language unless the skill explicitly allows it.\n"
11137
11652
  f"{trim(skill_text, 12000)}\n"
11138
11653
  f"</loaded-skill>"
11139
11654
  )
@@ -11594,14 +12109,28 @@ class SessionState:
11594
12109
  )
11595
12110
  return (
11596
12111
  f"ACTIVE SKILLS: {names}. "
11597
- "Follow loaded skill instructions precisely. "
11598
- f"If you encounter a step requiring a workflow you don't know, call load_skill ({skill_count} available). "
12112
+ "Follow the loaded skill instructions for the current step. "
12113
+ f"When moving to a different step that needs a DIFFERENT skill, call load_skill to switch "
12114
+ f"(or unload the current one first if it's no longer needed). "
12115
+ f"{skill_count} skills available total. "
11599
12116
  )
11600
12117
  return (
11601
12118
  f"SKILL SYSTEM: {skill_count} skills available. "
11602
- "Use list_skills to discover, load_skill to activate. "
11603
- "If unsure how to produce professional output (docs, slides, analysis), check skills first. "
11604
- )
12119
+ "Skills are loaded ON-DEMAND — decide when you need one based on the CURRENT step, not upfront. "
12120
+ "For specialized output (reports, slides/PPT, deep research, code review, PDF analysis): "
12121
+ "call list_skills to discover options, then load_skill to activate the right one. "
12122
+ "Load a skill AT THE MOMENT you begin the step that requires it. "
12123
+ "Unload it (via unload_skill) when moving to a different step that needs a different skill. "
12124
+ "For simple tasks, direct questions, and multimodal analysis, do NOT load skills. "
12125
+ )
12126
+
12127
+ def _skills_awareness_block(self, for_role: str = "developer") -> str:
12128
+ """Canonical skills-awareness block shared by single, sync, and plan-mode.
12129
+ Returns: loaded-skills hint + newline + 'Skills:\\n<catalog>'
12130
+ Keeps all three modes in sync — change here propagates everywhere.
12131
+ """
12132
+ hint = self._loaded_skills_prompt_hint(for_role=for_role)
12133
+ return f"{hint}\nSkills:\n{self.skills.descriptions()}\n"
11605
12134
 
11606
12135
  def _refresh_runtime_code_reference(self, text: str):
11607
12136
  cb = getattr(self, "reference_prepare_callback", None)
@@ -11663,10 +12192,40 @@ class SessionState:
11663
12192
  header += " [" + ", ".join(tags) + "]"
11664
12193
  return (
11665
12194
  f"{header}:\n"
11666
- "This is the global TF-Graph_IDF RAG knowledge library for imported documents and research material. "
11667
- "It lives at the workspace root, not inside the current session files directory and not inside `.clouds_coder`. "
11668
- "Do not infer knowledge-library readiness by inspecting `session/files`, `uploads`, or `.clouds_coder/long_output`. "
11669
- "Use `query_knowledge_library` to check readiness or retrieve grounded references from the global library."
12195
+ "Global TF-Graph_IDF RAG library for imported documents, PDFs, and research material. "
12196
+ "IMPORTANT: When the task involves a topic you may have documents for research, analysis, "
12197
+ "fact-checking, synthesis FIRST call query_knowledge_library(query='<topic>', top_k=8) "
12198
+ "to retrieve grounded references BEFORE generating your answer. "
12199
+ "Use route='hybrid' for best recall on broad topics; route='fast' for keyword lookups. "
12200
+ "Do not infer readiness from session/files or uploads — query the library directly."
12201
+ )
12202
+
12203
+ def _multimodal_capability_block(self) -> str:
12204
+ """Return a brief system-prompt note about native multimodal capabilities.
12205
+
12206
+ Injected into system prompt so the model knows it can directly analyze
12207
+ images/audio/video rather than falling back to text-only workarounds.
12208
+ Returns empty string when the active model has no multimodal input caps.
12209
+ """
12210
+ try:
12211
+ caps = self._capabilities_from_profile()
12212
+ except Exception:
12213
+ return ""
12214
+ types = []
12215
+ if caps.get("input_image"):
12216
+ types.append("images")
12217
+ if caps.get("input_audio"):
12218
+ types.append("audio")
12219
+ if caps.get("input_video"):
12220
+ types.append("video")
12221
+ if not types:
12222
+ return ""
12223
+ joined = "/".join(types)
12224
+ return (
12225
+ f"MULTIMODAL: This model supports native {joined} analysis. "
12226
+ f"When {joined} are attached or loaded via read_file, analyze them DIRECTLY "
12227
+ "using your built-in perception capabilities — do not describe them as "
12228
+ "inaccessible or attempt text-only workarounds. "
11670
12229
  )
11671
12230
 
11672
12231
  def _code_library_prompt_block(self) -> str:
@@ -11747,8 +12306,14 @@ class SessionState:
11747
12306
  plan_ctx = self._plan_steps_context_for_manager()
11748
12307
  if plan_ctx:
11749
12308
  plan_steps_block = f"{plan_ctx}\n"
12309
+ mm_block = self._multimodal_capability_block()
12310
+ mm_hint = f"{mm_block}\n" if mm_block else ""
11750
12311
  return (
11751
- f"You are a coding agent. Workspace: {self.files_root}. "
12312
+ f"You are a coding agent. Workspace: \"{self.files_root}\" ($SESSION_ROOT). "
12313
+ f"Offline JS libraries root: $JS_LIB_ROOT. "
12314
+ f"Structure: flat .js files at $JS_LIB_ROOT/<name>.min.js; "
12315
+ f"pptxgenjs at $JS_LIB_ROOT/pptxgenjs/dist/pptxgen.cjs.js (CommonJS require) or pptxgen.bundle.js (browser). "
12316
+ f"Do NOT look in node_modules — libs are installed directly under $JS_LIB_ROOT. "
11752
12317
  f"Task level={runtime_level}, mode={runtime_mode}, "
11753
12318
  f"budget={'unlimited' if budget <= 0 else budget}. "
11754
12319
  f"Context limit ~{self.context_token_upper_bound} tokens. "
@@ -11756,6 +12321,7 @@ class SessionState:
11756
12321
  "Use tools to inspect, edit, and execute. "
11757
12322
  "Call finish_current_task when done. "
11758
12323
  f"{skill_hint}"
12324
+ f"{mm_hint}"
11759
12325
  f"{plan_steps_block}"
11760
12326
  f"{html_block}"
11761
12327
  f"{research_block}"
@@ -13540,6 +14106,11 @@ class SessionState:
13540
14106
  if low.startswith("/workspace/"):
13541
14107
  rel = raw[len("/workspace/") :].lstrip("/")
13542
14108
  return rel or "."
14109
+ if low in {"/js_lib", "/js_lib/"}:
14110
+ return ".__js_lib__"
14111
+ if low.startswith("/js_lib/"):
14112
+ rel = raw[len("/js_lib/") :].lstrip("/")
14113
+ return f".__js_lib__/{rel}" if rel else ".__js_lib__"
13543
14114
  if low in {SKILLS_VIRTUAL_PREFIX, f"{SKILLS_VIRTUAL_PREFIX}/"}:
13544
14115
  return ".__skills__"
13545
14116
  if low.startswith(f"{SKILLS_VIRTUAL_PREFIX}/"):
@@ -13563,9 +14134,10 @@ class SessionState:
13563
14134
  return ""
13564
14135
  low = txt.lower()
13565
14136
  if (
13566
- low in {"/workspace", "/workspace/", SKILLS_VIRTUAL_PREFIX, f"{SKILLS_VIRTUAL_PREFIX}/"}
14137
+ low in {"/workspace", "/workspace/", SKILLS_VIRTUAL_PREFIX, f"{SKILLS_VIRTUAL_PREFIX}/", "/js_lib", "/js_lib/"}
13567
14138
  or low.startswith("/workspace/")
13568
14139
  or low.startswith(f"{SKILLS_VIRTUAL_PREFIX}/")
14140
+ or low.startswith("/js_lib/")
13569
14141
  ):
13570
14142
  return ""
13571
14143
  return (
@@ -13578,6 +14150,9 @@ class SessionState:
13578
14150
  if normalized == ".__skills__" or normalized.startswith(".__skills__/"):
13579
14151
  rel = normalized[len(".__skills__") :].lstrip("/")
13580
14152
  return safe_path(rel or ".", self.skills.skills_root)
14153
+ if normalized == ".__js_lib__" or normalized.startswith(".__js_lib__/"):
14154
+ rel = normalized[len(".__js_lib__") :].lstrip("/")
14155
+ return safe_path(rel or ".", self.js_lib_root)
13581
14156
  return safe_path(normalized, self.files_root)
13582
14157
 
13583
14158
  def _session_rel(self, path: Path) -> str:
@@ -13585,6 +14160,13 @@ class SessionState:
13585
14160
  root = self.files_root.resolve()
13586
14161
  if target.is_relative_to(root):
13587
14162
  return target.relative_to(root).as_posix()
14163
+ try:
14164
+ js_lib_root = self.js_lib_root.resolve()
14165
+ if target.is_relative_to(js_lib_root):
14166
+ rel = target.relative_to(js_lib_root).as_posix()
14167
+ return f"/js_lib/{rel}".replace("//", "/")
14168
+ except Exception:
14169
+ pass
13588
14170
  try:
13589
14171
  skills_root = self.skills.skills_root.resolve()
13590
14172
  if target.is_relative_to(skills_root):
@@ -13962,8 +14544,20 @@ class SessionState:
13962
14544
  }
13963
14545
 
13964
14546
  def _safe_upload_name(self, filename: str) -> str:
14547
+ # Use Path().name to strip any directory component first.
13965
14548
  raw = Path(str(filename or "upload.bin")).name
13966
- safe = re.sub(r"[^A-Za-z0-9._-]+", "_", raw).strip("._")
14549
+ # Only remove characters that are genuinely illegal or dangerous on
14550
+ # filesystems (path separators, null byte, control chars, Windows
14551
+ # reserved chars). Unicode letters/CJK/etc. are left untouched so
14552
+ # filenames like "我的数据.xlsx" remain readable.
14553
+ safe = re.sub(r'[/\\\x00-\x1f\x7f:*?"<>|]', "_", raw)
14554
+ safe = safe.strip(". ") # avoid hidden files (leading dot) and trailing issues
14555
+ # Enforce a byte-length ceiling safe across all filesystems (255 bytes max).
14556
+ # Trim the stem if needed while preserving the extension.
14557
+ if len(safe.encode("utf-8", errors="replace")) > 240:
14558
+ ext = Path(safe).suffix # e.g. ".xlsx"
14559
+ stem = safe[: max(1, 200 - len(ext))]
14560
+ safe = stem + ext
13967
14561
  return safe or f"upload_{int(now_ts())}.bin"
13968
14562
 
13969
14563
  def _decode_text_bytes(self, data: bytes) -> str:
@@ -15964,15 +16558,22 @@ class SessionState:
15964
16558
  skills_root = str(self.skills.skills_root.resolve())
15965
16559
  except Exception:
15966
16560
  skills_root = str(self.skills.skills_root)
16561
+ try:
16562
+ js_lib_root = str(self.js_lib_root.resolve())
16563
+ except Exception:
16564
+ js_lib_root = str(self.js_lib_root)
15967
16565
  mappings = [
15968
16566
  ("/workspace", workspace_root),
15969
16567
  (SKILLS_VIRTUAL_PREFIX, skills_root),
16568
+ ("/js_lib", js_lib_root),
15970
16569
  ]
15971
16570
  # Auto-quote raw absolute paths that contain spaces (prevents word-splitting)
15972
16571
  if " " in workspace_root and workspace_root not in ("/workspace",):
15973
16572
  mappings.append((workspace_root, workspace_root))
15974
16573
  if " " in skills_root and skills_root != SKILLS_VIRTUAL_PREFIX:
15975
16574
  mappings.append((skills_root, skills_root))
16575
+ if " " in js_lib_root and js_lib_root != "/js_lib":
16576
+ mappings.append((js_lib_root, js_lib_root))
15976
16577
  out: list[str] = []
15977
16578
  mode = "plain"
15978
16579
  i = 0
@@ -16313,6 +16914,7 @@ class SessionState:
16313
16914
  proc_env["SESSION_FILES_ROOT"] = str(self.files_root)
16314
16915
  proc_env["SKILLS_ROOT"] = str(self.skills.skills_root)
16315
16916
  proc_env["CLOUDS_CODER_ROOT"] = str(self.root)
16917
+ proc_env["JS_LIB_ROOT"] = str(self.js_lib_root)
16316
16918
  popen_kwargs = {
16317
16919
  "shell": True,
16318
16920
  "cwd": cwd,
@@ -16430,10 +17032,31 @@ class SessionState:
16430
17032
  def _run_bash(self, command: str) -> str:
16431
17033
  return self._run_shell_meta(command, self.files_root, 120)["output"]
16432
17034
 
17035
+ def _fuzzy_resolve_path(self, fp: Path) -> Path:
17036
+ """If fp doesn't exist, try stripping spaces from the filename to find a close match.
17037
+ Handles the common model error of hallucinating spaces in Chinese/mixed filenames.
17038
+ Returns the resolved Path if found, otherwise the original fp unchanged."""
17039
+ if fp.exists():
17040
+ return fp
17041
+ stripped = fp.name.replace(" ", "")
17042
+ if stripped != fp.name:
17043
+ candidate = fp.parent / stripped
17044
+ if candidate.exists():
17045
+ return candidate
17046
+ try:
17047
+ query = fp.name.replace(" ", "").lower()
17048
+ for sibling in fp.parent.iterdir():
17049
+ if sibling.name.replace(" ", "").lower() == query:
17050
+ return sibling
17051
+ except Exception:
17052
+ pass
17053
+ return fp
17054
+
16433
17055
  def _run_read(self, path: str, limit: int | None = None, offset: int | None = None) -> str:
16434
17056
  try:
16435
17057
  rel = self._normalize_tool_path_text(path)
16436
- fp = self._session_path(rel)
17058
+ fp = self._fuzzy_resolve_path(self._session_path(rel))
17059
+ rel = str(fp.relative_to(self.files_root)) if fp.is_relative_to(self.files_root) else rel
16437
17060
  # Multimodal: detect image/audio/video files and handle natively
16438
17061
  ext = fp.suffix.lower() if fp.suffix else ""
16439
17062
  if ext in IMAGE_EXTS:
@@ -16680,7 +17303,8 @@ class SessionState:
16680
17303
  def _run_write(self, path: str, content: str) -> str:
16681
17304
  try:
16682
17305
  rel = self._normalize_tool_path_text(path)
16683
- fp = self._session_path(rel)
17306
+ fp = self._fuzzy_resolve_path(self._session_path(rel))
17307
+ rel = str(fp.relative_to(self.files_root)) if fp.is_relative_to(self.files_root) else rel
16684
17308
  fp.parent.mkdir(parents=True, exist_ok=True)
16685
17309
  fp.write_text(content, encoding="utf-8")
16686
17310
  return f"Wrote {len(content)} bytes to {rel}"
@@ -16690,7 +17314,8 @@ class SessionState:
16690
17314
  def _run_edit(self, path: str, old_text: str, new_text: str) -> str:
16691
17315
  try:
16692
17316
  rel = self._normalize_tool_path_text(path)
16693
- fp = self._session_path(rel)
17317
+ fp = self._fuzzy_resolve_path(self._session_path(rel))
17318
+ rel = str(fp.relative_to(self.files_root)) if fp.is_relative_to(self.files_root) else rel
16694
17319
  content = fp.read_text(encoding="utf-8")
16695
17320
  if old_text not in content:
16696
17321
  diag = self._edit_mismatch_diagnostic(content, old_text)
@@ -19440,35 +20065,36 @@ class SessionState:
19440
20065
  if not current:
19441
20066
  return False
19442
20067
  text = (str(instruction or "") + " " + str(reason or "")).lower()
19443
- # Patterns that indicate step completion
20068
+ # Patterns that indicate step completion — only BACKWARD-looking signals
20069
+ # (agent/manager explicitly says a step is done, NOT forward-looking dispatch instructions)
19444
20070
  step_done_patterns = (
19445
20071
  "审查通过", "通过审查", "已通过", "已完成", "完成了",
19446
- "进入 step", "进入step", "enter step", "move to step",
19447
20072
  "step completed", "step done", "step passed",
19448
- "现在进入", "开始 step", "开始step",
19449
20073
  "阶段完成", "阶段通过", "phase complete",
19450
- # extended: manager says "now do step X.Y" implying prior step is done
19451
- "现在执行步骤", "执行步骤 1.2", "执行步骤 1.3", "执行步骤 2",
19452
- "进入下一步", "next step", "proceed to step",
19453
- "步骤 1.1 已完成", "步骤 1.2 已完成",
20074
+ "步骤 1.1 已完成", "步骤 1.2 已完成", "步骤 1.3 已完成",
20075
+ "步骤 1.4 已完成", "步骤 1.5 已完成",
19454
20076
  )
19455
- # Also detect "Step N 通过" or "进入 Step N+1" patterns
20077
+ # NOTE: intentionally excluded forward-looking dispatch patterns:
20078
+ # "现在执行步骤", "执行步骤 1.2", "执行步骤 1.3", "进入下一步", "next step",
20079
+ # "proceed to step", "进入 step", "开始 step" — these are manager dispatch
20080
+ # instructions, NOT evidence that the current step was completed.
19456
20081
  import re
19457
20082
  current_idx = int(current.get("plan_step_index", 0) or 0)
19458
- # "Step 2 已通过" / "Step 2 完成" / "进入 Step 3"
20083
+ # Only advance when an agent explicitly says "进入 Step N" where N > current+1
20084
+ # (skipping ahead), NOT when manager dispatches the very next step.
19459
20085
  next_step_pattern = re.search(
19460
20086
  r'(?:进入|enter|move\s+to|start|proceed\s+to)\s*(?:step\s*)?(\d+)',
19461
20087
  text, re.IGNORECASE
19462
20088
  )
19463
20089
  if next_step_pattern:
19464
20090
  mentioned_step = int(next_step_pattern.group(1))
19465
- if mentioned_step > current_idx + 1:
20091
+ if mentioned_step > current_idx + 2: # must skip at least 2 ahead to be meaningful
19466
20092
  return True
19467
- # Pattern: "继续步骤 1.2" / "完成步骤 1.1,开始 1.2"
19468
- step_ref = re.search(r'步骤\s*1\.(\d+)', text)
20093
+ # "完成步骤 1.1,开始 1.2" — only if explicitly marking current step done
20094
+ step_ref = re.search(r'(?:完成|finished|done)\s*步骤\s*1\.(\d+)', text)
19469
20095
  if step_ref:
19470
20096
  ref_sub = int(step_ref.group(1))
19471
- if ref_sub > (current_idx + 1):
20097
+ if ref_sub == current_idx + 1: # explicitly says current step (1-based) is done
19472
20098
  return True
19473
20099
  return any(pat in text for pat in step_done_patterns)
19474
20100
 
@@ -19524,7 +20150,7 @@ class SessionState:
19524
20150
  self._sync_todos_from_blackboard(reason=f"plan-step-advanced:{cursor + 1}", board=bb)
19525
20151
  if next_step:
19526
20152
  try:
19527
- self._refresh_loaded_skills_for_execution_focus(trigger="plan-step-advanced")
20153
+ pass # Skills are loaded on-demand by the model via load_skill
19528
20154
  except Exception:
19529
20155
  pass
19530
20156
  return True
@@ -20606,7 +21232,7 @@ class SessionState:
20606
21232
  f"budget={int(self.runtime_round_budget or 0)}.\n\n"
20607
21233
  f"{dims_ctx}"
20608
21234
  f"{skills_ctx}"
20609
- f"Workspace root: {self.files_root}\n"
21235
+ f"Workspace root: \"{self.files_root}\" ($SESSION_ROOT)\n"
20610
21236
  "Infer scale_preference by semantics (fast/balanced/thorough). "
20611
21237
  "When user preference is clear, prioritize it over your default plan. "
20612
21238
  "Remember: budget controls internal thought depth/round compactness, not early stop messaging. "
@@ -22384,14 +23010,13 @@ class SessionState:
22384
23010
  "round_budget": int(round_budget),
22385
23011
  "remaining_rounds": int(remaining_rounds),
22386
23012
  }
22387
- # advance_plan_step: 当前 plan step 完成,推进到下一步
22388
- should_advance = _to_bool_like(route.get("advance_plan_step", False), default=False)
22389
- # Auto-detect step advancement from instruction semantics
22390
- if not should_advance:
22391
- should_advance = self._instruction_implies_step_advance(
22392
- str(route.get("instruction", "") or ""),
22393
- str(route.get("reason", "") or ""),
22394
- )
23013
+ # advance_plan_step: only trust semantics from what the agent actually wrote,
23014
+ # NOT from the manager's own route JSON — the manager often sets this flag
23015
+ # simultaneously with dispatching the work, advancing the step before it runs.
23016
+ should_advance = self._instruction_implies_step_advance(
23017
+ str(route.get("instruction", "") or ""),
23018
+ str(route.get("reason", "") or ""),
23019
+ )
22395
23020
  if should_advance:
22396
23021
  self._advance_plan_step(
22397
23022
  evidence=trim(str(route.get("instruction", "") or ""), 200),
@@ -23391,19 +24016,32 @@ class SessionState:
23391
24016
 
23392
24017
  def _agent_role_system_prompt(self, role: str) -> str:
23393
24018
  role_key = self._sanitize_agent_role(role) or "developer"
23394
- skills_note = self._loaded_skills_prompt_hint(for_role=role_key)
24019
+ skills_block = self._skills_awareness_block(for_role=role_key)
23395
24020
  code_note = self._runtime_code_reference_prompt_block(max_chars=2600)
23396
24021
  base = (
23397
24022
  f"You are {self._agent_display_name(role_key)} in a multi-agent coding system. "
23398
- f"Workspace: {self.files_root}. Use relative paths. "
24023
+ f"Workspace: \"{self.files_root}\" ($SESSION_ROOT). Use relative paths or $SESSION_ROOT in bash. "
24024
+ f"Offline JS libraries root: $JS_LIB_ROOT. "
24025
+ f"Structure: flat .js files at $JS_LIB_ROOT/<name>.min.js; "
24026
+ f"pptxgenjs at $JS_LIB_ROOT/pptxgenjs/dist/pptxgen.cjs.js (CommonJS) or pptxgen.bundle.js (browser). "
24027
+ f"Do NOT look in node_modules — libs are installed directly under $JS_LIB_ROOT. "
23399
24028
  "Use blackboard for shared state, ask_colleague for inter-agent communication. "
23400
24029
  "Keep outputs concise and action-oriented. "
23401
- f"{skills_note}{code_note + ' ' if code_note else ''}"
24030
+ f"{code_note + ' ' if code_note else ''}"
23402
24031
  f"{_detect_os_shell_instruction()} "
23403
24032
  f"{model_language_instruction(self.ui_language)} "
23404
24033
  )
24034
+ mm_note = self._multimodal_capability_block()
24035
+ if mm_note:
24036
+ base = base + mm_note
24037
+ base = base + skills_block
23405
24038
  if role_key == "explorer":
23406
- return base + "Role: analyze goals, inspect codebase, produce research notes. Prefer read/search. "
24039
+ return base + (
24040
+ "Role: analyze goals, inspect codebase, produce research notes. "
24041
+ "For factual or background questions on any topic, FIRST call "
24042
+ "query_knowledge_library(query='<topic>', top_k=8, route='hybrid') to retrieve relevant documents. "
24043
+ "Prefer read/search tools. "
24044
+ )
23407
24045
  if role_key == "reviewer":
23408
24046
  if bool(self.reviewer_debug_mode):
23409
24047
  debug_ctx = trim(str(self.reviewer_debug_context or ""), 500)
@@ -23436,6 +24074,11 @@ class SessionState:
23436
24074
  )
23437
24075
  return base + (
23438
24076
  "Role: implement code changes, execute tools, record progress to blackboard. "
24077
+ "SKILL PRIORITY (critical): When ACTIVE SKILLS are listed above, find the "
24078
+ "<loaded-skill> messages in your context and READ them before starting any step. "
24079
+ "The skill's workflow, tools, and file structure OVERRIDE the plan's implementation "
24080
+ "approach — if the plan says 'use python-pptx' but the skill says 'use PptxGenJS', "
24081
+ "use PptxGenJS. The skill defines HOW to implement; the plan defines WHAT to do. "
23439
24082
  "TODO TRACKING (mandatory): "
23440
24083
  "After completing each logical step, call TodoWrite to update progress — "
23441
24084
  "mark completed items as 'completed' and set the next item to 'in_progress'. "
@@ -24389,7 +25032,8 @@ class SessionState:
24389
25032
  isinstance(it, dict) and str(it.get("status", it.get("state", ""))).lower() in {"completed", "done", "finished", "finish"}
24390
25033
  for it in new_items
24391
25034
  ):
24392
- self._refresh_loaded_skills_for_execution_focus(trigger="step-completed")
25035
+ self._refresh_loaded_skills_for_execution_focus(trigger="step-completed") # noqa: removed
25036
+ pass # Skills are loaded on-demand by the model
24393
25037
  except Exception:
24394
25038
  pass
24395
25039
  return result
@@ -24397,7 +25041,7 @@ class SessionState:
24397
25041
  result = self._todo_write_rescue(args)
24398
25042
  # Also recheck skills on rescue write (likely a recovery situation)
24399
25043
  try:
24400
- self._refresh_loaded_skills_for_execution_focus(trigger="todo-rescue")
25044
+ pass # Skills are loaded on-demand by the model via load_skill
24401
25045
  except Exception:
24402
25046
  pass
24403
25047
  return result
@@ -24848,8 +25492,6 @@ class SessionState:
24848
25492
  "</live-user-adjustment>"
24849
25493
  )
24850
25494
  self.messages.append({"role": "user", "content": payload, "ts": now_ts()})
24851
- # Merge user feedback with plan direction
24852
- self._merge_user_feedback_with_plan(content)
24853
25495
  self.runtime_reclassify_goal = trim(content, 4000)
24854
25496
  # Only trigger reclassification in auto mode (no user override)
24855
25497
  if int(getattr(self, 'user_task_level_override', 0) or 0) > 0:
@@ -24863,6 +25505,7 @@ class SessionState:
24863
25505
  "weight": weight,
24864
25506
  "priority": priority,
24865
25507
  "applied": applied,
25508
+ "content": content,
24866
25509
  }
24867
25510
  )
24868
25511
  row["applied_count"] = applied
@@ -24875,6 +25518,8 @@ class SessionState:
24875
25518
  self.updated_at = now_ts()
24876
25519
  self._persist()
24877
25520
  for item in injected:
25521
+ # Merge user feedback with plan direction (outside lock — may do LLM call)
25522
+ self._merge_user_feedback_with_plan(item["content"])
24878
25523
  self._emit(
24879
25524
  "status",
24880
25525
  {
@@ -24887,6 +25532,49 @@ class SessionState:
24887
25532
  )
24888
25533
  return len(injected)
24889
25534
 
25535
+ def _user_feedback_conflict_score(self, user_text: str, step_desc: str = "") -> float:
25536
+ """Score 0.0–1.0 via LLM semantic analysis: how strongly user feedback conflicts with current plan.
25537
+ Falls back to 0.5 on error."""
25538
+ if not str(user_text or "").strip():
25539
+ return 0.0
25540
+ try:
25541
+ ctx = [
25542
+ {"role": "system", "content": (
25543
+ "You are a semantic conflict analyzer. "
25544
+ "Given a user's mid-execution feedback and the current task step, "
25545
+ "output ONLY a JSON object: {\"score\": <float 0.0-1.0>, \"reason\": \"<brief>\"}. "
25546
+ "score=0.0 means fully aligned (minor tweak/clarification). "
25547
+ "score=1.0 means direct contradiction/override (user wants opposite direction). "
25548
+ "No other text."
25549
+ ), "ts": now_ts()},
25550
+ {"role": "user", "content": (
25551
+ f"Current step: {trim(step_desc, 300) or 'unknown'}\n"
25552
+ f"User feedback: {trim(user_text, 500)}\n"
25553
+ "Output JSON only."
25554
+ ), "ts": now_ts()},
25555
+ ]
25556
+ resp = self._chat_with_same_model_retry(
25557
+ ctx,
25558
+ tools=None,
25559
+ system=None,
25560
+ max_tokens=80,
25561
+ think=False,
25562
+ stream_thinking=False,
25563
+ context_label="feedback-conflict-score",
25564
+ retries=1,
25565
+ )
25566
+ text = str(resp.get("content", "") or "").strip()
25567
+ # Extract JSON from response
25568
+ import re as _re
25569
+ m = _re.search(r'\{[^}]+\}', text)
25570
+ if m:
25571
+ parsed = parse_json_object(m.group(0), {})
25572
+ score = float(parsed.get("score", 0.5) or 0.5)
25573
+ return max(0.0, min(1.0, score))
25574
+ except Exception:
25575
+ pass
25576
+ return 0.5
25577
+
24890
25578
  def _merge_user_feedback_with_plan(self, user_text: str):
24891
25579
  """When user provides feedback during execution, inject plan-aware merge note into manager context."""
24892
25580
  bb = self._ensure_blackboard()
@@ -24900,22 +25588,41 @@ class SessionState:
24900
25588
  break
24901
25589
  step_desc = trim(str(current_step.get("content", "") if current_step else "none"), 200)
24902
25590
  is_plan_executing = plan.get("phase") == "executing"
25591
+ conflict_score = self._user_feedback_conflict_score(user_text, step_desc)
25592
+ if conflict_score >= 0.8:
25593
+ conflict_level = "HIGH"
25594
+ directive = (
25595
+ "USER OVERRIDE: This feedback DIRECTLY CONFLICTS with the current plan step. "
25596
+ "You MUST prioritize the user's instruction over the original plan. "
25597
+ "Adjust the current step's approach immediately to comply with user's requirement. "
25598
+ "Do NOT continue the original approach."
25599
+ )
25600
+ elif conflict_score >= 0.5:
25601
+ conflict_level = "MEDIUM"
25602
+ directive = (
25603
+ "This feedback modifies the current approach. "
25604
+ "Re-evaluate the current step and adjust delegation to incorporate user's requirement. "
25605
+ "User's direction takes precedence over plan details."
25606
+ )
25607
+ else:
25608
+ conflict_level = "LOW"
25609
+ directive = (
25610
+ "Minor feedback — integrate with current work direction. "
25611
+ "Adjust approach if needed but maintain progress."
25612
+ )
24903
25613
  if is_plan_executing:
24904
25614
  merge_note = (
24905
- f"<user-feedback-merge>\n"
25615
+ f"<user-feedback-merge conflict=\"{conflict_level}\" score=\"{conflict_score:.2f}\">\n"
24906
25616
  f"User provided new input during plan execution: {trim(user_text, 500)}\n"
24907
25617
  f"Current plan step: {step_desc}\n"
24908
- f"Re-evaluate: Does this feedback change the current step's approach? "
24909
- f"If yes, adjust delegation accordingly. If it's a new requirement, "
24910
- f"integrate it into the current or next step. Do NOT restart from scratch.\n"
25618
+ f"{directive}\n"
24911
25619
  f"</user-feedback-merge>"
24912
25620
  )
24913
25621
  else:
24914
25622
  merge_note = (
24915
- f"<user-feedback-merge>\n"
25623
+ f"<user-feedback-merge conflict=\"{conflict_level}\" score=\"{conflict_score:.2f}\">\n"
24916
25624
  f"User provided new input: {trim(user_text, 500)}\n"
24917
- f"Integrate this feedback with current work direction. "
24918
- f"Adjust approach if needed but maintain progress.\n"
25625
+ f"{directive}\n"
24919
25626
  f"</user-feedback-merge>"
24920
25627
  )
24921
25628
  if self._is_multi_agent_mode():
@@ -24925,6 +25632,13 @@ class SessionState:
24925
25632
  "ts": now_ts(),
24926
25633
  "agent_role": "manager",
24927
25634
  })
25635
+ else:
25636
+ # single mode: inject directly into message history so the model sees it
25637
+ self.messages.append({
25638
+ "role": "user",
25639
+ "content": merge_note,
25640
+ "ts": now_ts(),
25641
+ })
24928
25642
 
24929
25643
  def _is_restart_scenario(self) -> bool:
24930
25644
  """Check if current state is a restart after finished/aborted task."""
@@ -25006,6 +25720,10 @@ class SessionState:
25006
25720
  if _awaiting_plan_choice:
25007
25721
  # Restore plan proposal so choice can be parsed
25008
25722
  self.runtime_plan_mode_needed = True
25723
+ # Reset completed plan/todo/skills blackboard state so the manager
25724
+ # does not see status=COMPLETED on the very first round and immediately finish.
25725
+ if not _awaiting_plan_choice:
25726
+ self._reset_blackboard_plan_state_locked()
25009
25727
  self.run_generation = int(self.run_generation) + 1
25010
25728
  clean_goal = trim(str(content or "").strip(), 4000)
25011
25729
  self._refresh_runtime_code_reference(clean_goal or content)
@@ -25525,6 +26243,7 @@ class SessionState:
25525
26243
  if self.stall_escalation_triggered:
25526
26244
  self._emit("status", {"summary": "sync loop break: stall escalated to plan mode"})
25527
26245
  break
26246
+ self._inject_pending_user_inputs()
25528
26247
  self._apply_auto_compact_if_needed("auto:multi-sync")
25529
26248
  # Periodic checkpoint in multi-agent sync loop
25530
26249
  if rounds_used % CHECKPOINT_INTERVAL_ROUNDS == 0:
@@ -25890,7 +26609,7 @@ class SessionState:
25890
26609
 
25891
26610
  # Auto-discover and load relevant skills before research
25892
26611
  try:
25893
- self._refresh_loaded_skills_for_execution_focus(trigger="plan-mode-start")
26612
+ pass # Skills are loaded on-demand by the model via load_skill
25894
26613
  except Exception:
25895
26614
  pass
25896
26615
 
@@ -25900,6 +26619,7 @@ class SessionState:
25900
26619
  for r in range(PLAN_MODE_EXPLORER_MAX_ROUNDS):
25901
26620
  if self.cancel_requested:
25902
26621
  return
26622
+ self._inject_pending_user_inputs()
25903
26623
  step = self._plan_mode_explorer_turn(pinned_selection, round_idx=r)
25904
26624
  if step.get("status") in ("no-tools", "skip", "interrupted"):
25905
26625
  break
@@ -25973,33 +26693,38 @@ class SessionState:
25973
26693
  f"## User Request\n{goal}\n\n"
25974
26694
  f"{skills_section}"
25975
26695
  f"## Instructions\n"
25976
- f"1. List all uploaded/workspace files with `ls uploaded/` or `ls` to know what inputs are available\n"
25977
- f"2. Read uploaded files (.parsed.md preferred over .pdf) to understand their content and structure\n"
25978
- f"3. If skills are loaded, analyze their <loaded-skill> content to identify concrete workflow steps, "
25979
- f"scripts, tools, and file paths each skill requires\n"
25980
- f"4. Identify key technical details, data points, and structure needed for the output\n"
25981
- f"5. Assess risks and note any ambiguities that need user input\n"
25982
- f"6. DO NOT write, edit, or create any files. Read-only analysis only.\n"
25983
- f"7. Write your findings to the blackboard under 'plan_findings'. Include:\n"
26696
+ f"1. Call `list_skills` FIRST to discover available skills identify which skills are relevant "
26697
+ f"to this task and note their names and capabilities in your findings.\n"
26698
+ f"2. List all uploaded/workspace files with `ls uploaded/` or `ls` to know what inputs are available\n"
26699
+ f"3. Read uploaded files (.parsed.md preferred over .pdf) to understand their content and structure\n"
26700
+ f"4. If relevant skills exist, call `load_skill` to load the most relevant one and analyze its "
26701
+ f"workflow steps, scripts, tools, and file paths\n"
26702
+ f"5. Identify key technical details, data points, and structure needed for the output\n"
26703
+ f"6. Assess risks and note any ambiguities that need user input\n"
26704
+ f"7. DO NOT write, edit, or create any files. Read-only analysis only.\n"
26705
+ f"8. Write your findings to the blackboard under 'plan_findings'. Include:\n"
26706
+ f" - Relevant skills found (names, what they do, how to invoke them)\n"
25984
26707
  f" - File inventory (uploaded files, their types, sizes, key content)\n"
25985
- f" - Skill workflow breakdown (concrete tools, scripts, paths for each loaded skill)\n"
26708
+ f" - Skill workflow breakdown (concrete tools, scripts, paths for each relevant skill)\n"
25986
26709
  f" - Content analysis (key themes, structure, data points extracted from inputs)\n\n"
25987
- f"Workspace: {self.files_root}\n"
26710
+ f"Workspace: \"{self.files_root}\" ($SESSION_ROOT)\n"
25988
26711
  f"{os_note}\n"
25989
26712
  f"{lang_note}"
25990
26713
  )
25991
26714
 
25992
26715
  def _seed_plan_mode_explorer_context(self, research_prompt: str):
25993
26716
  os_note = _detect_os_shell_instruction()
25994
- skills_hint = self._loaded_skills_prompt_hint(for_role="explorer")
26717
+ skills_block = self._skills_awareness_block(for_role="explorer")
25995
26718
  self._append_agent_context_message("explorer", {
25996
26719
  "role": "system",
25997
26720
  "content": (
25998
26721
  "You are Explorer in plan-mode (read-only research). "
25999
26722
  "Analyze the codebase to understand the task scope. "
26000
26723
  "Do NOT modify any files. Use read_file, bash (read-only commands), "
26001
- "and blackboard tools only. "
26002
- f"{skills_hint}"
26724
+ "list_skills, load_skill, and blackboard tools only. "
26725
+ f"{skills_block}"
26726
+ "IMPORTANT: If the task requires specialized output (PPTX, reports, deep research, code review), "
26727
+ "call list_skills first to discover relevant skills, then note in plan_findings which skills to use. "
26003
26728
  f"{os_note} "
26004
26729
  f"{model_language_instruction(self.ui_language)}"
26005
26730
  ),
@@ -26030,16 +26755,16 @@ class SessionState:
26030
26755
  self.current_phase = f"plan-mode:explorer:round-{round_idx}"
26031
26756
  self.current_tool_name = ""
26032
26757
  self.active_agent_role = "explorer"
26033
- # Build loaded-skills hint for system prompt
26034
- skills_hint = self._loaded_skills_prompt_hint(for_role="explorer")
26758
+ # Build skills awareness block (same as sync/single mode)
26759
+ skills_block = self._skills_awareness_block(for_role="explorer")
26035
26760
  response = self._chat_with_same_model_retry(
26036
26761
  ctx,
26037
26762
  tools=filtered_tools,
26038
26763
  system=(
26039
26764
  "You are Explorer in plan-mode research. Read-only analysis. "
26040
26765
  "Do NOT create, write, or edit files. "
26041
- f"Workspace: {self.files_root}. "
26042
- f"{skills_hint}"
26766
+ f"Workspace: \"{self.files_root}\" ($SESSION_ROOT). "
26767
+ f"{skills_block}"
26043
26768
  f"{_detect_os_shell_instruction()} "
26044
26769
  f"{model_language_instruction(self.ui_language)}"
26045
26770
  ),
@@ -26448,7 +27173,8 @@ class SessionState:
26448
27173
  synthesis_ctx = [
26449
27174
  {"role": "system", "content": (
26450
27175
  "You are a technical architect synthesizing research into actionable plans. "
26451
- "When loaded skills are available, incorporate their capabilities and best practices into plan options."
27176
+ "When skills are referenced in the findings, incorporate their actual workflow steps into plan options. "
27177
+ f"{self._skills_awareness_block(for_role='developer')}"
26452
27178
  ), "ts": now_ts()},
26453
27179
  {"role": "user", "content": synthesis_prompt, "ts": now_ts()},
26454
27180
  ]
@@ -26691,7 +27417,7 @@ class SessionState:
26691
27417
  except Exception:
26692
27418
  pass
26693
27419
  try:
26694
- self._refresh_loaded_skills_for_execution_focus(trigger="plan-approved")
27420
+ pass # Skills are loaded on-demand by the model via load_skill
26695
27421
  except Exception:
26696
27422
  pass
26697
27423
  # Pre-load skills explicitly mentioned in plan steps
@@ -26755,13 +27481,13 @@ class SessionState:
26755
27481
  )
26756
27482
  },
26757
27483
  )
26758
- # ── Auto-discover and load relevant skills BEFORE classification ──
27484
+ # ── Skills are loaded on-demand by the model via load_skill ──
26759
27485
  try:
26760
27486
  self._emit(
26761
27487
  "status",
26762
- {"summary": "initial skill discovery started"},
27488
+ {"summary": "skills available on-demand"},
26763
27489
  )
26764
- self._refresh_loaded_skills_for_execution_focus(trigger="pre-classify")
27490
+ pass # No automatic pre-classify skill discovery
26765
27491
  except Exception:
26766
27492
  pass
26767
27493
  initial_policy_media_inputs = self._recent_multimodal_inputs()
@@ -30581,16 +31307,32 @@ function _mathRunTypeset(root,key=''){
30581
31307
  if(!root)return;
30582
31308
  const k=String(key||'').trim();
30583
31309
  if(k&&root.getAttribute('data-math-key')===k)return;
31310
+ const mathJaxCandidates=[
31311
+ '/assets/js_lib/tex-mml-chtml.js',
31312
+ '/assets/js_lib/mathjax/tex-mml-chtml.js',
31313
+ '/assets/js_lib/es5/tex-mml-chtml.js',
31314
+ '/assets/js_lib/mathjax/es5/tex-mml-chtml.js',
31315
+ 'https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js'
31316
+ ];
31317
+ const loadMathJax=(idx=0)=>{
31318
+ const src=String(mathJaxCandidates[idx]||'').trim();
31319
+ if(!src)return;
31320
+ const s=document.createElement('script');
31321
+ s.src=src;
31322
+ s.async=true;
31323
+ s.dataset.mathjaxCandidate=String(idx);
31324
+ s.onerror=()=>{
31325
+ if(idx+1<mathJaxCandidates.length)loadMathJax(idx+1);
31326
+ };
31327
+ document.head.appendChild(s);
31328
+ };
30584
31329
  const run=(retry)=>{
30585
31330
  const mj=window.MathJax;
30586
31331
  if(!mj||typeof mj.typesetPromise!=='function'){
30587
31332
  // Lazy-load MathJax on first actual math demand
30588
31333
  if(!window._mjaxLoading){
30589
31334
  window._mjaxLoading=true;
30590
- const s=document.createElement('script');
30591
- s.src='https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js';
30592
- s.async=true;
30593
- document.head.appendChild(s);
31335
+ loadMathJax(0);
30594
31336
  }
30595
31337
  if(retry<20)setTimeout(()=>run(retry+1),200);
30596
31338
  return;
@@ -34349,7 +35091,7 @@ class RAGContentParser:
34349
35091
 
34350
35092
  text = extract_text(str(pdf_path))
34351
35093
  if text and text.strip():
34352
- return trim(text.strip(), 150_000)
35094
+ return trim(text.strip(), 800_000)
34353
35095
  except ImportError:
34354
35096
  pass
34355
35097
  except Exception:
@@ -34364,7 +35106,7 @@ class RAGContentParser:
34364
35106
  timeout=60,
34365
35107
  )
34366
35108
  if r.returncode == 0 and r.stdout.strip():
34367
- return trim(r.stdout.strip(), 150_000)
35109
+ return trim(r.stdout.strip(), 800_000)
34368
35110
  except Exception:
34369
35111
  pass
34370
35112
  try:
@@ -36525,7 +37267,7 @@ class RAGLibraryStore:
36525
37267
  backup_path.write_bytes(raw_bytes)
36526
37268
  elif source_fp and source_fp.exists():
36527
37269
  shutil.copy2(source_fp, backup_path)
36528
- semantic_text = trim(str(parse_result.get("text", "") or ""), 160_000)
37270
+ semantic_text = trim(str(parse_result.get("text", "") or ""), 800_000)
36529
37271
  multimodal_row = dict(multimodal or {})
36530
37272
  mm_summary = trim(str(multimodal_row.get("summary", "") or ""), 2400)
36531
37273
  mm_tags = [str(x).strip() for x in (multimodal_row.get("tags", []) or []) if str(x).strip()]
@@ -39952,20 +40694,12 @@ class AppContext:
39952
40694
  self.base_url = base_url
39953
40695
  self.model = model
39954
40696
  self.thinking = False
39955
- self.js_lib_root = offline_js_lib_root(self.workspace)
40697
+ self.js_lib_root = offline_js_lib_root(SCRIPT_DIR)
39956
40698
  self.offline_js_summary: dict = {}
39957
40699
  try:
39958
- self.offline_js_summary = ensure_offline_js_libs(self.workspace, force=False)
39959
- except Exception as exc:
39960
- self.offline_js_summary = {
39961
- "generated_at": int(now_ts()),
39962
- "js_lib_root": str(self.js_lib_root),
39963
- "total": len(OFFLINE_JS_LIB_CATALOG),
39964
- "available": 0,
39965
- "missing": len(OFFLINE_JS_LIB_CATALOG),
39966
- "fetched": 0,
39967
- "error": trim(str(exc), 220),
39968
- }
40700
+ self.offline_js_summary = load_offline_js_lib_index(self.js_lib_root)
40701
+ except Exception:
40702
+ self.offline_js_summary = {}
39969
40703
  self.default_language = normalize_ui_language(default_language)
39970
40704
  self.ui_style = normalize_ui_style(ui_style)
39971
40705
  self.context_token_limit = max(
@@ -40264,16 +40998,7 @@ class AppContext:
40264
40998
  return CODE_ADMIN_JS
40265
40999
 
40266
41000
  def rag_js_lib_asset_path(self, filename: str) -> Path | None:
40267
- safe = _safe_js_filename(str(filename or "").strip(), "lib.js")
40268
- fp = (self.js_lib_root / safe).resolve()
40269
- try:
40270
- if not fp.is_relative_to(self.js_lib_root):
40271
- return None
40272
- except Exception:
40273
- return None
40274
- if not fp.exists() or (not fp.is_file()):
40275
- return None
40276
- return fp
41001
+ return _resolve_js_lib_asset_path(self.js_lib_root, str(filename or "").strip())
40277
41002
 
40278
41003
  def rag_three_asset_info(self) -> dict:
40279
41004
  picks = [
@@ -41071,7 +41796,7 @@ class AppContext:
41071
41796
  f"{str(row.get('title', '') or '').strip()} "
41072
41797
  f"score={str(row.get('score', 0) or 0)}"
41073
41798
  )
41074
- snippet = trim(str(row.get("text", "") or ""), 320)
41799
+ snippet = trim(str(row.get("text", "") or ""), 800)
41075
41800
  if snippet:
41076
41801
  lines.append(snippet)
41077
41802
  return "\n".join(lines)
@@ -43150,8 +43875,8 @@ class RagAdminHandler(BaseHTTPRequestHandler):
43150
43875
  if path == "/assets/rag-admin.js":
43151
43876
  return self._send_text(self.app.web_ui_rag_admin_js(), "application/javascript; charset=utf-8")
43152
43877
  if path.startswith("/assets/js_lib/"):
43153
- filename = path.rsplit("/", 1)[-1]
43154
- fp = self.app.rag_js_lib_asset_path(filename)
43878
+ asset_ref = path[len("/assets/js_lib/"):]
43879
+ fp = self.app.rag_js_lib_asset_path(asset_ref)
43155
43880
  if not fp:
43156
43881
  return self._send_json({"error": "asset not found"}, status=404)
43157
43882
  try:
@@ -43159,7 +43884,7 @@ class RagAdminHandler(BaseHTTPRequestHandler):
43159
43884
  except Exception as exc:
43160
43885
  return self._send_json({"error": str(exc)}, status=500)
43161
43886
  content_type = guess_mime_from_name(fp.name, "application/javascript")
43162
- if fp.suffix.lower() == ".js":
43887
+ if fp.suffix.lower() in {".js", ".mjs", ".cjs"}:
43163
43888
  content_type = "application/javascript; charset=utf-8"
43164
43889
  return self._send_inline_bytes(data, content_type)
43165
43890
  if path == "/api/health":
@@ -43307,8 +44032,8 @@ class CodeAdminHandler(BaseHTTPRequestHandler):
43307
44032
  if path == "/assets/code-admin.js":
43308
44033
  return self._send_text(self.app.web_ui_code_admin_js(), "application/javascript; charset=utf-8")
43309
44034
  if path.startswith("/assets/js_lib/"):
43310
- filename = path.rsplit("/", 1)[-1]
43311
- fp = self.app.rag_js_lib_asset_path(filename)
44035
+ asset_ref = path[len("/assets/js_lib/"):]
44036
+ fp = self.app.rag_js_lib_asset_path(asset_ref)
43312
44037
  if not fp:
43313
44038
  return self._send_json({"error": "asset not found"}, status=404)
43314
44039
  try:
@@ -43316,7 +44041,7 @@ class CodeAdminHandler(BaseHTTPRequestHandler):
43316
44041
  except Exception as exc:
43317
44042
  return self._send_json({"error": str(exc)}, status=500)
43318
44043
  content_type = guess_mime_from_name(fp.name, "application/javascript")
43319
- if fp.suffix.lower() == ".js":
44044
+ if fp.suffix.lower() in {".js", ".mjs", ".cjs"}:
43320
44045
  content_type = "application/javascript; charset=utf-8"
43321
44046
  return self._send_inline_bytes(data, content_type)
43322
44047
  if path == "/api/health":
@@ -43938,6 +44663,17 @@ def main():
43938
44663
  except Exception as exc:
43939
44664
  print(f"[web-agent] failed to apply --config: {exc}")
43940
44665
  sys.exit(2)
44666
+ # JS lib download (default on; set download_js_lib: false in --config to disable)
44667
+ _js_dl_enabled = extract_js_lib_download_setting(external_config)
44668
+ if _js_dl_enabled is None:
44669
+ _js_dl_enabled = True
44670
+ if _js_dl_enabled:
44671
+ try:
44672
+ app.offline_js_summary = ensure_offline_js_libs(
44673
+ app.workspace, force=False, verbose=True, no_connection_deadline=60.0
44674
+ )
44675
+ except Exception as _js_exc:
44676
+ print(f"[js_lib] download error: {_js_exc}")
43941
44677
  web_ui_state = app.configure_web_ui(
43942
44678
  config_path=str(web_ui_config_path),
43943
44679
  ui_dir=resolved_web_ui_dir,