Skip to content

v7.64.0

Choose a tag to compare

@github-actions github-actions released this 28 Sep 12:56
· 3569 commits to main since this release
b55ffe4

Pairs with engine-service-v0.1.137. That engine's reads show only chunks with a live owning document (RDR-192 Step 5, nexus-wbfpw.10), which older clients do not expect, so it deploys in step with this release rather than before it: conexus has it armed and flips it once this client is published, the live(c) census has been reviewed, and the gate corpus has been re-seeded through an owner-registering path. engine-service-v0.1.135 and v0.1.136 were tagged on the way and never deploy.

Added

  • nx catalog footnotes FILE.md — the in-place converter GH #896 asked for (nexus-sxiay, closes #896). Converts every [label](nx://catalog/<tumbler>) markdown link IN PLACE in the SOURCE file (not a .rendered.md sidecar — GitHub/GitLab/VS Code preview render the source) into a stable GFM footnote marker [label][^tumbler-<slug>] — the label stays bracketed, which is what makes --to-links exact regardless of what precedes or follows a citation on its line, or how many citations share one — with a ## Footnotes section carrying title, content type, indexed date, link/merge/outbound-link data per unique tumbler. Idempotent: an unchanged catalog state re-run is a byte-for-byte no-op (trailing newline, or its absence, and CRLF-vs-LF preserved exactly); a drifted one (title edit, merge) rewrites only the footnote bodies — markers, once assigned, never move. --check (CI/pre-commit gate, exits non-zero on any needed change or dangling reference), --dry-run (unified diff, always shows the full picture, writes nothing), --to-links (reverse conversion, no catalog access needed), --refresh (only refresh existing footnote bodies, add no new markers), --style long|short (footnote body verbosity). GH #896's own acceptance criterion is held literally: a tumbler that fails to resolve means NOTHING is written for that file — not even the parts that would have converted cleanly — every failure is still reported (never silently dropped), and the run exits 1. It ships as a thin /conexus:tumbler-footnotes skill too.

  • nx doctor --check-references samples reference-only catalog documents and stats each one's currently-recorded freshness (nexus-tb2yj, RDR-169 Gap 6 leg 3): the first production caller of stat_source/staleness_signal. Reports fresh/stale/dangling/unknown counts per scheme (https://, obsidian://, x-devonthink-item://, nx-scratch://, chroma://), names the stale and dangling documents (capped), and exits 1 on any stale or dangling result — unknown never fails the check alone. --references-sample (default 10) and --references-seed mirror --check-embeddings' sampling contract; up to 8 samples are stat'd concurrently sharing one httpx.Client, and both the flag's help text and a run's own first printed line state the worst-case wall time, derived from aspect_readers' HTTP retry constants and the actual concurrency. Not applicable on a box with no reference-only documents. nx catalog backfill-etags is the named remedy for a document stuck at unknown because it predates ETag capture.

  • https:// catalog references record their ETag at register/refresh time and compare it when no usable Last-Modified is present (nexus-0ne1m, nexus.aspect_readers.HTTPS_ETAG_META_KEY): _stat_https_uri now takes a recorded_etag, strips a leading W/ weak-validator prefix from both sides before comparing, and reads StatOk(etag_stale=...) off it instead of an unconditional StatFail naming the gap. capture_https_etag/record_https_etag do one best-effort HEAD (connect and read are separate timeout budgets, so worst case is ~5s, not ~3s; one attempt, never fails the caller) and merge the ETag into catalog_documents.metadata; wired into nx catalog register/update --source-uri (refused in combination with --owner/--search batch mode) and the register MCP tool, all documented as a caller-visible side effect. NX_REFERENCE_ETAG_CAPTURE=0 disables the HEAD entirely, checked in capture_https_etag itself so every caller (register/update and the bulk backfill-etags sweep below) shares the same guarantee.

  • nx catalog backfill-etags captures the ETag in bulk for https:// references registered before ETag capture shipped: --owner/--dry-run/--limit (default 500, bounded), idempotent (a document that already has an ETag is excluded from candidates on every run), one shared httpx.Client across the run, honors NX_REFERENCE_ETAG_CAPTURE=0 the same as register/update.

  • nx collection aspects <name> [--enable|--disable] and nx collection aspects --from-config [--dry-run] (nexus-l46pu, follow-up to nexus-kk4ut, pairs with the next engine-service tag). The docs__ aspect-extraction opt-in moves off each machine's local aspects.docs_collections config.yml onto a new tenant-wide catalog_collections.aspects_enabled engine row (BOOLEAN NULL, no default), so two machines indexing the same shared collection make the same decision instead of giving partial document_aspects coverage. The engine is AUTHORITATIVE the moment it has an opinion — an explicit true/false written by one of these verbs (round-2 critic decision): the local config list is consulted only when the engine's attribute is NULL, meaning nobody has ever set it (a fresh install, an old engine, or an ordinary untouched row on a current one — all the same fallback). --from-config syncs every locally-matched collection onto the engine in one pass. --enable/--disable and a changing --from-config refuse a non-docs__ collection (also a 400 at the engine), print the tenant-wide blast radius and LLM-call cost before writing, and prompt for confirmation unless --yes/-y is given (--dry-run never prompts); the engine logs a structured audit event on every write (tenant, collection, new value). nx doctor's default sweep gains a docs-aspects-config row (N/A with an empty local list) warning when a local match still needs syncing. Nothing changes until an operator runs one of these — every existing row is NULL ("no opinion"), the same "not opted in" behaviour as today.

  • Windows subprocess trees are now contained by a Job Object (nexus-6y4e0), via nexus.util.win_job (ctypes only, no pywin32) and nexus.util.process_group.contain/kill_tree. start_new_session=True is silently ignored on Windows, so a timed-out subprocess there previously killed only the direct child and every grandchild survived; the four same-process spawn-then-kill sites that actually reach a Windows client (bounded_subprocess.run_bounded, aspect_extractor._run_claude_isolated, the MinerU per-batch worker in pdf_extractor, and operators.dispatch.claude_dispatch's timeout path) now assign the child to a job with JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE right after spawn, so closing the job reaches the whole tree. POSIX behaviour is unchanged (contain/kill_tree are no-ops there — the process group already contains the tree). Five further spawn sites were surveyed and left unwired, each for a stated reason: two are killed by pid from a wholly separate later CLI invocation (the mineru server and the storage-service supervisor spawn), and an in-process job handle cannot cross that boundary without a named job, a bigger design left for later; the storage-service supervisor class itself calls os.getuid() at construction and does not run on native Windows at all (RDR-218's Windows story is a WSL2 appliance); nx index's background spawn is fire-and-forget, with nothing that ever kills it; and the aspect worker daemon is retired by lease fencing, not by a kill. Measured on native Windows 11 (CPython 3.12.13 against the real kernel32): with a child that spawns three grandchildren, develop left all three running after every kill and every clean exit (120 of 120 across the four sites' timeout and success paths), and containment left none (0 of 120); closing the job on the success path also ends a grandchild the child left behind. On Windows, the uncontained run_bounded also overran its timeout by the 5 s reap drain, because orphaned grandchildren held its output pipe open. Only run_bounded ran as the real function; the other three sites were reproduced in shape around the same primitives. Not covered: real claude/node process trees, a native executable that spawns before contain() assigns it (the child is not started suspended, so that race is open), and a crash of the nexus process itself.

  • safe_killpg no longer raises AttributeError on Windows, nx-session-end-launcher has a Windows fast path, and start_new_session=True degrades honestly there (nexus-34f7r). os.killpg/os.getpgid/signal.SIGKILL do not exist on Windows; process_group.py now imports and degrades cleanly, routing a Windows kill through the single surviving process (or, after nexus-6y4e0, through a job object when one was assigned at spawn). The SessionEnd hook spawns a detached child (DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP, with a breakaway-from-job retry) instead of running cleanup inline, closing the "Hook cancelled" timeout Windows previously reported. Landed on develop in an earlier session (6aa5990, 5276887, e1a0191).

    The bead's closing bar was exercised live on native Windows 11 (qwentescence, 2026-09-27; T2 nexus/nexus-34f7r-live-windows-closing-bar-2026-09-27) and found the fast path necessary but not sufficient: with no service reachable, Claude Code still cancelled the hook at ~1.6s, because the launcher's PARENT ran _print_service_tier_summary() — a network read — AFTER the detached-child spawn, taking up to ~2.4s whenever CLAUDE_CODE_SESSION_ID is set (always, under Claude Code). The resulting "Hook cancelled" kill walked the process tree and took the detached child down with it, even though its CREATE_BREAKAWAY_FROM_JOB spawn had succeeded — with the service answering at once the hook instead completed in 356ms and the child outlived claude.exe by 52s. Fixed: main()'s no-fork branch never calls the tier summary now, in either the successful-spawn or inline-fallback case, so the parent does no network work after a successful spawn (no tier summary line at SessionEnd on native Windows — POSIX is unaffected). Two further leaks the same run caught: neither the POSIX grandchild nor the Windows detached child ever called configure_logging, so no session_end log line could land in <config>/logs/hook.log on any platform (_run_session_end_synchronously now calls nexus._hook_runtime._io.configure_hook_logging() first); and the launcher parent's own _print_service_tier_summary exception handler logged through the ambient, unconfigured structlog.get_logger(), which falls through to stdout — the hook's protocol channel (Claude Code logged "Hook output does not start with {"). That handler now uses a dedicated stderr-only emitter (_emit_parent_debug) instead. The no-breakaway spawn fallback arm was not exercised in the live run (breakaway succeeded every time). This fix has not yet been re-verified live on Windows; nexus-34f7r stays open until its closing bar is re-run there.

  • nx doc render and nx doc validate resolve nx://catalog/<tumbler> links (nexus-sevlu, refs #896). Render appends a Catalog References footnote block with each linked document's title, content type, owner and a link; a merged duplicate resolves to the surviving document; a machine-local file:// source or an absolute path is never emitted. Validate fails on every line citing a tumbler that no longer resolves, and exits 2 when the catalog is unreachable.

Fixed

  • The catalog link-safety helper (nx doc render, nx catalog footnotes) is now one shared, hardened function (nexus-w715w, follow-up review on nexus-sevlu's landed fix). safe_link_target is the single place in the codebase allowed to compose a markdown link from entry.source_uri/entry.file_path (mechanically enforced by a lint test, including a getattr(entry, "source_uri"/"file_path", ...) bypass form). Hardening fixes: the file:// scheme check is now case- and slash-form-insensitive via urlsplit plus an explicit link-scheme allowlist (FILE:///..., File:///... and file:/Users/... all previously slipped past a bare .startswith("file://"); nx-scratch was removed from the allowlist too — T1 scratch is session-scoped and unresolvable by any later reader); the "is this path safe to treat as repo-relative" check is now content-based rather than Path.is_absolute() (host-OS-native — a Windows drive-letter path, a UNC path, ~, or an upward-escaping .. sequence all read as "relative" on a POSIX host), and percent-decodes before classifying so %2e%2e%2f can't hide a traversal. A safe, repo-relative file_path now becomes a WORKING relative link — resolved against the citing file's own directory via the entry owner's repo_root — only when the citing file is actually inside that repo; outside it (or with repo_root unknown), it falls back to the repo-relative string emitted as-is, which only happened to resolve when the citing file sat at the repo root.

  • nx catalog footnotes reads its source file as strict UTF-8 and writes atomically (nexus-3ioz2, critical). The prior errors="replace" read silently corrupted non-UTF-8 input and could write that corruption back over the original; invalid UTF-8 bytes are now refused outright (exit 2, nothing written, byte-identical file afterward) rather than "fixed" lossily. Writes go through a sibling .tmp file plus an atomic rename (mirrors nexus.commands.t3._save_backfill_state's pattern), so a crash mid-write can never leave the file half-written. GH #896's own "must not partially write the file" criterion is now held literally: any dangling reference — in either conversion direction — means nothing is written for that file at all, not just the parts that failed to resolve; every failure is still reported and the run exits 1.

  • nexus.doc._common's shared markdown scanner now skips inline backtick code spans and 4-space-indented code blocks, not just triple-fenced blocks (nexus-3ioz2, follow-up review). A docs page explaining [label](nx://catalog/<tumbler>) syntax almost certainly quotes it inside inline backticks — this repo's own docs/cli-reference.md does — and the old scanner treated that literal example as a real citation. Every consumer of iter_plain_lines (catalog-link and chash-citation scanning, so nx doc render/validate too) benefits from the same fix, made once in the shared module.

  • REGRESSION in the same scanner, fixed one round later: the first cut of the indented-code heuristic wrongly treated a blank-line-preceded indented line as code even when it was a LIST ITEM's own continuation — a further paragraph, a table, or a nested bullet indented to match the item's content column, separated by an ordinary CommonMark blank line. nx doc render/validate silently stopped resolving citations and tokens inside these shapes; real cases in this repo's own docs are named in the fix's commit. iter_plain_lines now tracks open list context (a stack of each open level's content-indent) so a line still inside any open list's content zone is never misread as code, regardless of a preceding blank line. A full corpus diff over every *.md under docs/ (394 files) against the pre-fix baseline shows exactly 16 disappeared lines, every one independently confirmed to be genuinely indented code with no list context, and zero unexpected additions.

  • nx catalog footnotes's atomic write preserves the original file's mode and cleans up its .tmp file on any failure. Path.replace() renames the tmp file's OWN inode into place, so without an explicit mode copy a 0600 source file silently loosened to whatever umask gave the freshly-created tmp file (typically 0644); the mode is now copied onto the tmp file before the replace. Any failure between creating the tmp file and the replace succeeding now removes the tmp file and re-raises, rather than leaving a stray .tmp behind.

  • Engine (next engine-service tag): a bulk owner-agnostic file_path lookup backs nx index repo's batched cross-owner mint announcement (nexus-1vc0n). nexus-yzij1 closed the cross-owner path-duplication class for every per-document catalog writer, but left the batched nx index repo registrar out on purpose: wiring its existing per-document announce_cross_owner_mint there would cost one owner-agnostic /list?file_path= per file inside a register_many loop, turning one round trip into N+1. New engine route POST /v1/catalog/list_by_file_paths (CatalogRepository.documentsByFilePaths, partial index idx_catalog_documents_file_path_live) answers every live document for N file_paths in one call, made once per register_many page, immediately before it; HttpCatalogClient.find_all_by_file_paths pages it at 300 paths (QUOTAS.MAX_RECORDS_PER_WRITE) and degrades to the existing per-path lookup against an engine that predates the route. indexer._catalog_hook now runs one bulk lookup per register_many page, immediately before it, and feeds announce_cross_owner_mint/announce_cross_owner_resolve from it, so a cross-owner mint or resolve in a batched run is reported exactly like the per-file writers already do, at one extra round trip per page instead of one per file.

  • A board subscription starts at now and folds a wait's posts into one notification (nexus-zxthy). A fresh tuple_subscribe to a busy topic replayed every retained post as its own notification, 282 in 80 seconds on 2026-09-27, because the engine's per-subscriber stamp had no start position and the waiter asked for one row per wait. The subscription set now records the subscribe time per topic and restores it on /resume; the waiter asks for up to 100 rows per wait, drops rows created before the subscribe time (with a 30 second margin for clock skew between the engine and the client), and sends one notification per topic per wait naming the count, the first and last tuple id and the tuple_rd call that reads them; after board activity it settles 3 seconds so a cluster lands in one wait. The notification content stays identifiers only; the batch's ids, kinds and senders ride in the notification meta. tuple_subscriptions shows each board entry's since, and the channel status carries board_batches and board_backlog_dropped. Posts older than the subscription are read with tuple_rd, never pushed; a subscription persisted before this release has no start position and keeps delivering everything. A resumed session keeps its original start position, so a long absence comes back as folded batches of at most 100 posts each, the way a durable consumer replays its own gap. This is still the LIVE mechanism until the client's pinned engine carries the start-position change (engine-service-v0.1.137; see docs/wire-contract-pending.md) — see the pairing below.

  • Engine + client: a board subscriber's start position is now honoured by the engine itself, so a fresh subscription never walks the topic's backlog at all (nexus-n36sw, follow-up to nexus-zxthy). The fix above still dropped excluded rows client-side, after the engine had already selected and stamped them for this subscriber — a few extra waits per fresh subscription, no notifications, but still a walk. A per-subscriber announce spec (the board shape) may now also carry since; TupleRepository.queryOnceAnnounceSubscriber folds it into its own candidate predicate as a bare created_at > since timestamp compare (never the (created_at, id) row compare rd's cursor uses — a board subscriber's watermark is a subscribe time with no id to pair it with), so a row at or before the watermark is never selected, stamped, or returned. since alongside a row-level (mailbox) announce is still refused, unchanged. The waiter sends the subscribe time, widened backwards by the same 30 second clock-skew margin the old client-side compare applied. An engine predating this bead refuses since alongside any announce; the waiter detects that refusal on first contact, falls back to the pre-nexus-n36sw client-side drop for the rest of its life, and logs once, so a new client against an old engine still works. channel_waiter status() gains board_since_supported. Requires the next engine-service tag; see docs/wire-contract-pending.md.

  • nx collection re-embed works on a quarantine sibling (nexus-ny7j4). The write path's registration step derived registration fields from the collection name, and a quarantine-<type> first segment is not a content type, so the command refused with unknown content_type 'quarantine-code'. The client never registers a quarantine sibling (the engine does, from the origin's row), so the step now skips it.

  • nx doctor's taxonomy.discover health row reconciles against the engine's own discover stamp (nexus-l3dg2, the du6d0 residual). A best-effort T2 write failure right after a real engine-side discover success kept the row warning forever, because it only ever read the client-recorded outcome. The engine (next engine-service tag) gains a batched POST /v1/taxonomy/meta/last_discover_batch (cap 300 collections; taxonomy_meta's existing primary key answers the lookup, no new index or changeset); the client's HttpTaxonomyStore.get_last_discover_stamps reads it in one round trip per 300-collection page for every collection the row is about to warn on, and a collection whose engine last_discover_at is newer than the recorded failure is dropped from the warning. Fully additive: an engine that predates the route 404s and the row degrades to its prior behaviour, with one "(engine reconcile unavailable)" note.

  • The last two direct register_collection callers now route through the registration seam (nexus-aotql): the RDR-103 post-rename registration in nx index repo's legacy-collection migration, and nx catalog migrate-fallback's registration loop. Two behaviour changes fall out of that: a register-time 409 (another process won the race to register the same name) is now tolerated as already-registered instead of propagating as a crash, and an embedding-profile mismatch (the engine's profile still reflects an older local.embed_model/voyage_api_key decision) now fails early with a ClickException naming the collection, the engine's actual profile model, and the remedy (--target-model <profile model>, or a service restart), before any document is re-pointed — rather than surfacing later as a confusing write-time 422 or an escaped raw exception mid-loop.

  • .nxexp import registers an owner document before storing chunks, so an import stays live under RDR-192 Step 5's live(c) predicate (nexus-wbfpw.31). Export now resolves each live chunk's own catalog document (source URI, title, content type, manifest position) and carries it as an optional owner record field; import groups chunks by that identity across the whole file, registers or reconciles the document once every batch is upserted, and writes its manifest explicitly with the recorded positions — never through the per-batch manifest hook, whose position numbering restarts on every 300-chunk batch and would corrupt a multi-batch document's manifest. A record with no owner (an older export, or a live-but-unmanifested chunk) is grouped under one document per import file instead, keyed by the target collection and file name, so re-importing the same file stays idempotent. Importing into a collection other than the one the documents live in (--collection) copies: the source collection's documents are left untouched and stay live, and the target gets its own documents under nxexp://<target>/<original source URI>. A legacy record carrying doc_id keeps that document when it is live in the target collection, and otherwise gets a new one per original document (nxexp://<target>/<file>#<doc_id>). --skip-existing still ends every record owned; if some owner documents cannot be registered, the rest still are and the import fails naming them. Export fails loud if the catalog cannot answer rather than silently writing owner-less records.

  • nx doctor gains a taxonomy.discover health row, and the "no files changed — skipping discovery" line names a failing collection (nexus-du6d0). qgc4b's self-heal guard re-runs taxonomy discovery on a no-change nx index repo run only while a collection has zero topics; once a collection has topics, a later discover failure (credential expiry, quota, schema drift) had no operator-visible signal on a repo whose files stopped changing. Every discover attempt now records its outcome in T2, and the doctor row warns on a collection whose most recent attempt failed, naming the collection, failure time, error class and the nx taxonomy discover --collection <name> remedy — visible independent of whether the repo indexes again. A collection deleted or renamed since a failed attempt is dropped from the row rather than warning forever.

  • Engine (engine-service-v0.1.137): quarantine restore no longer overwrites a live row's metadata (nexus-brxnp, catalog-043). gc_restore_rereferenced and its bounded sibling copied a quarantine row back onto its origin with ON CONFLICT DO UPDATE, so a chash the current run had already re-inserted live was overwritten with the older quarantine copy's metadata — and, since catalog-042, its created_at too. The copy now uses ON CONFLICT DO NOTHING and still deletes the quarantine row; each restore's gc_audit row records details.already_live (candidates skipped because the origin row was already live). The client's quarantine legs now sample up to 5000 chashes into that audit instead of 20.

  • Engine (engine-service-v0.1.137): a bounded gc_restore_rereferenced_bounded sweep (nexus-e8h5x, catalog-042), mirroring catalog-037's bounded quarantine-in sweep for the opposite direction. The unbounded restore ran in one transaction per (tenant, quarantine, origin); about 36,000 code__1-1 rows left quarantine in a single unbounded call on 2026-09-16, the same edge-deadline exposure catalog-037 fixed for quarantine-in after a 41,032-row call took 58s against the ~30s edge deadline. nexus.gc_restore_rereferenced_bounded(row_limit) restores at most row_limit rows per call, returns (restored, remaining), and writes one gc_audit row per batch; POST /v1/vectors/gc/restore-rereferenced takes an optional row_limit. The client now drives both bounded routes — restore, and catalog-037's quarantine route, which no client had ever called, so its 41k-row edge-cut exposure was still reachable — looping at a 2000-row default until drained, with an older-engine fallback to the unbounded call.

  • The marketplace plugin set is now derived from marketplace.json, not hand-kept lists (nexus-smsau, RDR-221 prerequisite). Adding a plugin to .claude-plugin/marketplace.json previously had to be echoed by hand into three places — plugin_lockstep.py's PLUGINS tuple, the stdlib version_lockstep_hook.py's own copy, and routing_stats.py's fixed loop — so a plugin present only in the marketplace silently fell out of lockstep and routing-stats checks. nexus.plugin_registry.known_plugins() now derives the set from marketplace.json (env override, dev checkout, or installed clone, requiring the nexus-plugins name; a fallback is logged), and plugin_lockstep, routing_stats, and the stdlib hook all read it instead of their own copy. A fourth plugin added to a copy of the marketplace with no other edit now fails at least one test naming each site that still needed it.

  • A subagent's RDR-205 ledger REPORT tuple can no longer be dropped by claude -p teardown (nexus-egm7p, nexus-wgalh). The RDR-205 tuple-projection hooks (hook_subagent_start_tuple, hook_subagent_stop_tuple) shared the MCP-outage drop class nexus-5l8i8 fixed for the RDR-184 ledger — they were still mcp_tool entries depending on a live MCP connection — and once moved to command-tier nx-hook verbs (subagent-start-tuple, subagent-stop-tuple) they still ran async:true, which Claude Code kills mid-flight at claude -p teardown, exactly the moment a subagent's SubagentStop fires. Both verbs now run synchronously (about 0.16s warm and 0.4s cold added latency per SubagentStart/SubagentStop; during an engine outage, two 5s posts plus interpreter start, under the hooks' 20s timeout). Along the way: the stop hook now passes agent_transcript_path so REPORT rows carry a real VERIFY field instead of always reading absent, and the VERIFY extractor reads SendMessage's actual field, message, not content.

Changed

  • Engine (next engine-service tag): search and get return only chunks with a live owner in their own collection (RDR-192 Step 5, nexus-wbfpw.10). Plain, hybrid and topic-scoped search, store_get, store_list, the /get family and nexus.live_chunks all use one predicate, live(c) (changeset vectors-019). A chunk with no manifest row in its collection, or whose only owners are trashed, is no longer returned; before, a manifest-less chunk stayed visible, and live_chunks counted a chunk as live when its owner was in another collection. Nothing is deleted. The collection listing is split from liveness: every collection that stores chunks keeps its row, count is the live count and a new stored_count counts every stored chunk, so a quarantine collection or one whose chunks are all unowned still lists, with count 0. existing_ids asks the engine for stored ids with the new store-get include_non_live field, so verify, skip-existing and the multi-piece store_put delete guard still see a stored chunk without an owner. The PDF-stub health check now reports only stubs with an owner, since a stub with none is hidden and is the RDR-192 reaper's to remove. Not additive: deploy this engine only after the client release that carries its client half and nexus-wbfpw.31 (.nxexp imports register an owner first), and after the RDR-192 census reads zero legacy-unmanifested chunks.
  • scripts/ci_status.py has a cancelled verdict and exit code 4 (nexus-lgx93). Every row of a run whose own run post is cancelled reads cancelled, the aggregator job that reports failure because its shards never reported included, so a run a newer push superseded no longer reads as failed. A cancelled job inside a run GitHub did not cancel was not superseded and still reads failed; one whose run has no post reads cancelled, since the run post is a separate delivery. A genuine failure or a pending job outranks a cancellation.