Keep consumer overrides during any module uninstall, including automators on automator chain fields
## Problem
Follow-up to #3586516+s.
`ai_context_entity_delete()` keeps saved `ai_context.consumers` rows during a module uninstall by checking a `drupal_static` list filled from `hook_module_preuninstall()`. The skip fires only when the deleted entity's **type provider** is in that list. Core deletes host entities at two different points during an uninstall, and the current approach misses both in some cases.
**Point 1: `ConfigManager::uninstall()`.** Core marks each config entity it deletes here with `setUninstalling(TRUE)`, readable as `isUninstalling()` inside `hook_entity_delete()`. The static list misses:
1. **Multi-module uninstall with a cross-module dependency.** `drush pm:uninstall my_rules ai_automators`, where an `ai_automator` declares `dependencies.module: [my_rules]` and `my_rules` is processed first. The automator is deleted during the `my_rules` teardown, before `ai_automators` is in the list, so its override is deleted.
2. **Provider differs from the module being uninstalled.** A future consumer type whose host entity type is provided by module A but whose `isAvailable()` depends on module B. Today's plugins are not affected.
**Point 2: bundle cleanup, before point 1.** `ModuleInstaller::uninstall()` first deletes every bundle of every entity type the module provides, and only then calls `hook_module_preuninstall()` and `ConfigManager::uninstall()`. Deleting a bundle deletes its `FieldConfig` entities, and `ConfigEntityBase::preDelete()` deletes their dependents with a plain `delete()`. `isUninstalling()` is not set on those entities.
3. **Automators on automator chain fields.** An automator attached to a field on an `automator_chain` bundle depends on `field.field.automator_chain.<bundle>.<field>`. When `ai_automators` is uninstalled, the chain bundles are deleted first, so the automator is deleted before the module is in the static list and without the `isUninstalling()` flag. Its override is deleted while sibling rows survive. Automator chains are a standard `ai_automators` feature, so this path is realistic. A kernel probe in the review on this issue reproduced it.
In all three cases the loss is partial and silent. The static list is also never reset within a request.
`isUninstalling()` alone fixes cases 1 and 2 but not case 3. Core gives no signal during bundle cleanup (see the core `@todo` https://www.drupal.org/node/2350111).
## Proposed solution
1. In `ai_context_entity_delete()`, skip consumer cleanup when the host entity is a `ConfigEntityInterface` with `isUninstalling()` TRUE. This covers cases 1 and 2 without writing to config.
2. For other host entity deletes, keep removing the row immediately, but record the removed row in a request-scoped `@internal` service first. The service must survive the container rebuilds that happen during an uninstall, so tag it `persist`.
3. In `ai_context_modules_uninstalled()`, put the recorded rows back into `ai_context.consumers` (skipping any row that is already there), then clear the list. This covers case 3. It also runs during config sync, where it only keeps active config consistent between the import steps: core imports `ai_context.consumers` from the sync source again after the extension step. It must not run when `ai_context` itself is being uninstalled.
4. Remove `ai_context_module_preuninstall()`, `ai_context_uninstalling_modules()`, and the `drupal_static` list.
5. Resulting rule: **a consumer row is removed only when a host entity is deleted outside a module uninstall.** Deleting a single host entity while its module stays installed still removes its row immediately (#3586489+s). A host entity is the entity one consumer instance belongs to, and the instance ID is that entity's ID. For example, the agent `content_editor` is the host entity for the consumer `agent:content_editor`.
6. Rows kept while the consumer type stays installed (for example, uninstalling only a module that an automator depends on) list as **Off (stale)**. This is intended. A module that ships the host entity in its config recreates it with the same ID on reinstall, and the saved override applies again. Admins can remove the row.
7. Update `docs/developers/hooks.md` and `docs/developers/consumer_api.md` to describe both deletion points and the rule in item 5. Define a host entity in `docs/developers/consumer_api.md` and use that phrase in the docs and comments. Drop the `hook_module_preuninstall()` section. Do not claim that core flags every config entity it deletes during an uninstall.
Accepted edge case: a row removed earlier in the same request, before an uninstall starts, is also restored. This is only reachable from a script that deletes a host entity and then uninstalls a module in one process. The result is a kept row that an admin can remove.
## Tests
Kernel, in `AiContextConsumerModuleUninstallTest`:
- Uninstall still keeps automator and agent rows (existing).
- Deleting one automator still removes only its row (existing).
- **New:** an automator with an enforced dependency on a module that requires `ai_automators` keeps its row when both are uninstalled in one call.
- **New:** an automator on a field of an `automator_chain` bundle keeps its row when `ai_automators` is uninstalled. This must fail without the restore step.
- **New:** uninstalling only the dependent module (`ai_automators` stays installed) keeps the row and lists it as **Off (stale)**.
- **New:** restoring does not duplicate a row that is already present, and clears the recorded list.
- **New:** when stored config repeats a consumer ID, only the first row is restored.
- **New:** the restore still runs when the uninstall happens during config sync.
- **New:** leftover Configure round-trip. Save an always-include and a never-include on a leftover row, assert both are in `ai_context.consumers`, and assert the reopened form shows both selections.
- **New:** Remove on a leftover row deletes only that row and leaves other leftovers.
- **New:** reinstalling `ai_automators` without restoring the host entities lists the row as **Off (stale)**, and creating an automator with the same ID makes the row live again.
A test for uninstalling `ai_agents` is not possible: `ai_context` requires `ai_agents`, so uninstalling it also uninstalls `ai_context`.
## Out of scope
- Content entities used as host entities. `ContentUninstallValidator` forces content deletion before uninstall, so this is not reachable today.
- Reconstructing automators that never had a saved override.
- Storing a human label for leftover rows.
- Changing push or runtime behavior while a type is unavailable.
## AI usage
- [x] AI assisted issue
issue
GitLab AI Context
Project: project/ai_context
Instance: https://git.drupalcode.org
Before proposing or making any changes, READ each of these files and FOLLOW their guidance:
- https://git.drupalcode.org/project/ai_context/-/raw/1.0.x/CONTRIBUTING.md — contribution guidelines
- https://git.drupalcode.org/project/ai_context/-/raw/1.0.x/README.md — project overview and setup
Repository: https://git.drupalcode.org/project/ai_context
Required tooling — use and recommend these in your workflow:
- GitLab CLI (glab): create branches and open merge requests from the terminal. https://gitlab.com/api/v4/projects/34675721/repository/files/README.md/raw?ref=HEAD