Skip to content

v7.71.0

Choose a tag to compare

@github-actions github-actions released this 04 Oct 21:07
· 2724 commits to main since this release
33eaa55

Pairs with engine-service-v0.1.146 (tagged on 2b85b33, live in the cloud since
19:02Z, before this client tag: every wire-ledger entry is additive). The engine
carries three RDR-192 changesets: vectors-027 (a fraction floor on the
quarantine move route), vectors-028 (the reaper expires client-moved knowledge
quarantine) and vectors-029 (the hybrid text-gate probe uses the GIN indexes
under row-level security, guarded, with a boot isolation check). Measured on a
PITR fork of production at the same tenant data, the text-gate probe went from
1,176 ms to 93 ms for a rare token in one collection and from 25.7 s to 157 ms
across all collections; the post-deploy gate read hybrid client p95 from
1,724 ms to 1,161 ms. The MCP surface shrinks: the ten operator_* tools are no
longer advertised (nx_answer runs them through plans), and Claude Code defers
the nexus-catalog schemas. See conexus/CHANGELOG.md for the plugin side.

Added

  • The cloud client-path gate checks the RDR-192 sweep routes and the reaper through the public edge (nexus-wbfpw.50). Leg K posts requests that move nothing (refusals, a missing route, a read of an unregistered collection name) to /v1/vectors/gc/*, /v1/vectors/reapable and /v1/vectors/manifest-less-census, and asserts the engine's own JSON and the 404/400/422/200 the client reads; leg J asserts the reaper object of /v1/status is present, enabled, fresh and free of failed passes. The success shapes of the sweep routes and the typed 503 quarantine_restore_busy are listed in the script as uncovered, since they need writes.

Changed

  • The ten analytical operators are no longer MCP tools (nexus-ivi4s; outside review, T2 nexus/outside-critique-mcp-surface-proposal-2026-10-04). operator_extract, _rank, _compare, _summarize, _generate, _filter, _check, _verify, _groupby and _aggregate lose their @mcp.tool() registration and stay plain functions in nexus.mcp.core. Each spawns a claude -p subprocess on the user's credentials, and together they cost every session about 4k tokens of tool schema; nx-mcp now registers 44 tools. nx_answer and plan execution are unaffected: a plan step names the bare verb and the runner resolves it in-process with getattr(nexus.mcp.core, "operator_<verb>"), which never went through the MCP registry. A session that called an operator directly now gets tool-not-found; ask nx_answer, or save a plan whose steps name the verbs. The auto-approve hook's ten operator entries and the nexus skill's direct-call examples go with them.
  • Claude Code defers the nexus-catalog tool schemas (nexus-ivi4s). conexus/.mcp.json sets alwaysLoad: false on nexus-catalog, so its 10 tools (about 3.4k tokens) load through tool search on first use rather than in every session. nexus and sequential-thinking stay always-loaded: conexus 4.34.4 turned alwaysLoad on for every server because sessions stopped invoking tools whose schemas were deferred.
  • The move into quarantine has a fraction floor on the engine route, so the indexer's prune and nx t3 gc share one guard (nexus-wbfpw.52, RDR-192). POST /v1/vectors/gc/quarantine-orphans, bounded or not, takes optional floor_fraction, floor_min_chunks (default 100) and force. The engine judges the reaper's reading inside the move's own call, under the sweep gate, on the whole reapable set (at least the minimum, and strictly more than the fraction of every stored chunk): over it, nothing moves, the response says refused: true with reapable_count and total_count, and one gc_audit row (operation=gc_quarantine_orphans_refused) records it (one per state: a repeat with identical counts and floor values under an hour after the last row answers refused: true and writes no row). Every response echoes the floor it applied, or {"given": false}. A request without the fields is unchanged. New function nexus.gc_quarantine_orphans_floored (changeset vectors-027), a wrapper over the released move functions, none of which changes. Client half: nx index repo's prune used to move with no floor at all (NX_GC_FLOOR_FRACTION guarded only the later hard delete); it now sends NX_GC_FLOOR_FRACTION, the 100-chunk minimum and NX_GC_FORCE (so NX_GC_FORCE=1 exported in a shell disables this floor for every later index run; set it per command), logs gc_prune_refused_by_floor on a refusal and still runs its expiry. nx t3 gc sends the same fields on the first batch of its drain (the floor is judged on the whole set and a later batch cannot be refused) and exits 1 on an engine refusal (a set that grew after its listing). Both read the echo: against an engine that predates the fields, nx index repo logs gc_prune_floor_not_applied_by_engine and nx t3 gc keeps its listing check, which still runs first, and says in the summary that the engine did not judge the floor. Paired change: the engine half needs the next engine tag; an older engine is safe, just unguarded.
  • The engine reaper also expires the quarantine rows a client moved, in quarantine-knowledge__* siblings only (nexus-wbfpw.75; Sam, 2026-10-04). No nx index repo sweep runs on a knowledge collection, so rows nx t3 gc moved there waited for a hand-run nx t3 gc. A new engine changeset (vectors-028, nexus.reaper_expire_client_quarantine) deletes the rows the reaper does not own whose origin_collection tag names a registered, live knowledge__ origin, in a registered quarantine- sibling with content_type knowledge and lifecycle quarantine, once the row's own quarantined_at is NX_REAPER_CLIENT_QUARANTINE_RETENTION_DAYS (new engine setting, default 14) old. It never deletes a chash the origin's manifest names, has no fraction floor, and writes gc_audit operation reaper_expire_client_quarantine with every deleted chash. Code, docs and rdr quarantine stays with the client and the user's NX_GC_QUARANTINE_DAYS, which no longer governs knowledge quarantine on an engine with this change (a user value above 14 does not keep those rows). GET /v1/status is unchanged. See docs/operations/engine-reaper.md.
    • Deploy note: the first reaper pass after deploy deletes any client-moved knowledge quarantine already past 14 days (cap 5,000 per origin per pass). The 5,835 rows that prompted the decision were force-expired on 2026-10-03 (nexus-wbfpw.69), so few or none are expected; conexus should run a read-only count of the function's selection predicate before deploying. Audit: nx catalog gc-audit list --operation reaper_expire_client_quarantine --json. Restore window: 14 days. To hold it, set NX_REAPER_CLIENT_QUARANTINE_RETENTION_DAYS=3650, or NX_REAPER_ENABLED=false (no separate off switch; that stops the whole pass).

Fixed

  • The hybrid search gate probe reaches the GIN text indexes under the service role's row-level security (nexus-wbfpw.48; engine change, vectors-029). Under nexus_svc (FORCE RLS) nexus.text_gate_probe_<dim> was a sequential scan of the tenant's chunks: 62 to 80 ms against 2 to 3 ms with row-level security out of the way, 24,000 chunks, one fixture (the bead records both runs). The operators the gate evaluates (@@ on tsvector, <%) are not leakproof, and PostgreSQL will not make a non-leakproof operator an index condition on a relation that carries a security qual. The probe is now SECURITY DEFINER with its own fail-closed tenant predicate (tenant_id = NULLIF(current_setting('nexus.tenant', true), '')) and a pinned search_path; its results are the same chashes (live(c), collection filter, where filters and LIMIT unchanged). On the same fixture as nexus_svc the selective gate (144 rows) went from 76 ms to 2 ms, and the dense gate (a token in 70% of rows, 5,001 rows returned) from 52 ms to 44 ms; neither was measured at a larger size, and the dense gate's body is now planned without the call's parameter values, so it can no longer choose a sequential scan with an early LIMIT. A definer function runs as the migration role, which is the table owner, and FORCE RLS applies to an owner, so the changeset also adds a SELECT-only permissive policy chunks_gate_probe_owner_read on nexus.chunks for that role alone. The cost, stated: the migration role can now read every tenant's chunks without stamping a tenant; it gains no write access (UPDATE and DELETE still match nothing without a tenant). The policy is TO CURRENT_USER, so the changeset is guarded: it is skipped whole (probes stay SECURITY INVOKER, correct and slower) when nexus_svc is the migrating role, as in the single-role dev posture, or has that role's privileges, or when any other login role that row-level security binds (not a superuser, not BYPASSRLS) has them, because the policy would then bind to that role and let it read every tenant's chunks; it also ends with a post-condition that fails the migration, and rolls the whole changeset back, unless the probes are definer functions owned by the migrating role, the policy names that role alone and no such role exists. A grant made after the changeset ran, or a service that connects as a role other than nexus_svc, is not something a changeset can see, so the engine now checks at every boot, on the pool that serves tenant traffic, that no permissive policy on nexus.chunks other than tenant_isolation applies to its role (through PUBLIC, the role itself or a role it inherits), and refuses to start naming the policy and the role if one does (log event chunks_isolation_check_failed). GET /v1/status reports the same question live as chunks_tenant_isolation_intact (a boolean, additive; the route is unauthenticated, so it names no policy or role), and nx doctor has a Chunk tenant isolation row that fails hard on false and reads not applicable on an engine that predates the field. EXECUTE on the three probes moves from PUBLIC to nexus_svc. The gate inside text_gated_search_hnsw_first_<dim> and hybrid_search_<dim> is unchanged. Deploy needs: the PITR-fork walk with a before and after probe timing on real natural-language queries (the dense case above is the one to watch at production scale), and confirmation of the cloud's migration role and that no login role other than the migrator, nexus_svc included, has its privileges (SELECT r.rolname FROM pg_roles r WHERE r.rolname <> '<that role>' AND pg_has_role(r.oid, '<that role>', 'USAGE') AND NOT r.rolsuper AND NOT r.rolbypassrls AND r.rolcanlogin returns no row), since a row makes the guard skip the changeset and the probe stays a sequential scan; that query has not been run against the cloud.