fix(channels): prevent cold approval path from swallowing normal chat (#1164)#1165
Conversation
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
| 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()); |
There was a problem hiding this comment.
let all content through the prompt injection detector
| ResponseFactory = (feedback, _) => | ||
| { | ||
| return feedback is ToolInteractionTextResponse | ||
| ? Task.FromResult<ICommandReply>(CommandNack.For(sid, "approval_no_history")) |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
"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"; |
There was a problem hiding this comment.
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"; |
There was a problem hiding this comment.
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" }) |
There was a problem hiding this comment.
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" }) |
There was a problem hiding this comment.
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
Summary
HasApprovalHistorycheck inLlmSessionActor.TryResolveTextApprovalResponse— when no approval has ever been requested, returnapproval_no_historynack instead of emitting an expired noticeapproval_no_historyas "not consumed" so the message falls through to normal LLM ingressProblem
LooksLikeApprovalResponsematches single lettersa-eand numbers1-5by 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
LlmSessionActor.csHasApprovalHistoryproperty +approval_no_historynack pathSlackThreadBindingActor.csapproval_no_historyas not consumedDiscordSessionBindingActor.csMattermostSessionBindingActor.csSessionBindingContractTests.csNormal_chat_text_that_looks_like_approval_is_not_consumed_when_no_approval_historyRecordingSessionPipeline.csResponseFactoryfor scripted nack responses in testsTesting
Fixes #1164