← 목록으로

선발화·조용한 실패 핸드오프 (밤샘)

날짜: 2026-08-31 (새벽, 대표 취침 중 자율 작업)
상태: 지시 4건 처리·실측 완료. 대표 손이 필요한 것 하나(이메일 키).
앞 문서: claudedocs/handoff/2026-08-30-voice-color-push-playbook.md
— 그 문서의 "다음 세션 작업" 을 이어받았다. 그 문서는 고치지 않았다.

🎯 대표 지시

  1. 하단 탭/설정이 목록 맨 아래라 바닥까지 긁어야 눌린다 → 떠 있게
  2. 아침에 나를 먼저 찾아오는 크루가 누구냐 — 어떻게 준비됐고 어떻게 할 건지 보고 + 내일 아침 8시 실도달
  3. 이메일 폴백 정리
  4. 대화 송수신 로직·메커니즘 결함 발견 시 개선

✅ 푸시 — 검증 끝났다 (9b0d7de6)

폰에 켜는 버튼이 없었다. /app/me/ 는 1080px 이상에서만 설정 판을 띄우는데
(SettingsShell.tsx:95), 「기기 알림」 스위치가 그 판에만 있었다. 푸시를 받을 수 있는
유일한 기기에 스위치가 없었던 것
— 웹 토큰 0개의 진짜 이유다.

✅ 「나」가 바닥까지 밀리던 것 (1cf40df1)

폰 서랍은 한 몸으로 스크롤한다(.rail{overflow-y:auto} + .rail-list{flex:none}).
방이 쌓일수록 계정·설정이 아래로 밀렸다. .rail-acctposition:sticky 로 바닥에 붙였다.

⚠️ 서랍을 두 칸(박힌 머리 + 스크롤 몸통)으로 쪼개지 않았다. 그러면 "팀AI 로고까지
같이 올라가야 한다" 는 앞선 결정을 되돌리게 된다. 한 줄만 붙이는 게 맞다.

✅ 아침에 누가 나를 먼저 찾아오는가 (1cf40df1)

무엇이 잘못돼 있었나

배선은 살아 있었다 — 08-30 09:00 KST 에 실제로 돌았다({"skip":"오늘 이미 대화함"}).
문제는 후보 선정이 대표의 물음에 답을 못 하는 모양이었다는 것이다.

결함 결과
후보가 최근 방 3개 고정(slice(0,3)) 기억 13개(오사카 여행 날짜·가족 생년)를 가진 일본어센세가 영영 후보에 못 들었다
send:true 에서 break 최근순이 곧 우선순위. 근거가 강한 뒤 후보는 판단조차 안 됐다
메시지 0개인 빈 방도 후보 재료 없는 방이 자리를 먹었다
"어제도 보낸 비슷한 말" 을 대화 기록으로 짐작 실측 사유: "어제도 대화가 있었습니다" — 사용자가 말한 것과 내가 먼저 건 것을 한 갈래로 봤다
"오늘 이미 대화함" = KST 자정 기준 새벽 2시에 한 줄 적고 잔 사람은 그날 아침이 통째로 죽었다

고친 모양

크루당 최근 방 하나씩 모아 한 번에 저울질한다. 누가 오늘 말을 걸어야 하는지는
후보를 나란히 놓고 봐야 정할 수 있다. LLM 호출도 3번 → 1번.

실측

03시 dry-run → 조용한 시간, 판단 안 함
08시 dry-run → 다이어트현실
   "어젯밤 순대국집 가기 전에 계란 하나 먼저 먹기로 했던 것처럼, 오늘도 굶고 들어가지는 맙시다"
08시 진짜 발송(개발 계정) → 오사카단골, 방에 메시지 남음, outreach_log 기록됨
   "6살·4살 아이들과 부모님까지 있으면 우메다나 텐노지는 첫날에 섞지 마세요"
   (push:failed 는 시험용 가짜 토큰 때문. 진짜 토큰 발송은 리마인드로 이미 실측됨)

pg_cron crew-outreach-daily: 0 0 * * *0 23 * * * UTC (= 08:00 KST)

내일 아침 준비 상태: 대표 outreach_log 0건 · 웹 토큰 1개 · cron active.

✅ 대화 송수신 — 조용한 실패 넷 (a4697e76)

① 실패 이유가 받는 쪽에서 통째로 버려졌다

lib/chat.ts 는 이유를 정성껏 만들어 onFail 로 올려보내는데 Stage.tsx
if (r.reason !== 'out-of-credits') return; 로 나머지를 전부 삼켰다.
서버가 500 을 내도 사용자는 「생각 중」이 사라지고 아무 일도 없는 화면만 봤다.

같은 파일이 "예전엔 실패를 콘솔에만 찍고 화면은 아무 말도 안 했다 — 사용자는 답을 영영
기다린다"
고 적어 두고 배선만 깔았지 받는 끝이 비어 있었다. 병① 그대로다.

② 시한(90초)이 조용히 내려가기만 했다

한 줄도 못 받았을 때만 이유를 띄운다(replied ref). 답은 왔는데 completed 만 늦은
경우에 "못 했어요" 를 띄우면 그게 거짓말이다.

③ 돈 — 깎아 놓고 답을 못 내면 그대로 끝났다

크레딧은 생성 전에 깎는데(잔액 없는 사람에게 모델을 안 태우려고) 최상위 catch 에
되돌리는 자리가 없었다. 병② 의 "모를 때 뺏는 쪽" 이 그대로 있었다.

④ 오류 스택을 클라이언트로 내보내고 있었다

응답에서 뺐다(콘솔에는 남는다). 더불어 서버 내부 오류 문구가 화면에 그대로 나갈 뻔했다 —
사람 읽으라고 쓴 말(asleep)만 그대로 쓰고 나머지는 우리 문장으로 바꾼다.

🔴 대표 손이 필요한 것 — 이메일 폴백

배선은 다 돼 있다. 자격증명 하나만 없다.

SUPABASE_ACCESS_TOKEN=xxx supabase secrets set RESEND_API_KEY=re_xxx \
  --project-ref tshbvqqvdycougdqrgza

⚠️ 이 세션은 시크릿 쓰기가 권한에서 막혔다(auto mode classifier). 대표가 직접 치거나
규칙을 열어야 한다. 앞 문서의 D1 권한 건과 같은 종류다.

판단 재료: 대표 본인은 안드로이드 웹 푸시가 살아 있어 이 층에 안 걸린다. 아이폰
사용자(대략 5명 중 1명)에게만 값이 있다. 유료 0명인 지금은 급하지 않다 — 유입이 생기고
아이폰 사용자가 들어오면 그때 켜는 게 맞다는 게 이 세션의 판단이다.

🚧 다음 세션

  1. analyzeAndDelegate 위치 인자 11개 — 옵션 객체로. 오늘은 안 건드렸다(밤새 회귀
    검증할 사람이 없는 상태에서 대화 심장부를 리팩터링하지 않는다). 어차피 손댈 일이
    생겼을 때 같이 하는 게 싸다
  2. SKILL 발동률 0 — 크루가 특기를 들고도 모델이 직접 답해 skill_sessions 가 안 쌓인다
  3. 유입 — 오픈했는데 유료 0명. 기능이 아니라 여기가 병목
  4. 구독 시작 메일 배포 — slack-bot 세션 소유. 코드·검증 완료, 배포만 남았다

⚠️ 이 세션에서 새로 확인한 함정

💬 다음 세션 복사용 (Quick Start)

📄 핸드오프: claudedocs/handoff/2026-08-31-outreach-and-silent-failures.md
   (앞 문서: 2026-08-30-voice-color-push-playbook.md)

🚀 시작 프롬프트:
8/31 새벽 핸드오프 확인했어. 아침 8시 선발화가 실제로 왔는지부터 보고,
이메일 폴백 켤지 정하자.