Skip to content

Show topic thread_id in the first bot message of a new topic - #96

Open
Stelatix wants to merge 6 commits into
six-ddc:mainfrom
Stelatix:feat/topic-id-banner
Open

Stelatix wants to merge 6 commits into
six-ddc:mainfrom
Stelatix:feat/topic-id-banner

Conversation

@Stelatix

@Stelatix Stelatix commented Aug 9, 2026

Copy link
Copy Markdown

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.

  • Finding a topic's thread_id (needed for CCBOT_TOPIC_ALLOWLIST / CCBOT_TOPIC_AUTO_CONFIRM) meant digging through state.json or 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.
  • Re-added on every in-place edit of that same message (folder navigation, pagination, window-picker → directory-browser transition) so it stays visible for the whole session-creation flow, not just the first frame.

Test plan

  • uv run pytest tests/ — 286 passed (3 new for the banner helper)
  • uv run ruff check / uv run pyright — clean on changed files
  • Deployed to a live instance and verified the banner and its persistence across folder navigation

Co-Authored-By: Claude Sonnet 5 noreply@anthropic.com

Stelatix and others added 6 commits August 9, 2026 16:04
_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
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.

1 participant