Repository navigation
v7.89.0
Pairs with engine-service-v0.1.169 (REQUIRED_ENGINE_VERSION 0.1.167 -> 0.1.169; the engine-service-v0.1.168 halves, live in the cloud since 2026-10-29, reach local installs with it). The nx_answer coverage check is on by default from this release (NX_ANSWER_COVERAGE=0 turns it off).
Added
- RDR-232 measurement: the held-out set has its own oracle label files, and drafted labels are converted from drafter output (nexus-jypmj.3.10).
golden_heldout_oracle_labels_v1.yaml(all to-do) and a drafted file sit beside the golden pair;rdr232_phase3_measure --set heldout|golden|bothreads the matching files and--from-envelopes --set heldoutrescores with drafted labels;rdr232_oracle_drafts.pyturns drafter JSON plus the blind packet into the drafted YAML, keyed by chunk hash; the scorer and spot check no longer drop anchors that share an item id. Measurement tooling only. - The engine's collection rename, re-embed move, re-home, purge and re-embed counts now act on the unconsumed
aspectstasks, which name their collection in their body (nexus-5r2sc.2.6, RDR-235; engine half, rides the next engine cut,engine-service-v0.1.169; no client change, the queue is not wired to any producer or worker yet). A newAspectTaskRefshelper counts, rewrites (collectionin the JSON body) and deletes the tenant's available, claimed and deadaspectstasks, in the caller's transaction, and every collection-keyed site calls it:AspectRepository.renameQueueCollection, the catalog rename (renameCollectionTxn), a whole-collection re-home, a re-embed move of one document, the collection purge, the re-embed residue and plan counts,blockingTable,collectionHoldsContentandrehomeStatus. A claimed task keeps its claim and lease and a dead task stays dead. The task rows are locked first (FOR NO KEY UPDATE, in id order) anddocument_aspectsafter, the order a claim-fenced aspect write takes, so a rename concurrent with one cannot deadlock. A body that is not validjsonb(not JSON, or JSONjsonbrefuses such as a NUL escape; checked withpg_input_is_valid(body, 'jsonb'), PostgreSQL 16+) is skipped and logged asaspect_task_refs_non_json_skipped. New wire-visible keyaspects_tasks: in the rename's counts map and a re-home'smoved_by_table(always), in a re-home status'sremaining_by_table(when above zero), and as the blocking table a rename's merge refusal names; a collection that only a queued task names is no longer swept as a ghost. Nothing else in the purge's counts changes. Pinned byAspectTaskRefsRenameTestandAspectTaskRefsParityTest(a source scan that fails when a collection-keyed site has no helper call). - The RDR-232 Phase 3 measurement driver compares the two arms only where both ran the same plan, grows no plans unless asked, and scores drafted oracle labels apart from hand labels (nexus-jypmj.3.10). A plan-miss
nx_answergrows a plan, so the coverage-on arm could match the plan the coverage-off arm had just grown and skip the planner; the driver now pairs on the plan each arm chose at match time, reports the excluded pairs, grows no plans on--liveunless--allow-plan-growthis given, binds requests that name no project to--project-dir(default: this repository) so the active-project tier can resolve them, and--from-envelopesre-analyses an earlier run. The oracle gains adraftedprovenance, a 95 percent interval on every rate, a document-level label class, a blind drafting-packet extractor and a stratified spot-check sample.unassessed_by_causegains the keystopped_after_failure(always 0 since the assessment calls run concurrently, nexus-jypmj.17). New in-sample negative-control and near-miss synthesis request sets. Measurement tooling and one additive run-summary key; no change to answers. - The
nx_answerrun summary now counts what each coverage-assessment call came back as, and the envelope'scoverage.assessmentcarries the same counts (nexus-jypmj.3.10, RDR-232 Phase 3; client change, works on every engine; the check itself was off unlessNX_ANSWER_COVERAGE=1in 7.88.0 and is on by default from this release, see the entry below). Per assessment call the reply class is one ofsupport(an accepted record says an item supports a facet),neutral_or_contradict(accepted records, none supporting),all_rejected(records came back and every one failed validation),no_records(an empty reply, or one that is not the check shape) orraised(the call raised), counted in total, per evidence stream, and as an orderedassessment_call_logof[stream, facets, class]. A failed assessment is split intofailure_causeunusable_reply,raised_callormixed. A conflicted facet is counted infacets_conflicted(a subset offacets.covered, whose meaning is unchanged). Unassessed facets are counted by cause inunassessed_by_cause(unusable_reply,deadline_skip,budget_skip,raised_call,other); a facet whose later assessment call was skipped for time or budget, or whose call raised, now carries the reasonassessment_skipped_deadline,assessment_skipped_budgetorassessment_raisedinstead ofassessment_incomplete.repair_entered(andrepair.enteredin the run summary) says the run went into the repair step. All of it is closed names and counts, with no question, facet or evidence text with or withouttrace. This is additive to run-summary version 1: no key changed meaning, the version stays 1, and the engine, which stores the summary as opaque JSONB and checks only that it is an object under the size cap, needs no change. No state, skip or answer shape changes. - Measurement harness for the RDR-232 Phase 3 coverage check: a runtime-facet-to-golden-slug map, an envelope-to-
RunObservationadapter, item-by-facet oracle labels with a scorer, and a coverage-off versus coverage-on driver (nexus-jypmj.3.10, RDR-232 Phase 3; developer tooling underscripts/benchandbench/queries, no runtime change, works on every engine).bench/queries/golden_facet_map_v1.yamljoinsnx_answer'sf1..fNfacets to the goldenrequired_facetsby facet text;bench/queries/golden_oracle_labels_v1.yamllabels which retrieved item bears on which golden facet (provenancefixture,ground_truthorhuman, never a coverage ledger, so the assessor is never scored with its own output; 9 of 113 request-facet pairs are labelled, 104 are TODO for a human pass) andscripts/bench/rdr232_oracle.pyscores the assessor's false-negative, spurious-contradicts and false-skip rates against it.scripts/bench/rdr232_phase3_measure.pyruns the golden set withNX_ANSWER_COVERAGE=0and=1and writes the reply-class, unassessed-cause, repair-yield-by-facet-origin, conflicted, cost and latency numbers;--livecalls the realnx_answer(model calls, telemetry writes) and--stub FILEreplays a recorded envelope. - The engine applies a scoped
traverse's collection or subtree scope before its 500-node cut, and tells the client it did (nexus-jypmj.1.13, RDR-232 section 3; engine half, rides the next engine cut,engine-service-v0.1.168, with the client half in the same commit). The engine cut a catalog walk at 500 nodes ordered by depth and tumbler before any client saw it, so a scopednx_answerwhose walk reached hundreds of code documents lost its in-scope evidence to the cut (the RDR-084 canary: 243 of the 500 nodes were out of scope).POST /v1/catalog/traversenow takes an optionalscope_filter({"collections": [...]}or{"subtree": "1.2"}), removes the non-seed nodes it does not admit before the cut, and answers with ascope_filtermarker;GET /versiongainscatalog_graph_scope_filter_supported. The client sends the filter only to an engine that advertises the flag and requires the marker. Against an engine without it (and on the public cloud edge, which trims/version, until conexus allowlists the flag) a scoped walk that may have been cut is reported as an incomplete traversal and returns the seed documents and no secondary graph evidence, never a containment claim; a walk that was not cut is unchanged. Thetraversalreport gainsscope_prefilter,engine_dropped_out_of_scopeandsecondary_omitted. POST /v1/catalog/manifest/get_manytakes a per-document row limit, and a named-source read that exceeds its row budget samples every source instead of dropping the largest (nexus-jypmj.6, RDR-232; engine half, rides the next engine cut,engine-service-v0.1.168, with the client half in the same commit).max_rows_per_docreturns a longer document as an even sample (first and last row included) and the reply addsmax_rows_per_docand each document's truerow_counts;GET /versiongainscatalog_manifest_row_limit_supported. When the recorded chunk counts of the named sources exceedmax_manifest_rows_readand the engine advertises the flag,primary_evidenceasks for one sample per source at the water-fill level, so no source ismanifest_not_readand a sampled one is disclosed as a cap hit. Without the flag the read is the smallest-documents-first one of nexus-jypmj.1.17.- The engine stores each
nx_answerrun's out-of-step dispatch cost (nx_answer_runs.out_of_step_cost_usd: the inline planner call plus the request-extraction call), andnx answer-runs --stepschecks a run's cost from both sides (nexus-jypmj.5, RDR-232; engine half, rides the next engine cut,engine-service-v0.1.168, with the client half in the same commit). Liquibasetelemetry-017-1adds the nullable column;/record,/complete,/importand/import_batchcarry an optionalout_of_step_cost_usd,/queryreturns it (nullwhen none), andGET /versiongainsnx_answer_out_of_step_cost_supported. A row that stored the field, whatever itsplan_id, must have a run cost equal to its step sum plus the field, so an over-count is flagged; a row without the field keeps the one-sided planner allowance and the run summary's extraction fallback. A plan-match hit that made the extraction call stores the field too, and the column is NULL when the run made no out-of-step call. The client sends the field only to an engine that advertises the flag, and logsnx_answer_out_of_step_cost_unsupported_by_engineonce per process against one that does not. - The engine ships a literal
aspectstuple template, the one aspect-extraction queue per tenant; setNX_TUPLE_CLAIM_LOG_TTL_DAYSto at least 100 or leave it unset before this engine reaches a host, or the engine refuses to boot (nexus-5r2sc.2.4, RDR-235 E3; engine half, rides the next engine cut, no client change).aspectshas one key,queue, whose only allowed value isaspects;takeis on withmax_attempts6,max_lease_seconds900 and adefault_lease_secondsof 600 (so a worker'sinpor renew that sends nolease_sgets the worker's 600 s lease, not a 400);retention_secondsis 7,776,000 (90 days),max_live_rowsis 10,000 andclaim_log_ttl_secondsis 8,640,000 (100 days). A template's claim-log TTL may only shorten the engine's, so a host whoseNX_TUPLE_CLAIM_LOG_TTL_DAYSis below 100 fails the boot check with a message naming the variable; unset is 180 days and passes. The queue is not wired to any producer or worker yet. - The engine can hide a tuple from the claim until a database-clock instant:
outtakesdelay_s,nacktakesretry_after_s(nexus-5r2sc.2.1, RDR-235 E1; engine half, rides the next engine cut,engine-service-v0.1.169; no client change yet, a client that sends neither field sees no difference). Liquibasetuples-010-1adds the nullablenexus.tuples.available_at(ADD COLUMN only, no index, no backfill;migration_ddl_per_worker2148 -> 2149).POST /v1/tuples/outaccepts an optional whole-secondsdelay_sandPOST /v1/tuples/nackan optionalretry_after_s; each is stamped asnow() + secondson the database clock, refused with a 400 when negative, fractional or above the template'sretention_seconds, and absent or 0 changes nothing. A row withavailable_atin the future is skipped byin/inpand by the announce-moderd; a plainrdstill sees it, andsubspace_statsstill counts it as available. A nack withretry_after_scounts its attempt as before, and a nack that dead-letters leavesavailable_atempty.release, a nack withoutretry_after_s, the sweep's lapse release and a lapse that dead-letters all clear it with the claim. No signal fires when a delay runs out, but a parkedinre-runs its claim on a one-second timer as well as on signals, so it picks the task up within about a second ofavailable_atpassing. POST /v1/aspects/upsertcan carry the worker's claim, and the write is refused if the claim is not live: optionalclaim_idandclaimant(nexus-5r2sc.2.5, RDR-235 E5; engine half, rides the next engine cut,engine-service-v0.1.169; the client gains only the matchingClaimTaskMismatchErrorclass, the fourteenthTupleError,code = "ClaimTaskMismatch"; no caller sends the fence fields yet, and a client that sends neither sees no difference). The two fields come together or not at all: one without the other is a 400 naming both. When both are present the upsert looks the claim up in its own transaction, before the retired-collection check, and locks the claim's tuples rowFOR NO KEY UPDATE, so a re-take of a claim that lapses an instant later skips the row until the write commits. No live claim (lapsed, re-taken, released or already acked) is a 404ClaimNotFound; a live claim held by another claimant is a 403ClaimOwnership; a live claim that is not theaspectstask for the writtendoc_idis a 403ClaimTaskMismatch. Nothing is written on any of them, and the write and theackthat follows it give the same answer for the same claim.AspectHandlerrenders the tuple errors with their own status instead of a 500. Lock order for every path that touches both tables: the tuples row first, thendocument_aspects. Engine half:AspectRepository.upsertAspect,TupleRepository.lockLiveClaimRow,AspectHandler; testsAspectUpsertClaimFenceRepoTest,AspectUpsertClaimFenceTest. Engine deploy is safe before the client tag.nackcan dead-letter a claim at once:POST /v1/tuples/nacktakes an optional booleandead(nexus-5r2sc.2.3, RDR-235 E4 second half; engine half, rides the next engine cut,engine-service-v0.1.169; no client change yet, a client that does not senddeadsees no difference).dead: truedead-letters the claim immediately, for a failure no retry will fix: the attempt is counted, the claim log showsnackthendeadexactly as it does when the count reachesmax_attempts, the row counts underdeadinsubspace_stats, and it works under a template with nomax_attempts.dead: trueon a claim of alock: truetemplate is a 400SchemaViolationnamingrelease, asackis refused on one: a lock must never dead-letter, and the claim stays live. It goes through the same compare-and-swap as the cap path, so ownership and liveness are checked first (403 and 404 as before). A non-booleandeadis a 400, anddeadsent withretry_after_sdead-letters and leavesavailable_atempty. No schema change.outtakesmode: "replace"so the newest content wins on a deterministic id, and theackof a replaced claim returns the task to available (nexus-5r2sc.2.2, RDR-235 E4 first half; engine half, rides the next engine cut,engine-service-v0.1.169; no client change yet, a client that sends nomodesees no difference). Liquibasetuples-011-1addsnexus.tuples.dirty BOOLEAN NOT NULL DEFAULT FALSE(ADD COLUMN only, no backfill;migration_ddl_per_worker2149 -> 2150), in its own changeset becausetuples-010-1has already shipped.modeis absent or"replace"(anything else is a 400), and"replace"is allowed only on anid_from: keys+noncetemplate (any other is a 400 naming it). The response gains"outcome"(created,replacedordirtied); a plain out keeps{"id": ...}. A consumed, expired or dead row is reset to available (new body, attempts 0, retention clock restarted,available_atfrom this write'sdelay_s); an available row, or one whose lease lapsed, takes the new body and keeps its attempts andavailable_at; a claimed row takes the new body and is marked dirty, and its holder'sackreturns it to available (attempts unchanged, claim-log transitionack_dirty) instead of consuming it. Reviving a consumed or expired row counts againstmax_live_rowsand is refused when over; a dead row is already counted. Every replace stampssender_sessionwith the replacing writer's session (the stamp names the writer of the stored body, and a replace rewrites it), andmode: "replace"on alock: truetemplate is a 400 whatever itsid_from. Every claim clears the mark, so a lapsed holder's dirty flag never reaches the next holder.
Changed
-
nx_answercoverage check: on by default, and its assessment calls run concurrently (nexus-jypmj.17, nexus-jypmj.3.10, RDR-232 Phase 3; client change, works on every engine). Sam decided on 2026-10-28 that the coverage check is on: withNX_ANSWER_COVERAGEunset or blank the runner assesses the evidence before the final reduction;0,false,offornoturns it off andnx_answeris what it was before Phase 3. A value that is neither an on nor an off word (disable,off!) still reads as off, not as the default, and warns once, so a typo never runs the assessor. The default ships with the latency fix the live canary asked for: with the check on it added 118 s, because oneoperator_checkcall per evidence stream ran in a row, the 65-anchor graph call outran its 60 s timeout, and a call that raised stopped the loop. Now the calls run at most 4 at a time, or fewer when the direct-API backends'NX_LLM_MAX_CONCURRENCYis set lower (those backends gate every HTTP attempt at it, so a call past it would only wait for a permit inside its own timeout; theclaude -pbackend has no such gate and its 4 concurrent subprocesses are unmeasured), each call is cut at the assessment stage's share of the deadline (0.4 of what is left) even if the model call ignores its timeout, and the cut now kills theclaude -pprocess tree (a call cut from outside used to leave its child running to the end of its turn), and the USD allowance is checked as each call starts, counting the calls in flight and the starting call at the mean cost of those finished, or at a 0.02 USD prior before any call has reported a cost (the live canary's 4 calls cost 0.0166 USD in all); one call always starts when none is in flight and the allowance is above zero, so the overshoot is expected to stay about one call past a soft budget while real call costs stay under the prior. A call killed by the cut reports no cost (the CLI states its spend only in its result event), so its step record carries an unknown cost and the allowance does not count what it spent. A stream of more than 20 anchors is split into balanced packs of at most 20, each its own call, so the 65-anchor graph stream is four calls of 16 to 17. A call that raises or replies unusably affects only its own facets: the other calls still count, a facet another stream supports stays covered, and the unassessed counts, reply classes and call log are in plan order whatever order the calls finish in.unassessed_by_cause.stopped_after_failurestays in the wire form and is now always 0, because nothing stops after a failure. The terminal step'selapsed_ms(and sonx answer-runs --steps) no longer includes the assessment and repair, which have their own step records and the run summary'sassessment_latency_ms; the run's step records are listed by step index, not in the order the concurrent calls finished; andnx answer-runs --stepscounts the assessment stage once inby_plan.median_elapsed_ms(the stage'sassessment_latency_ms, not the sum of calls that overlapped), where a plain sum reports more than the run took. The 50 model-seconds (down from 130) is a unit-test timing model of the canary at 1/100 scale, not a measurement: no live run of the concurrent design was made. The live figures are the serial design's, one request, one run: 29.4 s and 0.433 USD with the check off, 147.3 s and 0.430 USD with it on, of which the assessment took 118 s and 0.0166 USD. The check adds latency and a small cost per run and can withhold an answer asuncovered. The suite pinsNX_ANSWER_COVERAGE=0so tests over operator stubs do not reach the boundary. -
nx_answercoverage check: an ambiguous named source no longer withholds the answer (nexus-jypmj.3.17, RDR-232 Phase 3; client change, works on every engine). When the coverage check runs (it is on by default from this release;NX_ANSWER_COVERAGE=0turns it off), a request naming a source that two projects carry (RDR-049in nexus and ART) reported the source asmissing, marked its facetsource_missing, and skipped generation: three golden requests came backuncoveredat zero cost while the coverage-off arm answered them. Such a source, and one whose lookup failed, timed out or hit its candidate cap, is nowunresolved: it is disclosed incoverage.sourcesandcoverage.primary_source, named in the insufficient-evidence notice, and blocks no facet, which is assessed on the evidence that was read. A source no document carries staysmissingand still decides its facets assource_missing. -
/conexus-prose-edit:prose-editasks its questions as choice prompts, and its setup walks a newcomer in (nexus-scmcs, plugin change; works on every engine). The first setup run on a 7.88.0 install listed about 180 unmapped Markdown and HTML folders (tests, fixtures, agent prompts) and asked the author to classify them in free text, with no defaults, no explanation of a genre and no closing step.setupnow reports at most eightgenre_candidates, folders that look like documentation, each with a genre suggested from the folder name, all six genres ranked and a sample file; a count of the files left out (unmapped_other);ready; and atry_path. It replaces theno_genrelisting. Tests, fixtures, scripts, hidden folders and similar are never offered. The skill asks throughAskUserQuestion: the setup wizard (the temp-folder permission rules, which can be added to~/.claude/settings.jsonon a yes behind Claude Code's own approval prompt; one question per candidate folder with the suggestion first; example passages) ends withReady.and a command to try; the run-time genre question, which no longer asks for the genre in free text and offers to remember the answer for the folder (BRIEF suggest-genre <path>); and the accept and reject step, which offers each edit as a tick box instead of asking for numbers (more than 16 edits still take numbers for the rest). The apply step still shows its dry run first and asks to apply, change the picks or cancel. The README opens with a three-step quick start and a table of the six genres, whose wording the scripts share. -
mineruis the default PDF extractor;autoanddoclingrun asmineruwith a one-line notice (nexus-tr1rj.5, RDR-237 Phase 1 Step 3; client change, works on every engine).pdf.extractor,nx index pdf --extractor,nx dt index --extractorandnx dt capture --extractordefault tomineru(they wereauto).autoanddoclingare still accepted, in the config and on the command line, and run asmineru; the first use in a process prints one line saying so (notice: pdf.extractor in your config says 'docling', which now runs as 'mineru' ...), and later uses in that process print nothing. Apdf.extractorvalue that is none ofmineru,pymupdf,autoordoclingfalls back tomineruwith a warning (it fell back toauto).pymupdfis unchanged. There is no formula pre-screen and no first pass any more: every PDF goes straight to MinerU, and a page whose MinerU attempt fails walks the per-page ladder (retry without formulas, then the PDF's own text layer, thenunextracted). A MinerU that cannot be imported on an install whose PDF stack reports itself present, or that fails on a PDF, still stops that PDF with a message of its own (PDF <name>: MinerU is not importable ...orPDF <name>: MinerU extraction failed: ... Run nx doctor); the old text said the PDF "contains formulas (detected N)", which was only true of the removed screen.--on-formula-oomis retired:nx index pdfstill acceptsfailordoclingafter it, ignores the value, prints one notice, and no longer lists the flag in--help. The library functionsindex_pdf,batch_index_pdfsandPDFExtractor.extractlose theiron_formula_oomparameter;PDFExtractor.extractraisesValueErrorfor an extractor name that is none of the four. Every PDF now runs MinerU, about 4 to 10 s per page, and the first PDF on a box downloads about 1.2 GB of model weights.nx index repo,nx collection reindexandnx dt indexrun MinerU on every PDF they index;--extractor pymupdf(orpdf.extractor: pymupdf) is the fast, text-only choice. -
nx_answercoverage check: a contradicted facet now counts toward a usable answer, so a result whose only settled facets are contradicted returns a partial answer with the contradiction named ahead of the prose (nexus-jypmj.3.13; client change, works on every engine). When the coverage check runs (on by default from this release),[insufficient-evidence]now means the evidence neither supported nor contradicted any facet, where 7.88.0 said it covered no facet. -
A MinerU breaker trip or machine-resource error stops the MinerU work of
nx dt indexandnx index repoand keeps the rest; a--extractor pymupdfrerun over a tripped MinerU buffer starts clean;nx dt indexandnx dt capturereadpdf.extractor(nexus-tr1rj.13, nexus-tr1rj.14, RDR-237; client change, works on every engine). A breaker trip (MineruSystemicFailure) orMineruMachineResourceError(POSIX semaphores exhausted) names the machine, not the file, sonx index pdf --dir(which already stopped on a trip),nx dt indexandnx index reponow all stop attempting PDFs once one is seen and list the PDFs not attempted, and exit non-zero.nx dt indexclassifies it as a breaker trip, not an unexpected defect (nodt_index_unexpected_error, no traceback), and keeps indexing markdown and web records.nx index repocontains it like the quality gate instead of aborting the run, so code, prose and RDR files are indexed; the PDFs it failed or did not reach hold the--since-headbase back. Apymupdfrerun over the pipeline buffer a tripped MinerU run kept (or one that names no rung, or a finished MinerU extraction) wipes it and extracts every page from the text layer, so the document is recordedpymupdf, notmineru+degraded.nx dt indexandnx dt capturetake--extractorfrompdf.extractorwhen no flag is given. -
nx doctoron a managed (cloud) deployment gives every row that needs local database credentials the same warning, never a checkmark (nexus-njn1a finding 10; client change, works on every engine).Storage service health,Schema migrations,RLS policies,tuples.dead_tuple_ratioandtuples.sweep_freshnessalready warned on a cloud box;Tenant partitions,Model partitionsandPartition leavesshowed the same fact as a green "not applicable". All eight now render a⚠with one reason text:managed deployment — not probeable from this client, so this check was not run: .... The check was not performed, so a checkmark would be false (Sam, 2026-10-28, nexus-g7ijj). A local box with nopg_credentialskeeps its own text. The setup row stands down on a cloud box exactly as before, by recognising that one reason. It also closes a hole this change would otherwise have opened:nx daemon service install-binaryand the upgrade finish pass classify the store through theSchema migrationsrow, and on a managed box they read "clean" after examining nothing; an unperformed row is now unknown whatever it is drawn as. TheEmbedding familyrow says what it can: when the engine's/versionnames no profile (and this client has none to read it against) but lists both the bge and the Voyage family, it passes and says both are served (the managed cloud engine reports this, measured on the live cloud on 2026-10-28); otherwise its "not applicable" now names the real cause (the engine reports noembedding_profile, and a managed engine is not read against this client's local profile) in place of "could not be determined". -
nx telemetry baseline --sincelabels the nx_answer runs timestamps as the window's:oldest_in_window=andnewest_in_window=(nexus-njn1a finding 11; client change, works on every engine). They were printed asoldest=andnewest=beside the all-timetotal=, but both come from the--sincewindow. Without--sincethe text is unchanged, and the JSON keys (oldest_created_at,newest_created_at) are unchanged. -
The
nx_answerrequest-extraction prompt now draws the lookup/synthesis line where RDR-232 section 1 does: several connected facts from one passage of one source are alookup; asynthesisgathers aspects across sections, sources or history (nexus-jypmj.13, RDR-232 Phase 2; client change, works on every engine). The prompt used to define synthesis as "a unified account of several aspects" and ask for one facet per aspect, so a compound question about one passage read as a lookup only when the extraction model departed from its prompt (Haiku did; Opus on the same prompt said synthesis, and so would a model alias re-point or anopenai_compatbackend). The first-pass rule still leaves a question with more than one facetunknownand sends it to the extraction, because the wording cannot say whether the facts share a passage. A new labelled set,bench/queries/golden_compound_lookup_v1.yaml(6 two-fact single-source lookups), and--set compoundonbench.rdr232_extract_measuremeasure the class. The extraction was re-measured with the new prompt on 2026-10-28 (98 calls: Haiku 67, Opus 31; no timeout, error or invalid reply): Haiku reads 5 of 6 compound single-passage requests as lookup and Opus 6 of 6, and the five approved gate thresholds read the same as before. -
The RDR frontmatter
idis recorded on the catalog document whennx index repoornx index rdrregisters it, and a named-source sibling lookup reads it first (nexus-jypmj.7, RDR-232; client change, works on every engine, local and cloud). Where one RDR number has several files,nx_answerpicks the one whose frontmatter carries the id; it used to learn the id from therdr-frontmatter-v1aspect row, which the aspect worker fills later, or from the file where the server could see it. The id is stored undermeta.rdr_frontmatter_id(""for a header with no id; absent when the header did not parse). Existing registrations get it on the nextnx index repoof the repo: an RDR whose document lacks the key, or carries a different one, is written once and then left alone.
Removed
- The Docling PDF extractor is gone: code, dependency, CI priming and doctor row (nexus-tr1rj.5, RDR-237 Phase 1 Step 3; client change, works on every engine). Docling extracted every PDF that the formula screen did not send to MinerU, and its whole-document convert went silent for hours on some PDFs (RDR-237 has the measurements). Removed: the router's formula screen and Docling pass, the per-process converter cache, the one-page Docling degrade, the
docling_titlemetadata key and its first place in the title chain (a PDF's title is now its metadata title, then its first heading, then its file name; the MinerU path reads the PDF's own title, author and creation date with PyMuPDF, as the text-only extractor does, so a MinerU document's catalog year now comes from the PDF's creation date, which may not be its publication year), andNEXUS_DOCLING_ARTIFACTS_PATHas a setting.docling-slimleaves thepdfextra anddocling-core,docling-ibm-models,docling-parseanddocling-slimleavesrc/nexus/_install/constraints.txt.uv.lockloses exactly 14 packages that only Docling needed: accelerate, doclang, docling-core, docling-ibm-models, docling-parse, docling-slim, jsonref, langcodes, latex2mathml, psutil, rtree, shellingham, tabulate and typer. No package changes version, andpluggyleaves the install constraints (it stays in the lock for pytest). pillow stays at 12.3.0 (the comment inpyproject.tomlthat blameddocling-slimfor the cap now names fastembed alone, which caps pillow below 13 through 0.9.0), and pypdfium2 stays at 4.30.0, which pdftext 0.6.3 pins exactly.nx doctor'sPDF extraction (docling)row is nowPDF extraction (mineru)and checksmineru,cv2,torchandtorchvision; the MinerU server row says "PDFs" where it said "math PDFs". A newDocling model cacherow (a warning, never red) appears only on a box that still has the models Docling downloaded, about 1.1 GB: directories whose names startmodels--ds4sd--docling,models--ds4sd--CodeFormulaormodels--docling-project--in the Hugging Face hub cache (~/.cache/huggingface/hub, or whereHF_HUB_CACHE,HUGGINGFACE_HUB_CACHE,HF_HOMEorXDG_CACHE_HOMEputs it;~and$VARare expanded), indocling-tools' default~/.cache/docling/models, and in the directoryNEXUS_DOCLING_ARTIFACTS_PATHnamed, if set. The row prints the size and anrm -rfcommand for the first two; for theNEXUS_DOCLING_ARTIFACTS_PATHdirectory, which you chose and may share, it prints no command and says to check that it holds only Docling models. Nothing deletes the cache for you, and a box that never ran Docling shows no row. CI: theprime-doclingaction, the "Prime docling artifacts" and "Verify docling extracts" steps (in the shard matrix and in the affected-integration job) andscripts/ci_warm_docling.pyare deleted; no MinerU step is added to PR CI. The job timeouts that the Docling model cache once forced up are unchanged, because no CI run without the cache has been measured. Documents already indexed keep theirextraction_methodofdocling,mineru+docling-degradedorpymupdf_normalized; they stay readable and--where extraction_method=doclingstill finds them, andnx index pdf --forcere-extracts any you choose. The install sizes quoted in the README, the docs, the setup tool and the.mcpbmanifest were measured on 7.86.0 with Docling in the stack and have not been measured again, so they now overstate the PDF support; only the list of what it contains changed.
Fixed
nx_answercounts the structured request extraction's call in the run'scost_usdand inbudget_usd's running spend (nexus-jypmj.9, RDR-232 Phase 2; client change, works on every engine). The Haiku extraction call (about 0.002 USD, on about two thirds of requests) was reported inrequest_coverage.extractiononly, so the envelope'scost_usd, the storednx_answer_runs.cost_usdand the cap abudget_usdcall stops at all left it out, andnx answer-runsunder-reported every run that made it. Its usage now joins the sink the inline planner's does, and the run's out-of-step share (planner plus extraction) is what the run stores inout_of_step_cost_usd(nexus-jypmj.5).nx answer-runs --stepsfollows: a row that stored the share is checked against the step sum plus the share; for a row without it, a run summary that records an extraction cost agrees at the step sum plus that cost (exact, so an over-count is still flagged), or at the step sum alone for a row written by 7.88.0, which recorded the call in the summary only; a row without a recorded extraction cost is held to the step sum as before.- The answer-kind gate's library check names each undeclared builtin row with its id, and does not raise on a malformed
dimensionsvalue (nexus-jypmj.15, RDR-232 Phase 2; client change, works on every engine).plan_eligibility.undeclared_builtin_namesand the once-per-process warning listed rows by name, and four live builtin rows are all nameddefault; each entry is now<name> (id <n>). A row whosedimensionsis not a string (a dict or list, unhashable) is skipped as unreadable instead of raisingTypeErrorout of the check. - The
library_not_reseededwarning names what actually clears the state (nexus-jypmj.16, RDR-232 Phase 2; client change, works on every engine). It saidnx plan reseed, which cannot clear a builtin row that the reseed itself refuses to write: a template the installed conexus rejects (the reseed printsseed load error; only an upgraded conexus clears it) and a template whose description another row already holds (not reconciled ... description already belongs to library row id=N; it clears when row N is deleted withnx plan delete Nand the reseed is run again). The warning'sremedy,docs/configuration.mdanddocs/cli-reference.mdnow say so. - A persistently failing structured request extraction logs one warning per process and has an
nx doctorrow (nexus-jypmj.16, RDR-232 Phase 2; client change, works on every engine). After three extraction calls in a row fail (noclaudeCLI and no API key, anopenai_compatendpoint that answers outside the schema, a timeout every time) while the answer-kind gate is enforcing, the process logsanswer_kind_gate_extraction_failingonce: every request keeps its first-pass kind and the gate enforces on it, the regime that lost lookup recall in the held-out measurement. The new "Answer-kind extraction" row ofnx doctorreads the same preconditions without a model call: it names the backend and model, is not applicable (a pass) when the extraction or the gate is switched off or the box has no LLM backend at all, and warns for anopenai_compatendpoint or a backend setting that cannot serve. - The routing guards no longer wait on their
routing_eventswrite, which now gets a 5 s budget of its own in a detached child (nexus-njn1a finding 9, nexus-njn1a.1; plugin change, works on every engine, live at the next plugin release). The hooks POSTed each deny or escape to the engine with a 0.25 s limit on every socket step, and 646 of the 650 entries in one box's drop meter (dropped_writes.jsonl) werecause=timeout, 641 of themrouting_events. Raising the limit inside the hook would have made every logged deny androuting-allowescape wait for the engine before it could be emitted (a 1.5 s limit measured 1.58 s per deny against a stalled engine, 0.35 s at the old limit). The POST now runs in a detached child with a 5 s total deadline; the hook emits its decision and exits without waiting (0.07 s against the same stalled engine). A failed POST still lands one drop-meter record, written by the child; a hook that cannot start the child records the drop itself asother. Nothing is spooled or retried. An answer that arrives after the child's deadline counts as a drop although the engine may hold the row. The 8capability_censusdrops are historical: that writer was deleted (nexus-0r1uz). What made those POSTs time out at 0.25 s per step is not established: an unauthenticated POST to the route measured 0.23 to 0.33 s in total with no single socket step over about 0.17 s, and the authenticated request was not measured. nx_answerwith the coverage check on (the default from this release) no longer withholds the answer asuncoveredafter recording every reserved chunk of a named source as dropped (nexus-jypmj.3.14, RDR-232 Phase 3; client change, works on every engine). The coverage assessment's ownoperator_checkcall went through the run's operator hydration while the reserved stream was still marked at the terminal step, so the stream read the assessment payload as the terminal input and recorded every reserved item dropped (reserved_stream.dropped_before_terminalof N of N); the repair's second ledger then marked the named source missing and every facetsource_missing. The assessment now runs with the reserved stream out of scope; coverage on and off agree on what reaches the terminal reduction.nx answer-runsno longer counts atrace=Falsecoverage non-answer as executed-ok, and thenx_answerrun summary no longer carries the question as a facet's text (nexus-jypmj.3.15, nexus-jypmj.3.16, RDR-232 Phase 3; client change, works on every engine). Atrace=Falserow records its text as[redacted], so the coverage notice could not classify it and an uncovered run landed in executed-ok while its traced twin was degenerate; the row's run summary now decides: annx_answersummary whosecoverage_stateisunavailableputs the row in degenerate asnon_answer/uncovered,non_answer/no_evidence(no evidence or a scope conflict) ornon_answer/no_usable_answer(the prose was no usable answer, or the summary's coverage block was cut). Awhole_questionfacet's text is the question itself, so the summary lists that facet with its id, origin and topics and no text at eithertracesetting; finer facets keep their text on traced runs.- An unusable MinerU time limit now stops a PDF command before it changes anything, and a skipped MinerU server restart is no longer reported as a replacement (nexus-tr1rj.24, nexus-tr1rj.25, RDR-237; client change, works on every engine).
pdf.mineru_page_timeout_stimespdf.mineru_page_batchabove 240 s (or a value that is no integer) used to be refused only when MinerU started on a document, after its catalog row was written:nx index pdf X --extractor mineru --forcewithpdf.mineru_page_batch2 turned a complete document intofailed, ran the--forceorphan cleanup over its chunks, and with a different--collectionpointed the row at an empty collection.nx index pdf(a file and--dir),nx collection reindex(before it purges),nx dt indexandnx dt capturenow ask first, when the extractor ismineru, and exit 1 withError: pdf.mineru_page_timeout_s (180 s) x pdf.mineru_page_batch (2) = 360 s, above the 240 s ..., the message the extraction gave, with the catalog, the chunks and the collection untouched.nx index pdfand--dirrefuse even when the PDF is already indexed and unchanged (they do not look first);--extractor pymupdfis not affected.nx index repoused to index code and prose and then abort at the first PDF, with the PDF's catalog row already written; it now holds the PDFs that need indexing (a PDF stale only by embedding model included; PDFs the walk finds current are left alone), touches no catalog row and no chunk for them, lists them with the setting, indexes the code, prose and RDR files, and exits 1 at the end.index_pdfrefuses the same way for a library caller, and the check at the start of extraction stays as a backstop. The lineMinerU server timed out on page(s) N — replacing it (pid P).was printed before the restart was tried, so a restart skipped inside the 120 s machine-wide cooldown still said the server was being replaced although it stayed up.replacing it (pid P)is now printed only once the restart is decided (after the restart limit, the spawn policy, the cooldown and the pid check have let it through), before the stop and the health wait of up to 60 s, followed byreplaced it (old pid P). A restart that is skipped prints onlynot replacing it (pid P)with the reason: the cooldown, the restart limit or spawn policy, a pid that is not recorded, or a pid whose process could not be identified (nothing was stopped); a replacement that did not come up saystried to replace it. nx store listand the MCPstore_listno longer abort on a chunk row whosettl_daysis out of range or not a number, and a bundle or a note write can no longer carry a TTL past 36500 days (nexus-njn1a, 7.88.0 shakedown finding 6; client change, works on every engine). The listing computedtimedelta(days=ttl_days)and caught onlyValueError, so one row with attl_daysof 10**12 ended the whole listing withOverflowError, and a stringttl_daysdied on the> 0comparison. The listing and the context annotation line (expires in Nd) now read a chunk row throughmetadata_schema.chunk_ttl_days: attl_daysthat is not a plain positive number (a string, a bool, NaN, zero, a negative) or is abovetimedelta's range (999,999,999 days) is permanent, and the listing also showspermanentfor one whose expiry date falls past the year 9999 (about 2.9 million days on). A numeric string such as"30"used to renderexpires in 30din the annotation line; on a chunk row it is now read as permanent, as the expiry sweep (is_expired) already read it. A note's own TTL in the catalog is still read bynote_ttl_days, which accepts"30"and cuts a float to whole days, so the two readers differ on strings; that is unchanged. Bundles:nx catalog importwrote a record'sttl_daysverbatim, past the 36500-day (100 year) limitnx store put --ttlrefuses. Export and import now read attl_daysabove 999,999,999 or not finite as permanent, and cap one from 36501 up to that at 36500, with arecovery_bundle_ttl_clampedwarning naming the original value.put_note,write_noteandT3Database.putnow refuse attl_daysabove 36500 with aValueError, as they refuse zero;nx memory promotecaps a T2 entry's remaining TTL at 36500 (the MCPmemory_puttakes any integer) instead of failing on it, including an entry whose TTL is too large for date arithmetic (about 2.9 million days on, where it raisedOverflowError).nx store importstill copies a chunk row's metadata whole; every reader above tolerates what that can carry.- The
store_get_count_unreportedwarning names the fallback that path really has (nexus-njn1a, finding 7; client change, works on every engine). It always said "Degrading to the pre-count store_get_truncated_at_limit heuristic", which is true for the collection stub'sgetbut not for the paged reader the expiry sweep guard and the recovery export use, which applies no heuristic. That path now says it has none, so a dropped row cannot be seen on an engine that sends nocount. Each of the two messages is logged once per process, so whichever path runs first no longer decides the text the other shows. nx index rdr <dir>on a directory outside any git repo now leavesnx enrich aspectssomething to attribute (nexus-njn1a, finding 12; client change, works on every engine). The documents registered under a curator owner, which has norepo_root, with a path relative to the directory and nosource_uri, sonx enrich aspectsloggedaspect_uri_for_relative_without_rootand skipped every one asunattributable_identity. Such a document now registers by its absolute path, asnx index mdandnx index pdfregister theirs, and the engine derives itsfile://<absolute path>source_uri(the raw path, not a percent-encoded one, so a directory with a space or a non-ASCII character matches what the engine and the staleness check derive). Two non-git directories with the same name, which shared one curator owner and so one document per relative path, now get a document each. A document an earlier release registered by its relative path is adopted on the nextnx index rdrrun and given itssource_uri, unless it holds a different one whose path still exists (another directory's): then it is left alone with acurator_legacy_document_not_adoptedwarning that names the path it holds, and the file gets a new document. A non-git document's identity is its location, as fornx index md: a document registered by absolute path whose directory later moves is a new document at the new path, and the old one is left fornx catalog reconcile-stale. The one exception is a legacy relative-path document whose recorded path no longer exists, which the moved directory adopts and re-stamps with its newsource_uriinstead of leaving it behind. A non-git directory that later becomes a git repo keeps its document. Thesource_uristamp on an existing document is its own advisory write, so a second document already holding that URI no longer stops the document'schunk_countandindexed_atfrom updating. Inside a git repo nothing changes.