Circular rlm_completion <-> rlm_direct import silently drops rlm_completion's local text_string/2 (and its completion_fault clause) when the host user module defines text_string/2 #302

Closed
opened 2026-09-02 17:25:36 +00:00 by nsaspy · 2 comments
Owner

Summary

rlm_completion and rlm_direct now import from each other (introduced by a89175b, "feat: run typed-plan model steps through native provider sessions"):

  • prolog/rlm_completion.pl:66 — :- use_module(rlm_direct, [rlm_direct_model_step/10]).
  • prolog/rlm_direct.pl:13 — :- use_module(rlm_completion, [... text_string/2 ...]).

When a host's user module already defines text_string/2 (any init-file-style host can legitimately do this), SWI-Prolog resolves rlm_direct's import of the not-yet-defined rlm_completion:text_string/2 against user. The import is then registered in rlm_completion as imported from user, and rlm_completion's own definition at lines 2634-2636 is rejected:

ERROR: .../prolog/rlm_completion.pl:2634:
ERROR:    No permission to redefine imported_procedure `text_string/2'
ERROR: .../prolog/rlm_completion.pl:2635:  (same)
ERROR: .../prolog/rlm_completion.pl:2636:  (same)

Consequences

  1. Load-time ERROR noise for every host that trips it (this is how the Agent Zero runtime worker host found it; loading is otherwise fine).
  2. rlm_completion:text_string/2 becomes imported_from(user) instead of the module-local definition.
  3. Validation semantics drift: the third local clause
    text_string(Value, _) :- throw(completion_fault(expected_text(Value))).
    
    is lost. Verified on main (6957c30): with the collision present,
    rlm_completion:text_string(foo(1), _) fails without exception instead of raising completion_fault(expected_text(foo(1))). Callers that rely on the fault envelope see a plain failure instead.

Reproducer (SWI-Prolog 10.0.2, main @ 6957c30)

:- multifile user:text_string/2.
:- dynamic user:text_string/2.

user:text_string(Value, Text) :- string(Value), !, Text = Value.
user:text_string(Value, Text) :- atom(Value), !, atom_string(Value, Text).

main :-
    asserta(user:file_search_path(library, '<repo>/prolog')),
    load_files('<repo>/prolog/rlm.pl', [silent(true)]),
    predicate_property(rlm_completion:text_string(_, _), imported_from(M)),
    format('imported_from: ~w~n', [M]),   % user, not local
    \+ catch(rlm_completion:text_string(foo(1), _), _, false),
    writeln('completion_fault(expected_text/1) lost: non-text input fails silently'),
    halt.

The same repro fires through a user-module init-file host even without the explicit user:text_string/2 above when the host defines its own same-named helper (our worker defined one).

Suggested fix

Break the import cycle for the shared text helpers: move text_string/2 (and any other helpers rlm_direct needs) into a leaf module (e.g. a small rlm_text/rlm_util) that both rlm_completion and rlm_direct import, or duplicate the three-clause helper locally in rlm_direct. Either way, a runtime module should not import validation helpers from a module that (transitively) imports it back; module resolution then becomes load-order and host-context dependent.

## Summary `rlm_completion` and `rlm_direct` now import from each other (introduced by a89175b, "feat: run typed-plan model steps through native provider sessions"): - `prolog/rlm_completion.pl:66` — `:- use_module(rlm_direct, [rlm_direct_model_step/10]).` - `prolog/rlm_direct.pl:13` — `:- use_module(rlm_completion, [... text_string/2 ...]).` When a host's `user` module already defines `text_string/2` (any init-file-style host can legitimately do this), SWI-Prolog resolves `rlm_direct`'s import of the not-yet-defined `rlm_completion:text_string/2` against `user`. The import is then registered in `rlm_completion` as *imported from user*, and `rlm_completion`'s own definition at lines 2634-2636 is rejected: ``` ERROR: .../prolog/rlm_completion.pl:2634: ERROR: No permission to redefine imported_procedure `text_string/2' ERROR: .../prolog/rlm_completion.pl:2635: (same) ERROR: .../prolog/rlm_completion.pl:2636: (same) ``` ## Consequences 1. Load-time ERROR noise for every host that trips it (this is how the Agent Zero runtime worker host found it; loading is otherwise fine). 2. `rlm_completion:text_string/2` becomes `imported_from(user)` instead of the module-local definition. 3. **Validation semantics drift**: the third local clause ```prolog text_string(Value, _) :- throw(completion_fault(expected_text(Value))). ``` is lost. Verified on main (6957c30): with the collision present, `rlm_completion:text_string(foo(1), _)` **fails without exception** instead of raising `completion_fault(expected_text(foo(1)))`. Callers that rely on the fault envelope see a plain failure instead. ## Reproducer (SWI-Prolog 10.0.2, main @ 6957c30) ```prolog :- multifile user:text_string/2. :- dynamic user:text_string/2. user:text_string(Value, Text) :- string(Value), !, Text = Value. user:text_string(Value, Text) :- atom(Value), !, atom_string(Value, Text). main :- asserta(user:file_search_path(library, '<repo>/prolog')), load_files('<repo>/prolog/rlm.pl', [silent(true)]), predicate_property(rlm_completion:text_string(_, _), imported_from(M)), format('imported_from: ~w~n', [M]), % user, not local \+ catch(rlm_completion:text_string(foo(1), _), _, false), writeln('completion_fault(expected_text/1) lost: non-text input fails silently'), halt. ``` The same repro fires through a `user`-module init-file host even without the explicit `user:text_string/2` above when the host defines its own same-named helper (our worker defined one). ## Suggested fix Break the import cycle for the shared text helpers: move `text_string/2` (and any other helpers rlm_direct needs) into a leaf module (e.g. a small `rlm_text`/`rlm_util`) that both `rlm_completion` and `rlm_direct` import, or duplicate the three-clause helper locally in `rlm_direct`. Either way, a runtime module should not import validation helpers from a module that (transitively) imports it back; module resolution then becomes load-order and host-context dependent.
nsaspy 2026-09-02 17:25:36 +00:00
  • closed this issue
  • added the
    bug
    label
Author
Owner

Fix in #331 (fresh-process hostile-host regression across three load orders; load-order cycle boundary plus rlm_text leaf module).

Fix in #331 (fresh-process hostile-host regression across three load orders; load-order cycle boundary plus rlm_text leaf module).
Author
Owner

Completed by #331 (merged as eed1639): rlm_text leaf module + load-order cycle boundary; fresh-process hostile-host regression across three load orders is in the deterministic corpus.

Completed by #331 (merged as eed1639): rlm_text leaf module + load-order cycle boundary; fresh-process hostile-host regression across three load orders is in the deterministic corpus.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
nsaspy/prolog-rlm#302
No description provided.