Skip to content

fix: light the typing indicator off the parsed status line - #98

Open
zellwk wants to merge 2 commits into
six-ddc:mainfrom
zellwk:pr/typing-indicator
Open

zellwk wants to merge 2 commits into
six-ddc:mainfrom
zellwk:pr/typing-indicator

Conversation

@zellwk

@zellwk zellwk commented Aug 14, 2026

Copy link
Copy Markdown

Depends on #97 — merge that first. Without it parse_status_line returns None while a tip block is showing, and the indicator drops out mid-turn.

Problem

The typing indicator fires only when the status text contains esc to interrupt:

if "esc to interrupt" in status_text.lower():
    await bot.send_chat_action(chat_id=chat_id, action=ChatAction.TYPING)

Claude Code moved that string out of the status line and into the footer hint below the input box, so the condition is now never true and the indicator never appears:

✳ Gitifying… (2m 1s · ↓ 6.5k tokens)      <- status line, parsed, no "esc"
────────────────────────────────────────
❯
────────────────────────────────────────
  ⏸ manual mode on · esc to interrupt     <- footer, never parsed

The status line samples in #52 show the same thing — none of them carry the string.

Even before the UI change, this only half worked: Telegram expires a chat action after ~5s, and a single send_chat_action fired per status change. A long turn showed the dots briefly and then went silent.

Fix

Drive the indicator from status polling, which already runs once a second and is the only place that knows whether a session is mid-turn — a parsed status line means working, no status line means idle. A keepalive task re-sends the action every 4s and is cancelled on idle, on status clear, on topic cleanup, and at shutdown.

Two smaller things fixed along the way:

  • the chat action now carries message_thread_id, so it lands in the right forum topic rather than the group's General (every other send in message_queue.py already passes it via _send_kwargs)
  • a failed action logs at debug instead of except Exception: pass

Note for reviewers

#52 also touches src/ccbot/handlers/status_polling.py. The overlap is small — this adds three lines next to the existing parse_status_line call, that one adds throttling around the enqueue — but whichever merges second will want a look.

zellwk added 2 commits August 14, 2026 11:52
parse_status_line checks the first non-blank line above the chrome separator
and returns None if it isn't a spinner. Claude Code now renders a tip block
between the status line and the chrome:

    ✳ Gitifying… (2m 1s · ↓ 6.5k tokens)
      ⎿ Tip: Use /btw to ask a quick side question without interrupting
         current work
    ────────────────────────────────────────
    ❯

so the scan stops on "current work" and reports no status. Whenever a tip is
showing, the topic's status message goes stale and the typing indicator never
lights.

Keep scanning upward instead. The line directly above the separator is still
accepted on the spinner glyph alone, preserving current behaviour; past that,
a line must also carry the ellipsis of a live status. That keeps out both the
prose bullets the original guard was written for and the finished
"✻ Sautéed for 7s" marker.
The indicator fires only when the status text contains "esc to interrupt".
Claude Code moved that string out of the status line into the footer hint
under the input box, so the condition is now never true and the indicator
never appears:

    ✳ Gitifying… (2m 1s · ↓ 6.5k tokens)      <- status line, no "esc"
    ────────────────────────────────────
    ❯
    ────────────────────────────────────
      ⏸ manual mode on · esc to interrupt    <- footer, not parsed

Even before that, a single send_chat_action covered roughly the first five
seconds of a turn; Telegram expires a chat action after that.

Drive the indicator from status polling instead, which already runs once a
second and is the only place that knows whether a session is mid-turn: a
parsed status line means working, no status line means idle. A keepalive task
re-sends the action every 4s and is cancelled on idle, on status clear, on
topic cleanup, and at shutdown. The action carries message_thread_id so it
lands in the right forum topic, and a failed send logs at debug rather than
being swallowed.

Depends on the tip-block parsing fix — without it parse_status_line returns
None while a tip is showing, and the indicator drops out mid-turn.
TejNote added a commit to TejNote/ccbot that referenced this pull request Sep 16, 2026
upstream 패치 자체는 맞지만 **이 fork 의 다른 부분과 겹쳐서 깨지는** 것들이다.
upstream 테스트로는 안 잡힌다.

🚨 죽은 토픽 정리 후 타이핑 keepalive 가 살아남아 무한 API 호출
  six-ddc#81 의 cleanup_dead_topic 은 clear_topic_state 를 부르지 않는 유일한 해제 경로였다
  (다른 셋은 전부 부른다). six-ddc#98 전에는 단순 누수였지만 붙고 나서는 _typing_keepalive 가
  while True 라 **텔레그램이 "그 토픽 없다" 고 답한 thread 로 4초마다 send_chat_action 을
  영원히** 던지게 됐다. 정리한 (user_id, thread_id) 목록을 반환하도록 바꿔 연결한다.

six-ddc#97 의 8줄 스캔이 `·` 불릿을 상태줄로 읽는 구멍을 비인접 줄에 열었다
  upstream 조건이 `"…" in line` 하나뿐인데 `·` 는 스피너이면서 평범한 불릿이다.
  Claude 답변의 `· 첫째 항목은…` 이 상태줄이 되고, six-ddc#98 이 그 값으로 타이핑을 켜므로
  유휴 세션에 유령 상태 메시지 + 영구 타이핑이 남는다. 비인접은 `…` **그리고** `·` 아님.
  ⚠️ 구분선 바로 위 불릿은 main 에도 있던 기존 오탐이라 손대지 않았다(실측 대조).

퍼미션 프롬프트 동안 "작업 중" 으로 보이던 것
  six-ddc#98 의 set_typing 이 함수 맨 끝 한 번인데 그 위에 조기 return 이 셋이다. 프롬프트는
  몇 시간도 떠 있고 실제로는 봇이 사용자를 기다리는 중이다. skip_status 는 제외했다 —
  큐가 찬 일시적 상태이고 에이전트는 실제로 작업 중이다.

secondary 태스크의 이미지가 고아로 남던 것
  six-ddc#94 가 모으는 id 에 _send_task_images 의 사진이 빠졌다. tool_result 는 항상 secondary 라
  다음 메시지가 오면 설명 텍스트만 지워지고 스크린샷이 남는다. send_photo 가 id 를 반환한다.

콜백 응답이 최대 45초 지연되던 것
  query.answer() 앞에 wait_for_claude_ready(30초) + wait_for_session_map_entry(5~15초)가
  있었다. 텔레그램은 약 15초를 넘기면 query is too old 를 내고 try/except 도 없다.
  응답을 맨 앞으로 옮겼다 — 결과는 safe_edit 가 본문으로 보여준다.

TOPIC_CHECK_INTERVAL docstring 60초 → 5초 정정
  ⚠️ 바인딩당 API 호출 12배로 six-ddc#52 스로틀과 방향이 반대다. 레이트 리밋을 실제로 맞은
  관측이 없어 upstream 값은 유지하고, 근거와 되돌릴 값(15~20초)을 주석에 남겼다.

Tested: pytest 440항 (신규 8) · pyright 0 errors · ruff 103 → 102, 파일·규칙 단위로
  main 에 없던 조합 0건. 신규 테스트는 **뮤테이션 4종**으로 유효성을 확인했다 —
  각 수정을 되돌리면 대응 테스트 하나씩만 실패한다.
Not-tested: 실제 텔레그램 대상 수동 검증(봇 재시작 필요 — 머지 후 별도)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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