Skip to content

[slack] 스트림 상태 유실 후 append/update 재시도가 폭주하고 진행 카드가 죽는다 #744

Description

@cosmosjeon

공통 인시던트 컨텍스트
2026-09-11 20:10:29 KST, Slack 채널 C0AKXLE61J5(#ceo_lead_align_input)의 스레드 1789104278.881179 에서 사용자가 봇을 멘션했다. 봇은 2초 뒤 "경과 531초 / 8분째 실행 중" 진행 카드를 올렸고(=이미 돌던 다른 런의 진행 상태), 약 3분 뒤 전혀 무관한 ReelBrain 티켓(exp3-cardnews-reunion-conditions, qa-delivery BLOCKED)의 ## Terminal phase summary 본문을 그 스레드에 게시했다. 게시된 두 메시지는 이후 수동 삭제했다.
로그: ~/.cli-jaw/logs/serve.log 19555-19670, jaw-serve.err. DB: ~/.cli-jaw/jaw.db messages 1666/1676/1677.
라벨 제안: 본문 상단 표기 참조.

제안 라벨: bug priority:P1 area:slack

제목

Slack 스트림이 닫힌 뒤 append/stop이 실패하고, 진행 카드 대신 본문이 새 메시지로 떨어진다

요약

진행 카드는 chat.startStream → appendStream → stopStream만으로 갱신돼야 한다. 스트림이 닫히면(message_not_in_streaming_state) 같은 ts를 chat.update로 이어가려 하지만 ts가 이미 없어 message_not_found가 나고 idle이 재시도만 반복한다. 카드는 멈추고 최종 본문은 sendSlackText가 항상 새 chat.postMessage로 올린다. 2026-09-11 20:10 KST CEO 스레드 오배송에서 무관한 ReelBrain 요약이 별도 메시지로 증폭됐다.

재현·증거

상태 머신: src/slack/progress.ts begin() L256–289 (startStream, unsupported만 postMessage), flush() L217–245 (native=appendStream, fallback=update), finish() L312–336 (stopStream→update), useMessageUpdates() L131–138. 요청 소유: progress-lifecycle.ts L205–222. 본문: send-only-client.ts L162 항상 postMessage. draft-stream.ts는 Slack native 경로가 아니고 chunk.ts는 본문 분할만 한다.

~/.cli-jaw/logs/serve.log(= jaw-serve.err 중복):

  • message_not_in_streaming_state 32회 (append 12, stop 20). stop 10회는 [slack:progress] 짝, 10회는 notice-transport.
  • startStream user_not_found 3회. update message_not_found 48회, 그중 47회가 19570–19662 한 줄 폭풍.
  • 인시던트: 19555 [slack:in] C0AKXLE61J5: 너도 한번 해봐! → 19565 exited … 19613 chars → 19566 slack.post of:1 → 19567 appendStream streaming_state → 19570–19662 update not_found → 19669–19670 slack.post of:2.

단위 테스트(tests/unit/slack-progress-stream.test.ts L275–287)는 5분 후 스트림 종료 뒤 같은 ts update가 성공한다고 본다. 실서비스는 update도 not_found.

원인 분석

  1. Slack 스트림 TTL(~5분) 또는 이미 stop된 ts에 append/stop → streaming_state 오류.
  2. fallback은 교체 게시를 하지 않는다(L133–134 주석). 의도는 맞다.
  3. flush()stopped_by_userremoteEnded. message_not_found는 멈추지 않고 startIdle() L246–254가 3.2초마다 dirty=true → 47회 재시도.
  4. 새 메시지의 실체는 진행 폴백이 아니라 본문 경로다. 카드 ts가 죽은 뒤 사용자에게 보이는 것은 postMessage뿐이고, 19613자는 chunk로 2통이 된다.
  5. 가설: “경과 531초 / 8분째”(progress-activity.ts L33, L222–223)는 새 handle이 아니라 이미 ~531초 산 카드다. 그 스트림이 닫힌 뒤 오배송 본문이 같은 스레드에 추가로 떨어졌다.

수정 제안

  • flush()/finish()에서 message_not_found·cant_update_messageremoteEnded=true, 타이머 중지, 후속 startStream/postMessage 금지. 카드는 조용히 포기.
  • streaming_state 이후 append/stop 재호출 금지, fallback update 1회.
  • 죽은 카드 빈자리를 새 진행 메시지로 메우지 말 것. 본문 post 여부는 타깃 가드(축 A/B)와 맞출 것.
  • 대안: ~4분에 선제 stop 후 update 전환. 만료 후 카드 재생성 금지.
  • metrics.ts에 stream_state_lost 카운터. 테스트에 “expiry 후 not_found → 재시도 0, postMessage 0”.

영향 범위

Slack 진행 카드·복원(notice-transport.ts). Discord draft-stream은 비대상. 오배송 타깃 바인딩은 별 이슈.

검증 방법

기존 stream 테스트 + not_found 시 재시도 0. 5분+ 잡에서 새 진행 메시지가 없고 idle이 멈출 것. 이 이슈만으로 라이브 Slack에 쓰지 말 것.

관련 이슈 후보

  • 멘션이 다른 런 본문을 C0AKXLE61J5로 보낸 타깃/세션 바인딩
  • 진행 카드와 본문 postMessage의 이중 소유
  • notice-transport 중복 stopStream

같은 인시던트에서 갈라진 이슈: #742 · #743 · #745

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:slackSlack transport and bot behaviorbugSomething isn't workingpriority:P1Next up after stabilization

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions