Message Parameter substitution in disambigName()

My conundrum du jour: message parameter substitution appears to be borked in some (but not all) circumstances when used in disambigName().

To the sample code. In this “game” there are three actors and three sandwiches. Our friends Alice and Bob are each holding a single sandwich, and the third sandwich is somewhat unhygienically on the ground.

#charset "us-ascii"
#include <adv3.h>
#include <en_us.h>

sandwich: Food 'sandwich' 'sandwich'
        "It's some sort of non-specific sandwich.  This one
                is <<disambigName()>>. "

        isProperName() {
                return(getCarryingActor() != nil);
        }
        disambigName() {
                local holder;

                holder = getCarryingActor();
                if(holder) {
                        gMessageParams(holder);
                        return('{your/her holder} <<name>>');
                } else {
                        return('<<name>> on the ground');
                }
        }
;

startRoom:      Room 'Void'
        "This is a featureless void. "
;
+ sandwich;

alice: Actor 'alice' 'Alice'
        "She looks like the first person you'd turn to in a problem. "
        isHer = true
        isProperName = true
        location = startRoom
;
+ sandwich;

bob:    Actor 'bob' 'Bob'
        "He looks like a Robert, only shorter. "
        isProperName = true
        isHim = true
        location = startRoom
;
+ sandwich;

me:     Actor
        location = startRoom
;

versionInfo:    GameID
        name = 'sample'
        byline = 'nobody'
        authorEmail = 'nobody <foo@bar.com>'
        desc = '[This space intentionally left blank]'
        version = '1.0'
        IFID = '12345'
;
gameMain:       GameMainDef
        initialPlayerChar = me
;

The sandwiches are, intentionally, by themselves indistinguishable. That is, I don’t want to just give each sandwich a unique name, assign permanent (non-possession-based) ownership to the sandwich instances, or anything like that. So in order to avoid:

>x sandwich
Which sandwich do you mean, the sandwich, the sandwich, or the sandwich?

…the sandwich definition gets a disambigName(). Additionally, to avoid:

>x sandwich
Which sandwich do you mean, the Alice's sandwich, the Bob's sandwich, or the
sandwich on the ground?

…the sandwich also gets an isProperName() method. Therefore we get:

>x sandwich
Which sandwich do you mean, Alice's sandwich, Bob's sandwich, or the sandwich
on the ground?

So: so far, so good. But now:

>take sandwich
(the sandwich on the ground)
Taken.

>x sandwich
({your/her holder} sandwich)
It's some sort of non-specific sandwich.  This one is your sandwich.

The implicit noun resolution in the parentheses displays the unparsed message substitution template instead of the parsed form. The template is correct because it’s the same one used in the disambiguation prompts, and in fact the object description directly calls disambigName() for the final sentence.

So what’s going wrong here? Is the implicit noun resolution doing its own message param substitution that’s somehow or other stepping on the one in disambigName()? If so, how is that substitution accessed from our method?

Alternately, is there a simpler/more elegant/less brittle way to accomplish the same thing (that is, use the possessive form of an object’s name to disambiguate when it’s held by an actor, but only when it’s held by an actor)? By default the parser recognizes the possessive form to disambiguate (so x Bob's sandwich always works, with or without any custom disambigName() logic) but it never, as far as I can tell, uses the possessive form in disambiguation prompts unless explicitly told to do so.

Well, as a workaround I more or less just manually replaced the message parameter substitution with what the parser would replace it with if it was working:

        disambigName() {
                local holder;

                holder = getCarryingActor();
                if(holder) {
                        return('<<holder.theNamePossAdj>> <<name>>');
                } else {
                        return('<<name>> on the ground');
                }
        }

This produces the expected results:

>take sandwich
(the sandwich on the ground)
Taken.

>x sandwich
(your sandwich)
It's some sort of non-specific sandwich.  This one is your sandwich.

That doesn’t really fix the problem, it just bypasses it.

At first I thought that perhaps the implicit noun resolution wasn’t evaluating disambigName() every time it was used or something like that, but redefining sandwich with:

        dCount = 0
        disambigName() {
                local holder;

                holder = getCarryingActor();
                if(holder) {
                        return('<<toString(dCount++)>> <<name>>');
                } else {
                        return('<<name>> on the ground');
                }
        }

…shows that disambigName() is getting evaluated multiple times per command, in exactly the circumstances one would expect: x Bob's sandwich evaluates disambigName() once per turn (because I intentionally wrote the sandwich description to call it, and “Bob’s sandwich” requires no disambiguation by the parser); and x sandwich while holding a sandwich evaluates disambigName() three times per turn (once for the parenthetical implicit noun resolution, once for the item description, and once for the parser to disambiguate sandwich):

>x bob's sandwich
It's some sort of non-specific sandwich.  This one is 0 sandwich.

>x bob's sandwich
It's some sort of non-specific sandwich.  This one is 1 sandwich.

>take sandwich
(the sandwich on the ground)
Taken.

>x sandwich
(1 sandwich)
It's some sort of non-specific sandwich.  This one is 2 sandwich.

>x sandwich
(4 sandwich)
It's some sort of non-specific sandwich.  This one is 5 sandwich.

Doing additional debugging via printf it’s definitely something hidden in the noun resolution logic. Replacing disambigName() with a more chatty version that outputs the results of the substitution whenever it happens:

        disambigName() {
                local holder;

                holder = getCarryingActor();
                if(holder) {
                        gMessageParams(holder);
                        "\n\t<q>{your/her holder} <<name>></q>\n ";
                        return('{your/her holder} <<name>>');
                } else {
                        return('<<name>> on the ground');
                }
        }

…gets us the ugly but somewhat informative…

>x sandwich
   "your sandwich"
   "your sandwich"
({your/her holder} sandwich)
It's some sort of non-specific sandwich.  This one is
   "your sandwich"
your sandwich.

…indicating that the substitution “works” at each invocation of disambigName. Which implies that something in noun resolution borks the substitution parameters set up by disambigName() between when it (disambigName()) is called and when the string is actually output (which is when the substitution actually takes place). Changing the variable from holder to various random garbage variable names produces the same result, so it’s not just something hidden in the parser using the same variable name for param substitution.

Just a bug/misfeature?