conexus 5.2.0__tar.gz → 5.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {conexus-5.2.0 → conexus-5.3.0}/PKG-INFO +1 -1
- {conexus-5.2.0 → conexus-5.3.0}/docs/rdr/README.md +5 -0
- {conexus-5.2.0 → conexus-5.3.0}/mcpb/pyproject.toml +2 -2
- {conexus-5.2.0 → conexus-5.3.0}/pyproject.toml +1 -1
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/aspect_worker.py +155 -95
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/commands/memory.py +42 -5
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/daemon/t2_client.py +6 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/daemon/t2_daemon.py +1 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/db/t2/__init__.py +50 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/db/t2/memory_store.py +86 -0
- {conexus-5.2.0 → conexus-5.3.0}/.beads/README.md +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/.gitignore +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/LICENSE +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/README.md +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/conexus/README.md +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/conexus/agents/_shared/README.md +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/conexus/daemon/com.nexus.t2.plist +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/conexus/daemon/com.nexus.t3.plist +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/conexus/daemon/nexus-t2.service +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/conexus/daemon/nexus-t3.service +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/conexus/hooks/scripts/routing/README.md +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/conexus/plans/builtin/abstract-themes.yml +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/conexus/plans/builtin/analyze-default.yml +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/conexus/plans/builtin/citation-traversal.yml +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/conexus/plans/builtin/debug-default.yml +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/conexus/plans/builtin/document-default.yml +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/conexus/plans/builtin/find-by-author.yml +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/conexus/plans/builtin/hybrid-factual-lookup.yml +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/conexus/plans/builtin/plan-author-default.yml +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/conexus/plans/builtin/plan-inspect-default.yml +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/conexus/plans/builtin/plan-inspect-dimensions.yml +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/conexus/plans/builtin/plan-promote-propose.yml +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/conexus/plans/builtin/research-default.yml +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/conexus/plans/builtin/review-default.yml +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/conexus/plans/builtin/traverse-then-generate.yml +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/conexus/plans/builtin/type-scoped-search.yml +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/conexus/plans/dimensions.yml +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/conexus/plans/purposes.yml +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/data/calibration/rdr-109/README.md +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/docs/README.md +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/docs/migration/README.md +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/dt/scripts/Index Current Group in nx.applescript +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/dt/scripts/Index Selection in nx (Knowledge).applescript +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/dt/scripts/Index Selection in nx.applescript +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/mcpb/src/server.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/scripts/cron/README.md +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/scripts/launchd/README.md +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/sn/README.md +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/sn/hooks/scripts/routing/README.md +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/__init__.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/_git_hooks_meta.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/_locking.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/_mineru_pid.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/_session_end_launcher.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/aspect_extractor.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/aspect_promotion.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/aspect_readers.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/bib_enricher.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/bib_enricher_openalex.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/bib_extractor.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/catalog/AGENTS.md +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/catalog/__init__.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/catalog/auto_linker.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/catalog/catalog.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/catalog/catalog_backup.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/catalog/catalog_db.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/catalog/catalog_docs.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/catalog/catalog_git.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/catalog/catalog_links.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/catalog/catalog_spans.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/catalog/catalog_sync.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/catalog/catalog_writes.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/catalog/collection_name.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/catalog/consolidation.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/catalog/dedupe.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/catalog/event_log.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/catalog/events.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/catalog/link_generator.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/catalog/manifest_backfill.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/catalog/orphan_backfill.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/catalog/projector.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/catalog/store_hook.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/catalog/synthesizer.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/catalog/tumbler.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/catalog/types.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/checkpoint.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/chunker.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/classifier.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/cli.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/code_indexer.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/collection_audit.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/collection_health.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/collection_rename.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/commands/__init__.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/commands/_helpers.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/commands/_migration_prompt.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/commands/_provision.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/commands/aspects.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/commands/catalog.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/commands/collection.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/commands/command_context.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/commands/config_cmd.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/commands/console.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/commands/context_cmd.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/commands/daemon.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/commands/doc.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/commands/doctor.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/commands/dt.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/commands/enrich.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/commands/hook.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/commands/hooks.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/commands/index.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/commands/mineru.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/commands/plan.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/commands/rdr.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/commands/scratch.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/commands/search_cmd.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/commands/store.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/commands/t3.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/commands/taxonomy_cmd.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/commands/tier_status.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/commands/upgrade.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/config.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/console/__init__.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/console/app.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/console/config.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/console/routes/__init__.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/console/routes/activity.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/console/routes/campaigns.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/console/routes/health.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/console/routes/partials.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/console/static/alpine.min.js +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/console/static/console.css +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/console/static/htmx.min.js +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/console/static/pico.min.css +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/console/templates/activity/_detail.html +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/console/templates/activity/_stream.html +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/console/templates/activity/index.html +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/console/templates/base.html +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/console/templates/campaigns/detail.html +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/console/templates/campaigns/index.html +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/console/templates/health/_cards.html +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/console/templates/health/index.html +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/console/watchers.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/context.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/corpus.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/cross_encoder.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/daemon/T2_DAEMON_WIP.md +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/daemon/__init__.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/daemon/discovery.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/daemon/t3_client.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/daemon/t3_daemon.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/db/AGENTS.md +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/db/__init__.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/db/chroma_quotas.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/db/local_ef.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/db/migrations.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/db/t1.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/db/t2/_tuning.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/db/t2/aspect_extraction_queue.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/db/t2/catalog.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/db/t2/catalog_taxonomy.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/db/t2/chash_index.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/db/t2/document_aspects.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/db/t2/plan_library.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/db/t2/telemetry.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/db/t3.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/db/t3_reidentify.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/devonthink.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/doc/__init__.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/doc/_common.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/doc/citations.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/doc/ref_scanner.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/doc/render.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/doc/resolvers.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/doc/resolvers_corpus.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/doc/tokens.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/doc_indexer.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/doctor_search.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/dropped_writes.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/errors.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/exporter.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/filters.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/formatters.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/frecency.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/glossary.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/health.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/hook_registry.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/hooks.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/index_context.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/indexer.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/indexer_utils.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/languages.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/logging_setup.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/mcp/AGENTS.md +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/mcp/__init__.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/mcp/_first_run.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/mcp/_t1_state.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/mcp/catalog.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/mcp/core.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/mcp/plan_cache_registry.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/mcp_infra.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/mcp_server.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/md_chunker.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/merge_candidates.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/metadata_schema.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/name_canaries.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/operators/aspect_sql.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/operators/dispatch.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/pdf_chunker.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/pdf_extractor.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/phase_review_sentinel.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/pipeline_buffer.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/pipeline_stages.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/plans/__init__.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/plans/bundle.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/plans/loader.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/plans/match.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/plans/matcher.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/plans/promote.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/plans/purposes.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/plans/repair.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/plans/runner.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/plans/schema.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/plans/scope.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/plans/seed_loader.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/plans/session_cache.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/prose_indexer.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/registry.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/retry.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/ripgrep_cache.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/routing_stats.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/salience.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/scoring.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/search_clusterer.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/search_engine.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/session.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/stage_timers.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/storage_boundary_lint.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/taxonomy.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/taxonomy_backfill.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/ttl.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/types.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/util/__init__.py +0 -0
- {conexus-5.2.0 → conexus-5.3.0}/src/nexus/util/process_group.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: conexus
|
|
3
|
-
Version: 5.
|
|
3
|
+
Version: 5.3.0
|
|
4
4
|
Summary: Self-hosted semantic search and knowledge management for LLM-driven development
|
|
5
5
|
Project-URL: Homepage, https://github.com/Hellblazer/nexus
|
|
6
6
|
Project-URL: Repository, https://github.com/Hellblazer/nexus
|
|
@@ -146,6 +146,11 @@ An RDR (Research-Design-Review) is a short document that records a technical dec
|
|
|
146
146
|
| [RDR-128](rdr-128-t2-single-writer-enforcement.md) | T2 Single-Writer Enforcement: One Owner for memory.db, or an Enforced Lock Discipline | Architecture | Closed 2026-05-25 (implemented, shipped 5.1.0) | 2026-05-25 |
|
|
147
147
|
| [RDR-129](rdr-129-t2-daemon-serving-path-cross-store-contention.md) | T2 Daemon Write-Path Hardening: Guaranteed-Single-Daemon Enforcement and Contention-Free Internal Serialization | Architecture | Accepted | 2026-05-25 |
|
|
148
148
|
| [RDR-130](rdr-130-command-preambles-via-nx-cli.md) | Command Preambles via the nx CLI: Thin Commands, Tested Logic, No Inlined Bash | Architecture | Accepted 2026-05-26 | 2026-05-26 |
|
|
149
|
+
| [RDR-131](rdr-131-t2-session-rollup-summaries.md) | T2 Session Rollup Summaries (MemTree-Lite): Recency-Windowed Memory Consolidation for Compact Context Injection | Architecture | Draft | 2026-05-27 |
|
|
150
|
+
| [RDR-132](rdr-132-scope-routed-t1-t2-promotion.md) | Scope-Routed T1 to T2 Promotion: Entity / Session / Project Scopes for Targeted Memory Retrieval | Architecture | Draft | 2026-05-27 |
|
|
151
|
+
| [RDR-133](rdr-133-entity-cluster-cross-tier-aggregation.md) | Entity-Cluster Cross-Tier Aggregation: A First-Class Entity Handle Unifying T2 Memory, T3 Catalog, and T3 Chunks | Architecture | Draft | 2026-05-27 |
|
|
152
|
+
| [RDR-134](rdr-134-taxonomy-aware-recall-in-nx-answer.md) | RDR-070 Phase 5: Taxonomy-Aware Recall in nx_answer — Teach the Composed-Retrieval Path to Read the Taxonomy It Already Has | Architecture | Draft | 2026-05-27 |
|
|
153
|
+
| [RDR-135](rdr-135-windowed-aspect-extraction.md) | Windowed Aspect Extraction with Cross-Window Merge: Stop Whole-Paper Single-Shot Extraction from Degrading on Long Inputs | Architecture | Draft | 2026-05-27 |
|
|
149
154
|
|
|
150
155
|
> **Scrapped 2026-05-19 (RDR-110-119 arc).** Bundled the storage-substrate split with new abstractions (tuplespace, ORB, host-trust, surfaces-as-tuples, UI fabric); scope discipline failed across nine RDRs and 67 stranded beads. Files preserved as tombstones per the "never delete RDR files" rule. Postmortem: [docs/postmortem/2026-05-16-rdr110-113-remediation-chain.md](../postmortem/2026-05-16-rdr110-113-remediation-chain.md). Active substrate work continues as [RDR-120](rdr-120-storage-substrate-split.md) with an explicit moratorium on co-shipped consumers. Numbers RDR-114 through RDR-117 are unused on `main` (drafted on feature branches that never merged).
|
|
151
156
|
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "conexus-mcpb"
|
|
3
|
-
version = "5.
|
|
3
|
+
version = "5.3.0"
|
|
4
4
|
description = "Conexus packaged as Claude Desktop .mcpb (Desktop Extension)"
|
|
5
5
|
requires-python = ">=3.12"
|
|
6
6
|
dependencies = [
|
|
7
|
-
"conexus>=5.
|
|
7
|
+
"conexus>=5.3.0",
|
|
8
8
|
]
|
|
9
9
|
|
|
10
10
|
[tool.uv]
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "conexus"
|
|
7
|
-
version = "5.
|
|
7
|
+
version = "5.3.0"
|
|
8
8
|
description = "Self-hosted semantic search and knowledge management for LLM-driven development"
|
|
9
9
|
readme = { file = "README.md", content-type = "text/markdown" }
|
|
10
10
|
requires-python = ">=3.12,<3.14"
|
|
@@ -158,7 +158,7 @@ class AspectExtractionWorker:
|
|
|
158
158
|
self,
|
|
159
159
|
*,
|
|
160
160
|
poll_interval: float = 2.0,
|
|
161
|
-
stale_timeout_seconds: int =
|
|
161
|
+
stale_timeout_seconds: int = 60,
|
|
162
162
|
batch_size: int = _DEFAULT_BATCH_SIZE,
|
|
163
163
|
) -> None:
|
|
164
164
|
self._poll_interval = poll_interval
|
|
@@ -272,10 +272,12 @@ class AspectExtractionWorker:
|
|
|
272
272
|
# contending on its single WAL writer lock. This is the
|
|
273
273
|
# every-2s contention behind the recurring
|
|
274
274
|
# `aspect_worker_claim_failed` / `database is locked`
|
|
275
|
-
# incidents (memory: daemon-restart-not-worker-fix).
|
|
276
|
-
#
|
|
277
|
-
#
|
|
278
|
-
#
|
|
275
|
+
# incidents (memory: daemon-restart-not-worker-fix).
|
|
276
|
+
# nexus-zir76: the persist path (_process_row /
|
|
277
|
+
# _process_batch) now routes too, via the daemon-side
|
|
278
|
+
# `complete_aspect` method (AspectRecord travels as an
|
|
279
|
+
# asdict() field dict). The worker no longer opens
|
|
280
|
+
# memory.db on ANY path.
|
|
279
281
|
def _poll(t2):
|
|
280
282
|
if do_reclaim:
|
|
281
283
|
t2.aspect_queue.reclaim_stale(
|
|
@@ -316,8 +318,10 @@ class AspectExtractionWorker:
|
|
|
316
318
|
P5.1 / nexus-8g79.34 — the batch path now mirrors the
|
|
317
319
|
single-doc extractor's read contract).
|
|
318
320
|
"""
|
|
321
|
+
import dataclasses
|
|
322
|
+
|
|
319
323
|
from nexus.aspect_extractor import select_config
|
|
320
|
-
from nexus.mcp_infra import
|
|
324
|
+
from nexus.mcp_infra import t2_index_write
|
|
321
325
|
|
|
322
326
|
# extract_aspects_batch requires every input to share a single
|
|
323
327
|
# ExtractorConfig. claim_batch grabs FIFO across collections, so
|
|
@@ -360,13 +364,14 @@ class AspectExtractionWorker:
|
|
|
360
364
|
row_count=len(rows),
|
|
361
365
|
exc_info=True,
|
|
362
366
|
)
|
|
367
|
+
# nexus-zir76: route through the daemon, never direct memory.db.
|
|
368
|
+
def _fail_all(db): # noqa: ANN001
|
|
369
|
+
for row in rows:
|
|
370
|
+
db.aspect_queue.mark_failed(
|
|
371
|
+
row.collection, row.source_path, error=str(exc),
|
|
372
|
+
)
|
|
363
373
|
try:
|
|
364
|
-
|
|
365
|
-
for row in rows:
|
|
366
|
-
t2.aspect_queue.mark_failed(
|
|
367
|
-
row.collection, row.source_path,
|
|
368
|
-
error=str(exc),
|
|
369
|
-
)
|
|
374
|
+
t2_index_write(_fail_all)
|
|
370
375
|
except Exception:
|
|
371
376
|
_log.warning(
|
|
372
377
|
"aspect_worker_batch_mark_failed_persist_failed",
|
|
@@ -374,45 +379,78 @@ class AspectExtractionWorker:
|
|
|
374
379
|
)
|
|
375
380
|
return
|
|
376
381
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
)
|
|
402
|
-
continue
|
|
403
|
-
t2.document_aspects.upsert(record)
|
|
404
|
-
t2.aspect_queue.mark_done(
|
|
382
|
+
# nexus-zir76: one routed write_fn does the whole batch's persist;
|
|
383
|
+
# each row clears via the daemon (``complete_aspect`` /
|
|
384
|
+
# ``mark_done``) instead of a direct memory.db transaction.
|
|
385
|
+
def _persist_all(db): # noqa: ANN001
|
|
386
|
+
for row, record in zip(rows, records):
|
|
387
|
+
if record is None:
|
|
388
|
+
# Unsupported collection — drop silently.
|
|
389
|
+
db.aspect_queue.mark_done(
|
|
390
|
+
row.collection, row.source_path,
|
|
391
|
+
)
|
|
392
|
+
continue
|
|
393
|
+
# nexus-8g79.34: ExtractFail per-row — typed read-failure
|
|
394
|
+
# sentinel from URI-based reading. Mark queue done so we
|
|
395
|
+
# don't retry an unreadable source on every drain;
|
|
396
|
+
# operators re-enqueue manually after fixing source
|
|
397
|
+
# identity (mirrors _process_row's handling).
|
|
398
|
+
if isinstance(record, ExtractFail):
|
|
399
|
+
_log.info(
|
|
400
|
+
"aspect_worker_batch_extract_skip",
|
|
401
|
+
uri=record.uri,
|
|
402
|
+
reason=record.reason,
|
|
403
|
+
detail=record.detail,
|
|
404
|
+
)
|
|
405
|
+
db.aspect_queue.mark_done(
|
|
405
406
|
row.collection, row.source_path,
|
|
406
407
|
)
|
|
408
|
+
continue
|
|
409
|
+
db.complete_aspect(dataclasses.asdict(record))
|
|
410
|
+
try:
|
|
411
|
+
t2_index_write(_persist_all)
|
|
407
412
|
except Exception:
|
|
408
413
|
_log.warning(
|
|
409
414
|
"aspect_worker_batch_persist_failed",
|
|
410
415
|
exc_info=True,
|
|
411
416
|
)
|
|
412
417
|
|
|
418
|
+
def _mark_failed_routed(self, row, error: str) -> None:
|
|
419
|
+
"""Route a queue ``mark_failed`` through the daemon (nexus-zir76).
|
|
420
|
+
|
|
421
|
+
The failure path must not open ``memory.db`` directly either: a
|
|
422
|
+
direct ``mark_failed`` losing the WAL writer race is exactly what
|
|
423
|
+
orphaned rows ``in_progress`` until the reclaim backstop. If even
|
|
424
|
+
the routed write raises (daemon down AND the direct fallback
|
|
425
|
+
contended), ``reclaim_stale`` recovers the row; we log and move on
|
|
426
|
+
without killing the worker thread.
|
|
427
|
+
"""
|
|
428
|
+
from nexus.mcp_infra import t2_index_write
|
|
429
|
+
try:
|
|
430
|
+
t2_index_write(
|
|
431
|
+
lambda db: db.aspect_queue.mark_failed(
|
|
432
|
+
row.collection, row.source_path, error=error,
|
|
433
|
+
)
|
|
434
|
+
)
|
|
435
|
+
except Exception:
|
|
436
|
+
_log.warning(
|
|
437
|
+
"aspect_worker_mark_failed_persist_failed",
|
|
438
|
+
collection=row.collection,
|
|
439
|
+
source_path=row.source_path,
|
|
440
|
+
exc_info=True,
|
|
441
|
+
)
|
|
442
|
+
|
|
413
443
|
def _process_row(self, row) -> None:
|
|
414
|
-
"""Run extraction on one queue row and dispatch on the result.
|
|
415
|
-
|
|
444
|
+
"""Run extraction on one queue row and dispatch on the result.
|
|
445
|
+
|
|
446
|
+
nexus-zir76: every persist routes through ``t2_index_write`` (the
|
|
447
|
+
daemon when reachable, a direct fallback when not) so the worker
|
|
448
|
+
never opens ``memory.db`` directly and cannot contend with the
|
|
449
|
+
daemon for the single WAL writer lock.
|
|
450
|
+
"""
|
|
451
|
+
import dataclasses
|
|
452
|
+
|
|
453
|
+
from nexus.mcp_infra import t2_index_write
|
|
416
454
|
try:
|
|
417
455
|
# Content was captured at enqueue time when in scope (MCP
|
|
418
456
|
# store_put). For CLI rows where content was not in scope
|
|
@@ -451,52 +489,46 @@ class AspectExtractionWorker:
|
|
|
451
489
|
source_path=row.source_path,
|
|
452
490
|
exc_info=True,
|
|
453
491
|
)
|
|
454
|
-
|
|
455
|
-
with t2_ctx() as t2:
|
|
456
|
-
t2.aspect_queue.mark_failed(
|
|
457
|
-
row.collection, row.source_path,
|
|
458
|
-
error=str(exc),
|
|
459
|
-
)
|
|
460
|
-
except Exception:
|
|
461
|
-
_log.warning(
|
|
462
|
-
"aspect_worker_mark_failed_persist_failed",
|
|
463
|
-
exc_info=True,
|
|
464
|
-
)
|
|
492
|
+
self._mark_failed_routed(row, str(exc))
|
|
465
493
|
return
|
|
466
494
|
|
|
467
495
|
try:
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
496
|
+
if record is None:
|
|
497
|
+
# Unsupported collection — drop silently.
|
|
498
|
+
t2_index_write(
|
|
499
|
+
lambda db: db.aspect_queue.mark_done(
|
|
472
500
|
row.collection, row.source_path,
|
|
473
501
|
)
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
502
|
+
)
|
|
503
|
+
return
|
|
504
|
+
# RDR-096 P1.2: ExtractFail is the typed read-failure
|
|
505
|
+
# sentinel. No row written; mark queue done so we
|
|
506
|
+
# don't retry the unreadable source on every drain.
|
|
507
|
+
# Operators can re-enqueue manually after fixing the
|
|
508
|
+
# source identity.
|
|
509
|
+
if isinstance(record, ExtractFail):
|
|
510
|
+
_log.info(
|
|
511
|
+
"aspect_worker_extract_skip",
|
|
512
|
+
uri=record.uri,
|
|
513
|
+
reason=record.reason,
|
|
514
|
+
detail=record.detail,
|
|
515
|
+
)
|
|
516
|
+
t2_index_write(
|
|
517
|
+
lambda db: db.aspect_queue.mark_done(
|
|
488
518
|
row.collection, row.source_path,
|
|
489
519
|
)
|
|
490
|
-
return
|
|
491
|
-
# AspectRecord — either a populated record or a null-
|
|
492
|
-
# fields record from a subprocess-side failure (the
|
|
493
|
-
# extractor already retried up to 3 attempts
|
|
494
|
-
# internally for those paths). Persist and remove
|
|
495
|
-
# from the queue.
|
|
496
|
-
t2.document_aspects.upsert(record)
|
|
497
|
-
t2.aspect_queue.mark_done(
|
|
498
|
-
row.collection, row.source_path,
|
|
499
520
|
)
|
|
521
|
+
return
|
|
522
|
+
# AspectRecord — either a populated record or a null-fields
|
|
523
|
+
# record from a subprocess-side failure (the extractor
|
|
524
|
+
# already retried up to 3 attempts internally for those
|
|
525
|
+
# paths). nexus-zir76: persist + clear the queue row in one
|
|
526
|
+
# daemon-routed call (``complete_aspect``) so the worker
|
|
527
|
+
# never writes memory.db directly. asdict() because the wire
|
|
528
|
+
# protocol decodes a dataclass arg to its field dict.
|
|
529
|
+
t2_index_write(
|
|
530
|
+
lambda db: db.complete_aspect(dataclasses.asdict(record))
|
|
531
|
+
)
|
|
500
532
|
except Exception as exc:
|
|
501
533
|
_log.warning(
|
|
502
534
|
"aspect_worker_persist_failed",
|
|
@@ -504,17 +536,7 @@ class AspectExtractionWorker:
|
|
|
504
536
|
source_path=row.source_path,
|
|
505
537
|
exc_info=True,
|
|
506
538
|
)
|
|
507
|
-
|
|
508
|
-
with t2_ctx() as t2:
|
|
509
|
-
t2.aspect_queue.mark_failed(
|
|
510
|
-
row.collection, row.source_path,
|
|
511
|
-
error=str(exc),
|
|
512
|
-
)
|
|
513
|
-
except Exception:
|
|
514
|
-
_log.warning(
|
|
515
|
-
"aspect_worker_mark_failed_secondary_persist_failed",
|
|
516
|
-
exc_info=True,
|
|
517
|
-
)
|
|
539
|
+
self._mark_failed_routed(row, str(exc))
|
|
518
540
|
|
|
519
541
|
|
|
520
542
|
# ── Module-level singleton ──────────────────────────────────────────────────
|
|
@@ -545,18 +567,56 @@ def _worker_lock_path(locks_dir: Path | None = None) -> Path:
|
|
|
545
567
|
return base / f"aspect_worker.{os.getpid()}"
|
|
546
568
|
|
|
547
569
|
|
|
570
|
+
def _sweep_dead_worker_locks(locks_dir: Path) -> None:
|
|
571
|
+
"""Remove ``aspect_worker.<pid>`` lock files whose PID is dead.
|
|
572
|
+
|
|
573
|
+
nexus-zir76: ``_remove_worker_lock`` only runs on a clean
|
|
574
|
+
``stop_worker``; a ``-9`` or a crash leaks the file. Over many
|
|
575
|
+
sessions these accumulate unbounded (85 found in the wild on
|
|
576
|
+
2026-05-27). Sweeping dead-PID locks at worker startup bounds the
|
|
577
|
+
pileup. Live locks (including this process's own) are left intact;
|
|
578
|
+
non-PID-shaped files are ignored. Best-effort — never raises.
|
|
579
|
+
"""
|
|
580
|
+
import os
|
|
581
|
+
|
|
582
|
+
if not locks_dir.exists():
|
|
583
|
+
return
|
|
584
|
+
own_pid = os.getpid()
|
|
585
|
+
for lock_file in locks_dir.glob("aspect_worker.*"):
|
|
586
|
+
try:
|
|
587
|
+
pid = int(lock_file.name.rsplit(".", 1)[-1])
|
|
588
|
+
except ValueError:
|
|
589
|
+
continue # not a PID-suffixed lock file
|
|
590
|
+
if pid == own_pid:
|
|
591
|
+
continue
|
|
592
|
+
try:
|
|
593
|
+
os.kill(pid, 0)
|
|
594
|
+
except ProcessLookupError:
|
|
595
|
+
try:
|
|
596
|
+
lock_file.unlink(missing_ok=True)
|
|
597
|
+
_log.info("aspect_worker_stale_lock_swept", pid=pid)
|
|
598
|
+
except Exception:
|
|
599
|
+
pass
|
|
600
|
+
except PermissionError:
|
|
601
|
+
# Alive under another user — leave it.
|
|
602
|
+
continue
|
|
603
|
+
|
|
604
|
+
|
|
548
605
|
def _write_worker_lock(locks_dir: Path | None = None) -> None:
|
|
549
606
|
"""Write a process-scoped lock file advertising this worker.
|
|
550
607
|
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
608
|
+
Sweeps dead-PID lock files first (nexus-zir76) so leaked locks from
|
|
609
|
+
crashed/killed predecessors do not accumulate. Non-fatal if the locks
|
|
610
|
+
directory cannot be created or the file cannot be written — the lock
|
|
611
|
+
is advisory; a missing file merely means ``drain_worker`` from another
|
|
612
|
+
process will not detect this worker.
|
|
554
613
|
"""
|
|
555
614
|
import os
|
|
556
615
|
|
|
557
616
|
try:
|
|
558
617
|
lock = _worker_lock_path(locks_dir)
|
|
559
618
|
lock.parent.mkdir(parents=True, exist_ok=True)
|
|
619
|
+
_sweep_dead_worker_locks(lock.parent)
|
|
560
620
|
lock.write_text(str(os.getpid()))
|
|
561
621
|
except Exception:
|
|
562
622
|
_log.warning("aspect_worker_lock_write_failed", exc_info=True)
|
|
@@ -576,7 +636,7 @@ def _remove_worker_lock(locks_dir: Path | None = None) -> None:
|
|
|
576
636
|
def ensure_worker_started(
|
|
577
637
|
*,
|
|
578
638
|
poll_interval: float = 2.0,
|
|
579
|
-
stale_timeout_seconds: int =
|
|
639
|
+
stale_timeout_seconds: int = 60,
|
|
580
640
|
_locks_dir: Path | None = None,
|
|
581
641
|
) -> AspectExtractionWorker:
|
|
582
642
|
"""Lazy-start the singleton worker. Returns the worker.
|
|
@@ -24,11 +24,39 @@ def memory() -> None:
|
|
|
24
24
|
@click.option("--title", "-t", required=True, help="Entry title/filename")
|
|
25
25
|
@click.option("--tags", default="", help="Comma-separated tags")
|
|
26
26
|
@click.option("--ttl", default="30d", show_default=True, help="TTL: Nd, Nw, or permanent")
|
|
27
|
-
|
|
27
|
+
@click.option(
|
|
28
|
+
"--merge",
|
|
29
|
+
is_flag=True,
|
|
30
|
+
default=False,
|
|
31
|
+
help="Canonical-fact merge: fold into an existing high-overlap entry "
|
|
32
|
+
"(non-destructive) instead of inserting a near-duplicate.",
|
|
33
|
+
)
|
|
34
|
+
@click.option(
|
|
35
|
+
"--merge-threshold",
|
|
36
|
+
type=float,
|
|
37
|
+
default=0.5,
|
|
38
|
+
show_default=True,
|
|
39
|
+
help="Word-set Jaccard threshold for --merge.",
|
|
40
|
+
)
|
|
41
|
+
def put_cmd(
|
|
42
|
+
content: str,
|
|
43
|
+
project: str,
|
|
44
|
+
title: str,
|
|
45
|
+
tags: str,
|
|
46
|
+
ttl: str,
|
|
47
|
+
merge: bool,
|
|
48
|
+
merge_threshold: float,
|
|
49
|
+
) -> None:
|
|
28
50
|
"""Write content to the T2 memory bank.
|
|
29
51
|
|
|
30
52
|
Use '-' as CONTENT to read from stdin.
|
|
31
53
|
|
|
54
|
+
With ``--merge`` (bead nexus-lhxz4), the new content is folded into the
|
|
55
|
+
most word-set-overlapping existing entry in the project when overlap is
|
|
56
|
+
at or above ``--merge-threshold`` (non-destructive: both texts are kept),
|
|
57
|
+
instead of inserting a near-duplicate. Without it, the default upsert
|
|
58
|
+
keyed on (project, title) is unchanged.
|
|
59
|
+
|
|
32
60
|
RDR-120 P6 follow-up (nexus-w6txl): routes through the T2 daemon
|
|
33
61
|
so host CLI + Cowork-bridged MCP + dev-container CLI all share
|
|
34
62
|
the same arbitrated state. Requires the T2 daemon running; start
|
|
@@ -41,10 +69,19 @@ def put_cmd(content: str, project: str, title: str, tags: str, ttl: str) -> None
|
|
|
41
69
|
except ValueError as exc:
|
|
42
70
|
raise click.ClickException(str(exc)) from exc
|
|
43
71
|
with t2_handle() as db:
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
72
|
+
if merge:
|
|
73
|
+
row_id, action = db.memory.put_or_merge(
|
|
74
|
+
project=project, title=title, content=content, tags=tags,
|
|
75
|
+
ttl=ttl_days, min_similarity=merge_threshold,
|
|
76
|
+
)
|
|
77
|
+
else:
|
|
78
|
+
row_id = db.memory.put(
|
|
79
|
+
project=project, title=title, content=content, tags=tags,
|
|
80
|
+
ttl=ttl_days,
|
|
81
|
+
)
|
|
82
|
+
action = "inserted"
|
|
83
|
+
verb = "Merged into" if action == "merged" else "Stored"
|
|
84
|
+
click.echo(f"{verb}: {project}/{title} (id={row_id})")
|
|
48
85
|
|
|
49
86
|
|
|
50
87
|
@memory.command("get")
|
|
@@ -285,6 +285,12 @@ class T2Client:
|
|
|
285
285
|
def rename_collection_cascade(self, *args: Any, **kwargs: Any) -> Any:
|
|
286
286
|
return self.database.rename_collection_cascade(*args, **kwargs)
|
|
287
287
|
|
|
288
|
+
def complete_aspect(self, *args: Any, **kwargs: Any) -> Any:
|
|
289
|
+
# nexus-zir76: aspect-worker persist (document_aspects.upsert +
|
|
290
|
+
# aspect_queue.mark_done) folded into one daemon-routable call so
|
|
291
|
+
# the worker stays off the direct memory.db write path.
|
|
292
|
+
return self.database.complete_aspect(*args, **kwargs)
|
|
293
|
+
|
|
288
294
|
def put(self, *args: Any, **kwargs: Any) -> Any:
|
|
289
295
|
# T2Database.put is a thin facade over memory.put; mirror it so a
|
|
290
296
|
# write_fn (or a helper handed the writer, e.g. T1Database.promote)
|
|
@@ -684,6 +684,28 @@ class T2Database:
|
|
|
684
684
|
session=session,
|
|
685
685
|
)
|
|
686
686
|
|
|
687
|
+
def put_or_merge(
|
|
688
|
+
self,
|
|
689
|
+
project: str,
|
|
690
|
+
title: str,
|
|
691
|
+
content: str,
|
|
692
|
+
tags: str = "",
|
|
693
|
+
ttl: int | None = 30,
|
|
694
|
+
agent: str | None = None,
|
|
695
|
+
session: str | None = None,
|
|
696
|
+
min_similarity: float = 0.5,
|
|
697
|
+
) -> tuple[int, str]:
|
|
698
|
+
return self.memory.put_or_merge(
|
|
699
|
+
project=project,
|
|
700
|
+
title=title,
|
|
701
|
+
content=content,
|
|
702
|
+
tags=tags,
|
|
703
|
+
ttl=ttl,
|
|
704
|
+
agent=agent,
|
|
705
|
+
session=session,
|
|
706
|
+
min_similarity=min_similarity,
|
|
707
|
+
)
|
|
708
|
+
|
|
687
709
|
def get(
|
|
688
710
|
self,
|
|
689
711
|
project: str | None = None,
|
|
@@ -961,3 +983,31 @@ class T2Database:
|
|
|
961
983
|
**extra,
|
|
962
984
|
)
|
|
963
985
|
return len(expired_ids)
|
|
986
|
+
|
|
987
|
+
def complete_aspect(self, record_fields: dict[str, Any]) -> bool:
|
|
988
|
+
"""Persist an extracted aspect and clear its queue row in one call.
|
|
989
|
+
|
|
990
|
+
nexus-zir76 (RDR-128 follow-up): the aspect worker previously
|
|
991
|
+
upserted ``document_aspects`` and called ``aspect_queue.mark_done``
|
|
992
|
+
via two DIRECT ``memory.db`` writes, competing with the daemon for
|
|
993
|
+
the single WAL writer lock. When the direct ``mark_done`` (or the
|
|
994
|
+
failure path's ``mark_failed``) lost that race, the row was
|
|
995
|
+
orphaned ``in_progress`` until the ``reclaim_stale`` backstop.
|
|
996
|
+
Folding both writes into one daemon-routable method keeps the
|
|
997
|
+
worker off the direct write path and closes that window.
|
|
998
|
+
|
|
999
|
+
*record_fields* is ``dataclasses.asdict(AspectRecord)`` — a plain
|
|
1000
|
+
JSON-shaped dict, because the daemon wire protocol decodes a
|
|
1001
|
+
dataclass argument to its field dict (it does not reconstruct the
|
|
1002
|
+
object). The ``AspectRecord`` is rebuilt here, server-side.
|
|
1003
|
+
|
|
1004
|
+
Returns the ``document_aspects.upsert`` result. ``mark_done`` is
|
|
1005
|
+
idempotent, so a reclaim-driven re-extraction after a crash
|
|
1006
|
+
between the two writes simply re-upserts — no duplicate, no stuck
|
|
1007
|
+
row.
|
|
1008
|
+
"""
|
|
1009
|
+
from nexus.db.t2.document_aspects import AspectRecord
|
|
1010
|
+
record = AspectRecord(**record_fields)
|
|
1011
|
+
upserted = self.document_aspects.upsert(record)
|
|
1012
|
+
self.aspect_queue.mark_done(record.collection, record.source_path)
|
|
1013
|
+
return upserted
|
|
@@ -890,6 +890,92 @@ class MemoryStore:
|
|
|
890
890
|
delete_ids,
|
|
891
891
|
)
|
|
892
892
|
|
|
893
|
+
def _content_words(self, text: str) -> set[str]:
|
|
894
|
+
"""Lowercased word set for Jaccard overlap: drop short tokens and
|
|
895
|
+
stopwords. Shared by write-time merge (``put_or_merge``); mirrors the
|
|
896
|
+
word-set logic inside ``find_overlapping_memories``."""
|
|
897
|
+
return {
|
|
898
|
+
w.lower()
|
|
899
|
+
for w in text.split()
|
|
900
|
+
if len(w) > 2 and w.lower() not in self._STOPWORDS
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
def put_or_merge(
|
|
904
|
+
self,
|
|
905
|
+
project: str,
|
|
906
|
+
title: str,
|
|
907
|
+
content: str,
|
|
908
|
+
tags: str = "",
|
|
909
|
+
ttl: int | None = 30,
|
|
910
|
+
agent: str | None = None,
|
|
911
|
+
session: str | None = None,
|
|
912
|
+
min_similarity: float = 0.5,
|
|
913
|
+
) -> tuple[int, str]:
|
|
914
|
+
"""Opt-in canonical-fact merge at write time (bead nexus-lhxz4).
|
|
915
|
+
|
|
916
|
+
MemForest-inspired (T3 research-memforest-nexus-leverage-2026-05-27,
|
|
917
|
+
idea #1): instead of accumulating near-duplicate entries that later
|
|
918
|
+
need a reactive ``memory_consolidate`` pass, fold the new content into
|
|
919
|
+
the most-overlapping existing entry at write time.
|
|
920
|
+
|
|
921
|
+
Returns ``(row_id, action)`` where ``action`` is ``"inserted"`` or
|
|
922
|
+
``"merged"``.
|
|
923
|
+
|
|
924
|
+
Merge is **non-destructive**: when ``content``'s word set overlaps an
|
|
925
|
+
existing *different-title* entry in ``project`` at Jaccard
|
|
926
|
+
``>= min_similarity``, the new content is appended to that entry under
|
|
927
|
+
a provenance separator (both texts preserved), its timestamp is
|
|
928
|
+
refreshed, and ``(existing_id, "merged")`` is returned — the requested
|
|
929
|
+
``title`` is not created. Otherwise a normal upsert runs and
|
|
930
|
+
``(row_id, "inserted")`` is returned.
|
|
931
|
+
|
|
932
|
+
Exact ``(project, title)`` collisions always take the normal upsert
|
|
933
|
+
path (identity update), never the cross-title merge. Empty content
|
|
934
|
+
(no word set) always inserts.
|
|
935
|
+
|
|
936
|
+
Selection is best-overlap-wins across the project's entries. There is
|
|
937
|
+
a benign TOCTOU window between the scan and the merge UPDATE (an entry
|
|
938
|
+
could change concurrently); accepted because the merge is opt-in and
|
|
939
|
+
non-destructive, mirroring ``merge_memories``' race posture.
|
|
940
|
+
"""
|
|
941
|
+
new_words = self._content_words(content)
|
|
942
|
+
if new_words:
|
|
943
|
+
best_id: int | None = None
|
|
944
|
+
best_jaccard = 0.0
|
|
945
|
+
best_content = ""
|
|
946
|
+
for entry in self.get_all(project):
|
|
947
|
+
if entry.get("title") == title:
|
|
948
|
+
continue # identity upsert, not a cross-title merge
|
|
949
|
+
existing_words = self._content_words(entry.get("content", ""))
|
|
950
|
+
if not existing_words:
|
|
951
|
+
continue
|
|
952
|
+
jaccard = len(new_words & existing_words) / len(
|
|
953
|
+
new_words | existing_words
|
|
954
|
+
)
|
|
955
|
+
if jaccard > best_jaccard:
|
|
956
|
+
best_jaccard = jaccard
|
|
957
|
+
best_id = entry["id"]
|
|
958
|
+
best_content = entry.get("content", "")
|
|
959
|
+
if best_id is not None and best_jaccard >= min_similarity:
|
|
960
|
+
timestamp = datetime.now(UTC).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
961
|
+
merged = (
|
|
962
|
+
f"{best_content}\n\n"
|
|
963
|
+
f"<!-- merged from {title!r} @ {timestamp} "
|
|
964
|
+
f"(jaccard={best_jaccard:.2f}) -->\n{content}"
|
|
965
|
+
)
|
|
966
|
+
with self._lock, self.conn:
|
|
967
|
+
self.conn.execute(
|
|
968
|
+
"UPDATE memory SET content = ?, timestamp = ? "
|
|
969
|
+
"WHERE id = ?",
|
|
970
|
+
(merged, timestamp, best_id),
|
|
971
|
+
)
|
|
972
|
+
return best_id, "merged"
|
|
973
|
+
row_id = self.put(
|
|
974
|
+
project, title, content,
|
|
975
|
+
tags=tags, ttl=ttl, agent=agent, session=session,
|
|
976
|
+
)
|
|
977
|
+
return row_id, "inserted"
|
|
978
|
+
|
|
893
979
|
def flag_stale_memories(
|
|
894
980
|
self,
|
|
895
981
|
project: str,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|