Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
c0edb12
fix(locking): use portalocker so the memory-root lock works on Windows
Sep 23, 2026
9d212db
ci: run the unit suite on Windows
Sep 23, 2026
5bb064c
docs: add the Windows / WSL2 install guide
Sep 23, 2026
f7a31c7
fix(windows): make file uris, text io, and port probing platform-correct
Sep 23, 2026
469e984
test(tui): read the exported svg as utf-8 and drain deferred scrolls
Sep 23, 2026
3964f43
docs(windows): state what the green Windows CI run proves
Sep 23, 2026
5d02932
feat(cascade): make scan_interval_seconds a [cascade] setting
Sep 23, 2026
fd3c9da
fix(cascade): record a late modified for a gone path as deleted
Sep 23, 2026
4734b9f
test(windows): cover watcher events and lance filesystem semantics
Sep 23, 2026
6ba1d56
docs(windows): name the VC++ Redistributable as a native prerequisite
Sep 23, 2026
d91702a
feat(windows): ship the MSVC runtime so pip install just works
Sep 23, 2026
9c0fe18
test: import everos first so the DLL hook runs before sqlalchemy
Sep 23, 2026
167473d
test: make the slow/live selection collectable and survivable on Windows
Sep 23, 2026
89ac544
test(cascade): rename test asserts the source leg after a sweep
Sep 23, 2026
47b96eb
fix(markdown): retry the staging swap past a Windows sharing violation
Sep 23, 2026
5692baf
fix(ome): never hand a strategy an event class it did not declare
Sep 23, 2026
06509b2
fix(api): reject an orphan tool row with 422 instead of 500
Sep 23, 2026
c99db49
fix(ome): let a manual trigger reach a Cron strategy again
Sep 23, 2026
45258d5
docs(windows): record the native Windows verification results
Sep 24, 2026
54b483b
test: read text fixtures as UTF-8 so a GBK-locale Windows passes
Sep 24, 2026
9e63279
style(tests): wrap the UTF-8 read_text calls to the line limit
Sep 24, 2026
b75e755
fix(windows): probe the per-user install dir for the runtime DLLs
Sep 24, 2026
ab33efe
docs(windows): claim only what was run, and where
Sep 24, 2026
bafe722
docs(windows): state the verified pip-install path and prerequisite
Sep 24, 2026
846331b
test(locking): give the spawned child 60 s to import before asserting
Sep 24, 2026
cf62b58
Merge branch 'main' into feat/windows-support
gloryfromca Sep 24, 2026
8ced012
Merge branch 'main' into feat/windows-support
gloryfromca Sep 24, 2026
b28148d
Merge remote-tracking branch 'origin/main' into feat/windows-support
Sep 24, 2026
9473019
docs(windows): title the page for Windows, native first, WSL2 second
Sep 24, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,30 @@ jobs:
- name: Unit tests
run: make test

unit-windows:
name: unit tests (Windows)
runs-on: windows-latest
steps:
- uses: actions/checkout@v6

- name: Install uv
uses: astral-sh/setup-uv@v8.2.0
with:
enable-cache: true
cache-dependency-glob: uv.lock

- name: Set up Python
run: uv python install 3.12

# No `make` on the Windows image — call uv directly. The lock is
# universal (it carries sys_platform == 'win32' resolution markers and
# pywin32, which portalocker requires on Windows), so --frozen resolves here.
- name: Install dependencies (frozen)
run: uv sync --frozen

- name: Unit tests
run: uv run pytest tests/unit -q

unit-py313:
name: unit tests (3.13)
runs-on: ubuntu-latest
Expand Down
10 changes: 9 additions & 1 deletion benchmarks/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -528,7 +528,15 @@ def _free_port(start: int) -> tuple[int, socket.socket]:
for port in range(start, start + 400):
s = socket.socket()
try:
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# SO_REUSEADDR means the opposite thing on Windows: it lets a
# second socket bind a port this one already holds, so the
# hold-until-spawn above would stop excluding anyone and two
# concurrent runs would be handed the same port. Windows spells
# the exclusivity this probe needs SO_EXCLUSIVEADDRUSE.
if hasattr(socket, "SO_EXCLUSIVEADDRUSE"):
s.setsockopt(socket.SOL_SOCKET, socket.SO_EXCLUSIVEADDRUSE, 1)
else:
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind(("127.0.0.1", port))
s.listen(1)
except OSError:
Expand Down
7 changes: 4 additions & 3 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -304,7 +304,8 @@ file (`episode-<YYYY-MM-DD>.md` etc.).
assistant emitted in this turn (OpenAI Chat Completions shape).

**`tool_call_id`** — When `role: "tool"`, the `id` of the call this
message is the response to.
message is the response to. Required for `role: "tool"`; a tool row
without it is rejected with `422`.

### ContentItem

Expand Down Expand Up @@ -1117,8 +1118,8 @@ the operational probes; clients that share a response parser with
| `runs` | `list[RunSummary]` | One entry per strategy run *attempt*, not per dispatched route: `{run_id: string, status: string, error?: string}`. A strategy that retried before settling contributes multiple entries sharing one `event_id`. `status` is one of `running` / `success` / `failed` / `dead_letter` / `crashed`. Includes dead-lettered runs |

**`not_dispatched`** means every subscriber was rejected by one of the
four dispatch gates (`_routes_to` / `enabled` / `applies_to` /
`Counter`). The most common cause is forgetting `"force": true` on a
five dispatch gates (event-class subscription / `_routes_to` / `enabled` /
`applies_to` / `Counter`). The most common cause is forgetting `"force": true` on a
strategy that is `enabled=false` in `ome.toml` — e.g. triggering
`reflect_episodes` without `force` while it is disabled in config
returns `{"status": "not_dispatched", "dispatched": 0, "runs": []}`
Expand Down
9 changes: 6 additions & 3 deletions docs/cascade_runbook.md
Original file line number Diff line number Diff line change
Expand Up @@ -235,9 +235,12 @@ error and silently see nothing.

Workarounds:

- Rely on the scanner — at default 30 s interval, throughput is
- Rely on the scanner — at the default 30 s interval, throughput is
bounded but eventually-consistent.
- Drop the scan interval to ~5 s if the memory root is small.
- Shorten the interval if the memory root is small:
`scan_interval_seconds = 5.0` under `[cascade]` in `everos.toml`, or
`EVEROS_CASCADE__SCAN_INTERVAL_SECONDS=5`. Every sweep stats every md
file, so over a slow mount a short interval is a steady I/O cost.
- With no server running, run `everos cascade sync` explicitly after batch
edits. A running server picks them up itself, and `sync` refuses to run
next to it (exit code 3): two processes writing the same index insert
Expand Down Expand Up @@ -303,7 +306,7 @@ and `everos.memory.cascade.worker.CascadeWorker`:

| Knob | Default | Effect |
|---|---|---|
| `scan_interval_seconds` | 30 | Scanner sweep cadence |
| `scan_interval_seconds` | 30 | Scanner sweep cadence — also settable under `[cascade]` |
| `worker_batch_size` | 50 | Rows claimed per worker cycle |
| `worker_max_retry` | 3 | Inline retries before `mark_failed(retryable=TRUE)` |
| `worker_poll_interval_seconds` | 1 | Idle wait between empty drain attempts |
Expand Down
2 changes: 2 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ specific thing (drain a queue, recover from a stuck row, etc.).
| Doc | Purpose |
|---|---|
| [cascade_runbook.md](cascade_runbook.md) | Cascade subsystem ops — drain queue, recover stuck rows |
| [windows.md](windows.md) | Install and run EverOS on Windows — natively or via WSL2 |
| [windows.zh.md](windows.zh.md) | Chinese mirror of the Windows / WSL2 guide |
| [github-sync.md](github-sync.md) | Guardrails for refreshing GitHub from internal exports without overwriting GitHub-only workflow files |
| [benchmarks/README.md](../benchmarks/README.md) | LoCoMo benchmark — run and evaluate |

Expand Down
291 changes: 291 additions & 0 deletions docs/windows.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,291 @@
# Running EverOS on Windows

> Also available in Chinese: [windows.zh.md](windows.zh.md)

EverOS runs on Windows in two ways. **Natively** — `pip install everos`
on a stock Windows 11 machine, verified end to end and covered by the
`unit tests (Windows)` CI job; that path is [Native Windows](#native-windows)
and is where to start. Or under **WSL2** — a real Linux kernel, so the
storage stack (file locking, LanceDB, inotify) behaves exactly as it does
on a Linux server; the rest of this page is that install, including the
parts that cannot be scripted and the one failure that is silent.

## Table of contents

- [Before you start](#before-you-start)
- [Will this machine need a reboot?](#will-this-machine-need-a-reboot)
- [Install](#install)
- [Phase 1 — WSL2 and the distro](#phase-1--wsl2-and-the-distro)
- [Phase 2 — EverOS inside the distro](#phase-2--everos-inside-the-distro)
- [Scripted install](#scripted-install)
- [Verify](#verify)
- [Where the memory root must live](#where-the-memory-root-must-live)
- [Office document support](#office-document-support)
- [Reaching the API from Windows](#reaching-the-api-from-windows)
- [Troubleshooting](#troubleshooting)
- [Native Windows](#native-windows)

## Before you start

| Requirement | Notes |
|---|---|
| Windows 11, or Windows 10 build 19041+ | `winver` to check |
| Administrator rights | Enabling the WSL feature needs elevation |
| Hardware virtualization enabled in firmware | Task Manager → Performance → CPU → "Virtualization: Enabled" |
| ~3 GB disk for the distro, ~1 GB more with LibreOffice | |

On a managed corporate machine, Hyper-V and the virtual machine platform
are sometimes blocked by policy. That is a hard stop — it needs IT, not a
workaround.

### Will this machine need a reboot?

`wsl --install` enables the `VirtualMachinePlatform` Windows feature, and
enabling it requires a restart. But the feature is often **already on** —
Docker Desktop, Hyper-V, Windows Sandbox, the Android emulator and
virtualization-based security all turn it on. Check before you plan around
a reboot:

```powershell
(Get-WindowsOptionalFeature -Online -FeatureName VirtualMachinePlatform).State
```

- `Enabled` → no reboot; the whole install runs in one pass.
- `Disabled` → one reboot, once, on this machine only.

## Install

### Phase 1 — WSL2 and the distro

Run PowerShell **as Administrator**:

```powershell
wsl --install --no-launch
```

`--no-launch` matters: without it, `wsl --install` opens the distro's
first-run wizard and blocks on an interactive "Enter new UNIX username"
prompt, which is what breaks unattended installs.

Reboot if the preflight above said `Disabled`. Then create the distro
without the interactive account setup:

```powershell
wsl --install -d Ubuntu --no-launch
ubuntu install --root
```

### Phase 2 — EverOS inside the distro

```bash
wsl -d Ubuntu -u root -- bash -lc '
set -euo pipefail
apt-get update -qq
apt-get install -y -qq python3-venv curl
curl -LsSf https://astral.sh/uv/install.sh | sh
export PATH="$HOME/.local/bin:$PATH"
uv venv --python 3.12 /opt/everos
uv pip install --python /opt/everos/bin/python everos
'
```

### Scripted install

The two phases combine into one idempotent script. Re-running it after the
reboot picks up where it stopped.

```powershell
# everos-setup.ps1 — run as Administrator
$ErrorActionPreference = 'Stop'

# Phase 1: WSL platform.
if (-not (Get-Command wsl -ErrorAction SilentlyContinue) -or -not (wsl --version 2>$null)) {
wsl --install --no-launch
Write-Host 'WSL installed. Reboot, then re-run this script.' -ForegroundColor Yellow
exit 0
}

# Phase 2: distro.
if (-not (wsl -l -q | Select-String -Quiet 'Ubuntu')) {
wsl --install -d Ubuntu --no-launch
ubuntu install --root
}

# Phase 3: EverOS.
wsl -d Ubuntu -u root -- bash -lc @'
set -euo pipefail
apt-get update -qq
apt-get install -y -qq python3-venv curl
curl -LsSf https://astral.sh/uv/install.sh | sh
export PATH="$HOME/.local/bin:$PATH"
uv venv --python 3.12 /opt/everos
uv pip install --python /opt/everos/bin/python everos
'@

Write-Host 'Done. Start with: wsl -d Ubuntu -- /opt/everos/bin/everos serve' -ForegroundColor Green
```

Detect WSL with `wsl --version`, not `wsl --status` — the latter's exit
code is not a reliable "is it installed" signal.

## Verify

```bash
wsl -d Ubuntu -- /opt/everos/bin/everos --version
wsl -d Ubuntu -- /opt/everos/bin/everos init
wsl -d Ubuntu -- /opt/everos/bin/everos serve
```

From there follow the [Quick Start](../README.md#quick-start) — inside the
distro everything behaves as on any Ubuntu host.

## Where the memory root must live

> [!IMPORTANT]
> Keep the memory root on the WSL2 filesystem (`/home/...`, `/opt/...`).
> **Never put it under `/mnt/c/`.**

Filesystem events do not propagate from the Windows host into WSL2. If the
memory root sits on a `/mnt/c` mount, the cascade watcher starts without
error, logs normally, and receives **zero events** — edits to markdown
files never reach the index, and searches silently answer from stale data.
Nothing in the logs says so.

If you must keep files on the Windows side, fall back to polling:

| Option | How |
|---|---|
| Scanner sweep (default 30 s) | Already on; bounded but eventually consistent |
| Faster sweep | Drop the scan interval to ~5 s for a small root |
| Explicit sync | `everos cascade sync` after batch edits |

Details in the [cascade runbook](cascade_runbook.md#wsl2--network-mounts).

## Office document support

Install the **Linux** LibreOffice inside the distro — the Windows build
cannot serve a process running in WSL2:

```bash
wsl -d Ubuntu -u root -- apt-get install -y libreoffice
```

The parser resolves the binary with `shutil.which("soffice")`, a plain
PATH lookup inside the distro, so a Windows `soffice.exe` is never found.
Without it, office uploads return `503 CAPABILITY_UNAVAILABLE`; see
[multimodal.md](multimodal.md#libreoffice-office-documents-only).

## Reaching the API from Windows

Yes — WSL2 forwards `localhost` by default (`localhostForwarding`), so a
server started inside the distro answers on the same port from Windows.
`everos serve` binds `127.0.0.1` inside the distro; the forwarder relays
to it, so the loopback default does not need changing:

```bash
wsl -d Ubuntu -- /opt/everos/bin/everos serve # listens inside the distro
curl http://localhost:8000/health # from Windows PowerShell
```

If that call does not connect, the usual causes are a VPN client taking
over the WSL network, or a wedged forwarder. Restart WSL first:

```powershell
wsl --shutdown # next `wsl` command restarts it and rebuilds the relay
```

> [!WARNING]
> Do not reach for `host = "0.0.0.0"` as a fix. **EverOS ships no
> authentication of its own** — loopback is what keeps the API private
> (see [SECURITY.md](../SECURITY.md)). How far `0.0.0.0` actually reaches
> depends on the WSL networking mode, and the two differ sharply:
>
> | `networkingMode` in `.wslconfig` | What `0.0.0.0` exposes |
> |---|---|
> | `nat` (default) | The distro's private virtual network plus the Windows host. Other machines on your LAN cannot reach it without an explicit `netsh portproxy`. |
> | `mirrored` (WSL 2.0+) | Every interface the Windows machine has — **the API becomes reachable from the LAN**. |
>
> Only bind `0.0.0.0` once your own gateway or auth layer sits in front.

## Troubleshooting

| Symptom | Cause | Fix |
|---|---|---|
| `wsl --install` hangs on "Enter new UNIX username" | `--no-launch` omitted | Ctrl-C, re-run with `--no-launch`, then `ubuntu install --root` |
| "The virtual machine could not be started" | Virtualization off in firmware | Enable VT-x / AMD-V in BIOS/UEFI |
| `WslRegisterDistribution failed with error: 0x80370102` | Same, or Hyper-V blocked by policy | Firmware setting, or IT |
| Install succeeded but nothing runs after reboot | Script not re-run | Re-run it; it is idempotent |
| Markdown edits never appear in search | Memory root on `/mnt/c` | Move it into the distro filesystem |
| Office upload returns `503` | LibreOffice missing **inside** the distro | `apt-get install -y libreoffice` |

## Native Windows

EverOS runs directly on Windows. Verified on a stock Windows 11 Enterprise
laptop (Intel Core Ultra 7 155H, 32 GB, no Visual C++ Redistributable
installed) with Python 3.12 from `uv`:

- `pip install` of the built wheel into a plain venv (dependencies from
PyPI: pyarrow 25.0.1, `msvc-runtime` 14.44, lancedb 0.34) →
`everos init` → put an LLM `api_key` in `everos.toml` (the server refuses
to start without one, and `init` says so) → `everos server start`: healthy
after 35 s, `/add` and `/search` answered. No other manual step. The
Windows-only dependencies are `msvc-runtime`, which supplies the C++
runtime `greenlet` needs (details below), and `pywin32`, which
`portalocker` uses for file locking.
- Test suites: the unit suite runs green in CI on `windows-latest`
(`unit tests (Windows)`, 2583 passed / 4 skipped — the same count as
Linux); on that machine, integration **183 passed / 5 skipped** and live
LLM (`slow`) **28 passed / 1 skipped**, both under Python 3.12.
- All four memory kinds — episode, profile, agent case, agent skill — were
produced by a Tier 3 server (real LLM, embedding and rerank providers) and
read back through `/get` and `/search`.
- A 10-hour soak (about 78 000 markdown entries written and rewritten,
16 000 searches, 2 300 `/add` calls through the extraction path, two
concurrent `everos cascade sync` processes on the same tree) ended with
the index intact: every table opens, schemas verify, and every well-formed
entry had its row — the only markdown entries without one were the
deliberately malformed files the run seeds. RSS levelled at about 2.3 GB
after three hours; the LanceDB directory peaked at 6.7 GB and reclaimed
to 2.7 GB (437 MB of live data). Two findings from that run are tracked
separately and are not Windows-specific: concurrent `cascade sync`
processes can insert the same row twice (about 4.5 % duplicate rows after
10 hours, no corruption), and request latency degrades under sustained
write load.

Python: 3.12 is what ran on that machine; 3.13 and 3.14 (regular build)
pass the same suites in CI and on macOS without changes; 3.11 is refused by
`requires-python` and by the PEP 695 syntax in `src/`; the free-threaded
3.14t build has no `lancedb` wheel.

Windows specifics worth knowing:

- `os.replace` fails with `PermissionError` while another process — the
cascade worker, or Defender scanning a fresh file — holds the target open.
The markdown writer retries with backoff (about 2.5 s of patience) and
logs each retry at debug level; the soak's load generator, which uses
the same idiom, hit 49 such violations and recovered from all of them.
- Windows Search indexes everything under `%USERPROFILE%`. A memory root
there costs about one CPU core of `SearchIndexer` under heavy writes; put
the root elsewhere or exclude the directory from indexing.
- File-change events come from `ReadDirectoryChangesW`, which can report a
rename as two events. The cascade scanner reconciles against the disk.
- Both machines that ran the suites had long paths enabled
(`HKLM\SYSTEM\CurrentControlSet\Control\FileSystem\LongPathsEnabled=1`;
GitHub's image sets it, a stock install does not). Memory roots nest
several directories deep and Lance index directories carry UUID names,
so a root under a long profile path can cross 260 characters — enable
long paths or keep the root short.

The one prerequisite that used to bite on a clean machine is the
**Microsoft Visual C++ Redistributable (x64)**. `greenlet`, which
SQLAlchemy's async engine depends on, is a C++ extension whose wheel does
not bundle the runtime, so without it every SQLite call fails with a cryptic
`DLL load failed while importing _greenlet`. GitHub's CI image has the
redistributable preinstalled, which is why CI never caught this. EverOS now
carries the runtime itself: the Windows-only `msvc-runtime` dependency puts
the DLLs in `sys.prefix`, and `everos/__init__.py` registers that directory
with the DLL loader before anything else is imported. Nothing to install,
no administrator rights needed. One dependency to watch: `msvc-runtime`
ships wheels only, one per CPython minor version, so a Python newer than
its latest wheel cannot install EverOS on Windows until upstream publishes
one.
Loading
Loading