# CLAUDE.md — matrx-ai

> **Operating Principle: Build the platform, not the artifact.** Every task is a probe that exposes a missing capability — build it, then consume it. Code that only serves one artifact is forbidden. **And extinction is layered, and loud:** a class of failure isn't dead until ≥2 *independent* layers each stop it alone, each SCREAMING when it fires — the second layer is what saves you the day the first silently breaks. Full doctrine: [/PRINCIPLES.md](../../PRINCIPLES.md).

**Package:** `matrx-ai` (PyPI) — Python 3.13+ — currently v0.4.0
**Role in the graph:** Top. Depends on `matrx-utils`, `matrx-connect`, `matrx-graph`, `matrx-runtime`. Host applications inject ORM models (matrx-orm or any compatible shape) via `matrx_ai.configure(db_models=…, db_bases=…)`. Nothing in the Matrx family depends on matrx-ai.

**Conversation executor on the Request Management Layer (built; consumed by aidream `/v2`).** matrx-runtime is the payload-blind substrate that owns the life of a request and its nested tree (identity, nesting, status, cost/metering, context, cancel, resume, lease/reaper). The conversation loop lives HERE as a CONSUMER of `matrx_runtime.ExecutionEngine` — each conversation is one `global_execution` (opaque `type="conversation"`), a sub-conversation/tool is a child execution, token spend is a `MeterEvent`. The pieces (`matrx_ai/orchestrator/`): `conversation_runtime.py::ConversationRunner` (the native loop), `conversation_provider.py::UnifiedProviderTurn` (real client → typed turn), `conversation_persistence.py::CxmConversationPersistence` (cx_ turn I/O via `queue_message_create` + `cxm` — **writes only flush inside a streaming RequestLane**). aidream consumes it at the top-level `/v2` namespace — `POST /api/v2/ai/chat` (+ `/manual`, `/agents/{id}`, `/conversations/{id}`) — today by wrapping the proven loop; the native-loop swap needs token streaming + live cx validation. Design: [docs/runtime/REQUEST_MANAGEMENT_LAYER.md](../../docs/runtime/REQUEST_MANAGEMENT_LAYER.md). (The earlier `orchestrator/v2` adapter was removed — it leaked agent concepts into the substrate; the corrected split keeps runtime generic.)

---

## Read this first

matrx-ai is the unified multi-provider LLM + tool + agent layer. It is also the reference implementation of the **"capability-within, injection-without"** pattern — look at `matrx_ai/__init__.py::configure()` and `matrx_ai/_ext.py` when in doubt about how to expose a new injection point.

The old root-level `ai/` folder has been **fully retired** — everything lives here now. If you find a reference to `from ai.` anywhere, that's a bug somewhere else in the repo; matrx-ai is canonical.

---

## What this package provides

- **Unified client** (`UnifiedAIClient`) — single interface over 12+ provider SDKs (OpenAI, Anthropic, Google GenAI, Groq, Cerebras, xAI, Together, ElevenLabs, Hugging Face, Cohere, Fireworks, Replicate, Generic OpenAI-compatible).
- **Config + message types**: `UnifiedConfig`, `UnifiedMessage`, `MessageList`, `UnifiedResponse`, enums (`Provider`, `Role`, `ContentType`, `FinishReason`), usage trackers (`TokenUsage`, `TimingUsage`, `ToolCallUsage`).
- **Orchestrator**: `AIMatrixRequest`, `CompletedRequest`, `execute_until_complete(...)` — autonomous multi-turn execution with tool loops.
- **Agents** (`matrx_ai.agents`): template + session-based agent system; caching; services. `AgentConfig` carries per-agent `excluded_tools` and `auto_tools_disabled` filters honored by the merge primitive, plus `output_schema` (carried through to `Agent.output_schema`) which drives automatic `STRUCTURED_OUTPUT` event emission at the end of `Agent.execute()` — see `matrx_ai/agents/response_parser.py` for the JSON-extraction funnel and `matrx_ai/agents/output.py` for `parse_agent_output` (the helper agents and tools use anywhere they need to consume their own structured output).
  When a host injects `internal_run_tracker`, its open is a mandatory gate:
  tracker failure refuses the paid execution before `Agent.execute`.
- **Skills** (`matrx_ai.skills`): `DbSkillProvider.list_hints` defaults to the complete visible universe (public + owned) for discovery; callers producing a personal filesystem projection pass `scope="owned"`, which never loads public rows. The returned IDs still go through the same owner/public body gate.
- **Capabilities** (`matrx_ai.capabilities`): client-capability registry — `Capability` dataclass, `ClientContext` envelope, `register_capability()`, `resolve_client_capabilities()`. Built-in bundles: `sandbox-fs` and `browser-dom` (the matrx-extend Chrome extension's 103-tool catalog). Hosts add product-level bundles via `matrx_ai.configure(capabilities=[...])`. See `matrx_ai/capabilities/browser_dom.py` for the reference implementation when adding a new client-type bundle.
- **Tools** (`matrx_ai.tools`): registry + built-in implementations (shell, code, context, context_patch, skill, `load_browser_tools`, …). Plus the unified injection layer:
  - `tools.specs` — `ToolSpec` discriminated union (`RegisteredToolSpec` / `InlineToolSpec` / `AgentToolSpec`).
  - `tools.merge` — `merge_request_tools()`, the single write path that mutates `config.tools` / `config.custom_tools` / `ctx.client_tools` with idempotent dedup and `(kind, delegate)` conflict detection.
  - `tools.agent_projection` — projects saved agents as opaque `custom_tool_N` tools so the model treats them like any other tool. Per-request map on `AppContext.metadata`.
  - `tools.dynamic_drain` — drains queued tool mutations between orchestrator iterations and emits `RESOURCE_CHANGED kind=active_tools` for FE re-render.
  - `tools.ToolContext.queue_tool_changes(add=..., remove=...)` — the mutation API a registered tool calls to load/unload tools mid-loop (Chrome-extension discovery pattern).
  - **Wire names** (`matrx_ai.config.wire_names`) — canonical tool names may carry a namespace colon (`bundle:list_supabase`), but every provider enforces `^[a-zA-Z0-9_-]{1,64}$`. The conversion (`:` → `__`) happens at exactly two seams and NOWHERE else: outbound in `BaseTranslator.build_provider_tools` + the `ToolCallContent`/`ToolResultContent.wire_name` serializers; inbound in `ToolExecutor._normalize_called_name`. A translator that replays a tool-call name MUST use `.wire_name`, never `.name`; everything between the seams (registry, alias map, `config.tools`, persistence) speaks internal names. `tools/validate.py` independently screams if a non-wire-safe name reaches a literal provider payload.
  - Tools that need host-specific context types or hooks (e.g. `ctx_write`) resolve them through **`get_ext(...)` injection** — the host registers `context_object_cls` / `context_manifest_cls` / `load_manifest_from_ctx` / `schedule_context_writeback` etc. in `configure(...)`. **No `aidream` imports.** Unconfigured behaviour is per-hook and must be deliberate: either return `None`/no-op (`get_post_finalize_hook`, `get_model_catalog`, `get_tool_source`) or raise `ExtNotConfiguredError` **at call time**. What is NEVER acceptable is resolving a hook at module import — that turns a missing capability into an un-importable package.
- **Evaluators** (`matrx_ai.evaluators`): the platform **Judge primitive** — `JudgeContract` (declared question, input shape, verdict vocabulary, named rubric + H/V/A provenance), `Judge` with two modes (`compare` ranks against a reference — preferred; `score` is absolute and only ever against a named rubric), `Selector` (pick among N by comparison, never absolute score), the `platform.judge_verdict` accuracy ledger, and Cohen's-kappa calibration. `AIJudge` remains the strict-JSON funnel it is built on. **D-15 is structural here:** a judged unit may never author or see its judge's rubric — three refusal layers, each loud. Contract: [`matrx_ai/evaluators/FEATURE.md`](matrx_ai/evaluators/FEATURE.md).
- **Context** (`matrx_ai.context`): re-exports `AppContext` / `Emitter` from matrx-connect so downstream code can `from matrx_ai.context import AppContext`.
- **Persistence** (`matrx_ai.db`): Supabase-backed conversation/request storage. Backed by host-injected DB models. Every `chat.request` copies the ambient AppContext's paired `execution_kind` / `execution_id`, giving hosts a generic indexed pointer from paid work to the durable execution that incurred it (never metadata scraping). `agx_manager._row_to_config` reads `excluded_tools` / `auto_tools_disabled` off the new `agx_agent.tool_config` JSONB column.
- **Providers** (`matrx_ai.providers`): per-provider translators, request shaping, streaming adapters, parameter modifiers, and the universal adaptive admission gate. **Callers dispatch full independent batches; provider protection belongs only at `UnifiedAIClient`.** Read [`matrx_ai/providers/FEATURE.md`](matrx_ai/providers/FEATURE.md) before changing provider dispatch or batch pacing.
- **Catalog TTS/STT**: TTS model/tier/deprecation/voice facts live in `ai.offering.metadata.tts`
  and model-linked `ai.voices.metadata`; provider clients consume `ResolvedCallProfile` and never
  carry fallback voice/model inventories. Speech-to-text uses the async `STTClient` protocol and
  `execute_stt()` catalog dispatcher; `GroqSTT` is one adapter, not a special call path. Client
  hosts supply the same `tts` envelope on injected model-catalog dicts. Podcast generation owns
  its provider/default-cast policy in `agent_runners/podcast_generator.py`; consumers preview it
  through the host endpoint and must not mirror the host-count routing rule.
- **🚫 NEVER call a provider SDK for a one-off LLM call — use a funnel primitive.** A raw `AsyncAnthropic`/`AsyncOpenAI` client skips the catalog (so it dies on any model name the catalog would have routed), skips cost/usage tracking (a **paid** call that records nothing), skips cancel/disconnect handling, and hand-manages sampling policy — which is how the adaptive-sampling 400 keeps coming back (Claude 4.6+/claude-5 reject ANY `temperature`/`top_p`/`top_k`). All three primitives live in [`graph_nodes/_strict_json.py`](matrx_ai/graph_nodes/_strict_json.py), all route through `execute_ai_request`, and all let you pass `temperature` freely — the translator drops it per `api_class`, so **you never branch on a model name**:
  - `llm_to_pydantic(...)` — structured output: schema in the system prompt, fence-stripped, validated, one retry. Optional `on_delta` streams the FIRST attempt's tokens to the caller (additive display — the repair retry never streams), and optional `wire_kind` injects a leading `__kind` const into the ENFORCED schema (stripped before validation) so a canonical client can kind-resolve the payload while it streams. Tests: `tests/test_llm_to_pydantic_streaming.py`.
  - `llm_to_text(...)` — free text, one shot.
  - `llm_stream_text(..., on_delta=...)` — free text with per-token deltas, for a caller that emits its own typed stream event or drives an incremental parser. Its `_DeltaEmitter` wraps the AppContext emitter for the call and **diverts** `send_chunk` rather than forwarding it — so an internal call (a judge, a classifier) can never leak its raw output into the user's chat.
  A package that must stay provider-agnostic (matrx-rag) reaches an LLM through its own injected `llm_provider` seam instead, which the host backs with these same primitives.

Public API is exactly what `__all__` in `matrx_ai/__init__.py` lists — keep that stable.

---

## The `configure()` injection pattern (canonical reference)

Read `matrx_ai/__init__.py::configure(...)` and `matrx_ai/_ext.py`. This is the pattern all other packages copy.

🚨 **SERVER vs CLIENT — never "with a database" vs "without one" (Arman, 2026-08-16).**
There is NO mode in which matrx-ai runs without a database. It always uses one and everything
always persists; matrx-ai is designed to work with the ORM no matter what — it gets the ORM from
the host (aidream), or it brings its own when standalone. The real distinction is **how it is
running**:

- **As a SERVER** — it has unconditional access and reads platform tables directly through the ORM.
- **As a CLIENT** — the mode **matrx-ai runs in inside matrx-local**. It still has a database and
  still persists everything (matrx-local's is SQLite), and it has full access to everything on the
  client side. What it CANNOT do is the things only a server can do — reach server-only tables. So
  for exactly those, it calls an **API** to get what it used to read directly. If something is
  missing on the client, the fix is to give it the API call it needs; it is never to guess, and
  never to hardcode.

Saying "DB-less" caused real damage: it made agents believe a no-database mode existed, which made
a hardcoded-agent fallback look reasonable. Never write it. The code's own predicate is
`_client_host_without_orm()` — client-vs-server.

**Client-host seams (0.4.0+):** a CLIENT host configures `conversation_store` / `model_catalog` / `api_key_resolver` / `tool_source` / `execution_agent_source` (+ `get_jwt` / `server_url` / `source_app`). The tool registry checks `tool_source` BEFORE the ORM base; with `server_url` + `source_app` set and no explicit source, it derives a `ServerToolSource` that fetches `/ai-tools/app/{source_app}/all`. `agx.load_for_execution()` likewise checks `ExecutionAgentSource` before ORM and runs both paths through the one canonical `ExecutionAgentDefinition -> AgentConfig -> UnifiedConfig` mapper. Listing/catalog projections are never executable inputs: a definition must carry a top-level model plus authored messages or validation refuses execution. The WriteCoordinator, inbox drain, conversation gate, tool logger, and cx_tool_trace sink all short-circuit to the store. The conformance invariant (full streaming request + tool call, ZERO ORM access) is pinned by `tests/client_host/test_no_db_streaming_with_tool.py`; source/mapping parity is pinned by `tests/client_host/test_execution_agent_source.py`.

```python
# aidream/package_integration.py (or any third-party host):
import matrx_ai
from aidream.settings import settings
from matrx_orm import get_supabase_client
from my_app.models import AiModel, TasksBase, ContentBlocksDTO
from my_app.client_capabilities import all_capabilities  # product-level bundles

matrx_ai.configure(
    db_models={"AiModel": AiModel},
    db_bases={"TasksBase": TasksBase},
    db_extras={"ContentBlocksDTO": ContentBlocksDTO},
    settings=settings,
    get_supabase_client=get_supabase_client,
    # Hosts contribute their own client-capability bundles on top of
    # matrx-ai's platform-level built-ins (sandbox-fs, browser-dom).
    # Same registry, layered registration. See aidream/api/client_capabilities.py
    # for an example.
    capabilities=all_capabilities(),
)
```

Host-backed dataset and picklist fetchers are also resolved lazily at call
time. Importing `matrx_ai.db.content_types.datasets` or `picklists` must work
before the host registers `dataset_reference_fetch` / `picklist_reference_fetch`.

**Mandate seam.** `matrx_ai.mandates.set_mandate_resolver(...)` accepts an async
host resolver returning `MandateResolution(source, config_overrides, complete)`.
The optional `complete(result, variables, user_input)` callback runs after the
exact resolved source finishes, allowing a host to structurally check output
and capture exemplars without any host import in this package.

🚨 **THERE IS NO SEED FALLBACK.** A class with a `mandate_key` resolves or the run raises
`MandateResolutionUnavailable` — no installed resolver and a failing resolver both refuse, after
calling the injected `record_error` seam with `mandate_resolution_failed`. **Every host must
install a resolver**: ORM-backed when the process owns a database (aidream's, or matrx-ai's own
standalone), HTTP-backed when matrx-ai runs as a CLIENT — the same shape as `ServerToolSource`.
Running the agent id written in a class body is never the answer; it is a paid call against an
agent nobody chose, months stale, immune to every org/user binding.

`_ext.py` stores injected values in a module-level dict; `get_ext("settings")` retrieves them. If a feature path tries to access something that wasn't configured, it raises `ExtNotConfiguredError` at **call time**, never at import time — so `import matrx_ai` always works in a minimal environment.

The `configure(db_models=…, db_bases=…, db_instances=…, db_extras=…)` API is unchanged — any host may pass the dicts directly. **The aidream host does not hand-write them:** what it needs is declared in the data-only manifest [`matrx_ai/db/db_requirements.py`](matrx_ai/db/db_requirements.py) (in THIS package) and resolved into the injected dicts by `python db/generate.py`. Add a table matrx-ai needs → add it to that manifest, not to aidream's `package_integration.py`. Consumer contract: aidream `_generated/FEATURE.md`.

**Host side-effect seams** the orchestrator calls (both optional, both in `_ext.py`, both no-op when unconfigured so matrx-ai stays standalone): `get_post_finalize_hook()` — fire-and-forget after a turn finalizes; `get_matrx_directives_dispatcher()` — **awaited** after the final structured output parses, before `send_end`, so an `__matrx_apply` envelope applies as a durable side effect that has the last word. matrx-ai never knows what either does. Reserved-key/registry semantics live in the host ([`aidream/services/output_directives/FEATURE.md`](../../aidream/services/output_directives/FEATURE.md)).

When adding a new cross-boundary dependency:

1. Add a new keyword arg to `configure(...)`.
2. Store it in `_ext.py`.
3. Access it via `get_ext("name")` at the call site (not at module top).
4. Document the new injection point in both this file and `aidream/package_integration.py`.

---

## Dependency rules specific to this package

- ✅ `from matrx_utils import …`, `from matrx_connect import …`, `from matrx_graph import …` — these are declared workspace deps.
- ✅ Provider SDKs: `anthropic`, `openai`, `google-genai`, `groq`, `cerebras_cloud_sdk`, `together`, `xai-sdk`, `cohere`, `fireworks-ai`, `replicate`, `elevenlabs`, `huggingface_hub`.
- ❌ No `from matrx_orm import …` at module top-level. The host injects ORM models. If matrx-orm is literally needed inside the package, import it lazily inside a function and treat `ImportError` as "feature unavailable".
- ❌ No `from aidream import …`. The `package-boundaries` CI gate enforces this against `scripts/package_boundaries_baseline.json` — a FRESH violation fails the build, but the baseline still GRANDFATHERS legacy matrx-ai violations (check the baseline before claiming zero; the ratchet only goes down).
- ❌ No root-module imports (`common`, `config`, `api_management`, `utils`).

---

## Streaming & context

- Emitters and `AppContext` come from matrx-connect. Don't re-implement them.
- Tools and agents that need request identity call `from matrx_connect import get_app_context`.
- Child agents fork context (see `matrx_ai.orchestrator` and `matrx_ai.agents` conventions) — they inherit the parent emitter but get their own request scope.
- Every long-running step emits a structured Pydantic data event. The frontend depends on that typed contract.

---

## Tool-dispatch telemetry — dual sink (file + DB)

Every call through `ToolExecutor.execute` emits to two sinks. They share the same caller in [`tools/executor.py`](matrx_ai/tools/executor.py); both are best-effort, both are gated by their own env knob.

- **File sink** — [`tools/_debug_log.py`](matrx_ai/tools/_debug_log.py). Synchronous append to `<repo>/.matrx-debug/tool-trace-<process-start>.log`. Maintains `current.log` and per-conv `conv-<short>.log` symlinks for fast tailing. Auto-deletes header-only files (≤100 bytes) from prior process runs. Disable with `MATRX_TOOL_DEBUG_LOG_DISABLED=1`.
- **DB sink** — [`tools/_db_log.py`](matrx_ai/tools/_db_log.py). `asyncio.create_task(db_log_event(...))` schedules an INSERT into `cx_tool_trace` (host-injected manager via the `cxm` bundle). Never awaited inline. Disable with `MATRX_TOOL_DEBUG_DB_DISABLED=1`.
- **Ambient trace tags** — a scope labels every trace row it produces by putting a dict on `AppContext.metadata[TRACE_TAGS_CONTEXT_KEY]` (exported from `matrx_ai.tools`); the DB sink merges it into `chat.tool_trace.metadata` without displacing the row's own keys. **The tool layer must never learn what the label MEANS** — it carries whatever the scope declared. First consumer: aidream's Hindsight replay, which tags each dispatch with `{kind: hindsight_replay, database: mirror|live, …}` so a verdict's tool behaviour is auditable and distinguishable from a user's own. Tests: `tests/test_trace_tags.py`.

The DB sink **always stores bounded args + `result_preview` for successful calls** so audit consumers can fact-check agent claims; `_db_log.py` caps each heavy field at 4 KiB. `MATRX_TOOL_DEBUG_VERBOSE=1` adds the same detail to the file sink for local investigation. Don't ship verbose file logging in production — file size grows fast.

**Arg recovery (reject less).** A rejected tool call still costs a full provider turn. Before dispatch the executor recovers when it can: missing `action`/`command` inferred from variant-unique fields (`INFERRED`), JSON-string containers decoded (`COERCED`), and clear action menus when inference is ambiguous. Contract: [`tools/ARG_RECOVERY.md`](matrx_ai/tools/ARG_RECOVERY.md).

**`ToolResult.output` validator**. The Pydantic `@field_validator` on [`tools/models.py::ToolResult`](matrx_ai/tools/models.py) rejects outputs containing a string field that parses as a Python literal or JSON object/array — the silent-failure shape from `str(error_dict)` / `json.dumps(error_dict)` antipatterns. When it fires it prints a loud red banner (full output dict, no truncation, self-identifying) and raises `ToolOutputContractError`. Full reference and "how to fix" recipe: [`TOOL_OUTPUT_VALIDATION_GATE.md`](matrx_ai/tools/TOOL_OUTPUT_VALIDATION_GATE.md). **There is no per-field opt-out — if a field looks like stringified JSON, return the native dict/list instead.** Tools that catch the gate's exception should unwrap it via the `_extract_gate_message` helper pattern in [`implementations/ctx.py`](matrx_ai/tools/implementations/ctx.py) so the stream emitter shows the clean message instead of Pydantic's wrap. Gate-triggered errors carry `error_type="matrx_validation_gate"` in `cx_tool_trace` so they're greppable separately from real failures.

**🚨 A tool error must NEVER destroy its traceback.** In an `except` block, build the error with **`ToolError.from_exception(exc, error_type=..., message=...)`** (same kwargs, plus it captures `exc.__traceback__`) — or just let the exception propagate, since `ToolExecutor.execute` already catches it and records the stack. **Forbidden: `ToolError(error_type=..., message=f"... {exc}")`** — that flattens the exception to a string before any sink can see it, so the failure reaches the operator with no file, no line, no stack. This is not theoretical: on 2026-07-13 *every* `rag_search` call failed with `invalid input for query argument $5: 0 (expected str, got int)` and the server log showed **nothing** — the real defect (an anchorless `CASE` param in matrx-orm that Postgres silently types as `text`) had to be reproduced by hand. Two layers now enforce it: `tests/test_no_traceback_destroying_tool_errors.py` forbids the antipattern in `implementations/` (build-failing), and the executor **SCREAMS a red `TOOL CALL FAILED` banner to the console on every FAIL** — naming the tool, args, error, and the stack (and saying so explicitly when a tool swallowed it). The traceback also rides into both sinks (`chat.tool_trace.metadata.traceback`). The two durable sinks are silent by design; the console is not.

**Embedded-envelope flip**. After dispatch, the executor checks `result.success is True AND isinstance(output, dict) AND output.get("success") is False` and flips the outer flag to `False` with `err_type="embedded_error_envelope"`. This is defense-in-depth against the same antipattern — both the file sink, DB sink, and `cx_tool_call` row then record the truth. If you see this error type in a trace, the tool implementation needs fixing (mirror the `usertable_create_advanced` caller pattern in [`datasets_tools.py:485`](matrx_ai/tools/implementations/datasets_tools.py#L485)).

---

## Python standards (same as root)

- Full type hints. Pydantic everywhere for provider request/response shapes.
- No docstrings except on the public API symbols in `__init__.py` (keep them one line).
- Hot paths: streaming token handlers, tool-call dispatch, conversation persistence writes. No dict allocations in the per-chunk path.
- Provider SDK error types must be caught explicitly and translated to `UnifiedResponse` / emitter events. Never swallow a provider exception.

---

## Testing this package in isolation

```bash
uv run pytest packages/matrx-ai/tests
```

Tests must pass in an environment with just matrx-utils, matrx-connect, matrx-graph, and the provider SDKs installed. ORM-dependent paths should be unit-tested with mocked injection, not a live database. If a test imports from `aidream`, move it out of this package.

**Persistence unit tests never write operational incidents.** The autouse
`capture_sink` in [`tests/persistence/conftest.py`](tests/persistence/conftest.py)
intercepts `record_error` and `record_failures` in memory; tests that verify the
capture contract request that fixture and inspect its lists.

---

## Documentation inside the package

- `matrx_ai/MODULE_README.md` — deep per-subsystem reference (~90 KB). Good for orientation; keep it in sync with code.
- `matrx_ai_implementation_master_plan.md` and `REMAINING_TASKS.md` — historical roadmap / open items. Not authoritative; refer to root `PACKAGES_MIGRATION_PLAN.md` for the live list.

---

## Known issues (tracked in root `PACKAGES_MIGRATION_PLAN.md`)

**Package-boundary status:** no NEW violations are possible (CI gate), but `scripts/package_boundaries_baseline.json` still grandfathers legacy matrx-ai entries — burn them down, never add. The 2026-07-13 client-host wave removed the `from db.…` skill/backfill couplings (routed through `db/_registry`); the remaining baseline entries are the next targets.

---

## Tool injection refactor — current state

The unified tool injection rollout (design history: `docs/archive/2026/cx_chat__TOOL_INJECTION_REFACTOR.md`) is **complete** for everything matrx-ai owns: `ToolSpec` types, `merge_request_tools` primitive, capability registry, agent projection, and dynamic mid-loop injection. Registered discovery additions persist through `UnifiedConfig.dynamic_tools`, so cache hits and DB reconstruction restore them before current-request authority/executor gates run. Inline definitions and agent-projection maps remain request-scoped until the full typed state moves onto `cx_conversation.dynamic_tool_state`.

The **single per-request entry point** for the host application is
`aidream/services/tooling/tool_merge.py::apply_unified_tools`. matrx-ai exposes
the building blocks (`ToolSpec`, `merge_request_tools`,
`resolve_client_capabilities`, `resolve_agent_specs`) but the per-route
orchestration lives in aidream. New hosts that consume matrx-ai
standalone (matrx-local, sandboxes) should mirror that helper.
