When using thread_signal(ThreadID, throw(my_exception)): foreign predicate system:>/2 did not clear exception

I am trying to support cancelling a long running goal in a thread. Moreover, I want to have the goal automatically cancelled by a timeout, but support manual cancellation earlier as well.

I started by running sleep(20000) in a thread started by thread_create/3 with the thread alias goal1.

I then try to cancel it by calling:

% Note that I don't throw abort/0 even though that is a more reliable
% "mostly uncatchable" exception because I want the thread to stick
% around to run more goals
thread_signal(goal1, throw(cancel_goal))

This works as expected and the exception bubbles up the thread properly.

However, if I wrap the goal first in call_with_time_limit/2 like this:

call_with_time_limit(10, sleep(20000))

and then cancel it the same way:

thread_signal(goal1, throw(cancel_goal))

I get this error spit out to the top level:

Thread 4 (goal1): foreign predicate system:>/2 did not clear exception: 
	cancel_goal

and the error that bubbles up is actually time_limit_exceeded, which is the exception that would be thrown normally by the call_with_time_limit/2 predicate on timeout (even though I am well before the timeout).

Am I doing something incorrectly?

[Edit: Here’s the actual code I’m using in case that helps. The issue seems timing related since it doesn’t always happen…]

% A successful test session, I don't seem to be able to repro now...
?- startGoalThread.
true.

?- sendGoalToGoalThread(20000, sleep(10000), []).
true.

?- cancelGoal.
error(cancel_goal)
true.

?- sendGoalToGoalThread(20000, sleep(1), []).
true.

?- cancelGoal.
true([[]])
true.
startGoalThread :-
   thread_self(CommunicationThreadID),
   thread_create(
                goal_thread(CommunicationThreadID),
                _,
                [alias(goal1)]
            ).

sendGoalToGoalThread(Timeout, Goal, BindingList) :-
    thread_send_message(goal1, goal(Goal, BindingList, Timeout)).

cancelGoal :-
    % Insert a known exception into the thread
    thread_signal(goal1, throw(cancel_goal)),
    % Wait for whatever response occurs
    thread_self(SelfID),
    thread_get_message(SelfID, result(Result)),
    writeln(Result).

% The worker predicate for the Goal thread.  Looks for a message, processes it, then recurses.
% Goals always run in the same thread in case the user is setting thread
% local information.
goal_thread(RespondToThreadID) :-
    thread_self(SelfID),
    thread_get_message(SelfID, goal(Goal, BindingList, QueryTimeout)),
    (
        % Exceptions that occur while executing the goal should be returned
        % run all commands in the context of the 'user' module so it acts like
        % the Prolog top level
        QueryTimeout == -1
        ->
        (
            ThreadGoal = catch(findall(BindingList, @(Goal, user), Answers), E, true)
        )
        ;
        (
            ThreadGoal = catch(call_with_time_limit(QueryTimeout, findall(BindingList, @(Goal, user), Answers)), E, true)
        )
    ),
    debug(prologServer(command), "Executing Goal: ~w", [Goal]),
    ThreadGoal,
    (
        var(E)
        ->
        (
            debug(prologServer(command), "Completed Goal: ~w", [Goal]),
            Answers == []
            ->
            (
                thread_send_message(RespondToThreadID, result(false))
            )
            ;
            (
                thread_send_message(RespondToThreadID, result(true(Answers)))
            )
        )
        ;
        (
            debug(prologServer(command), "Exception in Goal: ~w, ~w", [Goal, E]),
            thread_send_message(RespondToThreadID, result(error(E)))
        )
    ),
    % Use tail recursion to avoid growing the stack
    goal_thread(RespondToThreadID).