Claude/fix duplicate interactive messages fp kau#49
Open
JanusMarko wants to merge 24 commits intosix-ddc:mainfrom
Open
Claude/fix duplicate interactive messages fp kau#49JanusMarko wants to merge 24 commits intosix-ddc:mainfrom
JanusMarko wants to merge 24 commits intosix-ddc:mainfrom
Conversation
New env var sets a fixed starting directory for the directory browser, falling back to Path.cwd() if not set (preserving current behavior). https://claude.ai/code/session_01Vn1pxPc8KahAYpofYGhLjY
…6kvZ Add CCBOT_BROWSE_ROOT config for directory browser start path New env var sets a fixed starting directory for the directory browser, falling back to Path.cwd() if not set (preserving current behavior). https://claude.ai/code/session_01Vn1pxPc8KahAYpofYGhLjY
Four callback handlers (CB_DIR_SELECT, CB_DIR_UP, CB_DIR_PAGE, CB_DIR_CONFIRM) and build_directory_browser's invalid-path fallback used raw Path.cwd() instead of config.browse_root. This meant users could escape the configured browse root if user_data was lost or the path became invalid during navigation. https://claude.ai/code/session_01Vn1pxPc8KahAYpofYGhLjY
…6kvZ Fix inconsistent Path.cwd() fallbacks in directory browser callbacks Four callback handlers (CB_DIR_SELECT, CB_DIR_UP, CB_DIR_PAGE, CB_DIR_CONFIRM) and build_directory_browser's invalid-path fallback used raw Path.cwd() instead of config.browse_root. This meant users could escape the configured browse root if user_data was lost or the path became invalid during navigation. https://claude.ai/code/session_01Vn1pxPc8KahAYpofYGhLjY
- session.py: Replace deprecated asyncio.get_event_loop() with asyncio.get_running_loop() (Python 3.12+ compat) - session.py: Remove redundant pass statements - session_monitor.py: Consolidate double stat() call into one - screenshot.py: Add explicit parens in _font_tier() for clarity - bot.py: Add /kill command handler — kills tmux window, unbinds thread, cleans up state, and best-effort deletes the topic. Previously the /kill bot command was registered in the menu but had no handler, falling through to forward_command_handler. https://claude.ai/code/session_01Vn1pxPc8KahAYpofYGhLjY
…6kvZ Fix misc bugs: asyncio deprecation, double stat, missing /kill handler - session.py: Replace deprecated asyncio.get_event_loop() with asyncio.get_running_loop() (Python 3.12+ compat) - session.py: Remove redundant pass statements - session_monitor.py: Consolidate double stat() call into one - screenshot.py: Add explicit parens in _font_tier() for clarity - bot.py: Add /kill command handler — kills tmux window, unbinds thread, cleans up state, and best-effort deletes the topic. Previously the /kill bot command was registered in the menu but had no handler, falling through to forward_command_handler. https://claude.ai/code/session_01Vn1pxPc8KahAYpofYGhLjY
Add timestamp-based deduplication in handle_interactive_ui() to prevent both JSONL monitor and status poller from sending new interactive messages in the same short window. The check-and-set has no await between them, making it atomic in the asyncio event loop. Also add a defensive check in status_polling.py to skip calling handle_interactive_ui() when an interactive message is already tracked for the user/thread (e.g. sent by the JSONL monitor path). https://claude.ai/code/session_016c4b8ioybZyscNayeY6Y18
…returning list snapshot iter_thread_bindings() was a generator yielding from live dicts. Callers with await between iterations (find_users_for_session, status_poll_loop) could allow concurrent unbind_thread() calls to mutate the dict mid-iteration, causing RuntimeError: dictionary changed size during iteration. Fix: rename to all_thread_bindings() returning a materialized list snapshot. The list comprehension captures all (user_id, thread_id, window_id) tuples eagerly, so no live dict reference escapes across await points. Changes: - session.py: iter_thread_bindings -> all_thread_bindings, returns list - bot.py, status_polling.py: update all 4 call sites - Remove unused Iterator import from collections.abc - Add tests: snapshot independence, returns list type, empty bindings https://claude.ai/code/session_016c4b8ioybZyscNayeY6Y18
queue.join() in handle_new_message blocked the entire monitor loop while waiting for one user's queue to drain. If Telegram was rate-limiting, this could stall all sessions for 30+ seconds. Fix: use enqueue_callable() to push interactive UI handling as a callable task into the queue. The worker executes it in FIFO order after all pending content messages, guaranteeing correct ordering without blocking. Also fixes: - Callable tasks silently dropped during flood control (the guard checked task_type != "content" which matched "callable" too; changed to explicit check for "status_update"/"status_clear" only) - Updated stale docstring in _merge_content_tasks referencing queue.join() https://claude.ai/code/session_016c4b8ioybZyscNayeY6Y18
unpin_all_forum_topic_messages was used every 60s to detect deleted topics,
but it destructively removed all user-pinned messages as a side effect.
Replace with send_chat_action(ChatAction.TYPING) which is ephemeral
(5s typing indicator) and raises the same BadRequest("Topic_id_invalid")
for deleted topics. All existing error handling works unchanged.
https://claude.ai/code/session_016c4b8ioybZyscNayeY6Y18
- Add MAX_TASK_RETRIES=3 retry loop for short RetryAfter (sleep and retry) - Re-queue tasks on long RetryAfter (>10s) with MAX_REQUEUE_COUNT=5 cap - Convert callable_fn from Coroutine to Callable factory (coroutines are single-use; retry requires a fresh coroutine each attempt) - Catch RetryAfter from _check_and_send_status to prevent cosmetic status updates from triggering content message re-sends - Fix test isolation: clear _last_interactive_send in test fixtures https://claude.ai/code/session_016c4b8ioybZyscNayeY6Y18
The _file_mtimes dict used mtime+size to skip unchanged JSONL files, but this introduced edge cases (sub-second writes, clock skew, file replacement). For append-only JSONL files, comparing file size against last_byte_offset is sufficient and eliminates all mtime-related issues. https://claude.ai/code/session_016c4b8ioybZyscNayeY6Y18
Previously byte offsets were persisted to disk BEFORE delivering messages to Telegram. If the bot crashed after save but before delivery, messages were silently lost. Now offsets are saved AFTER the delivery loop, guaranteeing at-least-once delivery: a crash before save means messages are re-read and re-delivered on restart (safe duplicate) rather than permanently lost. https://claude.ai/code/session_016c4b8ioybZyscNayeY6Y18
Dead sessions were cleaned from persistent state but never from the in-memory _pending_tools dict, causing a slow memory leak over time. Add pop() calls in both cleanup paths (startup + runtime). https://claude.ai/code/session_016c4b8ioybZyscNayeY6Y18
Previously _pending_thread_text was cleared from user_data BEFORE attempting to send it to the tmux window. If send_to_window() failed, the message was lost and the user had to retype it. Now the pending text is only cleared after a successful send. https://claude.ai/code/session_016c4b8ioybZyscNayeY6Y18
Typing indicators in forum topics were silently failing because message_thread_id was not passed to send_chat_action calls. Users in forum topics wouldn't see typing indicators while Claude worked. https://claude.ai/code/session_016c4b8ioybZyscNayeY6Y18
The except Exception handler was catching RetryAfter (Telegram 429
rate limiting) and BadRequest("message is not modified"), preventing
proper rate limit propagation and causing unnecessary duplicate
message sends.
Changes:
- Re-raise RetryAfter in both edit and send paths so the queue
worker retry loop can handle rate limiting correctly
- Treat BadRequest "is not modified" as success (content identical)
- For other BadRequest errors (message deleted, too old), delete
orphan message before falling through to send new
- Log exception details in catch-all handler for debugging
https://claude.ai/code/session_016c4b8ioybZyscNayeY6Y18
When JSONL monitoring enqueues _send_interactive_ui, the callable may execute after the interactive UI has been dismissed. This caused stale callables to potentially send duplicate interactive messages. Fix: introduce a monotonically incrementing generation counter per (user_id, thread_id) key. Every state transition (set_interactive_mode, clear_interactive_mode, clear_interactive_msg) increments the counter. The JSONL monitor captures the generation at enqueue time and passes it to handle_interactive_ui via expected_generation parameter. If the generation has changed by execution time, the function bails out. The status poller is unaffected (passes None, skipping the guard). https://claude.ai/code/session_016c4b8ioybZyscNayeY6Y18
The second all_thread_bindings() call gets a fresh snapshot that naturally excludes entries unbound by the topic probe loop above. This is correct behavior, not a bug — add a comment to clarify the intent for future readers. https://claude.ai/code/session_016c4b8ioybZyscNayeY6Y18
The return value was already handled correctly (proceed regardless), but the ignored bool looked like a bug. Add a comment explaining that on timeout the monitor's 2s poll cycle picks up the entry, and thread binding, pending text, and topic rename work without session_map. https://claude.ai/code/session_016c4b8ioybZyscNayeY6Y18
…edia Telegram clients fail to re-render document thumbnails when editing document-type media in place via editMessageMedia, causing a "white circle with X" on screenshot refresh. Switch from reply_document + InputMediaDocument to reply_photo + InputMediaPhoto, which Telegram clients handle reliably for inline image edits. Also adds debug logging for the key-press screenshot edit path. https://claude.ai/code/session_016c4b8ioybZyscNayeY6Y18
Check pane_current_command before sending keys to tmux windows. If the pane is running a shell (bash, zsh, etc.), Claude Code has exited and user text must not be forwarded — it would execute as shell commands. Guards added to: send_to_window (safety net), text_handler (with auto-unbind), esc_command, usage_command, and screenshot key-press callback. https://claude.ai/code/session_016c4b8ioybZyscNayeY6Y18
When send_to_window detects the pane is running a shell, it now captures the pane content and looks for: - "Stopped ... claude" → sends "fg" (suspended process) - "claude --resume <id>" → sends the resume command Waits up to 3s (fg) or 15s (--resume) for Claude Code to take over the terminal, then sends the user's original text. If no resume command is found, the text_handler unbinds the topic and tells the user to start a new session. https://claude.ai/code/session_016c4b8ioybZyscNayeY6Y18
…_pane Reduces tmux subprocess calls from ~120/s to ~21/s with 20 windows by: - Adding 1-second TTL cache to list_windows() (all callers in the same poll cycle share one tmux enumeration instead of N) - Unifying capture_pane() to always use direct `tmux capture-pane` subprocess (plain text mode previously used libtmux which generated 3-4 tmux round-trips per call) - Invalidating cache on mutations (create/kill/rename) https://claude.ai/code/session_016c4b8ioybZyscNayeY6Y18
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.
No description provided.