Gracefully degrade unresolved fuzzy @mentions instead of failing the message#339
Gracefully degrade unresolved fuzzy @mentions instead of failing the message#339
Conversation
…message When a message contains multiple @mentions and one can't be resolved, the CLI now posts with valid mentions resolved and unresolved ones left as plain text, plus an informational notice in the response envelope. Previously a single bad @name aborted the entire operation — the sender got a "Person not found" error and nothing was delivered. Adds ErrMentionSkip sentinel in the richtext layer. The command helper downgrades only not-found and ambiguous errors to skip; transport, auth, and missing-SGID errors still hard-fail. Deterministic syntaxes (mention:SGID, person:ID) remain strict. All 11 call sites across chat, comments, messages, cards, schedule, and todos surface unresolved mentions via output.WithNotice. Fixes a regression where all-unresolved mentions would silently fall back to plain-text delivery, dropping the markdown-to-HTML conversion.
There was a problem hiding this comment.
Pull request overview
This PR changes rich-text mention resolution so that unresolved fuzzy @Name mentions no longer abort message delivery; instead they remain as plain text and callers surface a user-facing notice listing the unresolved mentions.
Changes:
- Introduces
richtext.ErrMentionSkipand changesResolveMentionsto return aMentionResultcontaining both resolved HTML and anUnresolvedlist. - Updates command helpers and multiple command handlers to downgrade “not found/ambiguous” fuzzy-mention failures into skips and attach
output.WithNotice(...)to responses. - Expands test coverage to validate mixed valid/invalid mention behavior and the new return type.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| internal/richtext/richtext.go | Adds skip sentinel + MentionResult; updates mention resolution flow to tolerate skip-worthy fuzzy failures. |
| internal/richtext/richtext_test.go | Updates existing tests for new return type and adds cases for skip behavior and strict deterministic schemes. |
| internal/commands/helpers.go | Wraps resolver errors to downgrade not-found/ambiguous into ErrMentionSkip; adds unresolvedMentionNotice(). |
| internal/commands/chat.go | Threads MentionResult through posting flow and adds notice emission. |
| internal/commands/chat_test.go | Adds tests asserting mixed/all-invalid mention posting behavior and notice presence. |
| internal/commands/messages.go | Threads MentionResult and emits notices in create/update flows. |
| internal/commands/comment.go | Threads MentionResult and emits notices in update and batch comment flows. |
| internal/commands/cards.go | Threads MentionResult and emits notices in create/update flows. |
| internal/commands/schedule.go | Threads MentionResult and emits notices in create/update flows. |
| internal/commands/todos.go | Threads MentionResult and emits notices in sweep flow. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
You can also share your feedback on Copilot code review. Take the survey.
There was a problem hiding this comment.
2 issues found across 10 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="internal/richtext/richtext.go">
<violation number="1" location="internal/richtext/richtext.go:875">
P2: Unresolved mentions are collected in reverse document order because the loop iterates from last match to first. Reverse the slice before returning so the notice lists mentions in the order users wrote them.</violation>
</file>
<file name="internal/commands/chat.go">
<violation number="1" location="internal/commands/chat.go:377">
P2: The `|| len(result.Unresolved) > 0` addition causes an unnecessary content-type upgrade to HTML when all mentions are unresolvable. Since `result.HTML == mentionInput` in that case (nothing was transformed), the original condition `result.HTML != mentionInput` correctly skips the block. The `mentionNotice` is already set outside this `if`, so removing the extra disjunct doesn't affect the notice.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
Narrow the ErrMentionSkip downgrade in resolveMentions to resolver-level errors only (HTTPStatus == 0). Previously, an API-originated 404 from the pingable endpoint would also match CodeNotFound and be silently treated as "leave mention as text" instead of failing the command. Surface notices on stderr in quiet/agent mode so automation consumers can detect degraded operations. The notice was only carried in the response envelope, which FormatQuiet strips. Emit the diagnostic before the --jq early-return so --agent --jq is also covered. Reverse the unresolved mentions slice to match document order. The end-to-start iteration in resolveNameMentions (needed for safe in-place replacement) was appending names in reverse.
The comment said "HTML-escape plain text first" but the block actually converts Markdown to HTML for the mention resolver. Updated to match the actual behavior.
There was a problem hiding this comment.
Pull request overview
This PR updates the CLI’s rich-text mention resolution so that unresolvable fuzzy @Name mentions no longer fail the entire operation. Instead, valid mentions are resolved, unresolved ones are left as literal text, and callers can surface a notice indicating which mentions were not resolved; deterministic mention syntaxes remain strict.
Changes:
- Introduces
richtext.ErrMentionSkipand returns arichtext.MentionResult(HTML + unresolved mentions) fromResolveMentions. - Updates command call sites to thread
MentionResultthrough and emit anoutput.WithNotice(...)when there are unresolved mentions. - Improves quiet/agent mode diagnostics by emitting notices to stderr (including when
--jqis used), with new/updated tests covering these behaviors.
Reviewed changes
Copilot reviewed 12 out of 12 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| internal/richtext/richtext.go | Adds ErrMentionSkip, MentionResult, and skip-aware fuzzy mention resolution collecting unresolved mentions. |
| internal/richtext/richtext_test.go | Updates existing tests for the new return type and adds coverage for skip/unresolved behavior and strict syntaxes. |
| internal/commands/helpers.go | Downgrades resolver-level not-found/ambiguous to ErrMentionSkip; adds unresolvedMentionNotice(). |
| internal/commands/chat.go | Threads mention results through chat posting and attaches unresolved-mention notices. |
| internal/commands/comment.go | Threads mention results and attaches unresolved-mention notices for comment operations. |
| internal/commands/messages.go | Threads mention results and attaches unresolved-mention notices for message create/update flows. |
| internal/commands/cards.go | Threads mention results and attaches unresolved-mention notices for card create/update flows. |
| internal/commands/schedule.go | Threads mention results and attaches unresolved-mention notices for schedule create/update flows. |
| internal/commands/todos.go | Threads mention results and attaches unresolved-mention notices for todo sweep comment pipeline. |
| internal/commands/chat_test.go | Adds tests ensuring mixed/all-invalid mentions still post and that unresolved mentions surface via notice. |
| internal/output/envelope.go | Adds ErrWriter option and emits notices to stderr in quiet mode before --jq output. |
| internal/output/output_test.go | Adds tests verifying quiet mode keeps stdout clean while writing notices to stderr (including with --jq). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
You can also share your feedback on Copilot code review. Take the survey.
WithNotice emits to the JSON envelope but not stderr; the new WithDiagnostic sets both the notice and a non-serialized flag that gates stderr output in quiet/agent mode. This prevents truncation notices (~15 list/search sites) from leaking to stderr, while unresolved-mention warnings (13 sites) still surface for automation consumers. WithNotice clears the diagnostic bit so option ordering can never accidentally promote an informational notice to stderr.
Summary
When a message contains multiple
@mentionsand one can't be resolved, the CLInow posts with valid mentions resolved and unresolved ones left as plain text,
plus a notice in the response (
"Unresolved mentions left as text: @Bobby").Previously a single bad
@Nameaborted the entire operation with a"Person not found"error and nothing was delivered.Deterministic syntaxes (
mention:SGID,person:ID) remain strict — only fuzzy@Nameresolution is gracefully degraded.How it works
ErrMentionSkipsentinel inrichtext— lookup functions wrap not-found/ambiguouserrors with this to signal "leave as plain text."
resolveNameMentions()checkserrors.Is(err, ErrMentionSkip)— skips the mentionand collects it in an
Unresolvedslice; non-skip errors still hard-fail.helpers.godowngrades onlyCodeNotFoundandCodeAmbiguousto skip; transport, auth, and missing-SGID errors propagate as before.
surface unresolved mentions via
output.WithDiagnostic.stderr scoping
Noticeis used by both mention warnings and ~15 truncation sites. Withoutscoping, every paginated read under
--agent/--quietwould write to stderr,breaking the data-only contract.
WithDiagnostic(s)setsNoticeplus an unexportednoticeDiagnosticflagthat gates stderr emission in quiet mode. Used by mention warning sites.
WithNotice(s)setsNoticeonly (and clears the diagnostic bit to preventoption-order leaks). Used by truncation and other informational notices.
noticeenvelope field.Files changed
richtext.goErrMentionSkip,MentionResult, updatedResolveMentions/resolveNameMentionssignaturesenvelope.goWithDiagnostic,noticeDiagnosticflag, stderr gatehelpers.gounresolvedMentionWarning()chat.go,comment.go,messages.go,cards.go,schedule.go,todos.goMentionResult+WithDiagnosticrichtext_test.go,chat_test.go,messages_test.go,output_test.goTest plan
bin/cigreenTestChatPostAgentModeWarningOnStderr— quiet mode emits mention warning to stderrTestMessagesListAgentModeTruncationSilent— truncation notice does NOT leak to stderrTestWriterQuietFormatNoticeDoesNotWriteStderr— plainWithNoticestays silentTestChatPostMixedValidInvalidMentions— JSON envelope still containsnoticefield