Commit Graph
457 Commits
Author SHA1 Message Date
kimandClaude Sonnet 5 164ce6dcc1 feat: k2-app 웹 UI에 STL 슬라이싱 기능 추가
로컬 PC의 STL 파일을 채팅 없이 K2 앱에서 바로 업로드→슬라이싱→K2 업로드까지
할 수 있는 모달 추가. print3d_model 채팅도구와 로직을 공유하도록
k2-slicer-core.ts로 공통 부분(프린터 프로파일, PrusaSlicer 실행, Moonraker
업로드, 시간 파싱)을 분리했다.

- k2-slicer-core.ts: print3d.ts에서 추출한 공용 슬라이싱 로직 + 오늘 실측한
  FAST_SPEED_ARGS(퍼리미터/인필/트래블 속도업, 브릿지는 유지)
- routes-k2.ts: POST /api/k2/slice(STL 원본 바이트 바디+쿼리스트링 설정,
  --duplicate로 동일 파츠 복제 지원 — 서로 다른 파츠 합치기(--merge)는 이
  헤드리스 환경에서 세그폴트 나서 미지원), POST /api/k2/print-start(Moonraker
  프록시, mixed-content 회피)
- k2-app.html: 헤더에 "🔪 슬라이싱" 버튼 → 모달(파일업로드/수량/소재/레이어
  높이/인필/속도/서포트) → 결과+출력시작 버튼. 실제 파일 업로드로
  슬라이싱→K2업로드까지 브라우저에서 확인함(출력 시작은 실제 프린트 중이라
  테스트 안 함)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-20 17:13:13 +09:00
kimandClaude Sonnet 5 9dfef8b568 refactor: robot-app 부품 리스트를 표에서 펼침형 카드로 변경
행마다 항상 다 펼쳐져 있던 표 대신, 이름/상태뱃지/소계만 보이는 헤더를
클릭하면 수량·단가·상태·메모 편집 필드가 펼쳐지는 카드로 교체. 펼침
상태는 화면 전용(서버 저장 안 함)이라 다시 그려도 유지됨.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-20 14:58:38 +09:00
kimandClaude Sonnet 5 54285c8a4c feat: robot-app에 내장 채팅 패널 추가 (doctor/lawyer 앱과 같은 사이드 패널 패턴)
부품 리스트를 보면서 바로 옆에서 로봇 프로젝트 얘기를 할 수 있게 리사이즈
가능한 채팅 패널을 대시보드에 붙임. 세션ID를 "rb_"로 시작하게 해서
tool-scope.ts가 이 세션 안에서는 키워드 매칭 없이 robot_project 도구를
항상 열어주도록 예외 처리(doctor_app의 'dr_'/lawyer_app의 'lw_'와 동일한
패턴) — "이 부품 지워줘"처럼 짧게 말해도 바로 동작한다.

채팅 응답이 끝나면 대시보드(부품표/예산/작업)를 자동으로 다시 불러와서
채팅으로 바꾼 내용이 즉시 화면에 반영되게 함.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-20 14:48:50 +09:00
kimandClaude Sonnet 5 e3d5da38cf feat: robot_project 전용 채팅도구 추가 — 로봇 프로젝트 대시보드를 채팅으로 편집
로봇 프로젝트("메카넘휠" 이동로봇) 대화에서 부품 상태 변경/추가/삭제, 작업
완료 체크, 새 단계/작업 추가, 메모 추가를 채팅으로 바로 할 수 있게 하는
도구. robot-app.html 대시보드와 같은 저장소(robot-storage.ts로 분리)를
쓰므로 채팅에서 바꾸면 대시보드에도, 대시보드에서 바꾸면 다음 채팅
조회에도 즉시 반영된다.

- robot-storage.ts: routes-robot.ts에서 인라인으로 있던 저장 로직을
  분리(case-storage.ts와 같은 목적) — 라우트와 도구가 공유
- robot-project.ts: get/add_part/update_part/delete_part/add_task/
  set_task_done/add_phase/add_note 8개 액션, 이름 일부일치 검색이라
  내부 id를 몰라도 채팅으로 지칭 가능. 물리적 동작이 없어 confirm 불필요
- registry.ts/build-tools.ts/tool-scope.ts 3곳 등록. 게이트 키워드는
  "로봇청소기"(기존 Valetudo 대화)와 안 겹치게 프로젝트 고유 어휘로만 매칭
- 실제 채팅으로 "예산 얼마야?+메카넘휠 주문완료로 바꿔줘" 동시 요청 테스트,
  정확한 응답+저장 확인

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-20 14:16:11 +09:00
kimandClaude Sonnet 5 b358cbc968 feat: robot-app에 K2/밀링 장비 연동 위젯(장비 탭) 추가
로봇 프로젝트 대시보드에 실제 공작 장비(K2 3D프린터, CNC 밀링 프린터실
PTZ카메라) 상태를 보는 "장비" 탭을 추가. 우선 가벼운 주기적 스냅샷 방식으로
구현(실시간 WebRTC 상시연결은 자원 부담 커서 보류, 추후 업그레이드 검토).

- routes-k2.ts: GET /api/k2/status — Moonraker print_stats/virtual_sdcard를
  요약해서 반환(온라인여부/상태/파일명/진행률/경과시간)
- nvr.ts: GET /api/nvr/frame/:channelId — go2rtc(127.0.0.1:1985)의
  frame.jpeg를 프록시(밀링 PTZ카메라, channelId=1000 "프린터실"), go2rtc
  포트를 브라우저에 직접 노출하지 않음
- robot-app.html: "장비" 탭 — K2는 20초 간격으로 짧게 WebRTC 연결→ontrack에서
  캔버스 캡처→즉시 종료(상시 연결 안 함), 밀링은 위 프록시로 img 새로고침.
  "장비" 탭을 보고 있을 때만 폴링, 다른 탭으로 나가면 타이머 정지

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-20 14:07:54 +09:00
kimandClaude Sonnet 5 9641f689b8 feat: 로봇 프로젝트 관리 대시보드(robot-app) 추가
메카넘휠 이동로봇 프로젝트(파이5+RPLidar, 추후 팔 추가)의 부품리스트/
단계별 작업/예산을 관리하는 웹앱. doctor-app의 GET/PUT 단일문서 패턴을
재사용하되 케이스 목록 없이 단일 프로젝트 문서로 단순화했고, 첫 로드시
project_robot_dog.md 메모 기준 실제 진행상황을 기본 시드로 반환한다.

- src/gateway/routes/routes-robot.ts: GET/PUT /api/robot/project,
  사용자별 workspace/.smallclaw/robot-project.json에 원자적 저장
- web-ui/html/robot-app.html: 부품/작업/메모 3탭, 실시간 예산 요약,
  디바운스 자동저장
- index.html 🏠 홈 드롭다운에 🤖 로봇 링크 추가

장비연동/전용챗봇/외부데이터연동/CAD플러그인은 다음 단계로 보류.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-20 13:46:37 +09:00
kimandClaude Sonnet 5 cd4843d480 refactor: print3d_model 슬라이서를 CuraEngine에서 PrusaSlicer로 교체
CuraEngine CLI는 구조적으로 예상 출력시간 헤더가 항상 더미값(6666)만 남는
한계가 있어(33b5a25 참고) PrusaSlicer(apt, gcode-flavor=klipper 네이티브
지원)로 교체. DISPLAY 없이도 --export-gcode가 완전 헤드리스로 동작함을
확인했고, 슬라이싱 후 파일에 실제 "estimated printing time"이 직접
기록되므로 stderr 파싱 없이 파일에서 바로 읽으면 됨. K2 프로파일/시작-매크로
값은 그대로 유지. 더 이상 쓰지 않는 resources/cura/*.def.json 제거.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-20 13:29:05 +09:00
kimandClaude Sonnet 5 33b5a258b2 fix: print3d_model 예상 출력시간이 항상 6666초로 나오는 CuraEngine CLI 버그 수정
CuraEngine 5.0.0 소스 확인 결과, CommandLine 백엔드는 isSequential()이 항상
true라 슬라이싱 "시작 전"에 G-code 헤더를 미리 써버리고(processStartingCode
-> getFileHeader, print_time 인자 없음) 끝난 뒤 되돌아가 실제값으로 덮어쓰는
코드가 어디에도 없다. 그래서 ;TIME: 헤더는 항상 gcodeExport.cpp에 하드코딩된
더미값(6666)만 남는다 — 확률적 버그가 아니라 CLI 모드의 구조적 한계였음.

실제 값은 슬라이싱 완료 후 stderr(verbose 로그)에 "Print time (s): N"으로만
찍히므로, G-code 파일 헤더 대신 stderr를 파싱하도록 변경.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-20 13:16:34 +09:00
kim fdbed6266e feat: STL→K2 프린터 자동 슬라이싱/출력 채팅 도구(print3d_model) 추가
CuraEngine(apt cura-engine, 클로서버)로 STL을 슬라이싱해 K2 콤보(Moonraker)에
업로드하는 도구. action="prepare"(다운로드+슬라이싱+업로드, 출력은 안 함)와
action="start"(confirm=true 필수, 실제 출력 시작)로 분리 — 재료/시간 소모되는
물리적 동작이라 사용자 확인 없이는 출력 시작 안 되게 함.

프린터 프로파일(빌드볼륨/노즐/리트랙션/온도/START_PRINT·END_PRINT 매크로 호출)은
추측이 아니라 K2에 이미 있던 Creality Print 슬라이싱 샘플 gcode 헤더에서 실측값
그대로 추출. fdmprinter.def.json/fdmextruder.def.json은 설치된 CuraEngine
5.0.0과 버전 맞춰서 받아야 함(main 브랜치 최신판 쓰면 설정 스키마 불일치로
슬라이싱이 즉시 실패함 — resources/cura/에 버전 고정해서 커밋).

tool-scope.ts에 3D프린트 관련 키워드 게이트 추가(다른 니치 도구들과 동일 패턴).
end-to-end 테스트 완료(3DBenchy STL로 다운로드→슬라이싱→K2 업로드 확인, 테스트
파일은 삭제함).
2026-09-20 00:19:02 +09:00
kim 101a673827 fix: K2 카메라 연결 지연(20~30초) 수정 — ICE gathering 완전종료 대신 1.5초 바운드
onicecandidate가 ev.candidate===null(전체 gathering 종료)까지 기다린 뒤에만 오퍼를
보내고 있었는데, 이 네트워크에서 그 신호가 20~30초씩 걸림(가상 인터페이스들 순회+
타임아웃으로 추정). 실제 필요한 STUN srflx 후보는 1~2초면 잡히므로, 완전종료를
기다리지 않고 1.5초 타임아웃 뒤 그때까지 모인 후보로 바로 오퍼를 보내도록 변경.
브라우저 실측: 연결까지 20~30초 → 3초 이내로 단축.
2026-09-19 23:55:22 +09:00
kim 5eed244340 fix: NVR go2rtc WS mixed-content, K2 재연결 레이스, 시그널링 타임아웃 + WS 프록시 공통화
- nvr-app.html: go2rtc WS URL을 페이지 오리진 기준 상대경로로 (하드코딩된 ws://LAN IP가
  HTTPS 페이지에서 mixed-content로 막히던 문제)
- k2-app.html: 카메라 재연결 시 오래된 연결의 실패 콜백이 새 연결 상태를 덮어쓰던 레이스 수정
- routes-k2.ts: K2 시그널링 fetch에 타임아웃 추가(프린터 응답없음 시 무한대기 방지)
- nvr.ts/routes-comfyui.ts/routes-android.ts에 중복돼있던 raw WebSocket 업그레이드
  프록시(connect→pipe→cleanup)를 ws-proxy-util.ts의 공통 헬퍼로 통합
2026-09-19 23:46:53 +09:00
kimandClaude Sonnet 5 d65e8d958d feat: K2/밀링 앱을 NPM 서브도메인(HTTPS)으로 전환, 홈 드롭다운 재추가
print.applecherry.net(K2 콤보 Fluidd) · mill.applecherry.net(CNC 밀링
OctoPrint)로 NPM 프록시+Let's Encrypt 설정 완료. 앱들이 이제 HTTPS라
mixed content 경고 없이 홈 드롭다운에 다시 노출.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PppKcQmeTJDQCpfcgKietu
2026-09-19 16:39:30 +09:00
kimandClaude Sonnet 5 f13b40712c feat: 복원된 옛 홈페이지(애플체리, XpressEngine) 앱 추가
트루나스 Plex/applecherry에 있던 옛 XpressEngine(XE 1.11.6) 사이트를
PHP7.4+MariaDB 도커 앱으로 복원하고 home.applecherry.net(NPM+Let's
Encrypt)으로 서빙. 홈클로 홈 드롭다운에 iframe 래퍼 앱으로 추가.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PppKcQmeTJDQCpfcgKietu
2026-09-19 11:24:36 +09:00
kimandClaude Sonnet 5 72d4ac62ee fix: K2 콤보 카메라 STUN 서버 복원 (host candidate만으론 응답 없음)
STUN을 빼고 host candidate만 쓰면 DTLS가 failed로 끝나고 프린터에서
데이터가 전혀 안 옴(bytesReceived 0) — 원인은 불명이나 재현됨. 크리얼리티
데모 코드처럼 STUN을 유지해야 정상 연결됨(다소 느리지만 20초 내외).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PppKcQmeTJDQCpfcgKietu
2026-09-18 23:17:57 +09:00
kimandClaude Sonnet 5 a8149c136a feat: 프린터실 카메라 웹코덱(WebRTC/MSE) 전환 + K2 콤보 카메라 프록시, 옛 3D프린터 앱 제거
- go2rtc가 CORS/Origin 체크로 브라우저 직접접속을 막던 문제 해결: 클라이언트
  스크립트를 벤더링해 같은 오리진으로 서빙하고, WS는 게이트웨이 프록시
  (attachGo2rtcWsProxy)가 Origin을 다시 써서 우회. CCTV 앱의 프린터실 카메라
  라이브뷰가 MJPEG 대신 WebRTC/MSE를 쓰도록 전환(대역폭 절감).
- go2rtc mjpeg 트랜스코더가 멈추는 경우를 감지해 자동 재기동하는
  /api/nvr/mjpeg-health 엔드포인트 추가.
- 신규 3D프린터(K2 콤보, Klipper 스탠드얼론)의 내장 WebRTC 카메라 시그널링을
  프록시하는 /api/k2/webrtc-signal 추가 — 홈클로가 HTTPS로 서빙되는데 카메라가
  평문 HTTP만 지원해 브라우저 mixed-content 차단을 우회하기 위함.
- 옛 3D프린터(단종된 Anet A8 Plus, OctoPrint) 앱과 홈 드롭다운 링크 제거.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PppKcQmeTJDQCpfcgKietu
2026-09-18 22:40:10 +09:00
kimandClaude Sonnet 5 e7a86d6f2d fix: go2rtc mjpeg 트랜스코더 멈춤 자동 감지/복구 엔드포인트 추가
프린터실 카메라의 go2rtc mjpeg 가상 스트림이 카메라 wifi 순단 등으로
멈추면 뷰어가 재연결해도 같은 멈춘 프로세스에 붙어 계속 안 나오는
문제가 있었음. /api/nvr/mjpeg-health가 컨트롤 탭 로드 시 go2rtc
스트림 상태를 확인해 정체된 트랜스코더만 재시작한다.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PppKcQmeTJDQCpfcgKietu
2026-09-18 14:27:38 +09:00
kimandClaude Sonnet 5 d34dd10ba7 feat: 독립 카메라에 ONVIF PTZ 지원 추가 (프린터실 카메라 등록)
extra-cameras API/UI에 ptz·onvifPort 필드 추가하고, 프린터실 머큐시스 MC230
카메라를 PTZ 지원 독립 카메라로 등록.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PppKcQmeTJDQCpfcgKietu
2026-09-18 00:31:38 +09:00
kimandClaude Sonnet 5 145f4e698f feat: 작가앱에 작품 제목 수정 기능 추가
- 백엔드 PUT /api/writer/works/:id는 이미 title 갱신을 지원했으나 프론트에 버튼이 없었음
- 헤더에 "✏️ 제목 수정" 버튼 추가, 작품 선택 시에만 표시

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PppKcQmeTJDQCpfcgKietu
2026-09-15 12:00:17 +09:00
kimandClaude Sonnet 5 e3f038617d feat: CNC 밀링 앱 추가, 3D프린터/밀링 서버 IP를 새 옥토서버(.3)로 갱신
- web-ui/html/milling-app.html 신규: OctoPrint 밀링 인스턴스(:5001) iframe 임베드
- 홈 드롭다운에 "⚙️ CNC 밀링" 버튼 추가
- printer-app.html: 미니PC(192.168.0.106) → HP 프로데스크 "옥토서버"(192.168.0.3)로 IP 갱신

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PppKcQmeTJDQCpfcgKietu
2026-09-13 20:27:59 +09:00
kimandClaude Sonnet 5 8eb1a6c160 feat: 3D프린터 앱(OctoPrint) 홈 드롭다운에 추가
작업실 미니PC(192.168.0.106)에 설치한 OctoPrint를 iframe으로 임베드.
홈 CCTV/HA 옆에 🖨️ 3D프린터 항목 추가.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E5kyXurhNSboRcY3dAGceY
2026-09-12 00:13:15 +09:00
kimandClaude Sonnet 5 c22377778e feat: 유저별 로고·브랜드명 커스터마이징 — .smallclaw/users/<user>/logo.png, brand_name.txt
헤더/환영화면/채팅 아바타·라벨을 로그인 유저별로 스왑. 파일 없으면 기본 HomeClaw 유지.
애플·자스민 계정에 기본 로고(이모지 아이콘) 적용.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E5kyXurhNSboRcY3dAGceY
2026-09-11 16:13:38 +09:00
kimandClaude Sonnet 5 aaf8dbfb33 fix: "Resolution too big" 채널은 서브스트림이 안 나옴 — 메인 강제 + 서브고정 토글 숨김
앞 커밋(24a24c6)으로 마당(ch5)이 6분할에 다시 뜨는데, 6분할 기본이
서브스트림이라 /ch5_1.264 요청 → NVR이 ch5를 제대로 인제스트 못해서
서브 스트림 자체가 안 나옴(메인 /ch5_0.264는 패스스루 정상).

- chnStreamOverride(): statusText가 "Resolution too big"면 무조건 stream 0(메인)
- 이런 채널엔 "저화질(서브) 고정" 토글 숨김 (눌러도 깨지기만 함)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017yEadXvr9ZWp2S5iRB1B25
2026-09-09 14:34:15 +09:00
kimandClaude Sonnet 5 24a24c6e7f fix: 홈CCTV 6분할이 online=false 채널을 빈 슬롯 처리 — 마당(ch5)이 안 보임
Xiongmai NVR이 3rd파티 카메라(Mercusys)를 "Resolution too big" +
online=false로 계속 보고하는데, 실제로는 RTSP 패스스루가 정상 동작함
(/ch5_0.264 프레임 정상 수신 확인). 그런데 quadChannels 필터가 c.online만
보고 있어서 마당이 6분할에서 빈 슬롯으로 렌더됐음.

chnStreamable() = online || "resolution too big" 상태 → 이런 채널도
6분할·라이브 버튼에 포함. 진짜 죽은 채널은 img.onerror가 "영상 없음" 처리.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017yEadXvr9ZWp2S5iRB1B25
2026-09-09 14:27:26 +09:00
kimandClaude Sonnet 5 2b1caf79e3 fix: NVR 녹화목록 검색이 채널 0~3만 조회 — Channel 배열 4개 하드코딩
MAX_CHN=10인데 R.SearchRecord Channel 배열이 4개라 ch4·5(거실·마당)
녹화가 목록에 안 떴음. 16개 True로 확장.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017yEadXvr9ZWp2S5iRB1B25
2026-09-09 12:15:03 +09:00
kimandClaude Sonnet 5 69efc44476 chore: 마당을 NVR 채널 5로 이관, standalone 카메라(1000) 제거
카메라 메인 스트림을 1920×1080으로 낮춰(ONVIF) NVR이 채널 5로
직접 물 수 있게 됨 → NVR 녹화 포함. standalone id 1000 불필요해져 삭제.

- nvr.channelLabels["5"] = "마당"
- nvr.channelStream에서 "1000" 제거 (extra camera 없어짐)
- nvr.extraCameras = []

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017yEadXvr9ZWp2S5iRB1B25
2026-09-09 12:12:11 +09:00
kimandClaude Sonnet 5 eb4805a27d fix: NVR 4분할에서 마당(독립 카메라) "영상 없음" — 강제화질 설정 무시하던 버그
routes/nvr.ts의 extra 카메라 매핑이 preferredStream을 null로 하드코딩해서
nvr.channelStream(채널별 서브스트림 고정)이 독립 카메라엔 적용되지 않았음.
Mercusys 마당은 메인 스트림(/stream1) 동시 RTSP 세션을 1개만 허용해서,
4분할 "메인(고화질)" 체크 시 다른 세션과 겹치면 Operation not permitted로
거부 → 타일이 "영상 없음". (확대 모달은 4분할을 먼저 정지해서 됐음)

- getChannelStreamPrefs()[id]를 독립 카메라 preferredStream에도 반영
- config: channelStream["1000"]=1 (마당 서브 720p 고정) — 채널3과 동일 방식

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017yEadXvr9ZWp2S5iRB1B25
2026-09-09 11:23:52 +09:00
kimandClaude Sonnet 5 4290b5f201 fix: 마당(머큐시스) 카메라 정위치 재설치에 맞춰 rotate180 해제
카메라를 물리적으로 바로 달아서 소프트웨어 180도 회전(ffmpeg hflip,vflip)이
더는 필요 없음 — extraCameras[1000].rotate180 true→false.

config.json에 이전 세션의 미커밋 변경도 함께 포함:
- nvr.channelLabels 채움 (0 선룸 / 1 옥상 / 2 대문 / 3 뒷산 / 4 거실)
- extraCameras[1000] 라벨 머큐시스→마당
- selfHealer.secondary 모델 kimi-k2.6:cloud→glm-5.3-flash:cloud

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017yEadXvr9ZWp2S5iRB1B25
2026-09-08 21:47:20 +09:00
kimandClaude Sonnet 5 f3c392bc1b fix: nvr_snapshot이 위치 이름 대신 외운 채널 번호를 써서 엉뚱한 카메라를 가져오던 문제
실사고(papa): 대화 초반 "2번이 옥상 카메라"를 모델이 외웠는데, 이후 채널 라벨을
붙이면서 채널 2 = 대문이 됨. "옥상 확인"에 모델이 camera:"2" 전달 → 대문 프레임을
받고도 "옥상 카메라 확인했습니다"라고 답함.

- 도구 설명/스키마: 채널 번호 말고 위치 이름(선룸/옥상/대문/뒷산/거실/마당)을 쓰라고 명시
- 번호로 호출 시 결과에 "[확인] 채널 번호 '2' = 대문 카메라입니다" 명시 —
  모델이 스테일 매핑을 자각하고 이름으로 재호출하게

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rfme1WVEPkpNwnf5oVNXXc
2026-09-07 11:25:21 +09:00
kimandClaude Sonnet 5 cf331a7440 fix: nvr_snapshot 오류 상세에서 RTSP 자격증명 제거 + ffmpeg 배너 잡음 제거
ffmpeg가 stderr에 rtsp://user:pass@host 를 통째로 찍어 도구 결과·모델 컨텍스트로
비밀번호가 샜다. user:pass@ → ***@ 로 가리고, 실패 원인 줄만 남긴다.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rfme1WVEPkpNwnf5oVNXXc
2026-09-07 11:20:03 +09:00
kimandClaude Sonnet 5 2e2a689b32 fix: nvr_snapshot이 NVR 채널 수를 4로 하드코딩 — 실제 조회로 변경 (우리 건 6채널)
/netsdk/Channel/IPCamInfo + Stat/IPC로 실제 연결된 채널만 나열(nvrEnabledChannelIds).
같은 라벨의 독립카메라(Mercusys)가 있으면 NVR쪽 항목 대신 직접 RTSP 쪽을 쓴다
(NVR ch5 = Mercusys인데 NVR쪽 연결은 Connect Failed).
독립카메라 라벨 변경 엔드포인트 POST /api/nvr/extra-cameras/:id/label 추가.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rfme1WVEPkpNwnf5oVNXXc
2026-09-07 11:18:30 +09:00
kimandClaude Sonnet 5 58a079fcb1 fix: nvr_snapshot 후속턴에 도구 사라지던 문제 + 캡처 이미지 채팅에 안 뜨던 문제
실사용(papa, 2026-09-07)에서 드러난 2건:
- "cctv확인해봐"로 도구가 돌았는데 다음 "아니다 cam2"/"사진 보여줘" 후속턴엔
  hasCctvKeyword가 현재 메시지만 보느라 도구가 빠졌고, 모델이 "저는 그런 툴이
  없습니다"를 3턴 반복 → topicText(직전 2턴 포함)로 검사, cam N·스냅샷·캡처 등 추가
- 캡처된 프레임을 모델이 프로즈로만 설명하고 이미지 마크다운을 안 뽑아 사용자가
  사진을 못 봄 → satellite_snapshot처럼 execute-tool에서 SSE로 이미지 직접 push
- 도구 결과 지시문을 "이전 대화 짐작 말고 이 프레임에 보이는 것만 구체적으로"로 강화
  (gemma4가 첫 응답에서 맥락에 기대 뭉뚱그린 사례)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rfme1WVEPkpNwnf5oVNXXc
2026-09-07 11:14:24 +09:00
kimandClaude Sonnet 5 1a40512270 feat: nvr_snapshot — 모델이 집 CCTV 화면을 직접 보고 답한다
"마당에 누구 있어?", "택배 왔나 봐줘" 같은 요청에 NVR/독립카메라의 현재 프레임을
ffmpeg로 한 장 캡처해 비전 모델에 임베드한다.

- src/tools/nvr.ts: RTSP → ffmpeg 단일 프레임 → workspace/nvr-snapshots/ → ![](/api/files/…).
  카메라는 이름("현관")·채널번호("2")·자유입력 모두 해석, 못 찾으면 목록 반환.
  main 실패 시 sub 스트림으로 1회 재시도. 파일명은 ASCII(썸네일 execSync 한글경로 회피).
- routes/nvr.ts: getNvrConfig/getExtraCameras/getChannelLabels export + buildRtspUrl 추출
  (라이브 스트림 라우트와 공유, 중복 제거).
- tool-scope.ts: hasCctvKeyword 게이트 — CCTV/카메라/마당/현관/택배/"누구 있" 등에만.
- handle-chat.ts: 비전 판정을 _modelSupportsVision 헬퍼로 통일 + **gemma 추가**
  (gemma4:31b-cloud는 멀티모달인데 4곳의 정규식에서 다 빠져 있어 weather_map_screenshot
  등에서도 "이미지 못 봄" 취급받고 있었음 — 2026-09-07 실측으로 비전 정상 확인).
  _imagingTools 3곳에 nvr_snapshot 추가.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rfme1WVEPkpNwnf5oVNXXc
2026-09-07 11:05:13 +09:00
kimandClaude Sonnet 5 5834242261 fix: "숫자+단위"뿐인 인라인 LaTeX($60\sim80\text{cm}$)를 평문으로 푼다
모델이 "60~80cm", "25°C" 같은 걸 습관적으로 `$...$`로 감싸는데, KaTeX로 렌더하면
수식 이탤릭체라 한글 문장에서 어색하고 TTS·복사도 깨진다. \text{}/\mathrm{} 단위가
있고 나머지가 숫자·범위기호·간단한 연산자뿐이면 유니코드 평문으로 치환한다
(\sim→~, \times→×, ^2→² 등). 치환 뒤 `\ ^ _ { } =`가 남으면 진짜 수식이라 그대로 둔다.

chat-render.js(메인챗)·katex-inline.js(앱페이지) 둘 다.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rfme1WVEPkpNwnf5oVNXXc
2026-09-07 10:27:01 +09:00
kimandClaude Sonnet 5 bf6aae12b3 fix: KaTeX 통화 가드가 $60\sim80\text{cm}$ 같은 숫자로 시작하는 수식을 죽이던 문제
`$` 다음에 숫자가 오면 무조건 통화로 보고 여는 `$`를 감췄는데, 모델이
`$60\sim80\text{cm}$`처럼 숫자로 시작하는 LaTeX를 내면 수식 전체가 날것으로
남아 `\sim`·`\text{cm}`가 그대로 노출됐다("단위가 이상하게" 신고).

다음 `$`까지의 구간에 백슬래시가 있으면 수식으로 판단해 그 `$`는 건드리지 않는다.
기존 통화 케이스(`$100 절감 … $\rightarrow$`)는 그대로 통화로 처리됨.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rfme1WVEPkpNwnf5oVNXXc
2026-09-07 10:23:30 +09:00
kimandClaude Opus 5 ddd276c15a docs: 수동 메모리 게이팅의 근거 정정 — USER.md는 prompts/ 아래 실재한다
a1033f6에서 "USER.md도 SOUL.md도 4명 전 사용자 누구에게도 없어서 memory_browse/read는
'not found' 에러만 낼 수 있다"고 적었는데 **틀렸다**. 감사 때 `ls workspace/*.md`로만 확인했고,
파일은 `workspace/prompts/` 아래에 있다 — resolvePromptPath가 PROMPT_FILES에 대해 그쪽을
**먼저** 본다(server.ts:1199). papa의 USER.md는 3.2KB에 카테고리 5개, 최종 수정 2026-08-06으로
homeclaw_memory 컬렉션의 최신 문서 날짜와 정확히 일치한다. 도구 셋 다 정상 동작한다.

게이팅 결정 자체는 유지한다 — 근거가 사용량이기 때문이다(1,011턴에서 browse 0 / read 2 /
write 1회인데 매 턴 357토큰). 다만 코드·테스트 주석에 박힌 거짓 근거를 걷어낸다. "죽은
레거시라 빼도 된다"와 "멀쩡하지만 거의 안 쓰여 게이팅한다"는 다음 사람이 내릴 판단이 달라진다.

memory_write는 파일이 없으면 만들지 않고 에러를 낸다는 점도 함께 정정했다(execute-tool.ts:1220).
앞선 커밋 메시지의 "없으면 새로 만든다"는 서술이 잘못이었다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rfme1WVEPkpNwnf5oVNXXc
2026-09-04 21:12:21 +09:00
kimandClaude Opus 5 a1033f6013 perf: USER.md 수동 메모리 도구 3종을 기억 의도가 있는 턴에만 싣는다
memory_browse / memory_write / memory_read는 지금까지 **매 턴 무조건** 실려 357토큰을 냈다.
실사용 1,011턴에서 호출은 browse 0회 / read 2회 / write 1회.

파고 보니 단순한 저사용이 아니었다: **USER.md도 SOUL.md도 4명 전 사용자 누구에게도 없다.**
resolvePromptPath에 템플릿 폴백이 없으므로(교차 오염 위험은 없다), memory_browse와 memory_read는
파일이 없어 "USER.md not found. Create it first." 에러만 돌려줄 수 있는 상태로 매 턴 실려 있었다.
실제 장기기억은 일별 로그 + Chroma 벡터 추출(자동)이 담당한다 — USER.md 경로는 그 시스템이
대체한 레거시다([[project_legacy_skill_system]]과 같은 유형: 죽은 하위시스템이 스키마 비용만 계속 냄).

제거가 아니라 게이팅인 이유: 사용자가 "기억해둬"라고 명시하는 턴에는 있어야 하고, memory_write는
파일이 없으면 새로 만들므로 그 경로 자체는 지금도 유효하다. topicText로 판정해 "그것도 기억해둬"
같은 후속 발화도 잡는다. memory_stats는 벡터 스토어를 보므로 게이트 대상이 아니다(13회 호출됨).

실측(고유 782턴): 도구 토큰 중앙값 1,221 → 864 (-357/턴), 누적 278,460tok 절감.
수동 메모리 도구가 실리는 턴은 0.3%로 떨어진다.

기존 계약 테스트 2개가 "memory_read는 핵심 도구라 항상 남는다"를 고정하고 있어 갱신했다 —
핵심 도구 목록에서 memory_read를 memory_stats로 바꾸고, 기억 의도가 있으면 다시 나타나는지를
새로 고정했다. 551개 통과.

⚠ 별건: 이번에 사용자가 "기억해둬"라고 5턴에 걸쳐 명시한 세션(0c367bd7 10925~10940)에서
memory_write가 한 번도 불리지 않았음을 확인했다. 모델은 매번 "기억해 두겠습니다"라고만 했다.
정보 자체는 일별 로그에 남아 벡터 추출이 받지만, 명시적 지시가 도구 호출로 이어지지 않는 것은
별도 문제다(MESSAGING 가드와 같은 유형의 도달 실패). 이 커밋은 그 문제를 고치지 않는다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rfme1WVEPkpNwnf5oVNXXc
2026-09-04 21:05:16 +09:00
kimandClaude Opus 5 46a5c3f59f perf: 검색 결과 읽기 지침을 시스템 프롬프트에서 빼 결과가 생긴 뒤에만 붙인다
효율 감사 실측(사용자 턴 1,011건 / LLM 호출 3,613건 / prompt 누적 29.1M tok):
  · 고정비 2,494tok/턴 = 전체 prompt의 31%
  · 출력 60토큰 미만 호출이 47% — 도구 라운드·가드 재프롬프트라 고정비가 라운드 수만큼 곱해진다
  · **59%의 턴이 grounding 도구를 한 번도 부르지 않는다**

searchReadingRule(481tok)에는 성격이 다른 두 가지가 한 덩어리로 섞여 있었다:
  ① 검색어를 어떻게 쓸 것인가(세대 추측 금지, 154tok) — 검색 **전에** 필요
  ② 돌아온 결과를 어떻게 읽을 것인가(스테일 타임스탬프·표 행 정렬, 333tok) — 결과가 **생긴 뒤**
     에만 쓸모가 있다
②를 groundingResultReadingRule()로 분리해, handle-chat이 첫 grounding 결과가 들어온 시점에
대화 뒤에 한 번 덧붙인다. 읽을 결과가 없는 59%의 턴은 이제 이 값을 아예 내지 않고, 부르는
턴도 검색어를 쓰는 1라운드에는 내지 않는다. 인사 턴 시스템 프롬프트 1,273 → 904tok(-29%).

**시스템 메시지를 갈아끼우지 않고 뒤에 덧붙인 이유는 KV 캐시다.** 시스템 프롬프트는 라운드 루프
앞에서 한 번 만들어지므로, 라운드마다 다시 만들면 프리픽스가 달라져 캐시가 통째로 무효화된다 —
라운드마다 7K 토큰을 다시 계산하게 되어 아낀 것보다 잃는 게 크다. 뒤에 붙이면 프리픽스는 그대로다.

부수 효과로 지침이 대상(검색 결과) 바로 옆에 놓인다. 제약이 겹치면 먼 것부터 버리는 모델에게는
이쪽이 유리하다([[feedback_local_model_needs_code_backstop]]).

2026-09-02 감사가 만든 hasGroundingTools 게이트는 사실상 죽어 있었다는 점도 이번에 드러났다 —
tool-scope에 web_search를 거르는 분기가 없어 그 조건은 항상 참이다. 이번 분리는 그 게이트에
의존하지 않는다.

기존 회귀 테스트가 이동을 정확히 잡아냈다(2026-08-10 캐시된 기상표 오독 사고의 예시 문자열).
테스트를 새 위치로 옮겨 내용이 사라지지 않았음을 계속 고정한다. 546개 통과.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rfme1WVEPkpNwnf5oVNXXc
2026-09-04 20:59:35 +09:00
kimandClaude Opus 5 83e9bd54f9 chore: 가드 발동률 실측 스크립트 — 회전에 견디는 누적 카운트 방식
첫 버전은 "몇 번째 줄 이후"로 기준선을 잡았는데, 로그 회전 설정을 확인해 보니 그 방식이
하루도 못 간다: logrotate가 크기 1M 기준 + `copytruncate`라, 회전 시 내용이 .1로 복사되고
원본은 0바이트로 잘린다. 패치 당일 gateway.log가 이미 1.05M이라 그날 밤 바로 잘릴 참이었다.

대신 gateway.log* 전 파일(.gz 포함)을 합쳐 패턴별 누적 횟수를 세고 기준선 값을 뺀다. 회전해도
내용은 .1/.2.gz로 옮겨갈 뿐 사라지지 않으므로 이 방식은 회전에 견딘다. rotate 14라 파일이
실제로 없어지려면 현재 증가 속도로 반년 이상 걸리고, 그때는 차이가 음수로 나와 경고한다.

패치 직전 기준(누적 1,011턴, 턴당): NUMERIC 재프롬프트 0.051 / AUTO-RECOVER 0.080 /
SEARCH-ABANDONMENT 0.010 / 중복 SKIP 0.035 / 합성 Answer: 0.009 / standing down 0(미존재).

2026-09-09 10:07 KST 1회 실행되도록 systemd user timer(guard-audit.timer) 등록. 이 박스는
매일 02시에 자므로 Persistent=true로 기상 후 따라잡게 했다. 결과는
.smallclaw/logs/audit/report-<날짜>.txt 로 남는다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rfme1WVEPkpNwnf5oVNXXc
2026-09-04 18:12:12 +09:00
kimandClaude Opus 5 1b52acf615 chore: 가드 발동률 실측 스크립트
2026-09-04 가드 패치(db6a863 / 10e54d3 / b2075c6)가 실사용에서 어떻게 도는지 며칠 뒤 재려고
만들었다. 기준선은 .smallclaw/logs/audit/guard-baseline.json(머신 로컬, gitignore 대상)에
줄 번호 + 꼬리 30줄 지문으로 박혀 있다 — 로그에 타임스탬프가 없어서다. 로테이션되면 지문이
안 맞고, 그때는 경고를 내고 전체를 잰다.

패치 직전 기준값(전체 340턴): NUMERIC 재프롬프트 24(0.07/턴), AUTO-RECOVER 37(0.11/턴),
SEARCH-ABANDONMENT 7(0.02/턴), 합성 Answer: 줄 1건, standing down 0(미존재).

판독 기준은 스크립트 말미에 적어 뒀다 — 관련성 관문이 헐거우면 NUMERIC이 계속 맞는 답을
지우고, 빡빡하면 standing down이 과해져 진짜 날조가 통과한다. 양방향 모두 봐야 한다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rfme1WVEPkpNwnf5oVNXXc
2026-09-04 18:09:01 +09:00
kimandClaude Opus 5 b2075c62d8 fix: buildDirectPriceAnswer가 무관한 달러 값을 "Answer:"로 합성해 붙이던 문제
이 함수가 만드는 줄은 검색 결과 stdout 맨 앞에 붙는다. 시스템 프롬프트의 ANTI-HALLUCINATION은
"도구가 돌려준 것을 정확히 보고하라, 절대 무시하지 말라"고 지시하므로, 여기서 틀리면 모델에게
틀린 값을 신뢰하라고 시키는 셈이다. 게다가 합성된 숫자는 도구 출력 텍스트의 일부가 되므로,
모델이 그대로 옮기면 NUMERIC-GROUNDING이 "소스에 있다"며 통과시킨다 — 우리가 만든 숫자가
'근거 있음' 도장을 받아 나간다.

실제 사고(프로덕션 로그):
  USER: 컨테이너선 급유비가 얼마나 될까?
  TOOL: web_search("average fuel cost for 20,000 TEU container ship per voyage")
  TOOL OK: Answer: The current price is approximately $38.00 USD.
$38은 스니펫에 있던 "신조선 7일 항해 시 TEU 1개당 추가 연료비"였고, 실제 답은 항해당 수백만
달러다. 그 턴은 모델이 결과 [1]을 직접 읽어 스스로 바로잡았지만 그건 운이다.

* isPriceQuery에 단어 경계가 없어 통화·가격 낱말이 다른 단어 **안에서** 걸렸다. "eur"가
  "Europe"·"Neuralink"에, "cost"가 "costume"에, "value"가 "valuable"에 들어맞는다. 실사용
  고유 검색어 195건 중 19건이 통과했고 그중 8건이 가격과 무관했다("Europe drought status
  August 2026", "Neuralink Blindsight resolution pixels" 등). 경계를 넣어 19 → 11건.

* generic 자산 경로를 제거했다. 금·은·비트코인은 타당성 범위(온스당 300~10,000 등)와 자산명
  대조가 성립해 판정이 의미가 있다. generic은 둘 다 없다 — 허용 범위가 $0.5~$5,000,000이라
  사실상 모든 달러 표기를 받고, 문구도 무엇의 가격인지 말하지 못한 채 "The current price"라고만
  쓴다. 이 로그 표본에서 generic 경로가 실제로 답을 낸 유일한 사례가 위 컨테이너선 건이다.

* 자산명 대조를 가점(+2)에서 **필수 조건**으로 올렸다. 가점일 때는 자산을 한 번도 언급하지
  않은 스니펫의 숫자가 score 0으로 통과했다 — "금 시세" 질문에 다른 상품 가격이 답으로 나갈
  수 있다는 뜻이다. 티커(XAU/XAG/BTC)도 함께 본다.

순효과(실사용 고유 검색어 195건): 발동 19 → 11건, 그중 실제로 "Answer:" 줄이 생길 수 있는
것 0건. 이 표본의 가격 질문은 전부 GPU·옵션·펌프 같은 generic 대상이라, 원래도 이 기능이
맞는 답을 낸 적이 없다.

테스트 544개 통과(+8).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rfme1WVEPkpNwnf5oVNXXc
2026-09-04 18:06:31 +09:00
kimandClaude Opus 5 10e54d38eb fix: 게이트 전수 감사 — 한국어 검색 랭킹이 관련성을 0으로 계산하던 버그 외 5건
실사용 로그의 사용자 메시지 1,011건(고유 782건)을 모든 게이트에 통과시켜 오탐/미탐을
실측했다([[feedback_calibrate_on_real_data]]).

가장 큰 건: rankResults의 토큰화가 `q.replace(/[^a-z0-9\s]/g, ' ')`였다. 이 문자 클래스는
한글을 전부 지운다. **모든 한국어 쿼리**에서 관련성 토큰이 빈 배열이 되고 rel 점수가 항상
0이 되어, 결과가 오로지 domainTrustScore로만 정렬됐다 — 질문에 답하는 페이지인지와 무관하게.
시어엔진 라이브 실측("영리의료법인 비영리 병원 비율"): 수정 전에는 "Chee Lai"라는 사람의
LinkedIn 프로필 4건이 1~4위였고 유일한 관련 기사가 아래로 밀려 있었다. 수정 후 그 기사가
1위가 된다. rankResults는 7개 provider 전부가 쓰므로 영향 범위가 검색 전체다.
유니코드 인식 토큰화로 교체(한글·한자 2자 / 영문은 기존 4자 유지). 시점 표현도 영어만
있어서 한국어 "지금/현재/최신"은 배수를 받은 적이 없었다.

나머지:

* isCancelIntent 오탐 — "하지마 유적지는 바다와 떨어져 있는데?"에서 지명 하지마(波島)가
  `하지\s*마`에 걸렸다. 참이면 진행 중이던 작업이 일시정지되고 지리 질문이 작업제어 응답으로
  하이재킹된다(server.ts 두 호출부 모두). 취소는 문장 끝 명령이므로 종결 위치로 앵커를 걸었다.
  영어 쪽은 표본에 사례가 없어 건드리지 않았다.

* isExecutionLikeRequest 오탐 — 맨 "패치"가 목록에 있어 "인슐린 패치는 혈당 측정 기능이
  없나?", "패치는 많이 비싼가?"가 실행형으로 분류됐다. 이 함수는 검증 강제에서 면제시키는
  쪽이라, 실존 의료기기의 기능·가격을 검색 없이 기억으로 답해도 아무도 막지 않았다.
  "구속"을 체포 맥락으로 좁힌 것과 같은 처리 — 소프트웨어 패치 활용형일 때만 걸린다.

* isFactualInfoRequest 미탐 — "얼마" 계열 수량·가격 질문. 표본에서 이 표현으로 물었는데
  어떤 검증 게이트에도 안 걸린 5건이 있었고 전부 정당한 사실 질문이었다(오탐 0):
  "tesla v100 32gb 메모리 대역폭은 얼마지?", "미국 올 해 예산은 전 부 얼마지?"(실제로
  기억에서 $7조 5,400억이 나갔다), "이자 비용은 연간 얼마지?" 등. 맨 "얼마나"는 비-사실
  용법이 흔해 넣지 않았다.

* tool-scope climate 오탐 — "메모리 가격 추세"가 기후 재분석 아카이브 4종(~820토큰)을
  통째로 실었다. "추세"/"장기"는 기후·기상 명사와 같이 나올 때만 걸리게 좁혔다.

* tool-scope kakao 오탐 — `![KakaoMap_….png](…)` 첨부 파일명이 kakao_send_message를 실었다.
  executeTool은 스키마 소속을 확인하지 않으므로([[project_tool_schema_leak]]) 이름만 언급돼도
  호출될 수 있는 종류의 도구다. 첨부 파일명을 판정에서 빼고 카카오맵도 제외했다.

순효과 측정(고유 782건): 검색 강제 리마인더 주입률 23.8% → 25.4%(+13건). 새로 잡힌 13건은
전부 정당한 사실 질문이고, 잃은 건 0건이다.

감사에서 이상 없음으로 확인한 것: EMPTY-GROUNDING·MESSAGING(전제를 직접 검증),
decideAutoRecover·correctNewsCategoryForBreadth(도구 출력 대조 안 함), link-validator(URL 직접
프로브), satellite 게이트("도달 > 비용" 원칙상 유지), 0건 발동 게이트 6종(전용 앱 탭 경로).

테스트 536개 통과(+15).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rfme1WVEPkpNwnf5oVNXXc
2026-09-04 18:01:26 +09:00
kimandClaude Opus 5 db6a8634c3 fix: 가드가 맞는 답을 지우던 경로 — 무관한 검색 결과에 대고 판정하던 문제
2026-09-04 메인챗 세션(0c367bd7) 감사. 사용자가 "옵션 프리미엄이 실물의 몇 %냐"를
다섯 턴에 걸쳐 물었고, 검색 6회에 산출물 0건으로 끝났다. 로그를 보니 원인이 모델이
아니라 우리 가드였다:

  TOOL: web_search("typical option premium as percentage of underlying asset price")
  TOOL OK: [1] TYPICAL Definition & Meaning - Merriam-Webster
  NUMERIC-GROUNDING POST-CHECK (1/1): 3 figure(s) not found in tool output [10%, 1%, 2%]

모델은 매 턴 답을 냈다(10%/1%/2%, 다음 턴엔 3~10%/0.1~1%). 검색엔진이 "typical"의
사전 뜻풀이를 물어왔고, 그 뭉치에 숫자가 없다는 이유로 답이 날조 판정을 받아 지워졌다.
이 가드의 전제는 "검색이 그 주제를 다뤘는데 숫자가 없다 → 지어냈다"인데, 앞부분을
한 번도 확인한 적이 없었다. 무관한 말뭉치에서의 부재는 날조의 증거가 아니다.

* 관련성 관문(sourceCoversQuery): 검색어의 내용어 중 결과에 등장하는 비율이 0.34
  미만이면 판정을 포기한다. 실측 — 사전 뭉치 0.14 / 정상 결과 0.86. 수치·날짜·부재주장
  세 갈래가 같은 전제 위에 있어 한꺼번에 게이트했다. 검색어를 못 넘기는 호출부는 기존
  동작 유지.

* SEARCH-ABANDONMENT도 같은 결함이 있었다(감사 중 발견). countSubstantiveResults는
  스니펫 길이만 봐서 사전 페이지 4건을 전부 "알맹이 있음"으로 세고, 모델에게 "그걸로
  답하라"고 민다 — 답을 지우는 대신 틀린 답을 강요하는, 같은 전제의 반대 방향 피해다.
  같은 관문을 공유시켜 기준이 갈라지지 않게 했다.

* 그 가드는 애초에 죽어 있었다. NOTHING_FOUND가 "정보가 없다"류만 담고 있어, gemma4가
  실제로 쓰는 "결과에 …이 포함되어 있지 않다"(주어가 결과)를 하나도 못 잡았다. 이
  세션의 포기 답변 6건 중 0건 탐지. 사과로 시작하면 판정 대상인 첫 문장이 "죄송합니다."가
  되는 두 번째 구멍도 함께 수정(한글 뒤 \b 미매칭 함정 주의).

* dead-end-fallback (신규): 두 번째로 빈손이면 검색을 끊고 "아는 것을 검증 못 했다고
  라벨 붙여 말하라"로 돌린다. 문제의 스레드에서 3턴 앞서 끊긴다. 재프롬프트가 숫자를
  요구하지 않아 NUMERIC-GROUNDING과 충돌하지 않는다.

* 제품·모델 선택 질문이 isFactualInfoRequest를 통째로 빠져나갔다("모델이 있을까",
  "고른다면", "신형/최신"). 그래서 소설-모델 3턴이 도구 0개로 굴러가 2026년 9월에
  "Gemma 2 27B"를 로컬 추천으로 냈다. 주제어가 앞 턴에만 있는 짧은 후속 질문용
  isFactualThreadFollowUp 추가.

* confirmsUserGuessWithoutGrounding: 직전 턴에 확인 못 했다고 해놓고 사용자 추측에
  "네, 맞습니다"로 동조하는 경로 차단(맥미니 M6 건). 4중 조건으로 좁혀 정당한 맞장구는
  건드리지 않는다.

* cms_hospital_compare 게이트가 "병원"에만 걸려 있어 사용자가 내내 쓴 "의료법인"으로는
  8턴 동안 스코프에 들지 않았다. 그 사이 공공 비중 답이 45~50%→39~40%→약 50%로 흔들렸다.

테스트 521개 통과(+65). 실사용 로그의 실제 발화·답변을 표본으로 고정했다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rfme1WVEPkpNwnf5oVNXXc
2026-09-04 17:51:12 +09:00
kimandClaude Sonnet 5 e658f6a634 fix: 사진 요청이 AUTO-RECOVER 재프롬프트 루프를 타던 문제
앞 커밋(2537b04)에 이어진 후속. papa "눈향나무 사진" 재현 로그:
  SKIP: duplicate search_images / AUTO-RECOVER (2/2) "live-data query answered
  without a grounding tool call" / LOOP WARN: search_images x3

원인: 모델이 사진 대신 눈향나무 설명 프로즈를 냈는데 그 안의 "5cm·30cm·500년"이
looksLikeUnverifiedSpecClaim에 걸려 unverifiedSpecClaim→liveDataRequest가 참이 됨.
search_images는 GROUNDING_TOOL_PATTERN에 없어(citable text 아님) hasGroundingToolCall
false → 재프롬프트 → gemma가 search_images 반복 호출.

수정: decideAutoRecover 앞에 사진 요청 + search_images 호출 시 재시도 안 함 단락.
이미지 누락은 appendDroppedSearchImages가 복구하므로 재프롬프트가 고칠 게 없음.
대조군 테스트로 unverifiedSpecClaim 가드 자체는 유지 확인. 474 통과.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HdchNLTHsyYahp2PVQjoym
2026-09-03 16:06:03 +09:00
kimandClaude Sonnet 5 2537b04c7a fix: search_images 결과를 모델이 프로즈로 바꿔치기해 사진이 안 나오던 문제
papa 세션 실측(2026-09-03): "눈향나무 사진 찾아줘" → search_images가 ![alt](url)
3개를 정상 반환했는데 gemma4가 "눈향나무 사진들입니다" + 설명만 내고 링크를 통째로
누락. 다음 턴 "사진이 어딧지?"에도 "죄송합니다..." 하며 또 0개.

- appendDroppedSearchImages(reply-content.ts): 최종 답변에 이미지 마크다운이
  하나도 없고 이번 턴 search_images가 성공했으면, 도구 결과의 ![](url)를 뒤에 붙임.
  하나라도 있으면 모델이 시도한 것으로 보고 안 건드림
- personality-context photo 프롬프트: "![alt](url) 줄을 VERBATIM으로 복사, 프로즈로
  대체 금지"로 강화 (코드가 backstop, 프롬프트는 보조 — [[feedback_local_model_needs_code_backstop]])

참고: 눈향나무 같은 특정 수종은 Pexels/Unsplash 커버리지가 나빠 "빨간 배경 분재"가
매칭되기도 함 — 그건 스톡 API 한계로 별개 이슈.

tests +5 (473 통과).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HdchNLTHsyYahp2PVQjoym
2026-09-03 16:00:42 +09:00
kimandClaude Sonnet 5 e441bc22fd feat: drug_dur_check — 성분(ingredient) 기반 DUR 조회 추가
DURIrdntInfoService03(성분 서비스)도 승인돼 있었다. 안 되던 건 함정 때문:
서비스명은 03인데 오퍼레이션 접미사는 02(getUsjntTabooInfoList02), 필터 파라미터는
병용금기만 ingrKorName·나머지는 ingrName, 응답은 items[].item로 한 겹 더 감쌈.

- ingredient 파라미터 추가 → 성분 서비스로 조회. 성분 단위라 결과가 깔끔하고
  행 수가 1/10. 노인주의(getOdsnAtentInfoList02)는 성분 서비스에만 있음
- drug_name(제품명)은 기존대로 품목 서비스(*List03, itemName 부분일치) 유지
- 둘 중 하나 필수. 모델엔 "성분 알면 ingredient 권장"으로 안내
- 기본 checks에 노인주의 추가

실측: 심바스타틴 → 케토코나졸/마크로라이드/프로테아제억제제 병용금기 + 노인주의,
이소트레티노인 → 임부금기 1·2등급. 468개 테스트 통과.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HdchNLTHsyYahp2PVQjoym
2026-09-02 17:25:21 +09:00
kimandClaude Sonnet 5 b0c098e76d feat: drug_dur_check — 식약처 DUR 의약품 안전정보 조회 도구
닥터앱 "복약·검사" 탭이 핵심인데 약 정보를 조회할 수단이 없어, 상호작용·금기를
모델이 기억으로 답하거나 web_search로 블로그를 물어오고 있었다.

- data.go.kr DURPrdlstInfoService03(품목 기반) 연동. 제품명 부분일치로
  병용금기/임부금기/특정연령대금기/용량주의/투여기간주의/효능군중복/분할주의 조회
- 병용금기는 상대 성분 + 사유별로 묶어 요약(제품 단위론 수백 행이라 무의미)
- getOldSderTabooInfoList03(노인주의)는 API가 폐기 응답 → 목록에서 제외
- 성분 기반 서비스(DURIrdntInfoService03)는 이 키에 미승인 상태라 미사용
- 게이트: 닥터앱(dr_) 세션 무조건 + 일반 챗은 약물 상호작용 문맥 키워드
- 키는 vault(mfds.dur_api_key), config.json엔 참조만. SECRET_FIELD_MAP 등재

tests +2, 468개 통과. 실 API로 타이레놀·심바스타틴 병용금기 확인.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HdchNLTHsyYahp2PVQjoym
2026-09-02 17:11:28 +09:00
kimandClaude Sonnet 5 7b0223b071 feat: 닥터앱 탭에 학술 검색 도구(PubMed/OpenAlex) 개방
닥터앱은 기본 도구만 받아서 "이 약 장기복용 괜찮나?", "이 백신 효과 몇 년?",
"이 수치 정상 범위인가?" 류 질문에 web_search로 블로그를 물어오고 있었다.
academicToolNames(pubmed_search/pubmed_fetch/pubmed_fulltext/openalex_search/
semantic_search)를 dr_ 세션 안에서만 키워드 없이 개방 — lawyer 탭이 법령 도구를,
investor 탭이 news_search를 무조건 받는 것과 같은 앱-탭 패턴.

- dr_main / dr_case_<id> / dr_<rand> 전부 dr_ 접두사라 한 번에 잡힘
- 메인챗은 불변(키워드 게이트 그대로) — 테스트로 누출 없음 확인
- 앱 상단 면책("진단·처방 대체 아님")은 유지되므로 페르소나 불변
- 프롬프트 추가 비용 0 (hasGroundingTools는 web_search로 이미 항상 참)

tests +1, 467개 전부 통과.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HdchNLTHsyYahp2PVQjoym
2026-09-02 16:10:35 +09:00
kimandClaude Sonnet 5 ef9f89db1e chore: config — primary를 gemma4:31b-cloud로 정정 + 법령 API 키 배선
- provider ollama_local→ollama, primary/roles를 gemma4:26b→gemma4:31b-cloud로
  (메모리 기준선과 일치, 런타임이 이미 이 값으로 운영 중이던 것을 커밋에 반영)
- gemma4:26b(지서버) / muse-glimmer 프로필에 fixedNumCtx, 26b엔 airkorea 안내 추가
- mistral-large-3 프로필에서 중복 forceToolChoiceOnLiveData 제거(전역 ON됨)
- legal.law_go_kr_api_key vault 참조 추가

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HdchNLTHsyYahp2PVQjoym
2026-09-02 15:37:45 +09:00
kimandClaude Sonnet 5 e12a327c9b fix: 탐정·투자 앱 탭에 세션 게이트 누락 — 탭 안에서 전문도구가 0개였음
앱 탭은 세션 접두사로 무조건 게이팅하는 설계인데(탭 안 후속질문은 키워드가 없어도
그 앱의 맥락이므로), 접두사를 쓰면서 게이트가 없는 앱이 둘 있었다.

- detective-app.html → 'dt_' : 페이지에 판례 12회·법률 11회 언급인데 법령 도구가
  하나도 안 실렸다. lawyer 탭과 같은 도구군이 필요 → 법령 3종 + news_search 개방.
- investor-app.html → 'iv_' : 환율 6회·주가·증시·엑셀·뉴스. "그럼 작년 대비?" 같은
  후속질문에 읽을 게 아무것도 없었다 → news_search + excel 개방.

doctor('dr_')·language('lg_')는 의도적으로 제외 — 전자는 개인 의료기록 앱
(기록 검색/복약·검사 항목), 후자는 튜터라 기본 세트로 충분하고 전문 스키마는 순비용.

메인챗 회귀 없음(키워드 없으면 그대로 닫힘)을 테스트로 고정. tests 466 통과.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XZX9JHFSLZRuK9rR4sCVBs
2026-09-02 15:22:21 +09:00
kimandClaude Sonnet 5 1ef06637a5 fix: 레거시 스킬 토글이 도구를 막던 문제 — 키워드 게이트로 일원화
사용자 확인: 스킬 on/off UI는 예전 것이고 지금은 앱별 게이팅을 쓴다. 그런데
skills_state.json이 tool-scope의 meteorologist/lawyer/presenter 게이트를 통해
여전히 도구를 **막는** 유일한 소비자로 남아 있었고, 아무도 갱신하지 않는 값이라
조용한 기능 상실을 만들고 있었다.

실측된 라이브 영향: papa는 meteorologist:false → "오늘 서울 미세먼지 어때?"에
weather_kma / weather_openmeteo 만 실렸다. weather_kma엔 대기질 데이터가 아예
없으므로(모델 프로필에도 명시돼 있음) 답은 실패 아니면 창작 둘 중 하나였다.
바로 옆에 있던 weather_airkorea(에어코리아, 키 승인·정상)가 죽은 플래그에 막혀 있었음.

- meteorologist/lawyer/presenter 스킬 조건 제거 → 키워드 게이트만 사용
- ToolScopeInput.isSkillEnabledForUser는 유지(앱세션 게이트용, 스킬이 실제
  프롬프트 파일을 갖게 되면 다시 쓸 자리)
- 검증: papa·jasmine 모두 미세먼지/발표자료/판례 도구 정상 노출

tests 463 통과.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XZX9JHFSLZRuK9rR4sCVBs
2026-09-02 15:08:22 +09:00