Skip to content

v7.72.0

Choose a tag to compare

@github-actions github-actions released this 06 Oct 17:41
· 2645 commits to main since this release
aa2edb6

Added

  • Native Windows x64 can install and run the local engine and PostgreSQL bundle, but Windows is not yet declared supported (nexus-f9bgu, RDR-224; nexus-f9bgu.8, .11, .15 to .23, .25, .43, .47, .49 to .52, nexus-efk2h, nexus-ijue9.20). On Windows 10 and 11 x64, nx init --service and nx daemon service install-binary fetch the windows-x64 assets of the pinned engine tag (see Changed): nexus-service-windows-x64.txz, holding nexus-service.exe and the four VC++ runtime DLLs it loads app-local, and a PostgreSQL 17.5 plus pgvector 0.8.2 bundle. Both are verified by sha256 and sigstore as on the other platforms. Windows on ARM and 32-bit Windows refuse with the reason. The executables carry no Authenticode signature; Windows code signing is deferred (nexus-dj01b). Windows stays unsupported until the RDR-224 Phase 5 gate passes on a clean machine in a native Windows Claude Code session (install, an MCP store-then-search round trip, a plugin hook firing, nx daemon service stop --with-pg leaving no process, claude -p returning); that gate has not run (nexus-f9bgu.26), and the desktop install path has not been walked (nexus-ijue9.21).

    What the client does on Windows. nx daemon service install --autostart registers a per-user Task Scheduler logon task, NexusStorageService, whose launcher runs under pythonw and starts the supervisor again 30 s after any non-zero exit, because the task's own restart-on-failure was measured to cover only a failed launch. Windows has no SIGTERM for this, so the supervisor runs in its own process group with a hidden console: nx daemon service stop sends CTRL_BREAK, confirms by process exit and falls back to a hard kill, and the engine runs in a Job Object that is terminated after the grace. A service running in another Windows session is refused by name and never killed. At sign-out, restart and shutdown the supervisor stops the engine and PostgreSQL cleanly (a hidden window answering WM_ENDSESSION, and an in-process PostgreSQL fast stop through its signal pipe, since a session that is ending cannot start pg_ctl). Before that fix every sign-out and restart on the clean Windows 11 guest crashed PostgreSQL and ran crash recovery at the next start; after it, sign-out took 0.46 s and restart 0.20 s with no crash recovery on the next boot. Sleep was not measured (a Hyper-V Gen2 guest cannot sleep). An engine or PostgreSQL bundle upgrade extracts the new files with the service still up, stops the service only around placement, places the file set as one unit with bounded retry and rollback, and restarts what it stopped.

    Identity and access. The service identity is the user's SID where POSIX uses the uid. Credential files that POSIX writes 0o600 get an owner-only DACL, and the read check reads the ACL in place of mode bits. Every directory nexus creates grants the user's own SID, so a directory an elevated process made stays usable by a normal session (without it, a non-elevated nx got WinError 5, measured), and a new Windows-only nx doctor row, Config directory access (non-elevated), names a config directory an elevated process made and prints the icacls remedy. schtasks, nx, claude and nx-mcp are never resolved through the current directory. Engine-side for Windows only: CTRL_BREAK stops the engine through OrtInitGate, including during ONNX Runtime initialisation, and boot sweeps dead onnxruntime-java temp directories, skipping any whose DLLs a live process holds.

    What was measured on Windows: the packaged engine and PostgreSQL archives from a release dispatch run passed the relocation smoke and the engine smoke on a clean Windows 11 Hyper-V guest with no developer tools and no VC++ redistributable (nexus-f9bgu.45); nx init passed on the same guest restored to its clean baseline (TLS entry under Changed); the win-release rehearsal runs the Windows test set natively (1249 passed on the TLS fix's run). nx self install on Windows is the separate entry below.

  • The desktop extension lists Windows as a platform (nexus-f9bgu.43, nexus-ijue9.20). mcpb/manifest.json compatibility.platforms adds win32. Windows has no exec, so the bundle's bootstrap runs the server as a child with the host's stdio handles inherited, resolves uv through PATHEXT without searching the current directory, and puts itself in a kill-on-close Job Object first, so killing the bootstrap kills the server. Measured on a Windows host (Python 3.13): a stdio round trip and exit code 7 propagate, and the server dies with the bootstrap; without the job it survives. A Desktop session on Windows has not been run end to end.

  • nx self install builds a new generation on native Windows without touching the venv a running process executes from (nexus-f9bgu.47, RDR-224). A new stdlib-only _install/generation_core.py states the bash installer's orchestration in Python: it builds gen-<stamp> with uv, points current and previous at it with directory junctions (create .new, rename aside, rename into place, rmdir the old one, never rmtree a junction; a crash between the renames is recovered by restoring the old pointer), and bridges a legacy uv tool install tree by reading its extras from uv-receipt.toml and registering it as a junction in the GC ledger. The Windows shim is a PATH entry, not a file: each generation gets <gen>\bin holding copies of its own Scripts\<name>.exe launchers, and the user PATH (HKCU\Environment, type and every other entry preserved, WM_SETTINGCHANGE broadcast) gets <tools>\current\bin ahead of uv's bin dir, so an upgrade only flips the junction. Nothing is written into ~\.local\bin, because a running launcher exe cannot be renamed, replaced or deleted on Windows. Terminals and Claude Code sessions started before the one-time PATH change keep the old nx until restarted; nx self gc removes uv's old launchers once the legacy tree is gone, skipping any that are running. The reap compares normcase(realpath) forms with the \\?\ prefix stripped for the current, previous and running-installer rules, deletes the receipt first, says kept ... in use rather than reaped whenever any file could not be removed, and keeps a half-deleted legacy tree's ledger pointer until the tree is gone. nx self install and nx self gc call it directly on Windows (never bash, which is the WSL launcher); on Windows repair_uv_takeover re-ensures the PATH entry and finishes a migration that stopped after the flip, and nx doctor checks the PATH entry instead of shim contents. POSIX behaviour and every .sh file are unchanged.

  • The engine serves per-collection top-K search in one request (nexus-tu8wp.1). POST /v1/vectors/search-per-collection takes one embedding-model group of up to 256 collections and returns each collection's own top per_collection_k, so a dense collection can no longer crowd a small one out of a flat LIMIT. The engine embeds the query once, runs one single-collection plain_search_<dim> statement per collection, applies the client's per-collection thresholds as each arm finishes into a heap that keeps the best limit by (distance, id, collection), and reranks once. Bounds: NX_SEARCH_FANOUT_CONCURRENCY (arms per request, default half the pool, clamped to the pool), NX_SEARCH_FANOUT_ARM_PERMITS (arms in flight across all requests, default half the pool, so plain search and writes keep the other half) and NX_SEARCH_FANOUT_BUDGET_MS (wall budget of one fan-out, default 20 s, under the edge's 30 s). A statement timeout, a dimension problem or the fan-out budget is isolated in per_collection[].error with a stable error_kind and the other collections are served; pool or admission exhaustion, an expired request budget and any other transient database failure fail the whole request with 503. No changeset. Engine half; the client calls it from this release (nexus-tu8wp.2 below).

  • A search asks the engine for each collection's own top-K, one request per embedding-model group (nexus-tu8wp.2). search_cross_corpus sends POST /v1/vectors/search-per-collection instead of batching a flat /v1/vectors/search fan-out, so a dense collection no longer crowds a small one out of the pool and a default four-corpus search is 2 or 3 requests rather than 6 to 30. per_collection_k is the count the pre-batching fan-out used (max(5, n_results * mult), the group's largest multiplier), limit is max(300, 4n) clamped to 1200, or 1000 with rerank, and only finite thresholds are sent. A collection that times out or runs out of fan-out budget is reported in failed_collections and the rest are served; the dimension kinds join the stale-orphan handling. When the route cannot serve a group the batched path runs exactly as before, remembered per process: for 10 minutes after a 404, a 403 or any edge refusal, a 405 or a 501, and for 60 seconds after a 500, each logged at WARNING with its status; a 429 or a 502 to 504 (the gateway has already retried those) and a 400 or 422 fail the group instead. The first request is single-flight, so one search probes an engine without the route once. The MCP server keeps the memo for its lifetime, while each nx invocation is a fresh process and pays one probe against an engine without the route. NX_SEARCH_PER_COLLECTION=0 (or false, off, no) turns the route off. A group whose collections the engine says mix embedding models is retried one request per collection through a pool of 8 and remembered for 10 minutes. A malformed envelope (a row without an id or distance, a non-numeric count) is refused as a whole and the group falls back. The reranked pool differs from the batched path's: with rerank the route thresholds each collection's rows first, merges the best 1000 per model group and reranks that once (the batched path reranked each batch's flat top 300, thresholds applied after), so the rerank-on page is not guaranteed to equal the old one; the final-page comparison is measured at nexus-tu8wp.3 after the route is deployed. --lexical keeps its /v1/vectors/hybrid-search leg. Paired with the engine half in nexus-tu8wp.1; this release's pin to engine-service-v0.1.149 (nexus-f9bgu.43) carries it.

  • The engine runs exact search for small collection sets (nexus-tu8wp.6). A plain-search statement, the flat one and each per-collection arm, first counts the rows its selected collections hold in the tenant, bounded at NX_SEARCH_EXACT_MAX_ROWS plus one; at or below it the statement runs exact instead of walking the shared HNSW index, so a small collection no longer pays a capped HNSW walk (measured 5-43 s on cold arms, 2026-10-05) before the empty-result re-run. Above the threshold it is today's path; 0 disables the router. The default, 10000, is provisional until the conexus fork measurement sets it. The route is visible only in the boot line (event=search_exact_router) and in event=vector_search_statement_slow, logged for a statement over 1 s with its route, probed rows and collections; a fast statement leaves no trace of it. No wire or status change.

Changed

  • This release pairs with engine-service-v0.1.149 (nexus-f9bgu.43). REQUIRED_ENGINE_VERSION moves from 0.1.146 to 0.1.149, and PINNED_SERVICE_TAG with it, so local-mode installs fetch v0.1.149. It is the first engine tag that publishes windows-x64 engine and PostgreSQL bundle assets. Since v0.1.146 the engine also carries the per-collection search route (nexus-tu8wp.1, first in v0.1.147), the cardinality router for plain search (nexus-tu8wp.6), the socket read bound on bounded statements (nexus-u9zkn), and the Windows OrtInitGate break handling and ONNX Runtime temp sweep (nexus-f9bgu.8, nexus-f9bgu.11). No changesets: the one changelog file touched in the range changes an XML comment only.

  • The conexus plugin's Python hooks launch through uv instead of python3, on every platform (nexus-efk2h). Seven hooks.json entries (version-lockstep, subagent-git-write-gate, credential-print-guard, mailbox-drain and the three nx-hook shim entries) now run as uv run --no-project --no-config --quiet <script>, the launcher the sn plugin already uses, because python3 is not on PATH on stock Windows and none of them fired there. Each script declares requires-python >=3.12 in a PEP 723 block, so uv never picks an older interpreter. Measured cost: about 14 ms per hook on macOS and about 100 ms on Windows. All seven entries ran on stock Windows from the command line; that they fire inside a native Windows Claude Code session is a Phase 5 gate assertion and has not been observed.

  • On Windows, nx verifies TLS through the operating system's trust store (nexus-f9bgu.52). A fresh Windows 11 holds about 16 root certificates and fetches others on demand only through its own verifier, so nx init failed downloading the PostgreSQL bundle with CERTIFICATE_VERIFY_FAILED; every urllib and httpx download (PG bundle, engine binary, ONNX models) was affected. truststore, a new Windows-only dependency, routes verification through CryptoAPI, activated once when the nexus package is imported. Verified on a clean Windows 11 guest with no priming request: nx init passed in 53 s. macOS and Linux are unchanged.

  • A search with no rerank plans 6 /v1/vectors/search calls instead of 28-30 at the MCP default fetch, and the candidate pool it reads is smaller (nexus-abdp2; Sam's 2026-09-29 stopgap "A" while the engine-side per-collection top-K, nexus-tu8wp, is built). _desired_candidate_count now has two per-collection floors. The lean floor, max(5, n_results // 2), applies when nothing reads the depth of the pool; the deep floor, the old max(5, n_results * mult), applies when server rerank is on, the lexical leg is on, or the caller filters the pool after retrieval (nx search --path and --max-file-chunks pass the new deep_candidates=True). A group's pool never falls below n_results * mult either way, so a lone collection keeps its full over-fetch.

    What it buys: the MCP search tool fetches n_results=30 (limit 10 plus two lookahead pages) with no rerank. For a group of 67 voyage-context-3 and 25 voyage-code-3 collections that is 28 calls under the old floor and 6 under the lean one; on the cloud tenant now (98 collections in the default corpora, 30 calls old) a warm search went from a median 12.5 s to 5.7 s over four interleaved rounds, same 69 rows. At n_results=300 a 28-collection group needs 14 calls instead of 28. nx search over several collections reranks, so it takes the deep floor and plans the same calls as before (12 calls, median 7.8 s against 7.5 s).

    What it costs: a smaller pool, roughly 125-170 rows where the old floor returned 300 at n_results=10. The pool feeds server rerank, apply_ranking_boosts (hybrid, frecency, quality, topic), the --path, --max-file-chunks and file-diversity filters, the lexical leg's per-batch limit, the MCP footer's "of N" and the diagnostics raw_count/dropped. The page a user reads after boosts, measured against an old-vs-old noise control (test_final_order_parity_old_vs_new_floor, T2 nexus/measurements-abdp2-final-order-2026-10-05), stays within noise without rerank at fetch sizes 10 and 30, with and without the distance threshold (mean top-10 overlap 0.94-0.98 against 0.96-1.00 for old against old). With rerank the lean floor does not: mean overlap fell to 0.68-0.77 where old against old was 0.98-1.00 (one query at 0.11), and floors of n_results and n_results * mult // 2 recovered only 0.85 and 0.93, which is why the reranked path keeps the old floor. A --path that selects a minority collection lost rows under the lean floor (18 survivors became 8 in one pair), which is why a post-filter takes the deep one. The reranked and filtered paths request the same candidates as before.

    The recall-parity gate (tests/test_search_fanout_recall_parity.py, raw top-10 at n_results=10, no rerank) passes with every query at Jaccard 1.000 under the lean floor. That gate is blind to everything above, which is why the final-order variants were added. This change is also what moves four corpora (knowledge, code, docs, rdr) from the gate's split floor (0.6) to its strict 0.9 per query: at n_results=10 only the 107-collection all corpus still splits. The strict floor fails on the reference's own jitter: OLD against OLD scored 0.818 on "release process and versioning" (docs) and the rdr query nexus-atylb recorded at 0.667, and the gate run at the old floor scored 0.818 on "error handling and retry logic" (code) and on the same rdr query; two intermediate floors each failed the gate once for that reason. The gate is therefore flaky for unsplit corpora independent of the floor (nexus-e9sux).

Fixed

  • A search statement's read on a silent Postgres server now fails at a bound instead of waiting on TCP (nexus-u9zkn). During the 2026-10-05 production failover new connections recovered after 31.5 s, but a search read already in flight hung 59 s: the old server sent no RST and the engine set no network timeout, so the read waited on TCP. The bound is per path, not pool-wide, and it covers the statements a path bounds, not every read of the request. Every Java-set statement_timeout now also gives that connection a socket read timeout of the statement bound plus NX_PG_SOCKET_TIMEOUT_MARGIN_SECONDS (default 30, 0 disables, a bad value fails the service at boot), set in the same PgSession.setLocal call so the two cannot drift and restored when the connection returns to the pool; the statement bound is set before the borrow's other setLocal round trips, so those are covered too. At the defaults a silent search statement ends at 30 + 30 = 60 s (taxonomy assign 60 s, reaper statements and gc batches 55 s, a bounded reaper census 90 s, scheduled sweeps 60 s, the sweep gate 35 s, tuple subspace list 40 s), which is about the 59 s the failover took: the gain is a deterministic end, not a faster one, and a smaller margin ends it sooner. Reads that stay unbounded: the token store's reads on an auth-cache miss and session resolution (TokenStore, plain connection, no stamp, no statement bound), CollectionRegistry.lookup on a cache miss, and every other read with no statement bound. A request can still wait on TCP in any of them, before or after its bounded statement. Paths with no statement bound get no read bound, on purpose: collection re-home (measured 128 s), the legacy orphan quarantine (5 m 41 s), purge-trash, delete and rename collection, the taxonomy link joins, VACUUM, and writes in general run for minutes with the server silent, and a socket timeout closes the socket without cancelling the backend, so a bounded write would roll back and its retry would stack a second backend behind the first. The tenant stamp that opens every borrow runs before a path sets its bound, so it gets its own read bound of the margin, never under 5 s, reset to none before the path runs. Some plpgsql functions (the gc_* and reaper_* ones) set statement_timeout themselves, 5 s or 25 s, which no Java bound covers; the paths that call a 25 s function set a Java bound of 25 s, so their read bound (25 s plus the margin) always outlasts it. tcpKeepAlive is also set on the main and migration pools, by a class that does not load PgSession, so a bad bound value still fails at the boot catch; its OS default of about 2 h makes it a backstop for idle connections only. Engine change; takes effect with the next engine tag.
  • The SessionStart upgrade hook no longer waits on nx upgrade --auto, so a version change cannot time it out (nexus-wozn6). Since 7.68 nx upgrade --auto runs the RDR-192 census whenever the installed version has no completion on file. Measured on 2026-10-05, that census took 51.5 s over 98 collections, plus about 2.7 s of other work, against the hook's 30 s timeout. nx-hook upgrade-auto waited on the child. After the 7.70 and 7.71 installs the hook was cancelled at 30 s (6 cancelled session starts in each window) and the ladder never recorded the rung for either version. The most likely cause is that the child died with the cancelled hook. That was not observed: nobody killed the old verb and looked at its child, and a census that defers records nothing either. The census cost also does not explain every cancel on record, since 23 fell on 2026-09-30, before 7.68 shipped the rung. The verb now starts nx upgrade --auto in a session of its own, discards its stdout, appends its stderr to <config>/logs/upgrade-auto-child.log (1 MiB, one rotation), writes one upgrade_auto_child_spawned event with the pid and argv to hook.log, waits at most 2 s to catch an nx too old to know --auto, and returns. No detached real run has been watched to completion. The check is the outcome: after the next version change, the upgrade ledger (nx upgrade --dry-run) advances to the installed version with no manual run; if it does not, the cause above is wrong. The child has no overall time limit, and several session starts after one install each start one (the rung's lock turns all but one into quick deferrals). On Windows the child starts with CREATE_NO_WINDOW and breakaway-from-job, falling back without breakaway; that path is pinned by a mocked Popen only and has not run on a Windows host.
  • The SessionStart RDR summary returns inside its 10 s timeout (nexus-wozn6). It was repeatedly cancelled with nothing printed. On the cloud tenant it spent 3.3 s on a T3 existence check that lists every collection's stats, and 1.7-8.9 s fetching the whole <repo>_rdr T2 project (1527 rows, 4.6 MB). The check now reads one collection's count (0.31 s), and the catalog lookup, the T3 check, the T2 fetch, the listing fallback and the git probes all run inside one 7 s budget, with the T2 fetch started first so it has the whole budget. End-to-end runs on the same tenant in the same minute took 9.63-9.84 s before the fix and 6.60-6.70 s with the first version of it (T2 fetched after the catalog and T3 legs), with no upgrade child running. Three runs of the shipped code (T2 started first) took 5.61 s, 5.71 s and 7.08 s; the 7.08 s run hit the budget with T2 late at 6.93 s. T2 is the dominant cost and is unchanged, so a fetch slower than the budget still happens: that session drops the status breakdown and rdr-fix pointers and says so on its own line, and hook.log gets an rdr_hook_t2_late event with the elapsed time so the rate can be counted. A narrower T2 read is nexus-pxp44. Two other cuts are said in the output rather than left silent: the rdr-fix check names itself as cut short when the budget ends it, and when the T3 check does not answer and the listing has no budget left, the line says whether the tree is indexed is unknown instead of "NOT indexed". One behavior change: the count is the collection's stored chunk count, so a collection whose chunks are all trashed or unowned now reads as indexed, where the tenant-wide listing counted live chunks and read it as absent.