Repository navigation
v7.87.0
Pairs with engine-service-v0.1.165 (REQUIRED_ENGINE_VERSION raised from 0.1.164), deployed to the cloud on 2026-10-26 at 14:49Z ahead of this tag. Its two changesets (catalog-046-1, vectors-038-1) change no existing data. The cloud's data-token mint limits were raised with that deploy (burst 40, 10 a minute per credential and tenant, 40 a minute in all). This release ships RDR-232 Phase 1 for nx_answer, the engine halves of per-document section labels and where filters, the superseded-collection skips, the data-token refresh fix, the conexus-tool-spill fixes, the plugin-only fast lane that replaces the anchored plugin channel, and the removal of the subagent git-integrity check.
Added
- An explicit
ownerorprojectbinding now narrows the secondary evidence streams, not only the named source (nexus-jypmj.1.18, RDR-232 Phase 1; found by the golden fixture: withscope="rdr"and the ART owner, another project's RDR-084 still reached the reducer through the vector stream, 6 of the 100 reducer items in the oversized-input case). After the section 2a corpus narrowing, each corpus-taking retrieval step expands its corpus to collections by the same routingsearchandqueryuse and keeps only thecode,docsandrdrcollections whose owner (catalog row,1.2is1-2) matches;knowledgeand other types pass. A step left with no collection is skipped, contributes no evidence and is reported (scope_diagnosticsentries withscope_kind: "owner", the owners and the collections removed;evidence_diagnostics.scope_check.owner_narrowing; the text-mode[scope conflict: ...]line), and never runs unscoped; a step pinned throughcollectionorcollectionskeeps its pin. The traversal policy takes a new optionalallowed_ownersand drops non-seed documents of a project-owned collection that lie outside the owners (dropped_out_of_owner); documents in other collections (knowledge, paper) pass, as in the vector stream. A project-owned collection whose owner cannot be read (an old two-segment repository name such asdocs__nexus-571b8edd, or one with no catalog row whose name fails the grammar) is kept and listed asunattributed, never dropped as foreign. Thesingle_queryfast path applies the same filter. An owner that resolution inferred narrows nothing, and neither does a qualifier that conflicted or did not resolve: the run proceeds unnarrowed andscope_check.owner_narrowing.not_appliedsays why. A catalog-subtreescopeis the owner qualifier for source resolution only and narrows nothing (RDR-232 section 2a, unchanged).nx_answeralso strips caller-supplied_nx_ownersand_nx_scopebindings, which are its own. nx_answerreserves the bindingssource,ownerandprojectfor the caller's evidence request (nexus-jypmj.1.2, RDR-232 Phase 1). They are read frombindingsand removed before plan binding resolution and beforeplan_match, so no plan is offered or run on them.plan_saveand the library's save path refuse a plan that declares one (inrequired_bindings,optional_bindingsordefault_bindings, or as$namein a step's args); a plan already in the library that does is excluded from matching and logged, not rewritten or deleted. A catalog-subtreescopewith noownerbinding is itself the owner qualifier; two qualifiers agree when one tumbler is a segment prefix of the other, otherwise the request reports the conflict and picks nothing. The structured envelope has a new, always-presentevidence_requestkey (Nonewhen no reserved binding was passed), and in text mode a request that could not be honoured adds a one-line[evidence request: ...]notice after any leading[budget exhausted ...]or[non-answer: ...]marker. Resolving the named source to a document is a later step (nexus-jypmj.1.4).nx_answer(scope=...)now narrows a plan's declared corpus instead of deferring to it (nexus-jypmj.1.3, RDR-232 Phase 1). Before, a corpus a plan pinned beat the caller's scope, soscope="rdr"on the factual-lookup builtin still searchedcorpus: all. Now a corpus-prefix or full-collection scope narrows the resolved corpus of every corpus-taking retrieval step (search,queryand the scoped search primitives), inplan_runand on thesingle_queryfast path alike: no corpus gives the scope,allgives the scope, entries under the scope (rdr__1-1__...underrdr) are kept, and a scope under an entry (knowledge__xunderknowledge) narrows that entry to the scope. A step whose corpus has nothing under the scope is a scope conflict: it is skipped, never run unscoped, and its output is an empty structured result plus ascope_conflictmarker, so a later$stepN.idsstill resolves. A run whose retrieval steps all conflict takes the existing no-evidence path and the notice names the conflict; a partial conflict adds a[scope conflict: ...]line after any leading[budget exhausted ...]or[non-answer: ...]marker, after the answer shape is classified. The structured envelope has a new, always-presentscope_diagnosticslist (step_index,tool,plan_corpus,caller_scope,scope_kind,outcome,resolved_corpus; empty when no scope was given). Steps pinned throughcollection/collectionskeep their plan and are reportedunscoped_collection_pin; a catalog-subtree scope (1.2) is not a corpus and is not intersected. The builtin plan YAML is unchanged, so a call with no scope resolves the corpora it did before. On the fast path a corpus-prefix scope used to ride assubtreeand be rejected as an unknown owner; it now goes intocorpus. A run in which every retrieval step conflicted with the caller's scope is recorded but counts no success or failure for the plan (it is the caller's error, not a plan failure); thescopedescription now says a catalog subtree filters only the single-step fast path.nx_answerresolves the named source to a document and says when it cannot (nexus-jypmj.1.4, RDR-232 Phase 1). Thesourcebinding takes one reference, a list, or a comma-separated string (evidence_requestkeeps its single-sourcesourceand gains asourceslist). Each RDR id is resolved inside the owner qualifier by one capped catalog search (at most 100 candidates, two searches and one owner check per source, six sources), reusingrdr_canonical's basename identity and no-guess rule: with no qualifier and two projects holding the number the result iscandidates, never a pick; a qualifier that names no registered owner is reported, not searched past; a search that reaches its cap is not resolved. The structured envelope has a newnamed_sourceskey (Nonewhen no source was named) and text mode adds a[evidence request: named source ...]notice for an ambiguous or unresolved source. The modulenexus.plans.primary_evidencealso reads a resolved document's manifest and selects section-diverse evidence under stated caps, labelling chunks from a manifest row'ssection_titleonly wherelabels_writtenis true and from the chunk's own headings otherwise; the runner merges that selection into the reduction operators' input (nexus-jypmj.1.6, below). A failed owner qualifier is stated once in text mode, and a source resolved without an owner qualifier gets a not-owner-qualified notice (on the single-step fast path it says the plan has no primary evidence stream). The envelope keysscope_diagnostics,evidence_requestandnamed_sourcesare documented indocs/cli-reference.md.traversetakes an evidence policy, andnx_answergives every traverse step one (nexus-jypmj.1.5, RDR-232 Phase 1). New optional argumentpolicy(allowed_collections,preserve_seed_documents,max_documents,max_chunks,require_link_role); without it the result is exactly what it was. With it, the policy runs after the catalog walk and before any manifest is read: documents outsideallowed_collectionsare dropped, the rest ranked by link role (citation, implementation, related), then link distance, then tumbler, the non-seed ones capped atmax_documents(default 50, not calibrated;nx_answersupplies 50 only when the caller gave a scope, and with no scope orscope="all"it supplies the engine's 500-node cap, which binds nothing, until nexus-jypmj.1.12 has measured a cap, so the document cap does not bind an unscoped traverse); manifests are read for the retained documents only;max_chunks(at most 300) is spent with a reserved share per document; seed documents are never evicted by the rank or the cap. The result addschunk_collections(one collection per id, aligned withids) andtraversal, a report with the walked, retained and dropped counts, counts by document content type (a catalog row field, not a parse of the collection name),widened(set only when no scope was given and an expansion admitted a corpus the seeds do not have) andscope_conflict(an implementation-role step whose expansion the caller's scope removed). The engine caps a walk at 500 nodes (CatalogRepository.MAX_GRAPH_NODES); a response with that many nodes is reported aspossibly_truncatedwithcontained: false, logged astraverse_graph_node_cap, and counted bycapped_traversal_countfor the engine pre-cap filter decision (nexus-jypmj.1.13). The detector also counts seeds missing from the reply, because the engine joins every non-seed node to a live document and so only a dead seed can disappear after the cut (499 nodes plus one missing seed reads as possibly truncated); it stays a detector, and the engine's scoped-result marker is what stops the cut. A policy that cannot be applied returns no graph evidence and awarning, never an unrestricted walk. The plan runner derives the policy from the caller'sscopeand the step's link types; of a policy a plan declared it merges only what tightens (a lowermax_documentsormax_chunks, arequire_link_rolethat narrowsfactual) and ignores the rest. A catalog-subtreescopeis now enforced on the graph stream insideplan_run(a document outside the subtree is dropped before its manifest is read); before, only the single-step fast path filtered by subtree. A bare-stringlink_typesis one type, and ascopethat is a list is used when it holds one entry and otherwise fails the policy parse (graph stream omitted with a warning) instead of becoming a prefix that admits nothing. On the RDR-084 fixture underscope="rdr"the graph stream was 300 code ids at the id cap; it is now the 18rdrchunks of the two RDR documents and the 115 code documents are dropped before their manifests are read. The MCP wire snapshot gains thepolicyparameter. An unscopednx_answertraversal is not the legacy result.nx_answeralways supplies the policy, so with no scope the document cap does not bind, but the documents are still ranked by link role then distance instead of coming back in graph order, the chunk budget is spent with a reserved share per document instead ofchunk_ids[:300]in graph order (past 300 chunks a later document is no longer cut to nothing), the output carrieschunk_collectionsandtraversal, and a step whose link types are one role admits only that role. A pinned test compares the two outputs.nx_answermerges the named source's primary evidence into the reduction ahead of the plan's own items (nexus-jypmj.1.6, RDR-232 Phase 1). When asourcebinding resolves and its document yields evidence,nx_answerbuilds a reserved stream from it (at most 50 items, below the operator input cap of 100) and passes it toplan_run. At the first operator that consumes retrieved evidence, inidsauto-hydration and in an explicitinputslist (includingextract, the first reducer inreview-default), the runner puts the reserved items ahead of the plan's items, drops any vector hit that is the same chunk (by chash forids, by text forinputs) and keeps the primary copy with its[primary source ...]attribution. The_OPERATOR_MAX_INPUTScap runs after the merge, so it cuts only the plan's own items; an explicit-inputsoperator is capped at that point only when a stream was merged, so a run with no source keeps today's uncapped inputs. The continuation envelope merges the same stream, so the caller-side reduction sees the same evidence as the bundle and per-step routes. Thesingle_queryfast path has no primary stream and says so. The structured envelope gainsprimary_evidenceandreserved_streamkeys (bothNonewhen no stream was built; seedocs/cli-reference.md), and text mode adds an[evidence request: ...]line when a read stream was never merged, items exceeded the share, or a rank or filter dropped reserved items before the final reduction. That drop count is made only where it is true: not for the deferred refs inside a bundle, and not after a summarize, generate or extract, which reduce rather than drop. A manifest longer thanmax_manifest_rows_read(3000 rows) is spread-sampled after the catalog reply; the reply itself is not yet limited by the service (nexus-jypmj.1.17).nx_answeradds the resolved RDR's T2 metadata and latest gate record to the reserved stream (nexus-jypmj.1.7, RDR-232 Phase 1, assumption A7). When asourceresolves to an RDR file (by id or by tumbler),nx_answerreads that RDR's two T2 records with exactgetcalls (never a search or a listing) and puts them at the front of the reserved share, ahead of the chunks, so neither the share cap nor the operator input cap evicts them; a failed chunk read still leaves a stream of the records. The T2 project is<owner name>_rdrfor the repo owner that holds the resolved document, never taken from the request: ART RDR-084 readsART_rdrand cannot readnexus_rdr. An owner that is not a repo owner, has no name, or is not in the catalog leaves the project undetermined and nothing is read. Titles are tried in the shapes the RDR tooling writes (84,084,RDR-84,RDR-084, then<shape>-gate-latest). An ambiguous, unresolved or non-RDR source gets no records and the envelope says why. Caps (LifecycleCaps, on the wire): 6 records, 24 T2 reads, 4,000 characters a record, 18,000 in all, 20 seconds (or the answer's ownbudget_seconds, if sooner; each read is handed what is left, at most 5 seconds, and is not retried at that clamp); every cap that cut something is stated, and a lookup a cap cut isread_capped, neverno_records. The reads go through the sharedt2_index_writetransport, and an RDR named twice (by id and by tumbler) is read once. The tenant comes from the bearer of the catalog and T2 clients, so a source in another tenant does not resolve and its T2 project reads empty. The structured envelope gainslifecycle_stream(seedocs/cli-reference.md),named_sources[].rdr_numberis new, and text mode adds an[evidence request: ...]line for a project that could not be determined, a failed or empty read, or a cut. Thesingle_queryfast path reads no records.nx_answerreports what each stream and the reducer received, and checks the named source and the scope (nexus-jypmj.1.8, RDR-232 Phase 1). The structured envelope has a new, always-presentevidence_diagnosticskey (Nonewhen no retrieval ran) and one log event,nx_answer_evidence_diagnostics, with the same counts and no question, label or text. It holds per-stream reference counts by collection (vector, graph, primary, T2), the reducer input counted by stream and collection (the runner records the list the first evidence-consuming operator was handed; a reserved item carries its stream and collection, any other item is attributed by the hash of its text against the ids the retrieval steps returned, and what matches nothing is countedunattributed), a source check (covered,partialormissingper named source, where reserved chunks an intermediaterankorfilterdropped before the final reduction make itpartialormissing, plus a lifecycle facet in whichno_records,project_undetermined,read_capped,read_failedandover_capareunresolved, not absent) and a scope check (satisfied,widened,conflictorviolated, andnot_evaluatedwhere it cannot say: a catalog subtree, no item examined, an item or stream reference with no collection, or no reducer input recorded; a traversal whosescope_conflictthe scope caused is aconflict), the steps skipped for a scope conflict (they still have no step record or telemetry row;step_countcounts the run's step outputs, so it includes their marker outputs, andstepsholds the executed ones), a summary per traversal, and the count of traversals that hit the engine's 500-node cap. The log event is emitted for every plan run whether or notstructuredis set (a text-mode single-query fast path run has no per-chunk output and logs none). Nothing is persisted before Phase 3. The named-source resolution, the owner-name lookup and the primary read run in a worker thread bounded byPrimaryEvidenceCaps.max_seconds(20 s) and the call's ownbudget_seconds; a hit is disclosed (stopped at the deadline) and the answer proceeds without that stream. A failed primary read is disclosed (primary_evidence.error) instead of silent. A comma-listscopethatnx_answerruns unscoped is now disclosed in the text and in a new always-presentscope_normalizedenvelope key, and the scope check saysnot_evaluated. A planner-made plan whose own retrieval found nothing is not grown even when the reserved stream fed the answer. A resolved source whose vector and graph streams found nothing is now answered from its primary chunks and T2 records instead of being replaced by the no-match notice, because the empty-retrieval guard counts a merged reserved stream as evidence (the continuation handoff's empty-evidence check does the same). The RDR-084 golden fixture now has distractor RDRs beyond the search limit, 62 secondary documents with three link roles (the traversal cap keeps every citation and implementation and only 8 of 20 related), and runsnx_answeritself against the substrate for the lookup plan, the taxonomy-domain builtin and thesingle_queryfast path. The envelope keys are documented indocs/cli-reference.md.nx_answermerges the reserved primary evidence and records the reducer input when the plan passes the evidence in the operator's own arg (nexus-jypmj.1.20, RDR-232 Phase 1; found by the live canary). The inline planner's documented Pattern A,summarize(content=$stepN.contents)(alsogenerate(context=...)as the first reducer, andrank/compare/filter/check/groupbyitems), resolves to a list of fetched texts in the operator's target arg, and reached neither the merge nor the reducer-input record, which fired only forids, explicitinputsandextract. On such a plan-miss run the stream was read and never merged (reserved_stream.mergedfalse, source checkmissing) andevidence_diagnostics.reducer_inputwasnull. The runner now treats a list of strings in the target arg like an explicitinputslist: reserved items first, the input cap on the merged list, the record of what the operator received; a string arg (a prior$stepN.summary) and a deferred bundle ref are not evidence and are left alone, and a later operator still does not merge again. A run with no reserved stream hands the operator the same bytes as before; the one change is thatreducer_inputis now recorded for these shapes.- A note keeps its own
tags,category,ttl_daysandsource_agentin its catalog entry'smeta(nexus-6pfgb.5; client change, works on every engine). Two notes with identical text share one T3 chunk row, and the row carries only the last writer's four values, so a note's own were unrecoverable once another note with the same text was stored. Every note put (nx store put, MCPstore_put,nx memory promote, a recovery import) now writes them to the note's catalog entry in the same catalog write that stampsmeta.doc_id(anupdate, or theregisterthat mints the entry) (the engine mergesmeta, so no engine change is needed). A key is sent only with a real value; a re-put under the same title replaces them, overwriting a key it leaves unset that the entry held with a retraction value (ttl_daysto0, the others to the empty string; catalogupdatecannot delete ametakey,0is not an acceptedttl_daysinput, and a reader must treat0and absent alike as permanent), and a put that does not land restores the old values with the olddoc_idstamp unless a newer put of the same content has since written different values for the keys this put wrote. A recovery import writes the values its bundle carries (the chunk row's, until nexus-6pfgb.14) andsource_agentrecovery-import. Nothing reads the keys yet; the readers (store_get/store_list,nx store expire, the recovery bundle, the contradiction flag) follow under nexus-6pfgb. Tests:tests/test_6pfgb5_note_meta_homes.py,tests/integration/test_6pfgb5_note_meta_homes.py.
Removed
- The subagent git-state integrity check is removed (nexus-i93tz; Sam's decision of 2026-10-25). The conexus hook
subagent_git_integrity_check.py(report-only since 7.83.1: a snapshot of the primary checkout's config, hooks and checked-out refs before each subagent Bash call, diffed after) and its threehooks.jsonregistrations are gone, and the planned second phase, which would have put changed config and hooks back, is never built. Three and a half days of real subagent work gave 188 rows and no change report, so the check had caught nothing and had no false alarm to judge. The subagent git-write guard (subagent_git_write_requires_orchestrator.py) keeps its logic and is the only line left; it is now frozen except for real incidents, and its docstring andconexus/hooks/scripts/routing/README.mdlist the routes it does not cover (raw writes into.git/configand.git/hooks, index, ref and HEAD writers through plumbing, verbs held in$VAR, decode-then-exec, write-a-script-then-run-it) as known and accepted. Rows the hook already wrote to a routing store (integrity_info,integrity_unverified,integrity_change) still read:nx hook routing-statscounts those outcomes underextra, and once the plugin copy that registers the hook is replaced it marks the rule(unregistered)(pinned bytests/test_hook_routing_stats.pyagainst the newhooks.json; an install still on a 7.83.x to 7.86.x plugin copy shows it registered until it upgrades). The hook's own pruning went with it, sonx-hook self-gc(the SessionStart sweep) now removesgit-integrity/session directories under the nexus config directory that have been untouched for two days, the bound the hook used, because installed 7.83.x to 7.86.x plugin copies keep writing there; a live session's directory is never that old. The test-run allowlist entry for that directory (tests/conftest.py) stays while the pinned plugin still registers the hook, since sessions on those copies keep writing it during any unit run they launch;tests/test_real_config_dir_mutation_guard.pyfails with a delete instruction once the conexus ref pinned in.claude-plugin/marketplace.jsonis a tag whosehooks.jsonno longer registers it (it skips with a stated reason where the pinned tag cannot be resolved locally, and fails there underNX_REQUIRE_PLUGIN_DRIFT_CHECK=1). The plugin side goes live at the next conexus cut (conexus/PENDING_RELEASE.md). - The anchored plugin channel (
plugin-vX.Y.Z-ncuts, RDR-197) is retired (nexus-rbmja.8; Sam's decision of 2026-10-26). Its cuts never reached installed users: Claude Code's plugin update keys on theversionfield, which an anchored cut leaves unchanged (nexus-konsk, nexus-semdv), and its rehearsal broke on main's code under develop's tests.scripts/cut_plugin_release.py,scripts/plugin_cut_back_merge.sh,scripts/check_cut_ledger_clean.py,.github/workflows/plugin-release.yml,tests/e2e/plugin-cut-rehearsal/and their tests are deleted;tests/test_plugin_channel_retired.pykeeps them gone. Plugin-only content now ships as a patch release off the last client tag: the release skill's "Plugin-only fast lane" section, gated byscripts/check_plugin_only_eligibility.py,tests/e2e/release-battery.sh --plugin-only vX.Y.Zand the post-publishtests/e2e/plugin-delivery-proof.sh. RDR-197 is superseded and stays as the record.
Fixed
- The indexer registers no document for a whitespace-only text file, and
.drawiois skipped (nexus-5j8fp, client change; works on every engine). A 1-2 byte file such as ghidra'sModule.manifest(classified as prose) or serena's__init__.pypassed the zero-byte guard, registered, and then failed or never completed its fence on every run: 11 of the unfenced zero-chunk documents in the cloud tenant (4 of the 107 NULL-fence and 7 of the 157 failed zero-chunk documents; the serena__init__.pyrows are NULL-fence, the ghidraModule.manifestrows are failed). Repo discovery, the RDR walk andnx index md/rdrnow refuse a non-empty file of at most 4 KB whose text is empty afterstr.strip(): the repo walk skips it silently (counted inskipped_unchunkable) and the single-file forms fail loud withUnchunkableContentError. A document an earlier run registered for a still-present whitespace-only file is tombstoned in the same run that skips it, on a full run for every such file and on--since-headfor a file the commit changed (an untouched blank file in a hook-only repo waits for a full run or for reconcile-stale), through the path a git deletion takes (its chunks follow atnx catalog purge-trash); housekeeping's two-run miss-count sweep stays the fallback, andnx catalog reconcile-stale --execute tombstone-zero-contentretires the rows of an install that does not re-index.nx catalog verifyandreconcile-staleclassify such a sourcezero_content_by_designand describe the bucket as zero-byte, binary or whitespace-only, so a census no longer prescribes a re-index for it. The fail-closed fence is unchanged: no document completes at zero chunks..drawio(draw.io XML, which fell through to prose and chunked as noise) joins the skip extensions: existing.drawiodocuments are tombstoned by housekeeping's sweep (two full runs) once the file leaves discovery, and their chunks follow atnx catalog purge-trash. Tests:tests/integration/test_5j8fp_whitespace_only_never_registers.py,tests/test_classifier.py,tests/test_doc_indexer.py,tests/test_catalog_reconcile_stale.py. nx rdr preamble rdr-createno longer offers an RDR number another branch, worktree or T2 record already holds (nexus-3piej). Next ID came from the current tree's RDR directory only, so a number on an unmerged branch or in another worktree's uncommitted draft was offered again, and the rdr-create skill's record write then overwrote that RDR's T2 record (RDR-233, 2026-10-26). The preamble now takes the highest number across every ref (git log --all), every worktree's RDR directory and the{repo}_rdrT2 titles, and lists the numbers it skipped with their holders; the skill reads the record before writing it and stops when the number belongs to another RDR.nx taxonomy hubs --warn-staleflags a hub when a contributing collection was discovered after the hub's latest projection, as RDR-077 specifies (nexus-9kp10; Sam's ruling 2026-10-26; client fix, engine change in the next engine cut). It read the reverse since RDR-077 shipped: it flagged hubs projected after the last discover, the normal discover-then-project order, and missed hubs a later rebuild had left behind. The client now derivesis_stalefrom the route'slast_assigned_at,max_last_discover_atandnever_discovered_count, comparing parsed timestamps, so the warning is right against any engine; the engine's ownis_staleflips in engine-service-v0.1.165. The STALE line namesmax_last_discover_at > last_assigned_at.- A saved plan with a
$stepN.fieldinside other text or an object is refused at save, skipped by the matcher and reported bynx plan hygiene; a plan-miss planner is asked once to fix it (nexus-jypmj.4, follow-up to nexus-jypmj.1.19). The run-time scan (runner.find_embedded_step_ref, the whole-plan form of the same raw-arg check) now also runs inplan_save(refused, nothing written), inplan_matchon both the cosine and the text path (the plan is excluded and aplan_match_embedded_step_ref_excludedrecord names it; the row is kept), and innx plan hygiene(flagged, disabled by--apply). On a plan missnx_answerscans the planner's output before dispatch; a plan that fails is sent back once with the error text appended to the same prompt, and the second planner call's cost joins the run's planner cost. A retry that fails (a timeout, unusable output, an empty plan) keeps the first plan, soplan_runstill refuses it by name; the error text appended to the prompt is capped at 200 characters. The seed loader refuses a template holding such a ref, by name, in its errors. A second bad plan goes toplan_run, which refuses it as before. Also: the single-query fast path refuses aPlanRunStepRefErrorlike an unresolved$varinstead of falling to the generic handler; a list evidence arg that mixes fetched texts with chunk ids ([$step3.contents, $step5.ids]) drops the ids with anevidence_step_ids_droppedwarning (count and a truncated sample) on all three branches that send evidence to a reducer (Pattern A, theinputsalias,extract(inputs=[...])), since a flat list has lost the paired collections that would hydrate them; the runner marks the strings that came out of a.idsor.tumblersref, so fetched text that reads like an id (1.2.3) is kept; thenx_answerenvelope comment oncost_usdis corrected.tests/test_rdr232_planner_reference_census.pyties every evidence arg_PLANNER_TOOL_REFERENCEdocuments to a reserved-stream merge site in_hydrate_operator_args.nx answer-runskeeps its one-sided allowance for the planner's share of a plan-miss run's cost: bounding it needsplanner_cost_usdstored on the run, a schema change not made here. - A plan-miss
nx_answerrun'scost_usdincludes the inline planner's own dispatch (nexus-jypmj.1.20, found by the RDR-232 canary). The planner's cost was captured in an ambient usage sink that only the budget check read, so the structured envelope'scost_usdand the storednx_answer_runs.cost_usdsummed the plan's steps alone and a plan-miss run's cost was a lower bound. Both now add the planner's known cost (one helper, so they agree); a plan-match hit has no planner dispatch and is unchanged, and a run with no known cost still reportsNone.steps[].cost_usdis unchanged, so on a plan-miss runcost_usdnow exceeds the step sum by the planner's cost.nx answer-runs --stepsaccounts for that: its cost-consistency check accepts a run cost at or above the step sum on an inline-planned run (plan_id0) and still flags one below it, or any mismatch on a library plan. - A plan step that puts a
$stepN.fieldinside other text or an object now fails before it dispatches, instead of handing the operator the literal token (nexus-jypmj.1.19; found by the RDR-232 canary, older than Phase 1). The runner substitutes a reference only when it is the whole string and never looks inside a dict, so an inline-plannedgenerate(context="Code: $step3.contents ... $step5.ids")(or a dict of refs) reached the model as placeholders, which answered that no source material had arrived, and the run still reportedanswered.plan_runnow scans every step's raw args before the first dispatch, and_resolve_argsscans again for the continuation composers, and raisesPlanRunStepRefErrornaming the step and the arg; a list of whole-value references still works. The scan reads the plan's own args, so a question that quotes$step1.idsthrough$intentis untouched. The planner's tool reference now says an argument is one reference or a list of them and showscontext=[$step3.contents, $step4.contents]for several sources. A plan already saved with an embedded reference now errors where it used to run with the placeholder text. - The provenance line on hydrated text and a search hit's age line name each document's own title, indexed date and year, from the catalog, and an expiry once the catalog records one (nexus-6pfgb.11, client change; works on every engine). A T3 chunk row is one per chunk text, so the title,
indexed_at,bib_yearandttl_dayson it are the last writer's. The[source: title . collection . indexed D . published Y . expires]line thatstore_get_many(structured=True)returns insource_notes(which the plan runner prefixes onto the text an operator prompt reads), and the per-hit age line in thesearchtext render, printed those row values, so a chunk two documents share named the same document and date for both. Now: hydrating under a document's tumbler (the plan runner's metadata-scoped, graph-hop and aspect-scoped paths) builds the note from that document's own catalog entry; hydrating a bare chunk id reads the live documents that hold the chunk in the collection it was found in (chash_positions, shared with the section-type read, then oneresolve_manyfor the batch) and keeps each of title, indexed date, year and expiry only when every owner states the same value. When owners disagree on a date, year or expiry, or one lacks it, that field is left out and the chunk row's value is never used; when owners disagree on the title, the note lists their titles, quoted and comma-joined (up to three, then "+N more"), and counts owners with no title ("N untitled") so the line cannot read as one document; the search render's display title still joins titles with " · ". When more than ten documents hold the chunk, when an owner the manifest names is not returned byresolve_many(so agreement cannot be checked), or when none resolves (a non-chunk-hash id, a chunk with no manifest row, a catalog that cannot answer), the note names the collection only. A search hit states its age only when every owner of the hit has resolved and those in its collection agree. The year is the catalog'sbib_year, else itsyear; an enriched paper whose catalog has neither (the enrichment year sat only on the chunk row) therefore loses "published Y", as the row's value is no longer a fallback. The[source: ...]instruction an operator prompt carries no longer promises a document and dates on every line. A search hit takes its owners from the lookup that already resolves its display title (_resolve_display_mapsnow also returns the owners' dates, year and expiry from the same entries), so the page adds no catalog call. An expiry shows only for a document whose catalogmetacarriesttl_days(the notes catalog-meta change, nexus-6pfgb.5, lands that); until then a note shows none.nx search --jsondoes not print the new working key_owner_facts.store_get_manycomputessource_notesonly instructured=True, which is the only mode that returns them, and the plan runner's tumbler hydration and thenx_tidyprefetch, which never read them, skip the ownerresolve_manybehind them. Tests:tests/test_6pfgb11_provenance_from_catalog.py,tests/test_context_annotations.py. - "Is this file already indexed" is answered from the catalog document, not from a shared chunk row's
content_hash(nexus-3ajpd). A T3 chunk row is one per (collection, chunk text), and every write merges its metadata last-writer-wins, socontent_hashon a row is the file hash of whichever document wrote that text last. The skip innx index md/pdf/rdrmatched a row on that hash, so a document whose rows another document had stamped last was judged changed (a single-chunk document sharing its chunk could re-index on every run), and a changed document with no fence (or, through the uncachedcheck_staleness, any changed document) was judged current when another document holding the same text had stamped a row with the new hash. Now a document is current when its index fence iscompleteat the file's hash and one chunk of its own manifest is present with the target embedding model;nx index repobuilds its staleness cache the same way (the cache records presence and model, the catalog fence supplies the hash), and the uncachedcheck_stalenessreads the fence instead of matching a hash collection-wide. One re-index per NULL-fence document (Sam's ruling 2026-10-26, T2 nexus/5xn3k-design-amendment-2026-10-26): only a document with a NULL fence (indexed before the fence existed) used to skip on a matching chunk row; afailedorindexingfence was already stale, and a pre-fence engine is below the engine floor. A NULL-fence document is now stale. A file-backed document that yields chunks is re-indexed once on the next index run of its source, the completion records the fence at the file's hash, and the run after that skips it. A document that yields no chunks has no chunk of its own for the skip to find, so it re-indexes on every run, as it did before (not changed here). A note or other non-repo document has no source file in an index walk, so it re-indexes only when its source is indexed again. The engine skips re-embedding chunks it already holds, so the one-time cost is extraction, chunking and upload, not embedding (a PDF or paper is re-extracted). A fence that cannot be read now re-indexes the document for that run instead of skipping it (the read failure is logged at WARNING).nx doctor's "stale index-run fences" row lists only documents indexed after the install's fence baseline; the older NULL-fence population is not listed, but every branch of the row counts it and states the same scope as above.nx catalog reconcile-fencesstamps non-repo documents with a digest key (the chunk's chash for a one-chunk document, a digest over the manifest chashes otherwise), so the next real index of a multi-chunk document sees it changed once and re-records the real value. Two reads that named a document by its chunk's hash changed with it:nx enrich biblooks a title group up under the title of the one document that owns all of its chunks (by the catalog manifest) and falls back to the chunk title when that is not exactly one document, where it used to name whichever document wrote a shared chunk last; and an.nxexpexport now carrieskey_registeredper owner (the document's registration recorded the same hash as its fence), which lets an import keep a document's content key when every chunk it holds was restamped by another document (a bundle without the field behaves as before; a PDF or note registers without a file hash, so for those a chunk carrying the key is still the only evidence). Stale docstrings that said a chunk keeps its first writer's hash and is never rewritten are corrected. Not changed here: the streaming PDF post-pass and the engine's quarantine reattach version check still select or compare by a chunk row'scontent_hash(link-existing still takes the row's content_hash as a candidate key; see nexus-6pfgb.22). nx index repo --since-headskips a delta file the catalog already holds at its current hash (nexus-a3rl7). The delta is the files that differ from the base commit, not from the last index, so a base that did not advance, or a file that changed and changed back, names files an earlier run already indexed. The delta path handed the staleness check an empty cache, so each of those went through chunking, upload and the fence again (the engine skips embedding chunks it already holds, nothing else). It now builds the cache for the delta's own documents from the catalog: the fence hash from the registration pass and the model from one chunk of each document's own manifest, in one batched manifest read and batched chunk reads for the whole delta, with no sweep of the collection. A document with nocompletefence, no chunk in T3 or another model is indexed as before, and afailedorindexingfence still forces a re-index.- The manifest heal marks a lost manifest-less document for re-index (nexus-v9xwj, follow-up to nexus-8o06q). A document with no manifest rows and a
chunk_countabove zero that the heal cannot rebuild (T3 holds no chunk under its content hash, no content hash or collection is recorded, or its collection does not exist) was only listedLOST, and its fence stayedcomplete.nx catalog reconcileand thenx index repoself-heal now fail its fence the way they do for a gapped document with rows, and the reconcileLOSTline says(marked for re-index);--dry-runsayswould be markedand writes nothing. An index run that walks a file already re-indexes a document whose manifest is empty when none of its T3 chunks carries adoc_idmetadata key (every chunk written since RDR-108 Phase 3; nexus-3ajpd), so for that document the mark changes what the fence says, which is what the PDF early probe innx index repo(it decides from the fence alone) andnx t3 gcread, not whether the nextnx index repoindexes the file; a pre-Phase-3 chunk that still carries adoc_idmetadata key puts the document in the staleness cache by that key, and then the mark is what makes the run re-index it. Not marked: a document whose T3 fetch failed or whose collection lookup failed for a service error (the collection listing raised; listedLOST, nothing written;HttpVectorClient.get_collectionused to read that failure as an empty listing and report the collection absent, so an outage could have marked), and a never-chunked document.HttpVectorClient.get_collectionnow lists strictly, so on an outage it raisesCollectionNotFoundErrorwith the messageservice unavailable checking collection X, chained from theVectorServiceError, where it saidcollection X not found in service, and it logshttp_vector_get_collection_service_unavailableas a warning (the swallowed listing loggedhttp_vector_list_collections_failed); the exception type is unchanged, so every caller that catches it behaves as before. On a pre-catalog-005 engine, a strict listing whose/collectionsfallback call fails now raises instead of returning an empty listing. When the heal stands down for a lost document (a run landed its rows, moved itschunk_countto 0, or holds its fence atindexing), or could not look for its chunks,nx catalog reconcileprints the reason on theLOSTline:(not marked: ...). A chunk that a document shares with a document that wrote it last carries the other document's hash, so such a document can readLOSTwhile its chunks exist; re-indexing its source restores its rows. A marked document makesnx t3 gcrefuse its collection unless--allow-incomplete-index-stateis passed. nx catalog orphan-backfill link-existinglinks only chunks no live manifest of the collection names (nexus-6pfgb.22). A chunk row is shared by every document holding that text and itstitleandcontent_hashare the last writer's, so--by titleand--by content_hashappended a whole group to one document: chunks the document already owned (a second manifest row for the same chunk, at a new position) and chunks another document owned, while a chunk the document owned that another writer had restamped was missed (or went to the other writer). Now a candidate chunk is linked only when no live document of the collection names it (the engine's chash-positions route, filtered to the collection by thecollectioneach row carries on engines newer than engine-service-v0.1.164; v0.1.164 and earlier send nocollection, so they use the referencing documents' manifests, filtered the same way); one the target document already holds (under any collection value of its manifest rows), and one another live document of the collection owns, are skipped and counted. New rows go after the highest position the target holds, not after its row count (a manifest can keep a gap, and an append upserts by position, so the old start could overwrite a row the target holds). Two live documents with one title: that title's group is skipped and counted, as a content hash two documents share already was, and is not handed to--also-synthetic.--also-syntheticno longer registers a chunk a live document of the collection already names: it counts them and registers no document for a group left empty. The directorphan-backfill syntheticverb is unchanged: it can be run again, a synthetic document's URI is the same each time and a manifest write replaces the document's rows, so leaving out the chunks an earlier run registered would drop them. Not guarded: a chunk whose only owner is a document in the trash, and a chunk an earlier version of the target left behind that nothing names, are manifest-less and ARE linked (the first is pinned intests/integration/test_6pfgb22_link_existing_manifest_less.py); the candidate target is still picked from the chunk row's last-writer title or hash, so a match is only a candidate. The ownership lookup and the writes are not atomic: a document registering between them is not seen.--dry-runruns the same selection and prints "Would link N chunks across M existing Documents" with the skip counts (link counts only: it stops before the--also-syntheticstep and does not count the chunks that step would leave alone), where it used to print only the number of documents with a title and the collection's chunk count. The--by content_hashhelp and output no longer sayhead_hash: the key is the document's file-content hash (index_content_hash, elsemeta.content_hash), as it has been since nexus-lhmu1, and a hash two documents share matches neither. A lookup that fails links (and registers) nothing. Client-only; works on engine 0.1.164.- A second substrate-heavy pytest run queues behind the first instead of exiting 75, and a Mac laptop refuses a parallel substrate run (nexus-tvt6u). The suite lease used to refuse at once unless
NX_SUITE_LEASE_WAIT=1, so ordinary contention between sessions sharing one box became red runs. It now waits up to 30 minutes (DEFAULT_WAIT_SECONDS), printssuite lease: waiting for <holder>; waited N s of M sat once and every minute (soscripts/watched_pytest.pydoes not kill a queued run as stalled), and starts when the holder exits; a wait that runs out exits 75 naming the holder.NX_SUITE_LEASE_WAIT=0keeps the immediate exit,=1is the same as the default, andNX_SUITE_LEASE_UNGUARDED=1still runs on at once. Inside a lab-run grant (LAB_CPUSorLAB_MEM_MB) on Linux the lease is skipped, since lab-run is the queue there (it admits by CPU and memory, so twolab-lsuites can run together on chas); on macOS alab-macgrant is admission only and the lease is still taken, whichtests/e2e/macos-guest/host.shguard and hold rely on. New intests/conftest.py: on macOS with no grant, an xdist run with more than one effective worker whoseNX_TEST_T2_SUBSTRATEis notnoneexits 75 withlab-remote -c lab-l -- uv run pytest <args>(NX_LOCAL_HEAVY=1opts out); serial,-n 1andNX_TEST_T2_SUBSTRATE=noneruns are not refused.tests/e2e/release-preflight.shreports a pytest leg stopped by the lease assuite lease held by <holder>(matched at line start) instead of "no verdict line matched", and its pytest legs andscripts/plugin_cut_back_merge.sh's default verify now run withNX_TEST_T2_SUBSTRATE=none(none needs the engine) so they never queue on the lease. Tests:tests/test_suite_lease.py,tests/test_laptop_heavy_run_guard.py,tests/test_release_preflight_lease_detail.py. - A cross-collection topic assignment no longer lands on a topic of a superseded collection (nexus-2k8wi; engine change, in the next engine cut). A supersede tombstone, the retired source of a re-embed cutover, keeps its taxonomy centroids, and a rename tombstone's are re-homed, so only the first is affected. The cross pass took the nearest centroid of any other collection, so a live collection's chunks could attach to the retired collection's topics and raise their
doc_count. Three things change. (1) The server-side cross pass (assign_from_chashes,cross_preview), the cross-collection centroid query and the foreign-centroid read (GET /v1/taxonomy/centroids/foreign) skip any collection whose registry row names a successor. That read also feedsnx doctor --check-assignmentsand the cross-link pass ofnx taxonomy discover(compute_cross_links, which writestopic_links), so a retired collection's topics get no new cross-links; existing links stay. (2)nx taxonomy project --persist(and--backfill --persist, and any other client postingassigned_by=projectionrows) reads each target's centroids by name and names the topic itself, so the engine now SKIPS a projection row whose topic belongs to a superseded collection, inPOST /v1/taxonomy/assignments/assign_manyand/assign, instead of writing it. Skipped, not refused, because one persist call spans every target and a refusal would roll back the live targets' rows with the retired one's.assign_manyanswers{"persisted": N, "skipped_superseded": M}(N counts rows written) andassignadds"skipped_superseded": true; centroid, manual and import writes are unchanged. (3) The CLI leaves superseded collections out of the default targets ofnx taxonomy projectand out of--backfill(one/catalog/collections/listread; the engine guard does not depend on it, so an older engine or a failed read only costs the extra rows). Reading a tombstone's own centroids by name is unchanged, and assignments already written are not rewritten. Liquibasevectors-038-1, function definitions only. - The projection commands report the rows the engine wrote and the rows it skipped as superseded, and discover and index leave superseded collections out of their projection targets (nexus-h2v0l, follow-up to nexus-2k8wi; client change, works on every engine).
nx taxonomy project --persist,--backfill --persist, the projection pass ofnx taxonomy discoverand the one innx index repocounted the rows they sent, so they said "Persisted N" for rows an engine with the nexus-2k8wi guard had skipped. They now sum thepersistedandskipped_supersededanswers of eachassign_manypage:Persisted N projection assignment(s), skipped M (superseded targets).The skip clause appears only when M is above zero, and an engine that sends noskipped_superseded(engine-service-v0.1.164 and earlier) prints what it printed before.HttpTaxonomyStore.persist_assignmentsnow returns the rows written; the newpersist_assignments_countedreturns(written, skipped_superseded). Discover's projection pass and the index pass also drop superseded collections from their sources and targets, asprojectand--backfillalready did, so a retired collection's centroid can no longer win a chunk's single best pick and cost it the live runner-up. Tests:tests/test_taxonomy_projection_skip_count.py,tests/db/test_http_taxonomy_batch.py; the engine test now also pins that centroid and manual rows onto a retired topic still land. section_type,section_titleandpage_numberfollow the document, not the shared chunk (nexus-tko2w, engine change with a client half). A chunk row is one per chunk text, so when two documents held the same text the labels on it were whichever document wrote last:--where section_type!=referencescould keep a references chunk or drop a body chunk (RDR-232 research 7). The three labels are now columns of the catalog manifest (catalog_document_chunks, changesetcatalog-046, nullable, no backfill: a re-index of a document fills them), written with every manifest row, together with alabels_writtenmarker that says whether a row's labels are its document's own answer. A--whereon them is answered per (document, position): a chunk several documents hold comes back once for each document whose label passes, with that document'sdoc_idand labels, and the limit counts after that (on/hybrid-searcheach of those rows also carries its own document'ssource_uri, and the clustered MCPsearchkeeps every one of them). An unlabelled chunk, an empty label included, is kept by!=and dropped by=(except for an explicit empty-string operand, which names the unlabelled value:=""matches it). A row whose document wrote its labels is authoritative: the shared chunk row's label, which is the last writer's, is never consulted for it, so a new document's chunk with no label does not inherit a sibling'sreferences.store_get_many'ssection_typesreads the owning documents' labels (empty when they disagree or when written and unwritten owners mix), and the plan runner's import-only exclusion reads the document's own manifest row. Documents indexed before this (their rows are notlabels_written), and the manifest backfill's rows, keep reading the chunk row's label until re-indexed; a backfill from the chunk row for chunks with a single owner is a follow-up. The MCPquerytool's plain path still shows a chunk shared by several documents once; the other owning documents are not listed. The search is cut at four times the limit before the filter runs, so a positive filter on a rare label can return fewer results than asked for.--whereonsource_author,indexed_atandbib_*follows the document, not the shared chunk (nexus-6pfgb.24, engine change, in the next engine cut; no client change, a client on engine 0.1.164 sends the same request and gets the chunk row's answer). Same defect and same remedy as the section labels above: a chunk row is one per chunk text and its metadata is the last writer's, so--where bib_year>=2020or--where source_author=Alicecould return a chunk for a document that does not satisfy it. The engine now answers these per owning document from the catalog and returns a chunk once for each owner that passes, with that owner'sdoc_id,positionand, on the filtered keys, that owner's value.source_authorandindexed_atread the document's author and index time (the chunk row's value when the author is empty or there is no index time); thebib_*keys read the document's bibliographic columns and are authoritative, so an unenriched document has nobib_yearand the shared row's is never read.bib_*answers only for documents enriched throughnx enrich(it fills those catalog columns);nx index pdf --enrichwritesbib_*on the chunk rows only, so a document enriched that way reads as unenriched on this engine andbib_year>=2020drops it.bib_year,bib_citation_countandpage_numberare numbers, and an equality (=,!=,in,not in) accepts a number's text (a range with a text operand matches nothing, as on the chunk row); every other answered key compares as an exact string (bib_semantic_scholar_id0042is not42). An explicit empty-string operand matches an empty or missing value (="",in [""]) or drops it (!="",not in [""]); the chunk-row filter matched only a stored"", so a missing value now also matches""and is dropped by!=""andnot in [""]; thenxclient refuses an empty value, so only a raw HTTP caller can send it. The catalogmetadatakeys (tags,category,ttl_days,source_agent,extraction_method,quality_gate_overridden,extraction_source,frecency_score) are not part of this change: no indexer writes them on the catalog document yet, so they stay filters on the chunk row, inside the search, as before. No new changeset. The search is cut at four times the limit before the filter runs (at most 1200), so a positive filter on a rare value,bib_venue=NeurIPSin a collection where few documents carry it, can return fewer results than asked for where the in-index scan kept walking.title=andcontent_type=are not part of this change, andsearch_metadata_scoped,search_aspect_scoped,search_graph_hopand the engine's get routes still read the chunk row.- A re-embed resume, and the
kept_tombstonedre-entry, are refused with the missing piece named when the engine cannot embed for the target's model (nexus-6zchp, follow-up to nexus-q6t89; engine change, local engine only). A start already refused this; a resume with the target present, and the re-entry, skipped the check and ran into another failed batch. A resume whose target is gone is unchanged. Held off 7.86.2: the change is in the engine, and engine-service-v0.1.164 does not carry it; it ships with the next engine tag. - A data token that is due for refresh adopts a sibling's fresh lease instead of minting, so a reboot no longer sets off a 429 burst, and a failed refresh keeps the still-valid token (nexus-dncjg, nexus-kcdbx). Processes that had borrowed one lease share its expiry, so after a reboot a dozen of them came due in the same second, minted at once past the engine's burst of 5 per credential and tenant, and every loser kept retrying a mint the limiter refused (the long-lived MCP server kept failing T2 calls for minutes while a fresh
nxcommand worked). The refresh branch ofDataTokenManager.bearer_fornow reads the lease first and adopts a sibling's remint (a different token that expires later than the cached one); otherwise it mints under the same per-host mint lock as a cold start, so one process per host mints. A refresh that fails while the cached token is still valid makes one mint attempt with no retry sleep (it holds the key lock and the mint lock, and used to stall them for up to 30 seconds), returns that token, and waits a jittered 15-45 seconds before the next mint, at least the server'sRetry-After, never past the token's own expiry; a sibling's lease is still adopted on every call, and when a sibling holds the mint lock the refresh waits at most 2 seconds for its lease before returning the cached token. Only a failure after the token has expired raisesDataTokenMintError(which now carriesstatusandretry_after), and a 401 or 403 on a refresh (revoked credential, wrong tenant) is logged at error level. Each manager draws its own refresh point once, between 15% and 25% of the TTL remaining, and a manager takes a lease on a cold read only when it is not yet due for its own draw, so an accepted lease is never refreshed on the next call (has_fresh_leaseandfresh_lease_tokenkeep the fixed 20% bar). The 1s/2s mint backoff is now full-jitter (a random wait up to those values, so the worst case is unchanged). A filesystem that cannot flock (ENOLCK) now falls back to an unguarded mint on the cold path too. The module docstring that said a refresh never consults the lease is corrected.