Skip to content

Gracefully degrade unresolved fuzzy @mentions instead of failing the message#339

Merged
jeremy merged 4 commits intomainfrom
graceful-fuzzy-mentions
Mar 18, 2026
Merged

Gracefully degrade unresolved fuzzy @mentions instead of failing the message#339
jeremy merged 4 commits intomainfrom
graceful-fuzzy-mentions

Conversation

@robzolkos
Copy link
Collaborator

@robzolkos robzolkos commented Mar 17, 2026

Summary

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 a notice in the response ("Unresolved mentions left as text: @Bobby").

Previously a single bad @Name aborted the entire operation with a
"Person not found" error and nothing was delivered.

Deterministic syntaxes (mention:SGID, person:ID) remain strict — only fuzzy
@Name resolution is gracefully degraded.

How it works

  • ErrMentionSkip sentinel in richtext — lookup functions wrap not-found/ambiguous
    errors with this to signal "leave as plain text."
  • resolveNameMentions() checks errors.Is(err, ErrMentionSkip) — skips the mention
    and collects it in an Unresolved slice; non-skip errors still hard-fail.
  • The command helper in helpers.go downgrades only CodeNotFound and CodeAmbiguous
    to skip; transport, auth, and missing-SGID errors propagate as before.
  • All 13 mention call sites across chat, comments, messages, cards, schedule, and todos
    surface unresolved mentions via output.WithDiagnostic.

stderr scoping

Notice is used by both mention warnings and ~15 truncation sites. Without
scoping, every paginated read under --agent/--quiet would write to stderr,
breaking the data-only contract.

  • WithDiagnostic(s) sets Notice plus an unexported noticeDiagnostic flag
    that gates stderr emission in quiet mode. Used by mention warning sites.
  • WithNotice(s) sets Notice only (and clears the diagnostic bit to prevent
    option-order leaks). Used by truncation and other informational notices.
  • JSON schema unchanged — both options populate the same notice envelope field.

Files changed

Area Files What changed
Core richtext.go ErrMentionSkip, MentionResult, updated ResolveMentions/resolveNameMentions signatures
Output envelope.go WithDiagnostic, noticeDiagnostic flag, stderr gate
Helper helpers.go Downgrade not-found/ambiguous to skip, rename to unresolvedMentionWarning()
Callers chat.go, comment.go, messages.go, cards.go, schedule.go, todos.go Thread MentionResult + WithDiagnostic
Tests richtext_test.go, chat_test.go, messages_test.go, output_test.go New: agent-mode stderr, truncation-silent regression; updated existing tests

Test plan

  • bin/ci green
  • TestChatPostAgentModeWarningOnStderr — quiet mode emits mention warning to stderr
  • TestMessagesListAgentModeTruncationSilent — truncation notice does NOT leak to stderr
  • TestWriterQuietFormatNoticeDoesNotWriteStderr — plain WithNotice stays silent
  • TestChatPostMixedValidInvalidMentions — JSON envelope still contains notice field
  • Existing mention and truncation tests pass unchanged

…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.
Copilot AI review requested due to automatic review settings March 17, 2026 14:46
@robzolkos robzolkos requested a review from a team as a code owner March 17, 2026 14:46
@github-actions github-actions bot added commands CLI command implementations tests Tests (unit and e2e) bug Something isn't working labels Mar 17, 2026
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

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.ErrMentionSkip and changes ResolveMentions to return a MentionResult containing both resolved HTML and an Unresolved list.
  • 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.

Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

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.
@github-actions github-actions bot added output Output formatting and presentation enhancement New feature or request and removed bug Something isn't working labels Mar 17, 2026
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.
Copilot AI review requested due to automatic review settings March 17, 2026 15:27
@github-actions github-actions bot added bug Something isn't working and removed enhancement New feature or request labels Mar 17, 2026
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

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.ErrMentionSkip and returns a richtext.MentionResult (HTML + unresolved mentions) from ResolveMentions.
  • Updates command call sites to thread MentionResult through and emit an output.WithNotice(...) when there are unresolved mentions.
  • Improves quiet/agent mode diagnostics by emitting notices to stderr (including when --jq is 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.

@robzolkos robzolkos requested a review from jeremy March 17, 2026 15:33
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.
@jeremy jeremy merged commit 1808923 into main Mar 18, 2026
26 checks passed
@jeremy jeremy deleted the graceful-fuzzy-mentions branch March 18, 2026 20:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working commands CLI command implementations output Output formatting and presentation tests Tests (unit and e2e)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants