Reclaim loader state when registries are destroyed #133

Merged
lost-rob0t merged 5 commits from fix/67-loader-registry-cleanup into main 2026-08-20 20:47:43 +00:00
lost-rob0t commented 2026-08-19 20:00:21 +00:00 (Migrated from github.com)

Closes #67.

Runtime invariant

Destroying a tool registry reclaims the rlm_tool_loader idempotency bookkeeping owned by that registry when the loader module is present. Registry ownership remains in rlm_tool; core does not import the loader, and capability/authority/MCP semantics do not change.

The invariant also holds when registry destruction races with a loader that already registered its tool but has not yet returned. A late successful loader return cannot resurrect bookkeeping for a dead registry.

Implementation

  • subscribe rlm_tool_loader to SWI-Prolog's built-in predicate lifecycle events for the core registry-liveness predicate;
  • use a named listener so module reloads replace the registration rather than stacking duplicate callbacks;
  • reclaim loader state only on the completed retractall lifecycle event for the affected registry ID;
  • after a trusted loader returns successfully, record its bookkeeping and immediately validate canonical registry liveness; if destruction won the race, reclaim the row and return structured registry_destroyed rather than preserving stale state;
  • retain rlm_tool_loader_forget_registry/1 as an explicit idempotent host operation;
  • remove the test helper's manual loader cleanup so ordinary loader tests exercise real registry destruction;
  • add deterministic coverage for automatic reclamation, cross-registry isolation, 32 create/load/destroy churn cycles, a registry that never used the loader, and a barrier-controlled destroy-vs-loader-return race;
  • reconcile docs/external-tool-libraries.md with the automatic cleanup contract.

Compatibility audit

The repository CI runtime is SWI-Prolog 9.0.4. The exact upstream V9.0.4 source was checked before retaining this design: prolog_listen/3 supports the named listener option and predicate retractall lifecycle events carry the module-qualified end(Head) form handled here.

Non-goals

  • no concrete filesystem/git/process/network tool implementations;
  • no capability or authority changes;
  • no MCP lifecycle redesign;
  • no registry ID reuse;
  • no DeepSeek Harness, AgentProlog UI, prompt compiler, skill compiler, or config-runtime changes.

Verification

Tests for the base lifecycle invariant were added before the implementation, and the later race test was added before the race hardening. No sleeps are used for correctness synchronization.

The exact PR-head GitHub Actions gates are authoritative because local SWI-Prolog is unavailable in this environment. Merge only after deterministic and required live gates are green; do not weaken or skip provider gates to force a merge.

Closes #67. ## Runtime invariant Destroying a tool registry reclaims the `rlm_tool_loader` idempotency bookkeeping owned by that registry when the loader module is present. Registry ownership remains in `rlm_tool`; core does not import the loader, and capability/authority/MCP semantics do not change. The invariant also holds when registry destruction races with a loader that already registered its tool but has not yet returned. A late successful loader return cannot resurrect bookkeeping for a dead registry. ## Implementation - subscribe `rlm_tool_loader` to SWI-Prolog's built-in predicate lifecycle events for the core registry-liveness predicate; - use a named listener so module reloads replace the registration rather than stacking duplicate callbacks; - reclaim loader state only on the completed `retractall` lifecycle event for the affected registry ID; - after a trusted loader returns successfully, record its bookkeeping and immediately validate canonical registry liveness; if destruction won the race, reclaim the row and return structured `registry_destroyed` rather than preserving stale state; - retain `rlm_tool_loader_forget_registry/1` as an explicit idempotent host operation; - remove the test helper's manual loader cleanup so ordinary loader tests exercise real registry destruction; - add deterministic coverage for automatic reclamation, cross-registry isolation, 32 create/load/destroy churn cycles, a registry that never used the loader, and a barrier-controlled destroy-vs-loader-return race; - reconcile `docs/external-tool-libraries.md` with the automatic cleanup contract. ## Compatibility audit The repository CI runtime is SWI-Prolog 9.0.4. The exact upstream V9.0.4 source was checked before retaining this design: `prolog_listen/3` supports the named listener option and predicate `retractall` lifecycle events carry the module-qualified `end(Head)` form handled here. ## Non-goals - no concrete filesystem/git/process/network tool implementations; - no capability or authority changes; - no MCP lifecycle redesign; - no registry ID reuse; - no DeepSeek Harness, AgentProlog UI, prompt compiler, skill compiler, or config-runtime changes. ## Verification Tests for the base lifecycle invariant were added before the implementation, and the later race test was added before the race hardening. No sleeps are used for correctness synchronization. The exact PR-head GitHub Actions gates are authoritative because local SWI-Prolog is unavailable in this environment. Merge only after deterministic and required live gates are green; do not weaken or skip provider gates to force a merge.
Sign in to join this conversation.
No description provided.