close
Skip to content

fix: surface permanent turn-failure reasons in Telegram replies (#1831) - #2565

Merged
iamwhatever merged 1 commit into
mainfrom
fix/telegram-surface-failure-reason-1831
Aug 10, 2026
Merged

fix: surface permanent turn-failure reasons in Telegram replies (#1831)#2565
iamwhatever merged 1 commit into
mainfrom
fix/telegram-surface-failure-reason-1831

Conversation

@bolichen97

Copy link
Copy Markdown
Collaborator

Summary

Fixes the Telegram surface replying ⚠️ Error — please try again to every failed turn, even when the failure is permanent and the underlying AcpError already carries a complete, actionable remedy (e.g. "Your account does not have access to model X — available: …, pick one in the model picker or set agent.model to auto. Retrying will not help."). The reporter's own unblock is switching the session's agent off the unavailable model — this change makes that discoverable from the chat itself, without reading gateway logs.

What changed

  • telegram/transport_dispatch.py: the handle_message exception handler derives a bounded, user-safe failure reason via a new _user_safe_failure_reason() and hands it to the renderer's finalization. Only a permanent AcpError (transient is False) qualifies — transient and unclassified failures keep the retry wording, and non-Acp exceptions never leak internals (CWE-209). The reason is treated as untrusted output: security.redact() (credentials + exfil URLs), redact_local_paths(), newline collapse, 500-char hard cap. The sanitizer is fail-closed: any internal error falls back to the generic text. Existing logger.exception + record_failure behavior is unchanged.
  • telegram/renderer.py: close() accepts an optional pre-sanitized failure_reason used as the finalization placeholder instead of the generic string. The generic text is preserved as the default, so every other caller is unaffected; the reason is ignored once the turn is finalized. The placeholder path sends plain text (parse_mode=None), so the reason cannot inject Telegram HTML.

Scope

Deliberately Telegram-only (the reported surface). Discord has the identical pattern at src/kiro_crew/discord/renderer.py:541 — noted as a follow-up candidate rather than widening this change to every transport.

Testing

  • 13 new tests in test/test_telegram.py, verified to fail before the fix:
    • end-to-end through handle_message: permanent AcpError message reaches the finalized placeholder (single-line, no retry text); transient error keeps the retry text; non-Acp exception stays generic and leaks nothing (asserts the message and a /home/... path do not appear)
    • renderer: reason replaces the placeholder; no-reason keeps generic; reason ignored after normal on_done
    • sanitizer unit tests: newline collapse, local-path redaction, 500-char hard cap, transient/unclassified/non-Acp/empty → None
  • Full local gates green: isort, flake8, mypy (865 files), python -m pytest (40366 passed; 75 failures verified pre-existing parallel-run environment flakes in unrelated app tests — the failing files pass standalone), test_telegram.py 190/190.

Backend-only: no dashboard surface, no screenshots required.

Closes #1831

@bolichen97
bolichen97 requested a review from a team as a code owner August 10, 2026 14:23
@github-actions github-actions Bot added the readiness: checking Automated validation is still running label Aug 10, 2026
@github-actions

github-actions Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Opus 4.8 Review — ✅ no blocking findings

Reviewed 17953dbc1da8531438016a2695177ce56dc20076 — this comment is updated in place on each push.

Review details

The change is careful and correct: the redaction chain indexes the tuple return properly, transient is not False correctly gates only permanent AcpErrors (excluding None/transient), the sanitizer is fail-closed inside the except block, the placeholder posts via plain-text send_message/edit_message (no parse_mode, so no HTML injection), and _failure_reason is ignored once finalized.

No findings.

[OPUS-REVIEWED] 17953db

Verdict parsed from the review's SHA-scoped output markers for commit 17953dbc1da8531438016a2695177ce56dc20076.

False positive or not applicable? A repository writer can comment:
/ai-review override fable 17953dbc1da8531438016a2695177ce56dc20076: <one-sentence reason>

@github-actions

github-actions Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Design Review (Fable 5) — ✅ PASS

Advisory design-level review of 17953dbc1da8531438016a2695177ce56dc20076 — updated in place on each push; does not block merge.

Design-Verdict: PASS

Real harm (misleading retry advice on permanent failures), fixed at the right seam — classification at dispatch, display-only in the renderer, fail-closed sanitization.

Suggestions

  • _user_safe_failure_reason is fully transport-agnostic (AcpError classification + redaction, nothing Telegram in it); hoisting it into messaging/ now means the acknowledged Discord follow-up (discord/renderer.py:541 has the identical pattern) reuses it instead of copy-pasting a security-sensitive sanitizer per transport.

[DESIGN-REVIEWED] 17953db

@github-actions

github-actions Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

GPT 5.6 Review — ✅ no blocking findings

GPT 5.6 completed its review of 17953dbc1da8531438016a2695177ce56dc20076 and found no blocking issues.

This comment is updated in place on each push.

Review details

FINDING -- src/kiro_crew/telegram/renderer.py:781 -- A permanent ACP error after a text chunk bypasses self._failure_reason, leaving only the incomplete reply -> Fix: have close() send the reason separately after finalizing nonempty output.
[GPT-REVIEWED] 17953db

False positive or not applicable? A repository writer can comment:
/ai-review override gpt 17953dbc1da8531438016a2695177ce56dc20076: <one-sentence reason>

A turn that failed with a permanent AcpError (e.g. the account lacks
the selected model) was finalized to the generic '⚠️ Error — please try
again' placeholder, hiding the actionable remedy the error already
carried and giving retry advice the error itself says is wrong.

The dispatcher's exception handler now derives a bounded, user-safe
reason — only for AcpError with transient=False; the message is
credential/exfil-redacted, local paths stripped, newline-collapsed and
hard-capped — and passes it to TelegramRenderer.close(), which shows it
as the finalization placeholder. Transient and unclassified failures
keep the retry wording; non-Acp exceptions never leak internals.

Discord has the identical pattern (discord/renderer.py) and is left as
a follow-up to keep this change scoped to the reported surface.

Closes #1831
@bolichen97
bolichen97 force-pushed the fix/telegram-surface-failure-reason-1831 branch from acb7944 to 17953db Compare August 10, 2026 14:44
@github-actions github-actions Bot added readiness: passed Eligible automated validation passed for the current revision and removed readiness: checking Automated validation is still running labels Aug 10, 2026
@iamwhatever
iamwhatever merged commit b0b8c85 into main Aug 10, 2026
52 checks passed
@iamwhatever
iamwhatever deleted the fix/telegram-surface-failure-reason-1831 branch August 10, 2026 15:32
@github-actions github-actions Bot removed the readiness: passed Eligible automated validation passed for the current revision label Aug 10, 2026
encomjp pushed a commit to encomjp/kirocrew-customapi that referenced this pull request Aug 22, 2026
…dotdev#1831) (kirodotdev#2565)

A turn that failed with a permanent AcpError (e.g. the account lacks
the selected model) was finalized to the generic '⚠️ Error — please try
again' placeholder, hiding the actionable remedy the error already
carried and giving retry advice the error itself says is wrong.

The dispatcher's exception handler now derives a bounded, user-safe
reason — only for AcpError with transient=False; the message is
credential/exfil-redacted, local paths stripped, newline-collapsed and
hard-capped — and passes it to TelegramRenderer.close(), which shows it
as the finalization placeholder. Transient and unclassified failures
keep the retry wording; non-Acp exceptions never leak internals.

Discord has the identical pattern (discord/renderer.py) and is left as
a follow-up to keep this change scoped to the reported surface.

Closes kirodotdev#1831
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Error catching telegram messages locally on my laptop, via Telegram connector

2 participants