[build-system] # setuptools>=77 is required for the SPDX `license = "Apache-2.0"` form. # Floor raised to 83.0.0 on 2026-09-17: that is the fix for CVE-2026-59890 / # PYSEC-2026-3447, and this is the pin that MATTERS for it -- the CVE is in # sdist packaging (MANIFEST.in globs vs Unicode-normalized filenames), so it # lives in the BUILD path, which is exactly what this block governs. # The old <82 ceiling only mirrored torch 2.11's `setuptools<82`; torch 2.14 # declares `setuptools>=77.0.3` with no upper bound. requires = ["setuptools>=84.0.0", "wheel"] build-backend = "setuptools.build_meta" [project] name = "m3-memory" version = "2026.9.19.0" description = "Give your AI agent a memory — local-first, private, easy to install. Guided setup wizard; works out of the box on your own machine, no cloud. For everyday agent users, homelabs, and developers alike · 99.2% LongMemEval-S retrieval @ k=10 · Works with Claude · Gemini · Antigravity · OpenCode · OpenClaw · Hermes · any MCP agent (native + one-command plugins) · Hybrid search (FTS5 + vector + MMR) · GDPR · FIPS 140-3 deployment-ready · 100% local (fully offline) or cloud capable" readme = "README.md" license = "Apache-2.0" # Ship the LICENSE and the Apache NOTICE (attribution to skynetCMD) in every # sdist/wheel — setuptools>=77 packages these globs into the distribution. license-files = ["LICENSE", "NOTICE"] requires-python = ">=3.12" # skynetcmd is THE author, maintainer, and copyright holder (see NOTICE). The AI # coding assistants that helped build m3 (Gemini CLI, Claude Code, Google # Antigravity) are credited in the README's "Built with" acknowledgment — NOT # listed here, so they aren't given equal-footing authorship with skynetcmd. authors = [ { name = "skynetcmd", email = "skynetcmd@users.noreply.github.com" }, ] maintainers = [ { name = "skynetcmd", email = "skynetcmd@users.noreply.github.com" }, ] keywords = [ "mcp", "mcp-server", "model-context-protocol", "agentic-memory", "ai-memory", "long-term-memory", "local-llm", "claude", "claude-code", "gemini", "gemini-cli", "aider", "rag", "rag-memory", "vector-search", "hybrid-search", "sqlite", "gdpr", "privacy", "homelab", "ai-agents", "ollama", "contradiction-detection", "bitemporal", "local-first", "multi-agent", "knowledge-graph", "persistent-memory", "longmemeval", "fips-140-3", "openclaw", # end-user / discovery intent (not just developer terms) — m3 is installable and # usable by anyone running an AI agent, via the guided setup wizard. "assistant-memory", "agent-memory", "self-hosted", "offline", "no-cloud", "easy-install", "mcp-agent", ] classifiers = [ "Development Status :: 5 - Production/Stable", # Audience is BROAD by design: a guided installer/wizard makes m3 usable by # anyone running an AI agent (end users), by homelab/IT operators, AND by # developers embedding it. Listing only "Developers" made discovery agents # report m3 as dev-only — it isn't. "Intended Audience :: End Users/Desktop", "Intended Audience :: System Administrators", "Intended Audience :: Developers", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", # 3.15 is NOT claimed: torch (requirements.txt:74, `torch>=2.11.0,<3`) # publishes cp310-cp314 and no cp315 as of 2026-09-13, so `pip install` # cannot succeed on 3.15 however green m3's own code is. Verified on CI: # setup-python resolves the rc fine, then Install Dependencies dies with # "No matching distribution found for torch<3,>=2.11.0". # Claiming a version the dependency tree cannot install is a promise with # nothing behind it. Restore this line when torch ships a cp315 wheel — # the CI matrix still runs a 3.15 cell precisely so that becomes visible. "Topic :: Scientific/Engineering :: Artificial Intelligence", "Topic :: Software Development :: Libraries :: Python Modules", "Operating System :: OS Independent", "Environment :: Console", "Typing :: Typed", ] dependencies = [ "fastmcp>=3.4.7,<4", "mcp>=1.30.0,<2", "httpx[http2]>=0.28.1,<1", "requests>=2.34.2,<3", "psycopg2-binary>=2.9.13,<3", "cryptography>=50.0.1,<51", "keyring>=25.7.0,<26", # Cap lifted 2026-09-17: the <2.5 ceiling existed because numpy 2.5's stub # uses PEP 695 `type` syntax, which mypy rejected under the OLD 3.11 floor. # The floor is now 3.12, where it is valid. Verified: mypy over bin/ with # numpy 2.5.3 reports zero errors across 281 files. "numpy>=2.5.3", "python-dotenv>=1.2.3,<2", "PyYAML>=6.0.3,<7", "psutil>=7.2.2,<8", "tqdm>=4.70.1,<5", # FastAPI + uvicorn are core: the web dashboard (bin/dashboard_server.py, # `m3 dashboard`) is the non-developer entry point to memory, the wiki and # the knowledge graph, and gating it behind an extra hid it from the people # who need it most. They also back bin/mcp_proxy.py and the bin/embed_server* # fallbacks, all of which import them unguarded at module level — so any user # reaching those surfaces already had to discover an extra first. Both are # pure-Python wheels with no heavy transitive deps (no torch), so core stays # light. The [dashboard] / [web] extras remain, and still resolve, for # anyone who pinned them. "fastapi>=0.141.1,<1", "uvicorn>=0.53.0,<1", # SQL-injection AST guard in bin/memory/util.py::_check_content_safety. # Was in requirements.txt but missing here, so `pip install -e .` omitted it # and the guard silently degraded to a no-op (returning "safe" for DROP/ # DELETE/ALTER). Runtime dependency, not optional. "sqlglot>=30.18.0,<31", # Transitively pulled by httpx/requests, but pinned directly to floor out # PYSEC-2026-141/142 (matches requirements.txt) — a security pin belongs in # the installed metadata, not only the dev requirements file. "urllib3>=2.8.0,<3", # Fuzzy string matching for entity coalescing. Has a difflib fallback, so # it's a perf/quality dep — but small, pure, and used on a common path, so # kept in core (requirements.txt had it; pyproject omitted it). "rapidfuzz>=3.14.6,<4", # PDF generation for report paths; HTML ingestion for the files subsystem # (bin/files_memory/chunkers/html.py -- added 2026-09-17, when .html was # found to be resolving to a filetype with no chunker). "fpdf2>=2.8.8,<3", "beautifulsoup4>=4.15.0,<5", # PDF READING (distinct from fpdf2, which only writes). Undeclared until # 2026-09-17, so chunkers/pdf.py silently fell back to the text chunker on # any install that did not happen to get pypdf transitively. PyMuPDF stays # optional: the chunker prefers it, but it is AGPL with native wheels. "pypdf>=6.19.0,<7", # Binary documents (.docx/.doc/.pptx/.ppt/.xlsx/.xls/.rtf/.epub/.od*). # Apache-2.0, covers the legacy OLE formats, and avoids ebooklib (AGPL). "sharepoint-to-text>=2.0.0,<3", # Hardened XML parsing (XXE / entity-expansion defense) for # bin/doctor/schedule_probe.py, which parses `schtasks` XML output. Was only # transitively present (via fpdf2) — pinned directly so the safe path is # guaranteed on a fresh install. "defusedxml>=0.7.1,<1", # Topic clustering for the Memory Wiki (bin/wiki/cluster.py, `m3 wiki # generate`). The wiki is a core feature, so networkx is a base dependency — # imported directly, no optional extra, no in-tree fallback (the pure-Python # clustering path was retired 2026-07-24 when the wiki became core). Pure-Python # wheel, no compiled/torch-class weight; deterministic run-to-run. "networkx>=3.6.1,<4", # Windows-only: keyring backend + hook process control. Marker-gated so it # installs only where relevant. # macOS-only: CoreWLAN for thermal/network probes. "pyobjc-framework-CoreWLAN; sys_platform == 'darwin'", ] [project.optional-dependencies] postgres = ["psycopg[binary]>=3.3.5,<4"] # Apple iWork full-fidelity extraction (.pages/.key/.numbers). # # OPTIONAL ON PURPOSE. bin/files_memory/chunkers/iwork.py reads the bundled # QuickLook/Preview.pdf with pypdf first -- already a core dependency, so the # common case costs nothing. This extra adds the Index/*.iwa path, which always # works but pulls protobuf + python-snappy + pillow and ~20 transitive packages # (measured 2026-09-17: a 14-package env became 36). Charging every m3 user that # for a format they may never ingest is the wrong default; needing it is opt-in. iwork = [ "keynote-parser>=1.14.5", # keynote-parser declares `Pillow>=7.1.0` — a 2020 floor that resolves to a # release carrying 14 known CVEs, one Critical (GHSA-3f63-hfp8-52jq). Pinned # here because the extra is the only path that pulls Pillow into m3 at all: # nothing in the codebase imports PIL. "pillow>=12.3.0", ] # Web surfaces: the FastAPI dashboard (bin/dashboard_server.py) and the # OpenAI-compatible MCP proxy (bin/mcp_proxy.py). Optional by design — the core # MCP server + CLI need neither. Were in requirements.txt but missing from # pyproject entirely, so `pip install m3-memory` never provided them and the # dashboard/proxy failed to import. web = [ "fastapi>=0.141.1,<1", "uvicorn>=0.53.0,<1", ] # The local web dashboard (bin/dashboard_server.py), launched by `m3 dashboard`. # Same FastAPI+uvicorn stack as [web], exposed under a discoverable name so the # install flow can offer it and users install exactly what the dashboard needs: # pip install "m3-memory[dashboard]" && m3 dashboard # Kept as its OWN extra (not folded into [web]) so [web] stays back-compatible # and still scopes the mcp_proxy surface; dashboard/web share deps by design. dashboard = [ "fastapi>=0.141.1,<1", "uvicorn>=0.53.0,<1", ] # Local embedding-server stack (bin/embed_server*.py, sentence-transformers / # FlagEmbedding path). Heavy (pulls torch) and optional — the in-process Rust # tier-1 embedder or an external OpenAI-compatible embed endpoint cover the # common case. Kept OUT of core so a default install stays light. See # requirements.txt for the transformers / torch pin rationale. embed-server = [ "sentence-transformers>=5.7.0,<6", # Floors raised 2026-09-17: transformers 5.17 clears four CVEs the old <5 # ceiling forced us to accept, and torch 2.14 drops the `setuptools<82` # constraint that forced the setuptools ceiling. Full reasoning, including # why the old FlagEmbedding blocker no longer applies, is in # requirements.txt beside these same pins. "transformers>=5.17.0,<6", "torch>=2.14.0,<3", "scipy>=1.18.1,<2", ] # Local zero-shot NER for bin/m3_entities_gliner.py — a fast on-device # alternative to the LLM-based entity extractor. Optional by design: core runs # fully without it (bin/m3_entities.py needs no extra deps). Pulls torch. entity-ner = ["gliner>=0.2.26"] # Native SQLite vector search via the sqlite-vec extension. The loader at # bin/sqlite_pragmas.py loads it opportunistically (sqlite_vec.load(conn)) and # the semantic-search path in bin/memory/search.py uses the vec0 functions when # present. Optional by design: core runs fully without it (the Python/numpy # cosine path is the fallback). The PyPI wheel ships the platform-native # extension binary, so no separate compilation/bundling step is needed. vector = ["sqlite-vec>=0.1.6"] # The Memory Wiki is a CORE feature and its clustering dep (networkx) is now a # BASE dependency (see [project].dependencies above) — `pip install m3-memory` # already includes everything the wiki needs. This `[wiki]` extra is kept as an # empty BACK-COMPAT alias so existing `pip install "m3-memory[wiki]"` commands and # scripts keep working; it installs nothing beyond the base. It can be removed in a # future major version. wiki = [] # LangChain / LangGraph integration (m3_memory/integrations/langchain/). Optional # by design: core m3 gains NO hard LangChain dependency — the adapter imports # langchain-core / langgraph lazily, so `pip install m3-memory` stays light and # the integration only activates for users who opt in with `[langchain]`. The # mem0-compat surface shadows mem0's API SHAPE but never imports mem0 (no dep on # it). Pinned to a tested range so the version-pinned parity tests (§5) track a # fixed target rather than a moving API. langchain = [ "langchain-core>=0.3.0,<1", "langgraph>=0.2.0,<1", ] # CrewAI integration (m3_memory/integrations/crewai/). Optional by design: core # m3 gains NO hard CrewAI dependency — the adapter imports crewai lazily and is # version-guarded, so `pip install m3-memory` stays light and the integration # only activates for users who opt in with `[crewai]`. Pinned to >=1.10 because # the unified-memory StorageBackend protocol (crewai.memory.storage.backend) first # shipped there (PR #4420, Feb 2026) — v1.0 GA predates it. NO mem0 dependency: # CrewAI reads the "memory" result key natively; m3 never imports mem0. # NOTE: CrewAI 1.x requires Python >=3.10,<3.14 — so this extra resolves only on a # 3.10-3.13 interpreter, even though m3 itself runs on 3.14. Run the crew on a # supported Python; it talks to the same m3 store regardless. crewai = [ "crewai>=1.10,<2", ] # PydanticAI integration (m3_memory/integrations/pydantic_ai/). Optional by design, # same lazy/version-guarded pattern as the others — `pip install m3-memory` gains # no hard dependency. PydanticAI has NO built-in persistent memory; this adapter # supplies deps-injected tools + a recall history-processor (Tier 1) AND a formal # `M3MemoryToolset(AbstractToolset)` (Tier 2). Unlike CrewAI there is NO Python cap: # pydantic-ai is built on Pydantic v2 (no chromadb/pydantic-v1), so it installs and # runs on 3.14. `pydantic-ai-slim` (core, no model extras) is enough for the adapter. pydantic-ai = [ "pydantic-ai-slim>=2.0,<3", ] # "Project Oxidation" Rust compute core (m3_core_rs). `m3 setup` installs the # right prebuilt wheel from the m3-core-rs GitHub Releases (tag v2026.9.16 — # per bin/../rust_core_install.py M3_CORE_RS_GIT_TAG), one per platform/backend # (linux/macos/windows × cpu/cuda/vulkan/metal), each bundling the in-process # bge-m3 EmbeddedEmbedder. From-source is only a fallback (needs a Rust # toolchain >=1.94 + maturin): # pip install "m3-core-rs @ git+https://github.com/skynetcmd/m3-core-rs.git@v2026.9.16#subdirectory=crates/m3-core-py" # Installed BY DEFAULT by `m3 setup` (install.sh's --no-native-wheel is the # opt-OUT). m3-memory still runs fully on the pure-Python path without it -- # results-equivalent, just slower -- and M3_CORE_RS_DISABLE forces that path # even when the wheel is installed. dev = [ "pytest>=9.0.3,<10", "pytest-asyncio>=1.4.0,<2", "ruff>=0.15.12,<1", "mypy>=1.20.2,<2", "types-requests>=2.33.0.20260906", # Capped <82 to track the setuptools<82 cap (torch 2.11 pins setuptools<82). "types-setuptools>=77.0.0,<82", "types-PyYAML>=6.0.0", "twine>=6.2.0,<7", "build>=1.4.4,<2", ] [project.scripts] m3 = "m3_memory.cli:main" mcp-memory = "m3_memory.cli:main" m3-team = "m3_memory.team_cli:main" [project.urls] Homepage = "https://github.com/skynetcmd/m3-memory" Repository = "https://github.com/skynetcmd/m3-memory" "Bug Tracker" = "https://github.com/skynetcmd/m3-memory/issues" Changelog = "https://github.com/skynetcmd/m3-memory/blob/main/CHANGELOG.md" Documentation = "https://github.com/skynetcmd/m3-memory/blob/main/docs/GETTING_STARTED.md" # NOTE: package discovery, package_dir and package_data are defined in setup.py, # NOT here. The root-level payload trees (bin/, docs/, _assets/, examples/) must # be MAPPED into the m3_memory package (setuptools has no pyproject force-include), # which requires programmatic package_dir/package_data — so setup.py is the single # source for all three. Declaring [tool.setuptools.packages.find] here as well # would override setup.py's `packages=` and silently drop the payload from the # wheel. Keep them out of pyproject. (Hermes vendored data + mcp examples are also # handled in setup.py's mapping / MANIFEST graft.) [tool.ruff] target-version = "py311" line-length = 120 [tool.ruff.lint] select = ["E", "F", "W", "I"] # E501: long lines handled by editor wrap, not blocker for this codebase. # E402: many bin/ entrypoints do sys.path setup or lazy imports deliberately # before importing project modules — buried imports are intentional. # E701/E702: one-liner `if x: return` / `try: foo` is used throughout bench # and ingest scripts as a terseness choice; not a readability problem here. # E741: single-letter vars (l, O, I) are used intentionally in math/loop contexts. ignore = ["E501", "E402", "E701", "E702", "E741"] [tool.ruff.lint.per-file-ignores] # memory_core.py uses a module-level __getattr__ for lazy submodule dispatch. # All private helpers (_db, _record_history, _embed, etc.) are registered in # _LAZY_IMPORTS and resolved at runtime — Ruff's static analysis can't see # through __getattr__, so F821 is a false positive for this file. "bin/memory_core.py" = ["F821"] # dashboard_server.py imports Any via TYPE_CHECKING guard; F821 is spurious. "bin/dashboard_server.py" = ["F821"] [tool.mypy] python_version = "3.12" ignore_missing_imports = true # Match the codebase's pre-PEP 484 style: `def f(x: str = None)` means Optional. implicit_optional = true # False positives from narrowing around None checks are noisy in legacy code # that still leans on runtime checks rather than type guards. strict_optional = false # warn_unused_ignores off globally because per-file overrides below use # ignore_errors = true (which trips this warning on real # type: ignore lines # elsewhere in those modules). Revisit if we get the legacy files typed. warn_unused_ignores = false # ── Per-module overrides ────────────────────────────────────────────────────── # memory_bridge.py exposes every tool from mcp_tool_catalog at import time via # `globals()[name] = fn`. Mypy can't see these, so any module that imports a # tool-shaped name from memory_bridge will light up with attr-defined errors # (~55 of 184 total). The registration pattern is load-bearing — refactoring # to an explicit registry would touch every call site across bin/ and memory/. # Silence the false-positive class by not checking memory_bridge itself and # ignoring attr-defined on its importers. [[tool.mypy.overrides]] module = "memory_bridge" ignore_errors = true # Legacy untyped modules — typing them is a separate project, not a CI gate. # Each file listed here has >3 errors that aren't real bugs, just missing # annotations on params/returns or loose dict/list unions. Trim this list as # modules get typed. [[tool.mypy.overrides]] module = [ "test_memory_bridge", "memory_core", "m3_sdk", "chatlog_core", "agent_protocol", "chatlog_init", "knowledge_helpers", "mission_control", "chatlog_redaction", "temporal_utils", "setup_secret", "migrate_flat_memory", "chatlog_ingest", "aider_patch", "test_mission_control", "mcp_proxy", "dashboard_server", ] ignore_errors = true [tool.pytest.ini_options] asyncio_mode = "auto" testpaths = ["tests"] # --strict-markers: an unregistered/typo'd marker is now a COLLECTION ERROR, not # a silent no-op. Every marker a test uses must be declared below. addopts = "--strict-markers" markers = [ "slow: long-running tests (>1s); use -m 'not slow' to skip", "real_is_current: opt out of the rust_core_install is_rust_core_current autouse stub", # ── capability gates (see docs/design/TEST_SUITE_DESIGN.md) ──────────────── # A test that needs an external service/resource carries ONE requires_* marker; # conftest's pytest_collection_modifyitems auto-skips it when the probe says the # resource is absent (no per-file skipif/pytestmark needed). All requires_* # imply `integration`, so CI's hermetic lane is `-m "not integration"`. "integration: needs some external service/resource (umbrella; implied by every requires_*)", "requires_pg: needs a reachable PostgreSQL (M3_PRIMARY_PG_URL or M3_PG_URL; never PG_URL)", "requires_embedder: needs the local embedder endpoint (M3_EMBED_FALLBACK_URL, default :8082)", "requires_native: needs the m3_core_rs native wheel (not the pure-Python fallback)", "requires_gguf: needs a GGUF model file (M3_TEST_GGUF)", "requires_files_db: needs the shipped files_database.db present", "requires_memory_core: needs `import memory_core` to succeed (guards a broken transitive ML dep)", "requires_llm: needs a reachable local chat model (M3_WIKI_DRIFT_URL / LM Studio) for the citation-drift judge", ] # Warnings are ERRORS by default: a NEW, un-reviewed warning fails the suite, so it # can't slip in unnoticed. Every `ignore` below is a REVIEWED exception with its # rationale — the list IS the audit trail (repo policy: never hide a warning without # recording why). To add an entry you must justify it here. Two classes only: # (A) third-party warnings we don't control (fixed by upgrading the dep), and # (B) our own BY-DESIGN process-lifetime singletons (the shared asyncio daemon # loop-thread, the M3Context SQLite connection pool, the shared embedder HTTP # client) — torn down at interpreter exit / atexit, not per test, so they warn # under -W error only as finalizer-timing artifacts, never a real leak. # Genuine per-test resource leaks are FIXED at the source (close()/with), not listed. filterwarnings = [ "error", # ── (0) harness/config, not a test-code signal ──────────────────────────── # The pre-push drift check runs a subset of tests with # PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 (bin/check_tool_catalog_drift.py) to shave # startup cost — which unloads pytest-asyncio, so this file's `asyncio_mode` # option is then "unknown" and pytest emits a PytestConfigWarning. That's a # harness-configuration artifact of the plugin-disabled invocation, NOT a # warning from test code; without this the drift hook INTERNALERRORs under # `error`. (Full runs load the plugin and never hit it.) "ignore:Unknown config option. asyncio_mode:pytest.PytestConfigWarning", # ── (A) third-party, not fixable in our code ────────────────────────────── # langchain_core still imports pydantic.v1 shims; both warn on Python 3.14 and # are removed in 3.16 — resolved only by a langchain release that drops v1. "ignore:.*ForwardRef._evaluate is a private API.*:DeprecationWarning", "ignore:.*Core Pydantic V1 functionality isn't compatible.*:UserWarning", # langchain_core/runnables/utils.py uses the deprecated asyncio form (ours was # migrated to inspect.iscoroutinefunction; this remaining one is upstream). "ignore:.*asyncio.iscoroutinefunction.*is deprecated.*:DeprecationWarning:langchain_core", # with_m3_history intentionally wraps LangChain's RunnableWithMessageHistory, # which LangChain marked *Pending*Deprecation (favoring LangGraph persistence). # Deliberate compat shim; migrate when LangChain promotes it to real @deprecated. # Category is the built-in PendingDeprecationWarning (LangChainPendingDeprecationWarning # subclasses it) — NOT the langchain_core class path, because pytest imports the # category module to resolve the filter, and the drift-check CI job runs without # langchain installed (that import would fail the whole run under `error`). "ignore:.*RunnableWithMessageHistory is deprecated.*:PendingDeprecationWarning", # starlette >=1.3 deprecates driving its TestClient with httpx (favoring the # httpx2 rewrite); fastapi pulls that starlette in the [web]/[dashboard]/ # [embed-server] extras. The warning fires from starlette.testclient at import # of the TestClient — third-party, resolved only when we (or fastapi) move to # httpx2. Our test client usage is otherwise correct. Category is # StarletteDeprecationWarning; match by message to avoid importing starlette to # resolve the class path (the drift-check CI job runs without the web deps). "ignore:.*Using .httpx. with .starlette.testclient. is deprecated.*", # FastMCP's Settings model (mcp.server.fastmcp.server) declares a `lifespan` # field whose annotation is a forward reference pydantic_settings can't resolve # at import, so it emits IncompleteFieldDefinitionWarning when memory_bridge # constructs the server (import time). Third-party (mcp + pydantic_settings) — # not fixable in our code; resolved when FastMCP calls model_rebuild() or drops # the forward ref. Without this, `error` makes test_bridge_timeout_pop and # test_lazy_tool_loading un-collectable though memory_bridge imports fine at # runtime. Match by message to avoid importing pydantic_settings.exceptions to # resolve the class (the drift-check CI job runs without that dep). "ignore:.*Field .lifespan. has an incomplete definition.*", # ── (B) our own process-lifetime singletons (by design; not per-test leaks) ─ # M3Client's shared daemon asyncio loop-thread (§8) is alive at test end by # design (torn down at interpreter exit). See tests/test_langchain_*.py comments. "ignore:.*non-main thread.*:UserWarning", # The M3Context SQLite connection pool + shared embedder httpx.AsyncClient are # process singletons closed via atexit/_close_db_pools, not per test — so under # -W error their connections/sockets read as unclosed only as a finalizer-timing # artifact. (Genuine per-test connection/file leaks were fixed at the source.) "ignore:unclosed database.*:ResourceWarning", "ignore:unclosed