Skip to content

fix(channels): prevent cold approval path from swallowing normal chat (#1164)#1165

Merged
Aaronontheweb merged 2 commits into
netclaw-dev:devfrom
Aaronontheweb:fix/cold-approval-false-positive
May 25, 2026
Merged

fix(channels): prevent cold approval path from swallowing normal chat (#1164)#1165
Aaronontheweb merged 2 commits into
netclaw-dev:devfrom
Aaronontheweb:fix/cold-approval-false-positive

Conversation

@Aaronontheweb

Copy link
Copy Markdown
Collaborator

Summary

  • Add HasApprovalHistory check in LlmSessionActor.TryResolveTextApprovalResponse — when no approval has ever been requested, return approval_no_history nack instead of emitting an expired notice
  • All three channel bindings (Slack, Discord, Mattermost) treat approval_no_history as "not consumed" so the message falls through to normal LLM ingress
  • New cross-channel contract test validates the fix on all three bindings at once

Problem

LooksLikeApprovalResponse matches single letters a-e and numbers 1-5 by design. When the session has never had an approval request, this produced a false positive: normal chat like "yes" or "a" was consumed by the cold approval path and a phantom "approval prompt expired" notice appeared to the user.

Changes

File Change
LlmSessionActor.cs HasApprovalHistory property + approval_no_history nack path
SlackThreadBindingActor.cs Channel-side: treat approval_no_history as not consumed
DiscordSessionBindingActor.cs Same fix
MattermostSessionBindingActor.cs Same fix
SessionBindingContractTests.cs New test Normal_chat_text_that_looks_like_approval_is_not_consumed_when_no_approval_history
RecordingSessionPipeline.cs ResponseFactory for scripted nack responses in tests

Testing

  • 1983 tests pass (all existing approval tests green + 3 new contract test instances)
  • Slopwatch: 0 issues
  • Copyright headers: all present

Fixes #1164

The cold-text-approval path matches short replies like 'yes', 'a', '1' as
potential approval responses regardless of context. When no pending approval
exists and the session has never had an approval request, the message was
consumed and a spurious 'approval prompt expired' notice was emitted.

Check _resolvedToolApprovals and history for redrivable tool batches before
emitting the expired notice. If no approval history exists, return
approval_no_history so the channel falls through to normal LLM ingress.

Fixes netclaw-dev#1164
@Aaronontheweb Aaronontheweb added channels Discord, Slack, and other channels. config Configuration issues, netclaw doctor, schema validation. sessions LLM session actor, turn lifecycle, pipelines and removed config Configuration issues, netclaw doctor, schema validation. labels May 25, 2026
public async Task Normal_chat_text_that_looks_like_approval_is_not_consumed_when_no_approval_history()
{
var ct = TestContext.Current.CancellationToken;
var detector = new ConfigurablePromptInjectionDetector(PromptInjectionResult.Safe());

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let all content through the prompt injection detector

ResponseFactory = (feedback, _) =>
{
return feedback is ToolInteractionTextResponse
? Task.FromResult<ICommandReply>(CommandNack.For(sid, "approval_no_history"))

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

send a CommandNack back if the feedback is part of a tool text interaction, i.e. sending A/B/C/D instead of clicking a button for approval signaling.

// Send a message that LooksLikeApprovalResponse matches ("yes" -> ApproveOnce)
// but is ordinary conversation. With the fix, the message falls through
// to normal ChannelInput ingestion.
actor.Tell(CreateInboundMessage("yes", "user-1"), TestActor);

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"yes" is a keyword in the text matching pipeline apparently? We should double check that - that was not my understanding.

{
_log.Warning("Ignoring text tool interaction response with no pending approvals for sender {SenderId}", msg.SenderId);
EmitExpiredPromptNotice();
nackReason = "approval_prompt_expired";

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

needs to be a const string if it has semantic meaning inside the application.

// ordinary conversation (e.g., "yes", "a", "1"). Don't emit a
// user-visible notice and don't consume — the channel should
// fall through to normal LLM ingress. See #1164.
nackReason = "approval_no_history";

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above.

// approval_no_history means the session has never had an approval request.
// The message was a false-positive from LooksLikeApprovalResponse.
// Don't consume — let it fall through to normal LLM ingress. See #1164.
if (reply is CommandNack { Reason: "approval_no_history" })

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These reasons either need to be an enum or a constant.

// approval_no_history means the session has never had an approval request.
// The message was a false-positive from LooksLikeApprovalResponse.
// Don't consume — let it fall through to normal LLM ingress. See #1164.
if (reply is CommandNack { Reason: "approval_no_history" })

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as the other comments about CommandNack reasons.

- Add ApprovalNackReasons static class with const string values to eliminate
  magic strings across session actor, channel bindings, and HTTP endpoints
- Replace all inline approval_* strings with ApprovalNackReasons constants
- Update MattermostActionEndpointExtensions to use constants in switch expression
- Test now sends CommandNack for ToolInteractionTextResponse to simulate
  real session behavior when no approval history exists
- Confirmed 'yes' is a keyword matched by LooksLikeApprovalResponse via
  TryParseNamedSelection

Fixes netclaw-dev#1164
@Aaronontheweb Aaronontheweb merged commit 2f562c0 into netclaw-dev:dev May 25, 2026
14 checks passed
@Aaronontheweb Aaronontheweb mentioned this pull request May 26, 2026
3 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

channels Discord, Slack, and other channels. sessions LLM session actor, turn lifecycle, pipelines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(approvals): phantom "approval prompt expired" notice on normal chat

1 participant