v7.23.0
Paired release with engine-service-v0.1.89 (tagged on 55be868db;
REQUIRED_ENGINE_VERSION bumped in this release, deploy fires at the
client-tag push). The engine tag carries no Liquibase changeset: the
engine half of this release is GET /v1/catalog/search ordering by tumbler
(nexus-fgxmk), the read-only GET /v1/aspects/list_without_catalog_document
census route (nexus-mlu3k), the manifest-drop sweep counting tombstoned
referrers (nexus-0cwre), and the U+0000 literal fix (nexus-eeuyg).
Plugin surface that becomes live with this pin (from conexus/PENDING_RELEASE.md):
the RDR-184 ledger audits re-keyed for the CC 2.1.251 SubagentStart payload
(conexus/hooks/scripts/expectations.sh, nexus-houpu) and the matching
runbook text in the orchestration skill and the continuation command.
Fixed
nx doctorand the uv-takeover repair no longer treat uv's
~/.local/bin/python3.12interpreter link as a reclaimed nexus shim
(GH #1487, nexus-50hm9). The owned shim set is now what the installed
conexusdistribution DECLARES — the generation's own interpreter is
asked, the same query the installer runs — never a listing of
<current>/bin, whose versioned interpreter shares its name with the
linkuv python installleaves in the shared bin dir. Doctor's
"Generation layout" row stays green on such a box;nx upgrade's
[uv-takeover]precondition andnx self installnever name it and never
offer to rewrite it. A generation that cannot answer the query makes doctor
WARN ("could not ask … which console scripts it declares") and leaves the
repair's shims untouched with a printed line — never a green over an owned
set derived by guessing.- Ghost documents are cleaned up by the production delete paths
(nexus-sz89e, verified then fixed).store_delete/nx store delete/
HttpVectorClient.expireall pass a real collection, and the nexus-c53hy
mismatch guard compared it to a ghost's blankphysical_collectionand
skipped the cleanup every time — the nexus-d9fwj retraction fix beneath
it was unreachable from production. A blank collection now has nothing to
mismatch; a document that names a different collection is still refused. - The unit suite cannot dial the live managed service (nexus-d5aye).
An autouse fixture pointsDEFAULT_MANAGED_SERVICE_URLat a loopback
port nothing listens on, so a CliRunner-invoked command that reaches an
unpatchedprobe_managed_service()with no pinned endpoint (the pin-off
NX_TEST_T2_SUBSTRATE=noneruns, or a test that clearsNX_SERVICE_URL)
fails fast naming127.0.0.1:9instead of probing production;
@pytest.mark.real_managed_defaultopts out for tests that assert the
constant's own value. - Gates restore
release.propertiesbytes, nevergit checkoutit to
HEAD (nexus-iws18).tests/e2e/local-service-gate.sh(both restore
sites),tests/e2e/migration-rehearsal/run.sh(_guided_restore) and
tests/e2e/published-client-write-gate.sh(cleanup) now snapshot the
file at start and put those bytes back on exit, so a gate run no longer
destroys an uncommitted edit to that file. Pinned by
tests/test_gate_release_props_restore_lint.py. run.sh --shakeoutrefuses--no-buildlike every other native
leg (nexus-mbeke): the pre-tag candidate gate can no longer be satisfied
by a stale binary. Pinned by
tests/test_rehearsal_native_legs_refuse_no_build.pyacross the live
native legs.- Engine sources are text again (nexus-eeuyg):
AspectRepository.java
andPlanRepository.javaeach embedded a raw NUL byte inside a
string-literal map key, sofile(1)called themdataandgrep -I
zero-hit every audit over them. Both now use the Java unicode escape for
U+0000 (identical runtime bytes);tests/test_java_sources_are_text.py
scans every Java source for raw NULs. - Engine: the manifest-drop chunk sweep honours tombstoned referrers
(Tier 1) (nexus-0cwre).sweepChunksQuery's shared-chash union guard
carriedDELETED_AT IS NULL, so a soft-deleted document's manifest row
did not count as a reference. Measured: the sweep then tried to delete
the chunk and failed onfk_catalog_chunks_chunk, ending every such
write witherrored=true, reason=sweep_failedand the data saved only
by the constraint. The guard now matchesPgVectorRepository#delete's
anti-join andgc_expire_quarantine: a tombstoned owner still protects
untilpurge_trashreaps both together. The notes guard keeps its live
filter on purpose (a manifest-less tombstoned note has no row for
purge_trashto reap with; that is the Tier-2 side, unchanged). nx doctorverdict lines stop contradicting their own block
(nexus-eg5tw): the plan-library check now says "All checks passed, with
N warning(s)." when it emitted a WARN, instead of an unqualified pass on
the next line; rc is unchanged (a WARN alone never flips it).nx doctor's aspect-queue check signals failure like its siblings
(nexus-fylxo): a failed-row backlog now emits✗ FAIL: N failed aspect-extraction row(s)...and exits 1 on the standalone
--check-aspect-queue(which already propagated its rc) instead of a
purely descriptive count.nx doctor --check-taxonomyfails loud on an engine error;--check- schema --fail-on-violationmakes the honest N/A fatal (nexus-b1v9z).
A genuine engine-side failure (an HTTP 500) is now exit 1 rather than a
silent fall-through to the RDR-176 SQLite census (kept, on purpose, for
the no-engine troubleshooting case it was built for).--check-schema
keeps its intentional N/A-exit-0 (nexus-vl8lk) interactively; both
release-sandbox.shcall sites pass--fail-on-violation, where an
N/A must not read as a pass.- Catalog
find()has an order contract and an exact-title sibling
(nexus-fgxmk). The engine's/searchhad noORDER BY, so
find(title)[0]was heap placement (measured reversing on a churned
database); it now orders by tumbler as text (string order — stable, not
numeric, not relevance). Because the English tsquery drops
stopwords,find("Paper A")legitimately matches "Paper B" too, so
HttpCatalogClient.find_by_title_exact(title)is the promoted
resolution entry point (the idiom two test files had grown locally);
the three latentfind()[0]sites now use it, and the stopword case is
pinned on both sides of the wire. - Ghost-document manifest retraction no longer aborts catalog cleanup
(nexus-d9fwj).store_hook._retract_manifest_rows_for_chashguards a
blankphysical_collection(a ghost/sourceless document): it falls back
to the caller's expected collection, else skips the retraction with a
structured warning, sodelete_documentand the reap tombstone run
instead of dying on the GATE-2 blank-collectionValueError. Scope
caveat: the three production callers all pass a real
expected_collection, and the pre-existing nexus-c53hy mismatch guard
short-circuits a ghost (blank collection) BEFORE this retraction is
reached, so today the fix is exercised only by the
expected_collection=Nonecalling shape; whether that guard should
admit ghosts is nexus-sz89e. nx indexcounts taxonomy-assign batch failures and says so
(nexus-7lw6a).taxonomy_assign_batch_failed(an HTTP 500 from the
assign endpoint) was logged and then forgotten; a run that lost 1,254
chunks' topic assignments reportedDone.and exit 0. The run summary
now carriesWarning: N/M taxonomy-assign batch(es) failed (K chunk(s) affected)in the same shape as the flush-failure line, with no phantom
line on a clean run, and the run exits non-zero on ANY lost assignment
(a data-correctness event an exit-code-only consumer must not read as
success; total loss is named as such).nx indexcatalog-hook progress is TTY-gated (nexus-c14zi). The
\rrepaint writes are routed through the phase channel when stderr
is not a TTY, so a redirected log gets discrete lines instead of
Catalog: linking ... Catalog: housekeeping...mashed together with a
structlog event on the end.
Removed
- Every read of the frozen pre-PG SQLite files (Hal, 2026-08-29:
"there is no path back to chromadb sqlite").nx doctor --check-taxonomy's SQLite census fallback and the legacy.catalog.db
row-count probe were the last twosqlite3.connectsites insrc/,
kept under RDR-176 Gap 2's downgrade rationale; that rationale is
retired with them andSQLITE_CONNECT_ALLOWLISTis empty. The doctor
rows for a leftovermemory.db/.catalog.dbnow name them as relics
to delete.--check-taxonomyis engine-only: exit 2 when no engine
answers or the route is missing, exit 1 on an engine error or drift.
The vestigialsqlite3imports inretry.py(the Chroma "database is
locked" retry leg),hooks.py,collection_audit.pyand
db/t2/__init__.pyare gone;src/nexusimportssqlite3nowhere. chash_indexed_ratio(nx collection health) and the identical
chash_indexcoverage section (nx collection audit) (nexus-70vpz):
both compared a count served from the chunks tables to the chunks
tables and read 1.000 on every collection, zombies included, since
RDR-187 droppedchash_index. A health signal that cannot signal is
deleted, not repointed.HttpChashIndex.count_for_collectionnow has no
application caller (kept; contract tests still exercise it).
Added
nx enrich aspects-without-catalog(nexus-mlu3k): read-only census
of aspect rows no live catalog document claims, served by a new engine
route (GET /v1/aspects/list_without_catalog_document, aspects-004's
attribution predicate negated, withtombstoned_matchper row, at most
300 rows per call — the verb pages through) and bucketed client-side by URI scheme, tombstoned matches, extraction era,
extractor/model, and on-disk presence (--match-basenamesadds the
registered-under-another-URI signal). Exit 2 on an engine without the
route; an unverifiable census is never reported as empty.deployed-engine-versiontracker written from conexus's STEP-6 gate
report (nexus-nx3l5, shape c). Newnexus.deploy_trackermodule: the
post-tag verifyscripts/check_engine_release_floor.py --record-deploy-from-gate-report DIR(orNX_GATE_REPORT_DIR) selects
the latest report byrun_timestampthat gated the live
release_version, requires it green, and writes the tracker with the
report basename as thegateprovenance; exit 3 and nothing written on
no report / red / schema drift. A bare verify with no report directory
also exits 3 — the only way to run it without recording is the explicit
--no-record-deploy REASONopt-out, reason printed — and thecommit
provenance is resolved
from the live version's tag after the probe, never the floor tag.
nx service record-deploygains--gate-report-dir/--gate-report
(the same path; the typed--gatebecomes the manual fallback and is
mutually exclusive with them). Closes the premature-write vector by
construction (gate PASSEDtyped 17 s before a red report, 2026-08-28)
and the omission vector for every verify that runs: it cannot pass
while silently skipping the record (v0.1.17 stale across three deploys
was a skipped step; a skipped verify is still a skipped verify). conexus
stays side-effect-free.
Changed
- Aspect worker idle poll: 2 s -> 30 s (
DEFAULT_POLL_INTERVAL_S,
nexus-59611 stage 1). Theclaim_batchpoll was ~45k requests/day,
~80% of all edge traffic, 99% of them empty, against ~16 arrivals/hour
(conexus edge measurement, conexus-ppwe). 30 s cuts ~93% of that and
still samples ~8x faster than items arrive; extraction is async
(~26 s median) so the added claim latency is invisible to consumers.
No knob: stage 2 is event-driven wake-up. Explicitpoll_interval
arguments are unchanged.