{"repo":"NousResearch/hermes-agent","public_id":"35692","display_label":"Windows scheduled task config causing gateway crashes","summary":"A bug where default Windows Scheduled Task settings (such as ExecutionTimeLimit and battery-related options) cause periodic gateway crashes, along with fixes using hardened and XML-based task registration to address those defaults.","subsystem":"gateway","counts":{"members":3,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":1,"merged_prs":1,"attempt_prs":1},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":45610,"merged_at":"2026-06-23T23:07:53+00:00"},"resolution":{"state":"unresolved","by":null},"freshness":{"state":"verified","last_verified_at":"2026-06-23T23:07:53+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_SPANNING","link_density":0.6666666666666666},"fix_edges":[{"issue_number":30308,"pr_number":45610,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false,"pr_merged_at":"2026-06-23T23:07:53+00:00"},{"issue_number":35692,"pr_number":45610,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false,"pr_merged_at":"2026-06-23T23:07:53+00:00"},{"issue_number":38387,"pr_number":45610,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false,"pr_merged_at":"2026-06-23T23:07:53+00:00"},{"issue_number":45599,"pr_number":45610,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true,"pr_merged_at":"2026-06-23T23:07:53+00:00"}],"members":[{"kind":"issue","number":35692,"title":"[Bug] Windows Scheduled Task defaults cause gateway crash every ~24-72h (ExecutionTimeLimit, battery settings)","url":null,"state":"closed"},{"kind":"pr","number":35693,"title":"fix(windows): use XML task registration for proper Scheduled Task settings","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"433db17c0a8d5581b4fb38289539fc1ee5cc7696","number":0},{"kind":"pr","value":"#45610","number":45610}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":45610,"title":"fix(windows): harden gateway scheduled task","url":null,"state":"merged","close_reason":{"category":"merged"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["hermes_cli/gateway_windows.py","tests/hermes_cli/test_gateway_windows.py"],"fix_hunks":[{"file":"hermes_cli/gateway_windows.py","hunks":["@@ -38,6 +38,7 @@\n import sys\n import time\n from pathlib import Path\n+from xml.sax.saxutils import escape\n \n # Short timeouts: schtasks occasionally wedges and we don't want to hang forever.\n _SCHTASKS_TIMEOUT_S = 15","@@ -51,6 +52,9 @@\n \n _TASK_NAME_DEFAULT = \"Hermes_Gateway\"\n _TASK_DESCRIPTION = \"Hermes Agent Gateway - Messaging Platform Integration\"\n+_TASK_LOGON_DELAY = \"PT30S\"\n+_TASK_RESTART_INTERVAL = \"PT1M\"\n+_TASK_RESTART_COUNT = 999\n \n \n def _schtasks_encoding() -> str:","@@ -358,12 +362,13 @@ def _build_gateway_cmd_script(\n     lines.append(f'set \"HERMES_HOME={hermes_home}\"')\n     lines.append('set \"PYTHONIOENCODING=utf-8\"')\n     lines.append('set \"HERMES_GATEWAY_DETACHED=1\"')\n+    pythonw_path, venv_dir, extra_pythonpath = _resolve_detached_python(python_path)\n     # VIRTUAL_ENV lets the gateway's own python detection find the venv\n     # if someone imports hermes_constants-based logic during startup.\n-    venv_dir = str(Path(python_path).resolve().parent.parent)\n     lines.append(f'set \"VIRTUAL_ENV={venv_dir}\"')\n+    pythonpath_entries = [str(Path(__file__).resolve().parent.parent), *extra_pythonpath]\n+    lines.append(f'set \"PYTHONPATH={\";\".join([*pythonpath_entries, \"%PYTHONPATH%\"])}\"')\n \n-    pythonw_path = _derive_venv_pythonw(python_path)\n     prog_args = [pythonw_path, \"-m\", \"hermes_cli.main\"]\n     if profile_arg:\n         prog_args.extend(profile_arg.split())","@@ -379,6 +384,78 @@ def _build_gateway_cmd_script(\n     return \"\\r\\n\".join(lines) + \"\\r\\n\"\n \n \n+def _quote_vbs_string(value: str) -> str:\n+    \"\"\"Quote a value as a VBScript double-quoted string literal.\n+\n+    VBScript escapes an embedded double-quote by doubling it. A newline cannot\n+    appear inside a literal, so refuse it (same guard as ``_quote_cmd_script_arg``).\n+    \"\"\"\n+    if \"\\r\" in value or \"\\n\" in value:\n+        raise ValueError(f\"refusing to quote VBScript value containing newline: {value!r}\")\n+    return '\"' + value.replace('\"', '\"\"') + '\"'\n+\n+\n+def _build_gateway_vbs_script(\n+    python_path: str,\n+    working_dir: str,\n+    hermes_home: str,\n+    profile_arg: str,\n+) -> str:\n+    \"\"\"Build a console-less ``gateway.vbs`` launcher (CRLF-terminated).\n+\n+    The Scheduled Task runs this through ``wscript.exe`` instead of ``cmd.exe``.\n+\n+    Why: issue #45599 root cause #1. Driving the gateway through ``cmd.exe``\n+    allocates a console, and during logon Windows broadcasts ``CTRL_CLOSE_EVENT``\n+    to console process groups — reaping cmd.exe and the half-initialized gateway\n+    with ``STATUS_CONTROL_C_EXIT`` (``0xC000013A``). Task Scheduler treats that\n+    code as a user cancel, so the ``RestartOnFailure`` policy never fires and the\n+    gateway silently disappears on every reboot.\n+\n+    ``wscript.exe`` and ``pythonw.exe`` are both GUI-subsystem executables with\n+    no console, so this launcher receives no console control events. It mirrors\n+    ``_build_gateway_cmd_script`` (same env + argv via ``_resolve_detached_python``)\n+    but sets the environment on the WScript.Shell process and ``Run``s pythonw\n+    directly — no cmd.exe anywhere in the chain.\n+    \"\"\"\n+    pythonw_path, venv_dir, extra_pythonpath = _resolve_detached_python(python_path)\n+\n+    prog_args = [pythonw_path, \"-m\", \"hermes_cli.main\"]\n+    if profile_arg:\n+        prog_args.extend(profile_arg.split())\n+    prog_args.extend([\"gateway\", \"run\"])\n+    # list2cmdline gives CreateProcess-correct quoting for WScript.Shell.Run.\n+    command_line = subprocess.list2cmdline(prog_args)\n+\n+    repo_root = str(Path(__file__).resolve().parent.parent)\n+    static_pythonpath = os.pathsep.join([repo_root, *extra_pythonpath])\n+\n+    lines = [\n+        f\"' {_TASK_DESCRIPTION}\",\n+        \"Option Explicit\",\n+        \"Dim sh, env, existing_pp\",\n+        'Set sh = CreateObject(\"WScript.Shell\")',\n+        'Set env = sh.Environment(\"PROCESS\")',\n+        f\"env.Item({_quote_vbs_string('HERMES_HOME')}) = {_quote_vbs_string(hermes_home)}\",\n+        f\"env.Item({_quote_vbs_string('PYTHONIOENCODING')}) = {_quote_vbs_string('utf-8')}\",\n+        f\"env.Item({_quote_vbs_string('HERMES_GATEWAY_DETACHED')}) = {_quote_vbs_string('1')}\",\n+        f\"env.Item({_quote_vbs_string('VIRTUAL_ENV')}) = {_quote_vbs_string(str(venv_dir))}\",\n+        # Mirror the cmd wrapper's ``PYTHONPATH=<static>;%PYTHONPATH%``: chain onto\n+        # whatever PYTHONPATH the task environment already carries, at runtime.\n+        f\"existing_pp = env.Item({_quote_vbs_string('PYTHONPATH')})\",\n+        \"If Len(existing_pp) > 0 Then\",\n+        f\"  env.Item({_quote_vbs_string('PYTHONPATH')}) = {_quote_vbs_string(static_pythonpath + os.pathsep)} & existing_pp\",\n+        \"Else\",\n+        f\"  env.Item({_quote_vbs_string('PYTHONPATH')}) = {_quote_vbs_string(static_pythonpath)}\",\n+        \"End If\",\n+        f\"sh.CurrentDirectory = {_quote_vbs_string(working_dir)}\",\n+        # Window style 0 = hidden; bWaitOnReturn False = detached/async. pythonw is\n+        # GUI-subsystem so no console is ever created for the gateway either.\n+        f\"sh.Run {_quote_vbs_string(command_line)}, 0, False\",\n+    ]\n+    return \"\\r\\n\".join(lines) + \"\\r\\n\"\n+\n+\n def _build_startup_launcher(script_path: Path) -> str:\n     \"\"\"The tiny .cmd that goes in the Startup folder. Just minimizes and chains.\n ","@@ -425,6 +502,15 @@ def _write_task_script() -> Path:\n     tmp = script_path.with_suffix(\".tmp\")\n     tmp.write_text(content, encoding=\"utf-8\", newline=\"\")\n     tmp.replace(script_path)\n+\n+    # Also render the console-less .vbs launcher the Scheduled Task runs via\n+    # wscript.exe (issue #45599 fix A). The .cmd above stays for the\n+    # Startup-folder fallback and direct /Run paths.\n+    vbs_content = _build_gateway_vbs_script(python_path, working_dir, hermes_home, profile_arg)\n+    vbs_path = script_path.with_suffix(\".vbs\")\n+    vbs_tmp = vbs_path.with_name(vbs_path.name + \".tmp\")\n+    vbs_tmp.write_text(vbs_content, encoding=\"utf-8\", newline=\"\")\n+    vbs_tmp.replace(vbs_path)\n     return script_path\n \n ","@@ -443,6 +529,74 @@ def _resolve_task_user() -> str | None:\n     return f\"{domain}\\\\{username}\" if domain else username\n \n \n+def _build_scheduled_task_xml(task_name: str, launcher_path: Path, user: str | None) -> str:\n+    \"\"\"Render a Task Scheduler XML definition with safe long-running defaults.\n+\n+    ``launcher_path`` is the console-less ``.vbs`` the task runs via\n+    ``wscript.exe`` — not the ``.cmd`` (see ``_build_gateway_vbs_script`` /\n+    issue #45599 root cause #1).\n+    \"\"\"\n+    user_principal = f\"\\n      <UserId>{escape(user)}</UserId>\" if user else \"\"\n+    return f\"\"\"<?xml version=\"1.0\" encoding=\"UTF-16\"?>\n+<Task version=\"1.4\" xmlns=\"http://schemas.microsoft.com/windows/2004/02/mit/task\">\n+  <RegistrationInfo>\n+    <Description>{escape(_TASK_DESCRIPTION)}</Description>\n+  </RegistrationInfo>\n+  <Triggers>\n+    <LogonTrigger>\n+      <Enabled>true</Enabled>\n+      <Delay>{_TASK_LOGON_DELAY}</Delay>\n+    </LogonTrigger>\n+  </Triggers>\n+  <Principals>\n+    <Principal id=\"Author\">{user_principal}\n+      <LogonType>InteractiveToken</LogonType>\n+      <RunLevel>LeastPrivilege</RunLevel>\n+    </Principal>\n+  </Principals>\n+  <Settings>\n+    <MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>\n+    <DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>\n+    <StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>\n+    <AllowHardTerminate>true</AllowHardTerminate>\n+    <StartWhenAvailable>true</StartWhenAvailable>\n+    <RunOnlyIfNetworkAvailable>false</RunOnlyIfNetworkAvailable>\n+    <IdleSettings>\n+      <StopOnIdleEnd>false</StopOnIdleEnd>\n+      <RestartOnIdle>false</RestartOnIdle>\n+    </IdleSettings>\n+    <AllowStartOnDemand>true</AllowStartOnDemand>\n+    <Enabled>true</Enabled>\n+    <Hidden>false</Hidden>\n+    <RunOnlyIfIdle>false</RunOnlyIfIdle>\n+    <WakeToRun>false</WakeToRun>\n+    <ExecutionTimeLimit>PT0S</ExecutionTimeLimit>\n+    <Priority>7</Priority>\n+    <RestartOnFailure>\n+      <Interval>{_TASK_RESTART_INTERVAL}</Interval>\n+      <Count>{_TASK_RESTART_COUNT}</Count>\n+    </RestartOnFailure>\n+  </Settings>\n+  <Actions Context=\"Author\">\n+    <Exec>\n+      <Command>wscript.exe</Command>\n+      <Arguments>//B //Nologo \"{escape(str(launcher_path))}\"</Arguments>\n+    </Exec>\n+  </Actions>\n+</Task>\n+\"\"\"\n+\n+\n+def _write_scheduled_task_xml(task_name: str, launcher_path: Path, user: str | None) -> Path:\n+    xml_path = launcher_path.with_suffix(\".task.xml\")\n+    xml_path.write_text(\n+        _build_scheduled_task_xml(task_name, launcher_path, user),\n+        encoding=\"utf-16\",\n+        newline=\"\",\n+    )\n+    return xml_path\n+\n+\n def _install_scheduled_task(task_name: str, script_path: Path) -> tuple[bool, str]:\n     \"\"\"Create or replace the Scheduled Task. Returns (success, detail).\n ","@@ -451,8 +605,6 @@ def _install_scheduled_task(task_name: str, script_path: Path) -> tuple[bool, st\n     preserves those stale triggers and can make the gateway relaunch every\n     minute. Delete+create gives us a clean ONLOGON task every install.\n     \"\"\"\n-    quoted_script = _quote_schtasks_arg(str(script_path))\n-\n     delete_code, delete_out, delete_err = _exec_schtasks([\"/Delete\", \"/F\", \"/TN\", task_name])\n     delete_detail = (delete_err or delete_out or \"\").strip()\n     if delete_code != 0 and delete_detail and \"cannot find\" not in delete_detail.lower():","@@ -460,32 +612,28 @@ def _install_scheduled_task(task_name: str, script_path: Path) -> tuple[bool, st\n             return (False, f\"schtasks /Delete failed (code {delete_code}): {delete_detail}\")\n         # Non-fatal: /Create /F below may still replace it. Keep the detail in\n         # the final error if creation also fails.\n-    # password\" variant; if that fails, retry without /RU /NP /IT.\n-    base = [\n-        \"/Create\",\n-        \"/F\",\n-        \"/SC\",\n-        \"ONLOGON\",\n-        \"/RL\",\n-        \"LIMITED\",\n-        \"/TN\",\n-        task_name,\n-        \"/TR\",\n-        quoted_script,\n-    ]\n     user = _resolve_task_user()\n-    variants = []\n-    if user:\n-        variants.append([*base, \"/RU\", user, \"/NP\", \"/IT\"])\n+    # The Scheduled Task launches the console-less .vbs (issue #45599 fix A), not\n+    # the .cmd. The .cmd stays for the Startup-folder fallback and direct /Run.\n+    launcher_path = script_path.with_suffix(\".vbs\")\n+    xml_path = _write_scheduled_task_xml(task_name, launcher_path, user)\n+    base = [\"/Create\", \"/F\", \"/TN\", task_name, \"/XML\", str(xml_path)]\n+    variants = [[*base, \"/RU\", user, \"/NP\", \"/IT\"]] if user else []\n     variants.append(base)\n \n     last_code = 1\n     last_err = \"\"\n-    for argv in variants:\n-        code, out, err = _exec_schtasks(argv)\n-        if code == 0:\n-            return (True, f\"Created Scheduled Task {task_name!r}\")\n-        last_code, last_err = code, (err or out or \"\")\n+    try:\n+        for argv in variants:\n+            code, out, err = _exec_schtasks(argv)\n+            if code == 0:\n+                return (True, f\"Created Scheduled Task {task_name!r}\")\n+            last_code, last_err = code, (err or out or \"\")\n+    finally:\n+        try:\n+            xml_path.unlink(missing_ok=True)\n+        except OSError:\n+            pass\n     if delete_detail and \"cannot find\" not in delete_detail.lower():\n         last_err = f\"{last_err.strip()} (delete detail: {delete_detail})\"\n     return (False, f\"schtasks /Create failed (code {last_code}): {last_err.strip()}\")"]},{"file":"tests/hermes_cli/test_gateway_windows.py","hunks":["@@ -190,7 +190,11 @@ def fake_install_startup_entry(path: Path) -> Path:\n \n def test_gateway_cmd_script_uses_pythonw_without_replace_or_start_churn(monkeypatch):\n     \"\"\"Scheduled Task wrapper should launch pythonw once and avoid replace loops.\"\"\"\n-    monkeypatch.setattr(gateway_windows, \"_derive_venv_pythonw\", lambda exe: exe.replace(\"python.exe\", \"pythonw.exe\"))\n+    monkeypatch.setattr(\n+        gateway_windows,\n+        \"_resolve_detached_python\",\n+        lambda exe: (exe.replace(\"python.exe\", \"pythonw.exe\"), r\"C:\\\\Hermes\\\\hermes-agent\\\\venv\", []),\n+    )\n \n     content = gateway_windows._build_gateway_cmd_script(\n         r\"C:\\\\Hermes\\\\hermes-agent\\\\venv\\\\Scripts\\\\python.exe\",","@@ -206,6 +210,41 @@ def test_gateway_cmd_script_uses_pythonw_without_replace_or_start_churn(monkeypa\n     assert \"exit /b 0\" in content\n \n \n+def test_gateway_cmd_script_uses_uv_safe_base_pythonw(monkeypatch, tmp_path):\n+    \"\"\"Scheduled Task wrapper should share the detached uv-venv workaround.\"\"\"\n+    project = tmp_path / \"project\"\n+    scripts = project / \"venv\" / \"Scripts\"\n+    site_packages = project / \"venv\" / \"Lib\" / \"site-packages\"\n+    hermes_home = tmp_path / \"hermes-home\"\n+    base = tmp_path / \"uv\" / \"python\" / \"cpython-3.11-windows-x86_64-none\"\n+    scripts.mkdir(parents=True)\n+    site_packages.mkdir(parents=True)\n+    hermes_home.mkdir()\n+    base.mkdir(parents=True)\n+\n+    venv_python = scripts / \"python.exe\"\n+    venv_pythonw = scripts / \"pythonw.exe\"\n+    base_pythonw = base / \"pythonw.exe\"\n+    for exe in (venv_python, venv_pythonw, base_pythonw):\n+        exe.write_text(\"\", encoding=\"utf-8\")\n+    (project / \"venv\" / \"pyvenv.cfg\").write_text(\n+        f\"home = {base}\\nimplementation = CPython\\nuv = 0.11.14\\nversion_info = 3.11.15\\n\",\n+        encoding=\"utf-8\",\n+    )\n+\n+    content = gateway_windows._build_gateway_cmd_script(\n+        str(venv_python),\n+        str(hermes_home),\n+        str(hermes_home),\n+        \"\",\n+    )\n+\n+    assert str(base_pythonw) in content\n+    assert f'set \"VIRTUAL_ENV={project / \"venv\"}\"' in content\n+    assert str(site_packages) in content\n+    assert str(venv_pythonw) not in content\n+\n+\n def test_elevated_gateway_command_uses_pythonw_hidden_console(monkeypatch):\n     \"\"\"UAC handoff should not leave a second elevated cmd.exe window open.\"\"\"\n     calls = []","@@ -239,14 +278,18 @@ def test_install_scheduled_task_recreates_instead_of_change(monkeypatch, tmp_pat\n     \"\"\"Install must delete+create so stale minute-repeat task settings are not preserved.\"\"\"\n     calls = []\n     script_path = tmp_path / \"Hermes_Gateway_alice.cmd\"\n+    xml_seen = {}\n \n     monkeypatch.setattr(gateway_windows, \"_assert_windows\", lambda: None)\n+    monkeypatch.setattr(gateway_windows, \"_resolve_task_user\", lambda: r\"DOMAIN\\\\alice\")\n \n     def fake_schtasks(args):\n         calls.append(tuple(args))\n         if args[0] == \"/Delete\":\n             return (0, \"SUCCESS\", \"\")\n         if args[0] == \"/Create\":\n+            xml_path = Path(args[args.index(\"/XML\") + 1])\n+            xml_seen[\"text\"] = xml_path.read_text(encoding=\"utf-16\")\n             return (0, \"SUCCESS\", \"\")\n         raise AssertionError(f\"unexpected schtasks args: {args}\")\n ","@@ -257,8 +300,88 @@ def fake_schtasks(args):\n     assert \"/Change\" not in [arg for call in calls for arg in call]\n     assert calls[0][:4] == (\"/Delete\", \"/F\", \"/TN\", \"Hermes_Gateway_alice\")\n     assert calls[1][0] == \"/Create\"\n-    assert \"/SC\" in calls[1]\n-    assert \"ONLOGON\" in calls[1]\n+    assert \"/XML\" in calls[1]\n+    assert \"/SC\" not in calls[1]\n+    assert \"<Delay>PT30S</Delay>\" in xml_seen[\"text\"]\n+    assert \"<StartWhenAvailable>true</StartWhenAvailable>\" in xml_seen[\"text\"]\n+    assert \"<StopOnIdleEnd>false</StopOnIdleEnd>\" in xml_seen[\"text\"]\n+    assert \"<DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>\" in xml_seen[\"text\"]\n+    assert \"<StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>\" in xml_seen[\"text\"]\n+    assert \"<ExecutionTimeLimit>PT0S</ExecutionTimeLimit>\" in xml_seen[\"text\"]\n+    assert \"<RestartOnFailure>\" in xml_seen[\"text\"]\n+    assert \"<Count>999</Count>\" in xml_seen[\"text\"]\n+    # Scheduled Task launches the console-less .vbs via wscript.exe, never cmd.exe\n+    # (issue #45599 fix A: no console -> no logon CTRL_CLOSE_EVENT / 0xC000013A).\n+    assert \"<Command>wscript.exe</Command>\" in xml_seen[\"text\"]\n+    assert \"//B //Nologo\" in xml_seen[\"text\"]\n+    assert \"Hermes_Gateway_alice.vbs\" in xml_seen[\"text\"]\n+    assert \"cmd.exe\" not in xml_seen[\"text\"]\n+\n+\n+def test_gateway_vbs_script_is_console_less(monkeypatch):\n+    \"\"\"The .vbs launcher must avoid cmd.exe entirely and Run pythonw hidden\n+    (issue #45599 fix A: no console -> no logon CTRL_CLOSE_EVENT / 0xC000013A).\"\"\"\n+    monkeypatch.setattr(\n+        gateway_windows,\n+        \"_resolve_detached_python\",\n+        lambda exe: (r\"C:\\venv\\Scripts\\pythonw.exe\", Path(r\"C:\\venv\"), []),\n+    )\n+    content = gateway_windows._build_gateway_vbs_script(\n+        r\"C:\\venv\\Scripts\\python.exe\",\n+        r\"C:\\Hermes\",\n+        r\"C:\\Hermes\",\n+        \"--profile work\",\n+    )\n+    assert \"cmd.exe\" not in content.lower()\n+    assert 'CreateObject(\"WScript.Shell\")' in content\n+    assert \"pythonw.exe\" in content\n+    assert \"hermes_cli.main\" in content\n+    assert \"gateway run\" in content\n+    assert \", 0, False\" in content  # hidden window, detached/async\n+    for var in (\"HERMES_HOME\", \"PYTHONIOENCODING\", \"HERMES_GATEWAY_DETACHED\", \"VIRTUAL_ENV\", \"PYTHONPATH\"):\n+        assert var in content\n+    assert \"--profile\" in content and \"work\" in content\n+    assert content.endswith(\"\\r\\n\")\n+\n+\n+def test_gateway_vbs_script_quotes_spaced_paths(monkeypatch):\n+    \"\"\"Spaced exe/dir paths stay correctly quoted through the VBScript literal.\"\"\"\n+    monkeypatch.setattr(\n+        gateway_windows,\n+        \"_resolve_detached_python\",\n+        lambda exe: (r\"C:\\Program Files\\Py\\pythonw.exe\", Path(r\"C:\\v env\"), []),\n+    )\n+    content = gateway_windows._build_gateway_vbs_script(\n+        r\"C:\\Program Files\\Py\\python.exe\",\n+        r\"C:\\work dir\",\n+        r\"C:\\h home\",\n+        \"\",\n+    )\n+    # list2cmdline quotes the spaced exe; _quote_vbs_string doubles those quotes.\n+    assert '\"\"C:\\\\Program Files\\\\Py\\\\pythonw.exe\"\"' in content\n+    assert 'sh.CurrentDirectory = \"C:\\\\work dir\"' in content\n+\n+\n+def test_gateway_vbs_script_pythonpath_chains_runtime_value(monkeypatch):\n+    \"\"\"PYTHONPATH chains onto the task env's existing value, like ;%PYTHONPATH%.\"\"\"\n+    monkeypatch.setattr(\n+        gateway_windows,\n+        \"_resolve_detached_python\",\n+        lambda exe: (r\"C:\\v\\pythonw.exe\", Path(r\"C:\\v\"), [r\"C:\\v\\Lib\\site-packages\"]),\n+    )\n+    content = gateway_windows._build_gateway_vbs_script(\n+        r\"C:\\v\\python.exe\", r\"C:\\w\", r\"C:\\h\", \"\",\n+    )\n+    assert 'existing_pp = env.Item(\"PYTHONPATH\")' in content\n+    assert \"If Len(existing_pp) > 0 Then\" in content\n+    assert r\"C:\\v\\Lib\\site-packages\" in content\n+\n+\n+def test_quote_vbs_string_doubles_quotes_and_rejects_newlines():\n+    assert gateway_windows._quote_vbs_string(\"plain\") == '\"plain\"'\n+    assert gateway_windows._quote_vbs_string('a\"b') == '\"a\"\"b\"'\n+    with pytest.raises(ValueError):\n+        gateway_windows._quote_vbs_string(\"line1\\nline2\")\n \n \n def test_install_scheduled_task_success_start_now_uses_direct_spawn_not_task_run(monkeypatch, tmp_path, capsys):\n"]}]}],"trust":{"default_tier":"verified","includes_suggested":false}}