Skip to content

v7.85.0

Choose a tag to compare

@github-actions github-actions released this 23 Oct 19:32
· 575 commits to main since this release
270c6da

Pairs with engine-service-v0.1.164 (REQUIRED_ENGINE_VERSION 0.1.163 to 0.1.164), deployed to the managed service before this tag (2026-10-23 10:32Z; every wire entry is additive). A local install's first start on this engine applies three changesets: vectors-037-1 and vectors-037-2 (the reembed_jobs and reembed_job_moves tables of the RDR-210 re-embed job, FORCE RLS) and memory-005-1 (a partial index on nexus.memory, the table not rewritten). No data-effecting changeset.

Added

  • nx doctor has a "Re-embed jobs" row, and the "Collections off-profile" row names the right remedy and says what it could not judge (nexus-s08oe, RDR-210 Phase 6 Step 5; client half, the engine tag and REQUIRED_ENGINE_VERSION are paired at the release). The new row reads the job table through the status route in local mode only. It is not applicable and green on a box that is not in local mode, when the table is empty or unreadable, and when nothing in it is running or failed; it is green with counts by state for running, planned, cutover and paused jobs; it is a soft warning for a failed job, naming its reason, its last error and the command that moves it on (--resume JOB; --resume JOB --token-cap N after token_cap; --abort JOB, then --all, after profile_changed, which a resume would only repeat), and for a job parked in a blocked cutover, which is offered only --abort (the engine has no resume out of cutover, and refuses the abort too once the delete step has started, which the row then says). The off-profile row now suggests nx collection re-embed --all in local mode (nx collection reindex <name> elsewhere, where the job does not run), and a kept_tombstoned source with a live document restored into it is named as a superseded source that re-embed --all moves, not as a collection whose reads route by its own model. When the job table or the document counts cannot be read, the row says superseded sources not judged (job table unread) or (document counts unread), whichever failed, instead of reading a clean none over the superseded rows it skipped. A stale collection a running or failed job already holds is named with that job and the row points at --status, since --all names the job and plans nothing for it. One difference from the RDR's Day 2 table stays: after purge-trash on a kept_tombstoned source the doctor is clear while --all still lists a delete-only cutover re-entry.
  • nx collection re-embed --all --skip NAME leaves a collection out of the run (nexus-kyzmw). --all plans every stale collection and starts none when any plan is refused, so one collection refused for good (a held target name, a target collision) made --all unusable. --skip is repeatable and goes with --all only; a skipped collection is not planned and is listed as NAME: skipped (--skip), and a NAME that is not one of --all's candidates (stale, held by a job, or a re-entry) is an error that names them. Without --skip the run is still all-or-nothing.
  • nx collection re-embed runs the cross-model re-embed job: --all, a cross-model --to, --keep-source, --force-delete, --token-cap, and --status, --pause, --resume, --abort (nexus-cnxjk, RDR-210 Phase 6 Step 3; local engine only, refused in any other mode). --all plans every live collection whose model differs from the engine's profile for its content type; NAME --to MODEL with a model other than the collection's own does the same for one collection, and MODEL must be the profile's (bge-base-en-v15-768 is now a choice). Dry run stays the default and states the documents and chunks that move, the token estimate against the cap (10,000,000 for a Voyage target, none for the on-device bge model), and the counts the source's delete would destroy: quarantine rows, topic assignments of other collections and topic links that hang off its topics, and satellite rows naming it whose document does not move. Every collection is planned before any is started, and a plan above the cap, a target model the engine cannot serve (/version embedding_models; the refusal names the missing key or bge files and the restart, nexus-q6t89), or any other refusal starts none. A source another job holds is named, not planned again; --all lists a kept_tombstoned source as a re-entry once its tombstones are gone, and does not start it until the engine's re-entry route lands (nexus-09iho), and never lists a kept_by_flag or kept_residue source. --wait is not implemented yet. Engine half: the /v1/vectors/reembed plan and status routes (below); rides the next engine tag.
  • A re-embed start with no token_cap is capped at 10,000,000 tokens when its target is a Voyage model, and the plan reports the cap and the live documents homed at the source whose chunks sit elsewhere (nexus-8zzjh, nexus-bi56e; rides the next engine tag, local engine only). RDR-210 Cost says each run has a cap of 10 million by default; until now only the nx client sent one, so a start made with curl or another client on a billed target ran uncapped. The engine now applies the default when the request sends none, unless the target model is a local one (bge, MiniLM), which stays uncapped; a model of any other family is treated as billed. A value sent is kept as sent, higher or lower, and a start whose plan's token estimate is above the cap is refused with a 409 before anything is written. POST /plan gains token_cap (what a start with no cap would apply, null for a local target) and live_homed_elsewhere, a count of the residue the cutover already held the delete for (a live document homed at the source whose manifest names only other collections), so a source can no longer plan clean and then stall in cutover. nx collection re-embed prints that count in place of the line that said the plan did not predict it.
  • The engine serves POST /v1/vectors/reembed/plan and GET /v1/vectors/reembed/status (nexus-cnxjk; rides the next engine tag, local engine only). The plan repeats every refusal a start makes, writes nothing, and counts what the source's delete would remove; the status lists every job row of the tenant, newest first. HttpVectorClient gains reembed_plan, reembed_start, reembed_status, reembed_pause, reembed_resume, reembed_abort and served_embedding_models, each sending exactly the fields its route takes (the engine refuses an unknown one). All four route groups are [additive] entries in docs/wire-contract-pending.md.
  • nx config set / nx config unset and nx daemon service start name nx collection re-embed --all when collections are on the previous embedding model (nexus-cnxjk, RDR-210 Phase 6 Step 3). Setting or unsetting local.embed_model or voyage_api_key prints the line after the restart line when the change flips the embedding profile (a Voyage key set beside a recorded bge choice does not). nx daemon service start prints it when a live collection is off the engine's profile, read once from the catalog with a 5 second bound: after the running line in the plain output, on stderr under --announce-stdout (stdout stays one JSON object), and from the supervisor under --foreground.
  • A re-embed job ends by what its source still holds: deleted, kept_tombstoned, kept_by_flag, kept_residue, or failed with parity_mismatch (nexus-09iho, nexus-6grho, RDR-210 Phase 6 Step 4; engine change, rides engine tag engine-service-v0.1.164, local engine only). After the pass the cutover looks for a live document or a manifest row still naming the source before it supersedes, then, in the RDR's order: a soft-deleted document still in the source ends the job done/kept_tombstoned (source superseded and kept, no parity check); --keep-source ends kept_by_flag; the parity check fails the job failed/parity_mismatch with the counts and the documents to inspect in last_error when a manifest row still names the source, a live document is homed there, or a moved document is not on the target by both columns, and deletes nothing (--resume re-runs the pass and the check and fails the same way while the cause stands, --abort leaves the source superseded and undeleted); aspect, queue or highlight rows naming the source that match no moved document end done/kept_residue unless the job was started with --force-delete, which now lifts that refusal (nothing read it before) and deletes them with the source. --force-delete does not reach the parity check: no flag deletes a live document's only copy. A live document that arrives in a source the job has already superseded (after a parity failure and a resume, or a re-entry) sends the job back to running to move it, instead of ending done_with_skips with the document stranded in a source nothing routes to; only a document the job already tried and could not move ends done_with_skips, and the job says where it is. The parity check and the trash count run again immediately before the delete, past the delete_started marker too, so a document registered at the source since (the engine refuses chunk and manifest writes there but not a bare registration) fails the job parity_mismatch instead of dying with the source. The --force-delete note is written with the marker and the delete's removed counts after it, so a restart keeps them. This replaces Step 2's interim behaviour of waiting in cutover with a blocked message.
  • A kept_tombstoned job re-enters through the start route, and nx collection re-embed --all --no-dry-run starts the re-entries it lists (nexus-09iho; rides engine tag engine-service-v0.1.164). A start whose source is the kept source of a done/kept_tombstoned job takes that job back to running (a document was restored) or cutover (the trash was purged) with its own target, cap and flags; the body is the source alone. Until now --all listed these sources and could not start them.
  • nx collection re-embed --wait follows the jobs it started (or --resume JOB) to done, done_with_skips, aborted, failed or paused, names each state, and runs taxonomy discover for the target of the first two (nexus-09iho, RDR-210 Phase 6 Step 4). It exits 1 when a job stopped short (aborted, failed, paused, or parked in a blocked cutover, which it reports with the job's last error instead of polling forever) or when the discover failed. A re-entry takes the same rename and cross-job guards as a start. Without --wait, the start and --status name nx taxonomy discover --collection TARGET for a finished job: until it has run, topic-scoped search and topic boosts miss the moved documents.
  • nx collection re-embed --status shows what a job's source delete removes: quarantine rows, topic assignments of other collections, topic links, and unmatched satellite rows (nexus-09iho; GET /v1/vectors/reembed/status?counts=true, rides engine tag engine-service-v0.1.164). The counts are read while the delete is still ahead of the job, or the source was kept, and are the ones the delete removes.

Changed

  • nx t3 gc no longer probes for quarantine siblings, and a first expire page that names none is an exit 1 (nexus-6bk7i, follow-up to nexus-wbfpw.64). The dry-run quarantine-restore probe was kept only for an engine from before engine-service-v0.1.163, which is now the oldest engine the client supports (REQUIRED_ENGINE_VERSION), so the probe path, resolve_quarantine_siblings(probe_engine=) and siblings_after_first_page(probe_fallback=) are gone. A first page that answers neither quarantine_collections nor sibling_resolution comes from an engine below that floor (a managed endpoint below it is refused before any request, but a local engine not yet restarted after a client upgrade still answers) or from a defect: both nx t3 gc and nx index repo go on with the row-derived name and quarantine-<collection>, the index path logs quarantine_sibling_resolution_unanswered, and nx t3 gc expires from those two, then exits 1 saying the siblings were not named and naming nx doctor and nx daemon restart-stale. A first page that committed with its answer lost is now also an exit 1 after the same expiry; it probed and exited 0 before (re-run to ask again). Fewer siblings are visited then, never more, so nothing is expired that a resolved list would not also reach. A timed-out lookup is unchanged and still exits 0: the engine did answer, and the verb says another sibling may wait for the next run. The engine's quarantine-restore route and its dry_run field stay (nx t3 quarantine restore --dry-run). docs/operations/engine-reaper.md and docs/cli-reference.md say the same.
  • nx doctor's "Collections off-profile" row, the re-embed hint and re-embed --all share one off-profile rule, so the row and the hint clear after re-embed --keep-source (nexus-n6mh0, RDR-210 Phase 6 Step 5). The doctor counted every live collection on a model other than the profile's, including a superseded source --keep-source kept, so its row stayed lit after the documents had moved and the hint (which already left superseded rows out) disagreed with it. Both now read reembed_job.off_profile_rows, which --all shares through candidates(): a live, unsuperseded collection on another model is off-profile, and a superseded one is not, with the RDR's one exception, a source a kept_tombstoned job left superseded that holds a live document again (a restore; nothing serves it until --all moves it). A kept_by_flag or kept_residue source, a kept_tombstoned source whose documents are only tombstones, and a rename's tombstone no longer count. One deliberate difference remains: a kept_tombstoned source with no document row at all (purged) is still listed by --all as a delete-only cutover re-entry, while the row and the hint stay clear, since nothing in it is stranded. The job table is read only when some collection is superseded; if it cannot be read, the superseded rows are left out and the rest still count. The row's remedy is now re-embed --all where it applies (nexus-s08oe, below).
  • --lexical search runs its lexical leg on /hybrid-search again; the per-collection route's lexical field is sent only with NX_SEARCH_ROUTE_LEXICAL=1 (nexus-n51v1). With 7.84.0 and engine-service-v0.1.163, the field made the engine run one lexical statement per collection, 0.3 to 0.65 s each, and a --lexical search over 118 collections took a median of 11.99 s, against 6.90 s for the batched /hybrid-search leg (nexus-pq3wr, measured live 2026-10-22). The default gives back what the field bought: the leg is again one /hybrid-search call per batch of collections, and a collection with many text matches can again crowd its batch-mates' lexical rows out. A search without --lexical is unchanged. NX_SEARCH_ROUTE_LEXICAL (1, true, on, yes) sends the field, as 7.84.0 did.
  • The engine's RDR-225 lock preflight counts the lock table Postgres guarantees (every backend slot), not max_connections alone (nexus-xzit5, rides the engine tag after engine-service-v0.1.163). At the default max_locks_per_transaction of 64 the walk is refused above about 48 tenants instead of 39, and at the managed 128 above about 99 instead of 81.

Fixed

  • The re-embed plan and start refuse, naming the missing piece, when the engine cannot embed for the target model (nexus-q6t89, RDR-210 Phase 6 Step 3; engine change, rides engine tag engine-service-v0.1.164, local engine only). A degraded boot serves one family (a voyage profile with no key, a bge profile with no model files), and a job started into the other family registered an empty target, then failed at its first batch. POST /v1/vectors/reembed/plan and /jobs now read the models the embedder router serves (the list GET /version reports as embedding_models) before the target is looked at, and answer 409 with the same sentence the client's /version check prints (the missing Voyage key or bge files, and the restart), before anything is registered. A router that cannot answer is refused, not read as serving everything. A job that later fails its first batch for another reason still leaves its empty registered target, as before; --abort or --resume handles it. The client's check was already in place (nexus-cnxjk).
  • A rejected T1 session token is remembered across the process, so tuple_session_header_dropped logs once, not on every channel-waiter tick (nexus-z1xox, follow-up to nexus-ztwp4). 7.84.0 kept the rejected-token memo on the HttpTupleStore instance, but the channel waiter and every MCP tool open a fresh T2Database (hence a fresh store) per call, so the memo never survived one: a server whose NX_T1_SESSION the engine rejects re-sent the header, paid the 401-then-retry round trip and logged at INFO on every ~38 s waiter tick (261 times in one afternoon). The memo is now process-wide, scoped by endpoint and data token so one tenant's rejection never suppresses another tenant's valid token, and it lapses after 5 minutes, when the token is offered once more. It holds hashes only, never a token, and a changed NX_T1_SESSION is sent at once. The event also carries pid, so a log line names the process behind it.
  • A collection rename refuses a conformant new name whose model token disagrees with the source collection's catalog row, and the indexer keeps a legacy collection whose row model is not the write model (nexus-7f4o4). The engine rename keeps the source row's embedding_model, so nx collection rename and nx catalog rename-collection could leave a name claiming a model the vectors were not in: nx collection rename compared model tokens derived from the two names, so a legacy bge-row collection renamed to a voyage-context-3 token passed and the correct bge rename was the one refused, and nx catalog rename-collection checked only that the new name is conformant. Both now compare the new token with the row's model and refuse a mismatch, naming both models and the name that would be accepted. nx catalog rename-collection reads the model from the engine row it has already fetched, not from the stats-view cache, so a failed listing cannot let a mismatch through; with no row, or a row with no model, its fallback reads only a conformant token's own model, so a legacy name still renames as before. nx collection rename falls back to the names' derived models when no row backs the old name, as before. A disputed source (its bytes disagree with its row) is not compared and prints a warning on both verbs. nx collection rename --force-prefix-change gets past the guard on that verb; it renames and never changes the row, so it is for a rename the operator knows is right. nx catalog rename-collection has no override of its own. The first-index Phase-4 migration is unchanged when the legacy row's model is the model the indexer writes (or no row backs it). When the row names a different model it no longer renames: the legacy collection stays the indexing target on every run, and each run logs phase4_migration_row_model_differs_from_write_model naming both models; the way to move it is a re-embed to the write model. Renaming it to the write model's token would claim vectors it does not hold, and renaming it to the row's token would strand it, because later runs mint from the write model. The row is read from the catalog the migration already holds, and a failed read keeps the legacy name for that run rather than renaming blind. Only the rename case is guarded: a legacy collection beside an existing write-model sibling still resolves to the sibling, as before. nx collection rename still reads the row from the stats-view cache, so a failed listing there falls back to the name comparison it used before. Client only.
  • A session's name stays in the peer directory across a restart, --resume or /clear, so mailbox_send(to=<name>) resolves without the model re-subscribing (nexus-u839f). A name was held only after the model called tuple_subscribe("mailbox/<name>"), and nothing re-armed it, so after a coordinated restart every name answered "no live holder" until each session happened to subscribe again. The MCP server now reads the name from Claude Code's own session registry (<config>/sessions/*.json, the live interactive entry whose sessionId is this server's session id, newest updatedAt first) when it starts its channel waiter: at server start, after a deferred T1 mint and after a /clear handoff for the new id. It leases directory/<name> through the ordinary lease. The registry read is local file I/O on the calling thread; the first engine write is the lease thread's first tick, so a down engine is that thread's 5-second retry and never a stall of the server's event loop. The lease thread then re-reads the registry at most once every 60 seconds, and swaps the row (releases the old name, arms the new) when the name changed, so a /rename, a /clear whose registry update lagged, or a startup miss is picked up without a second thread. A miss, or a name that is not a safe address name (one with a space, say), keeps what is held. A miss on a registry directory that exists logs one INFO line, session_name_unresolved (session id, directory, number of files read; never their contents), which is also how a Claude Code format change shows; a missing directory logs nothing, and an unsafe name logs at DEBUG. An explicit tuple_subscribe of the same name changes nothing (the SessionStart line asks every session to make that call, and the registry is still followed); of another name it replaces the registry-derived one; an explicit name is never replaced by the registry. A name another live session still holds is refused at send time as before. Hosts without Claude Code's registry still name themselves with tuple_subscribe.
  • A stored note is split to the token window of the model its catalog row names, not the model its collection name derives (nexus-z8mi2, RDR-210 follow-up to nexus-noayy and nexus-ml9vu). store_put, nx store put, nx memory promote and the recovery import chose the split window from the collection name. A legacy name carries no model token, so on a bge-profile engine a collection whose row says voyage-context-3 was cut to bge's 512-token window instead of Voyage's none. The row's embedding_model now decides, the name only when no row backs it or the row has no model; that covers a conformant name too, whose token a rename can leave disagreeing with the row the engine embeds by. The row comes from the shared collections cache, which a cold or stale cache fills with one catalog-only listing, so a fresh nx store put pays one listing request and a warm cache pays none. MCP store_put empties that cache after each committed note, so in a run of consecutive store_put calls every note after the first pays one listing. A listing that cannot be fetched also falls to the name.
  • A stored note's chunk metadata stamps the embedding model its catalog row names (nexus-ldh33, follow-up to nexus-z8mi2). Each chunk's embedding_model was derived from the collection name, so a legacy collection whose row says bge was stamped voyage-context-3 while the engine embedded it with bge. The stamp now comes from the same row read that picks the split window, at no extra request; with no row it follows the install's profile for a legacy name, as the window does. The engine embeds and searches by the row and never reads this field itself (a caller's where filter can still name it). Its only internal reader is the indexers' unchanged-content skip, which a note chunk reaches only when a file's hash equals the chunk's; nx doctor takes the model from the collection name, not the chunks. Existing chunks are left as stamped.
  • nx index md and nx index pdf (and nx collection reindex and nx dt, which call them) stamp, compare and split by the embedding model the collection's catalog row names (nexus-7jy5r, sibling of nexus-ldh33). Both took the model from the collection name, which guesses Voyage for a legacy name and keeps the source's token after a rename, so a collection whose row says bge got bge vectors stamped voyage-context-3, and the unchanged-content skip compared against that guess. The model now comes from the row, read through the indexer's own store (its tenant, the shared cache for the process's default client), with the name as the fallback. A store without a catalog (an in-memory double, a dry run's throwaway store) and local mode's own embedder are unchanged. A file whose chunks carry the old guess is re-indexed once on its next run, to the row's model. The same model picks the token window chunks are split to, so in a legacy collection whose row names bge that re-index also re-splits the file to bge's 512-token window (the Voyage guess left it unsplit, and bge truncated the long chunks). A non-default-tenant client now reads the row from the routing listing, which reads the catalog alone, instead of the full listing that counts every collection's chunks. nx index repo is unchanged: its names are conformant and it reads the token by design.
  • A collection-row read that fails no longer makes nx index md/pdf re-index unchanged files (nexus-s8r5o, follow-up to nexus-7jy5r). When the catalog listing could not be read, the indexer took the name's model as the target and the unchanged-content skip compared stored stamps against it, so a legacy or renamed collection whose row differs from the name re-indexed its files during the outage, re-stamped them with the guess, and re-indexed them again once the listing was back. The indexer now asks for the row strictly (mcp_infra.get_collection_row(strict=True), HttpVectorClient._resolve_collection_row(strict=True)), and when it cannot be read the skip judges by content hash alone and logs index_target_model_row_unreadable; a changed file is still written, with the name's model, and re-indexed once to the row's model on the next run that can read it. The strict read covers a client for a non-default tenant too. Every other caller keeps the non-strict read.
  • nx_answer and the other claude -p operators work on Claude Desktop after a plugin update (nexus-x6qc7, shakeout F19). Desktop starts its MCP servers in the plugin's install directory and deletes that directory when it updates the plugin, while the servers keep running there. Every claude -p the server spawned then inherited the deleted directory and exited 1 ("The current working directory was deleted"). The operator spawn now names its working directory: the caller's while it exists (a Claude Code server's project directory, as before), otherwise the filesystem root (the nexus-x3m8e stable cwd); an operator that reads project files (nx_enrich_beads, nx_plan_audit) is refused with a message instead, since it has no project to read. Before every tool call, both MCP servers also move themselves off a deleted working directory, so a subprocess a tool starts after that inherits the root; a directory deleted while a call is running is not covered. mcp_cwd_deleted_healed logs once per process.