KISS Sorcar on Windows: full test run, triage and fixes

2026-09-19. Host: Windows Server 2022 (ksen@34.133.160.141, 16 cores, 32 GB), Python 3.13.15, Node 24.21, Git 2.55. Baseline: origin/main at b38a7fa0e. Linux control: this repository's usual build host (Python 3.14).

1. Summary

The repository had never been run on Windows. At the baseline it could not even be cloned there (a tracked file name contains < and >), and once checked out the whole kiss package failed to import (import fcntl in kiss.core.models.model_info). After those two fixes the first complete run produced roughly 700 failing tests, three pytest processes killed by a stray Ctrl+C, and two hung processes. Four fix-and-rerun rounds later the suite stands at 10,061 tests: 0 failures apart from documented load flakes, 1,128 skipped on Windows, and all 333 JavaScript suites of the VS Code extension pass there. The Linux suite stays green (10,111 tests), with the same three load-sensitive tests as before this work.

Python test failures per Windows run (log scale) 1 10 100 1000 ~830* run 1 134 run 2 2 + 1 killed split run 3 4 run 4 2 (pass alone) run 5
* run 1: 647 failures + 54 errors in the 12 splits that finished, plus 62 collection errors; three splits were killed by a console Ctrl+C and two hung, so the true count was higher. Each run is the whole Python suite in 14 parallel pytest processes (~10 minutes wall time).

2. How the run was set up

The box was empty: no Git, Python, Node or package manager. Bring-up followed the shape of ./rsorcar (copy the SSH identity, clone origin/main, ship ~/.kiss/api_keys.env, uv sync --all-extras, npm ci, npm run compile) but in a Windows flavour, because rsorcar itself is bash + systemd. Three Windows-specific environment steps were needed:

Two harness lessons cost time. Processes started from an SSH session are killed when the session ends (Windows OpenSSH puts them in the session's job object); launching each split through Win32_Process.Create from a .cmd wrapper made them survive. And the default SSH shell is PowerShell 5.1, whose quoting and stderr handling differ from bash; generating a .cmd file per invocation and running it with cmd /c avoided both problems. Fix-and-verify cycles used a private git worktree per area on the Windows box (C:\Users\ksen\kiss-wt\<area>) that receives the local git diff HEAD before every run and reuses the shared virtualenv via PYTHONPATH.

3. Classification method

Each failure was classified as one of three kinds, and fixed accordingly:

Shared gates were added to src/kiss/tests/conftest.py so that every skip states its mechanism: posix_only(reason), requires_unix_sockets, is_root() (instead of os.geteuid() == 0, which raises at import time on Windows), plus fixture helpers install_cli_script and install_fake_cloudflared that produce a runnable fake CLI on both platforms (a #! script cannot be executed directly on Windows).

4. Project bugs found and fixed

BugWhat happened on WindowsFix
Tracked file with < > in its name
papers/kisssorcar/ablation/artifacts/…/<__main__.BytesPath object at 0x104bf7a10>.1
git clone aborts the checkout: NTFS forbids those characters. The file is a 2-byte junk artefact written by an experiment.Removed from the repository.
Unconditional import fcntl (5 modules)kiss.core.base imports model_info, so every kiss.* module failed to import; nothing could run.One cross-platform primitive, kiss.core.file_lock (lock_exclusive, unlock, exclusive_file_lock; fcntl.flock on POSIX, msvcrt.locking on Windows). All lock users migrated; the sorcar persistence and worktree locks, previously no-ops on Windows, now really lock. scripts/remote_config.py, which is shipped alone to remote hosts, keeps a self-contained copy.
os.kill(pid, 0) as a liveness probeOn Windows signal 0 is CTRL_C_EVENT: the probe delivered Ctrl+C to every process on the console. This is what killed three pytest processes with KeyboardInterrupt, and in production it would interrupt the daemon whenever a worktree or bridge liveness check ran.kiss.core.processes.pid_alive (OpenProcess/GetExitCodeProcess on Windows). All probes, including 31 in tests, use it.
Process-tree termination: os.killpg, start_new_session, signal.SIGKILLNone exist on Windows; the Bash tool, cron command jobs, CLI children, the WhatsApp bridge and the Signal linker could not be stopped. A first port to taskkill /F /T then proved insufficient: Git bash's fork+exec leaves background children with an already-exited parent, which taskkill's tree walk cannot follow (verified with a probe: sleep.exe survived with a dead ParentProcessId).popen_process_group() starts the child and, on Windows, enrols it in a Job Object; kill_process_group() terminates the job (every descendant, however re-parented), falling back to taskkill, and raises OSError when it cannot, so callers fall back to proc.kill() as they did after a failed os.killpg. Eight spawn sites migrated.
Owner-liveness by pid alone (worktree reclaim)Windows recycles pids within seconds under load. A dead worktree owner's pid was reused by another process, so reclaim_orphaned_worktrees treated the orphan as live and left it behind.save_owner_pid also stamps kiss-owner-identity (process_identity(): start time + executable, prefixed with the pid); _owner_alive() requires both. Legacy pid-only stamps keep the old behaviour.
Daemon startup with Unix-domain socketsCPython on Windows has no AF_UNIX; asyncio.start_unix_server does not exist. Startup logged a traceback warning and left partial state._setup_server checks _unix_sockets_supported(), logs one INFO line and serves WSS only; the UDS liveness probe, stale-socket unlink and shutdown-signal set (_SHUTDOWN_SIGNALS, no SIGHUP) are guarded. daemon_client.run raises a clear ConnectionError naming the path.
Update button runs install.sh with shellPath: '/bin/bash' (VS Code extension)There is no /bin/bash; the update terminal died with ENOENT. The JS test had skipped the execution on win32 and hidden this.updateShellPath() resolves Git for Windows bash.exe (Program Files, per-user install, the extension's own ~/.local/git, then PATH minus the WSL launcher in System32) and shows an error toast when none exists. The test now executes the real update command on Windows too.
Files opened without encoding="utf-8"Windows' default code page (cp1252) raised UnicodeDecodeError reading the README, model tables, tokens and logs.Explicit UTF-8 at every affected read (README parsing in update_models, memory pages, channel tokens, subprocess output).
Replacing or deleting a file another process has openWindows raises PermissionError; atomic publishes of MY_MODELS.json, tokens, tab registry and memory pages failed intermittently under readers.atomic_write_text retries the rename briefly on Windows (replace_waiting_for_readers, also used by the web server's _atomic_publish); read_bytes_waiting_for_writer for the readers; write_private_file delegates to atomic_write_text(mode=0o600).
GNU grep in fs_actions.find_in_folderNo grep on Windows.git grep --no-index --no-color --no-column --no-full-name: same path:line:text output, honours .gitignore, present wherever Git is.
Chromium profile lock and leaked-browser identification (web_use_tool)The POSIX SingletonLock symlink and ps-based identity check do not exist; the unresponsive-page test hung forever.Windows probes the profile's lockfile; process_identity() reads creation time and image path via kernel32.
AssortedBash tool and cron command jobs shelled out with shell=True (cmd.exe); diff_merge matched .gitignore against backslash paths; stream_abort could not shut a socket down; signal_agent could not find signal-cli.bat; ThreadedHTTPServer.allow_reuse_address allowed two servers on one port; commands._restart_kiss_web_daemon claimed a restart it never did; the extension's file-link regex and explorer ignored drive letters; the parent-repo path guard was case-sensitive on a case-insensitive filesystem.Each fixed at the root: Git bash for shell commands (PowerShell fallback), as_posix() matching, socket.close() on Windows, shutil.which-based CLI lookup, reuse-address off on Windows, honest "no managed daemon" message, drive-letter aware link/explorer paths, Unicode case-insensitive guard on Windows only.

5. Test-side fixes

289 test files changed. The largest groups:

6. Final state

RunTestsFailedSkippedJS suitesNotes
Windows run 1 (fcntl fixed only)7,581*701*1997 failed / 167 in the half that finished3 splits killed by Ctrl+C, 2 hung
Windows run 210,0591341,057333 / 333no hangs
Windows run 39,343*21,011333 / 3331 split killed by a test's stale-pid kill
Windows run 410,06141,128333 / 333all fixed afterwards (see below)
Windows run 5 (final)10,06321,128333 / 333all 14 splits completed; both failures pass when re-run (see below)
Linux (final)10,1110-1~90333 / 333only the pre-existing load flakes listed below

Run 4's four failures: a thread-safety hole in the new Job Object registry (fixed with a lock); the recycled-owner-pid case (fixed with the identity stamp); a fixed 3 s sleep in the periodic-flush test (now polled); and one wedge of the _RWLock injection stress test, which passed 3/3 isolated re-runs on Windows and whose lock documents a deliberate residual two-injection window, so it is treated as that documented probabilistic case.

Run 5's two failures are load flakes of the 14-way parallel run, not regressions: test_openai_compat_agent::test_chat_completions_requires_bearer_token (the client saw ConnectionAbortedError 10053 while the box was saturated) and test_history_continuation_context::test_followup_with_distinct_tab_id_keeps_context (a worker thread had not yet exited at the moment of the is_alive() assertion). Re-running both modules alone on the same Windows box immediately afterwards gave 24 passed in 14.09 s.

Linux load flakes seen once each across three full runs and passing alone: test_content_tab_file_links::test_out_of_range_line_clamps_to_last_line (Playwright 30 s wait), test_remote_model_dropdown_mobile::test_model_pill_truncates_from_start, test_audit0902_fix_vscode_install_lock::test_eight_simultaneous_contenders_admit_exactly_one_installer (loser reports a previous trial's winner pid), and one live-network stall in test_update_models_decisions (SSL read to a model provider). None touch code changed here.

uv run check --full (ruff, mypy, pyright, extension typecheck and lint) passes.

7. Review

A read-only gpt-5.6-sol review of every changed project file and of the test diff found seven demonstrated issues, all fixed: kill_process_group swallowed taskkill failures; remote_config.py used a bare os.replace; the parent-repo guard used ASCII-only case folding and treated \ as a separator on POSIX; git grep output shape depended on the user's git config; the hard-coded /bin/bash above; over-broad UDS skips on eight files. Two test names cited by the test reviewer did not exist and were ignored. The framework's cap on review rounds was reached before the round-2 changes could be reviewed by the second model; those (Job Objects, owner identity, helper deduplication, the polling fix) were verified directly on both platforms instead.

8. What still does not work on Windows, by design

The VS Code extension talks to the daemon over a Unix-domain socket only (AgentClient, sorcarSockPath()), isDaemonRunning() returns false on win32 and restartKissWebDaemon() is a no-op there, and install.sh/rsorcar are bash. So on Windows today: the Python package, CLI agents, the Bash tool, cron command jobs, worktrees and the web app over WSS work; the extension's local daemon channel does not. The natural follow-up is a Windows named-pipe transport (\\.\pipe\…; Node's net accepts the same {path} API, Python needs loop.start_serving_pipe) plus a Windows daemon launcher without systemd. The 335 UDS-gated tests and 234 Muse-auth tests are the coverage that would come back with it.

9. Reproducing

# on the Windows box (PowerShell), after the tooling in §2 is installed
cd $env:USERPROFILE\kiss
uv run pytest -q -p no:cacheprovider --timeout=900 --timeout-method=thread src/kiss/tests
cd src\kiss\agents\vscode; npm run compile; node test\run-all.js

Working notes, per-run logs and the triage guide the area agents followed are in the task's tmp/ directory (PROGRESS.md, WINDOWS_TRIAGE_GUIDE.md, run1…run5).