Repository navigation
Conversation
_render_expandable_quote() built raw MarkdownV2 blockquotes as ">line1\n>line2||" — missing the "**" that Telegram's grammar requires to open an *expandable* quote. Without it, the trailing "||" is an unmatched spoiler entity: Telegram rejects the whole sendMessage call with "Can't parse entities", and the caller's fallback resends the message as unformatted plain text with no collapsing at all. This path is hit by most non-trivial notifications: thinking blocks, Bash/Grep/Glob/Task/WebFetch/WebSearch tool_result output, multi-line tool errors, and Edit diffs all go through _format_expandable_quote(). So instead of a collapsed "N lines, tap to expand" block, the full raw content (including repeated tracebacks in log-heavy output) was landing directly in the chat. Fix: promote the first ">" of the rendered block to "**>", matching telegramify_markdown's own render_quote() implementation. Existing tests asserted the old (broken) shape; updated them and added coverage for multi-line and truncated quotes. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Live notifications for a topic used to accumulate indefinitely: every thinking block, tool_use/tool_result, local-command echo, and even the bot's own echo of the user's outgoing message stayed in the chat forever, alongside the actual assistant answer. For tool-call-heavy sessions (e.g. a monitoring loop polling journalctl) this buried the real answer under a growing wall of technical messages. Add per-topic tracking of the last "secondary" message (_secondary_msg_info, keyed like the existing _status_msg_info / _tool_msg_ids). Secondary means everything except the final assistant text answer: thinking, tool_use, tool_result, local_command, and the role=user echo of a forwarded message. Whenever a new message arrives for a topic, the previously tracked secondary message is deleted; the new message is tracked in its place if it's itself secondary, or left untouched if it's the final answer. Whatever happens to be last simply never gets deleted, so a completed turn naturally leaves only the answer behind — no separate "is this the final message" detection needed. Final text answers are never deleted or tracked, per prior discussion (multiple text blocks in one turn should all persist). tool_use -> tool_result keeps its existing edit-in-place behavior (same message_id, so the secondary tracking stays valid across the edit) rather than being folded into delete+resend. _can_merge_tasks() now also refuses to merge a secondary task with a non-secondary one, since the merged task can only carry one is_secondary value. clear_topic_state() clears the new tracking on topic close. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
_process_content_task's rolling delete-on-next logic popped the tracked secondary message out of _secondary_msg_info before attempting bot.delete_message, then swallowed all errors with a bare `except Exception: pass` — which also caught RetryAfter. Under a burst of several messages to the same topic within a couple seconds (thinking + tool_use/tool_result + task-notification text + interactive-UI polling, all hitting the same chat), Telegram's flood control could reject the delete; the exception was silently dropped and the message was already forgotten, so nothing ever cleaned it up. Keep the entry in _secondary_msg_info until the delete actually succeeds (or is given up on for a non-retryable reason), and re-raise RetryAfter so the existing _process_content_with_retry wrapper retries the whole task — a retry now finds the same tracked message and can still delete it, instead of leaking it forever. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
_merge_content_tasks folds several secondary entries that arrive close together (task-notification bursts observed at 3-4 messages within tens of milliseconds) into one MessageTask with multiple `parts`. Each part is sent as its own Telegram message, but _secondary_msg_info only recorded the id of the last one — the earlier N-1 messages were sent, immediately forgotten, and never deleted by the rolling cleanup, no matter how many further messages arrived afterward. Same issue applies to a single (unmerged) secondary task whose content gets paginated into multiple messages by split_message. Change _secondary_msg_info's value from a single message_id to a list of ids, collected from every part actually sent (including one converted from a status message). Deletion now walks the whole list; on RetryAfter mid-list, already-deleted ids are dropped and the remainder is written back before re-raising, so a retry of the task resumes exactly where it left off instead of re-deleting or losing track of entries. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…confirm Adds two independent, opt-in CCBOT_TOPIC_* env vars for running several ccbot instances (e.g. on different servers) against topics of one shared Telegram group without them fighting over topics that belong to another instance, and for letting specific unattended topics (autonomous loops) proceed without a human tapping every permission prompt: - CCBOT_TOPIC_ALLOWLIST: comma-separated topic (thread) IDs this instance handles. Enforced by one new handler (_topic_gate) registered in an earlier PTB group via ApplicationHandlerStop, so a disallowed topic never reaches any of the ~8 existing per-handler thread_id checks. Empty/unset (default) allows every topic, unchanged from before. - CCBOT_TOPIC_AUTO_CONFIRM: comma-separated topic IDs where Permission Prompt / Bash-approval UIs get Enter sent directly (as if the pre-selected "Yes" were tapped) instead of posting the interactive card. Deliberately scoped to only those two UI types — AskUserQuestion, ExitPlanMode, RestoreCheckpoint, and Settings have no universally safe default answer and are always shown normally. Implemented in status_polling.update_status_message, the same poller that already detects these UIs (they have no JSONL tool_use entry to hook into). Verified live against a running instance: a fresh topic bound and resumed a session correctly under an active allowlist, and a Permission Prompt in an auto-confirm topic was silently approved with no card sent, while a different topic without auto-confirm still got the normal interactive card. 24 new tests (config parsing/validation, _topic_gate, auto-confirm and its Settings-UI exclusion). README.md and .env.example document both vars. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Finding a topic's thread_id to put in CCBOT_TOPIC_ALLOWLIST /
CCBOT_TOPIC_AUTO_CONFIRM meant digging through state.json or service
logs. Add a small banner ("📌 Topic ID: `<id>` (for
CCBOT_TOPIC_ALLOWLIST)") prepended to the window picker / directory
browser message sent the first time an unbound topic is used.
The banner is re-added on every in-place edit of that same message —
folder navigation (CB_DIR_SELECT/CB_DIR_UP/CB_DIR_PAGE) and the window
picker → directory browser transition (CB_WIN_NEW) — so it stays
visible for the whole session-creation flow instead of only the very
first frame.
Stacked on six-ddc#95 (feat/topic-allowlist-and-auto-confirm) — the banner's
CCBOT_TOPIC_ALLOWLIST reference only makes sense once that var exists,
so this PR's diff includes six-ddc#95's commits until it merges.
3 new tests for the pure banner-text helper. 286 passed total, ruff/pyright
clean on changed files.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
paoloantinori
added a commit
to paoloantinori/ccbot
that referenced
this pull request
Aug 21, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Depends on / stacked on #95 — this diff will include #95's commits until that one merges, since the banner text references
CCBOT_TOPIC_ALLOWLIST, which only exists after #95.thread_id(needed forCCBOT_TOPIC_ALLOWLIST/CCBOT_TOPIC_AUTO_CONFIRM) meant digging throughstate.jsonor service logs. Adds a small banner (📌 Topic ID: \` (for CCBOT_TOPIC_ALLOWLIST)`) prepended to the window picker / directory browser message sent the first time an unbound topic is used.Test plan
uv run pytest tests/— 286 passed (3 new for the banner helper)uv run ruff check/uv run pyright— clean on changed filesCo-Authored-By: Claude Sonnet 5 noreply@anthropic.com