On native Windows the text-mode ``handle.write("\n")`` that seeds an empty
``--lock-file`` is translated to ``\r\n``, so a fresh lock file is two bytes
and the msvcrt regression test fails on the platform it protects. Seed the
file with ``os.write(fd, b"\0")`` on the descriptor instead: exactly one byte
whatever the text-mode newline translation. The Windows-branch tests now run
under a ``\n`` -> ``\r\n`` translating ``open`` so this failure reproduces
on any host.
Remove the two ``... not in src`` assertions added for the previous review
from test_issue2628: the behaviours they hinted at (every missing-index
projection uses the pre-aggregated ordering; listing never creates an
index) are already asserted through query plans, result equivalence and
``PRAGMA index_list`` in test_pr7445_readonly_review_followups.py,
test_state_db_readonly_readers.py and test_issue3887_index_prime.py.
On the msvcrt branch of scripts/ensure_state_db_read_indexes.py the tool
locked byte 0 of --lock-file right after open(..., "a+"). When that open
had just created the file (or the file was empty) the lock range lay beyond
EOF, which Windows refuses, so the documented locked-maintenance workflow
raised instead of acquiring the lock (greptile P1).
Write one byte when the lock file is empty, then seek(0) and lock as
before. Only the Windows branch changes; POSIX flock is untouched and an
existing (deployment-owned) lock file is never rewritten.
Tests (tests/test_pr7445_readonly_review_followups.py): a strict msvcrt
stand-in that refuses to lock past EOF proves the lock is taken on byte 0
of a fresh empty file, that an existing lock file keeps its content, and
that a held lock still propagates before the database is touched.
Review follow-ups on #7445 (nesquena-hermes CHANGES_REQUESTED + Greptile).
api/agent_sessions.py
- The read-only pre-aggregated `latest_messages` candidate ordering now
applies to every missing-index projection (default sidebar, gateway
watcher, cron/webhook/kanban views), not only `include_sources ==
("cron",)`. Since the listing no longer self-heals the index, the
correlated `MAX(mx.timestamp)` scan re-ran on every load: 9.86 s on a
5,000-session / 20,000-message DB vs 0.044 s after this change, with
identical selected rows and order. The indexed seek is unchanged.
- `state_db_file_uri()` / `state_db_readonly_uri()` replace
`Path.as_uri()`: a UNC `HERMES_HOME` produced `file://server/share/...`
and CPython's bundled SQLite (3.11-3.13 Windows builds, no
SQLITE_ALLOW_URI_AUTHORITY) rejected the authority, so every agent-backed
session vanished. The builder emits the empty-authority
`file:////server/share/state.db?mode=ro`, keeps `file:///C:/...` for
drive letters, strips `\\?\` / `\\?\UNC\` prefixes, quotes `?`/`#`, and
leaves POSIX output byte-identical. Strict `mode=ro`, no writable
fallback.
scripts/ensure_state_db_read_indexes.py
- `fcntl` / `msvcrt` are imported conditionally; the module imports and
the no-`--lock-file` path runs on native Windows. `--lock-file` uses
`flock` on POSIX, `msvcrt.locking(LK_NBLCK)` on Windows, and fails with
a clear RuntimeError when neither exists. The rw open reuses the
UNC-aware URI builder.
docs/troubleshooting.md
- Operator section: read-only contract, degraded missing-index listing,
drained maintenance prerequisites, lock behaviour, failure modes, UNC.
Tests: tests/test_pr7445_readonly_review_followups.py (19 tests, red
before this change: correlated-subquery plan, missing URI builder,
ImportError on fcntl). Query-plan contract via EXPLAIN QUERY PLAN — no
CORRELATED SCALAR SUBQUERY on missing-index listings, identical rows to
the indexed listing; platform-parametrised URI cases; maintenance module
imported with `sys.modules["fcntl"] = None`. test_issue2628 no longer
pins the cron-only gate; the flock-held test skips without fcntl.