Commit Graph
100 Commits
Author SHA1 Message Date
kimandClaude Opus 5 6c02050db6 fix: news_search 파라미터 보정을 코드 레벨에서 강제 (프롬프트 반복 실패 후)
같은 문제("태풍 돌핀" 쿼리 → 0건)를 프롬프트/스키마 설명으로 4번
연속 고치려 했으나, 실사용 재현 테스트마다 모델이 매번 예전 방식
그대로 호출했다(country 안 넣거나, 카테고리어+고유명사를 한 쿼리에
계속 붙임). prompt-gates.ts 파일 자체의 설계 철학대로 — "프롬프트
지시만으론 안 믿을 때 결정적으로 강제하는 백스톱" — 이번엔 코드가
직접 보정하도록 바꿨다.

execute()에 두 가지 결정적 로직 추가:

1. 쿼리에 한글이 있는데 country/language 둘 다 없으면 자동으로
   country="kr" 적용. 모델이 계속 빠뜨리던 파라미터를 기본값으로
   메운다.
2. 검색 결과가 0건이고 쿼리에 재해 카테고리어(태풍/지진/산불 등)가
   섞여 있으면, 그 단어만 제거하고 자동 재시도. 라이브 API로 직접
   검증: q="태풍 돌핀"은 0건, q="돌핀"은 5건 — 실제 기사 제목이
   "태풍 '돌핀'"(따옴표 있음)이라 연속 문자열 매칭이 실패했던 것.

두 로직을 resolveNewsParams/stripCategoryWordForRetry로 분리해
네트워크 없이 단위 테스트 가능하게 했다(link-validator.ts의
rewriteDeadLinks와 같은 패턴) — 실제 사고 수치를 그대로 회귀
테스트에 고정, 10개 추가.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 16:17:15 +09:00
kimandClaude Opus 5 cea2a49efb fix: news_search 쿼리 설명 정정 — 커버리지 문제 아니라 구문 매칭 문제였음
이전 커밋에서 "country 파라미터 누락이 원인"이라 진단했는데, 사용자가
"국내 사이트로 검색하면 쏟아질 텐데?"라고 반박해서 NewsData.io API를
직접 여러 조합으로 호출해 재검증했다:

  q="태풍 돌핀"+country=kr → 0건
  q="태풍"+country=kr      → 12건 (돌핀 기사 포함)
  q="돌핀"+country=kr      → 5건 (전부 관련 기사)

실제 원인은 country 누락이 아니라 **일반 카테고리어("태풍")와 고유명사
("돌핀")를 한 쿼리에 붙여 쓴 것**이었다. 실제 기사 제목은 "태풍 '돌핀'"
처럼 따옴표가 끼어 있어 "태풍 돌핀"이라는 연속 문자열과 리터럴 매칭이
안 됐다. 이전 커밋의 country 안내 자체는 여전히 유효하지만(비영어
쿼리엔 필요), 그것만으론 이 사고를 설명 못 했다.

query 파라미터 설명에 실제 API 호출로 검증한 세 가지 결과를 근거로
"특정 개체/사건이 있으면 카테고리어와 합치지 말고 그 이름 하나만
쓸 것"이라는 지침을 추가했다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 16:12:29 +09:00
kimandClaude Opus 5 e987522d9d fix: news_search 쿼리 설명에 country 파라미터 필수 안내 추가
실제 사고(2026-08-10): news_search({"query": "태풍 돌핀"})가 country
없이 호출돼 "(no articles found)"를 반환했다. 같은 시점에 국내
언론사 6곳 이상이 그 사건(중국 상륙, 100만명 대피)을 보도 중이었다.

원인: NewsData.io는 country 없이 호출되면 영어 위주로 치우친 풀에서
쿼리 텍스트를 그대로 매칭한다. 한국어 쿼리 텍스트는 그 풀 안의 기사와
문자 그대로 안 맞아서 빈 결과가 나온다. 기존 설명엔 "포괄적인 요청엔
쿼리를 생략하라"는 지침만 있었고, "특정 주제의 비영어 쿼리면 country를
같이 넣어라"는 지침은 없었다.

query 파라미터 설명에 이 사례를 실측 근거로 추가해, 비영어 쿼리엔
반드시 country를 함께 넣도록 안내했다. 재해 리마인더가 아니라 도구
스키마 자체에 넣어서 재해 질문뿐 아니라 모든 news_search 호출에
적용된다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 16:06:58 +09:00
kimandClaude Opus 5 19f6658b46 feat: 메인 채팅 재해 질문에 KMA 태풍지도·news_search 연동
실제 사고(2026-08-10): "태풍 돌핀 지금 어디 있지?"가 메인 채팅에서
web_search만 쓸 수 있었다. weather_map_screenshot(기상청/JMA 태풍
예상진로 지도 캡처)은 날씨 앱 탭에만 열려 있었고, news_search는
"뉴스"라는 단어가 없으면 안 열렸다 — 재해 질문은 둘 다 못 썼다.
그 결과 모델이 자체 생성한 "현재 위치 실시간 정보" 쿼리로 검색해
빈 포털 페이지만 받았고, 정직하게(수치를 지어내지 않고) "이미
소멸했을 가능성이 높다"고 결론 냈다 — 실제로는 중국에 상륙해
100만명 이상 대피, 500mm 폭우 경보 중이었다.

사용자 요청대로 "위치/경로"와 "피해상황" 두 축으로 나눠 연동했다:

- weather_map_screenshot: hasSatelliteKeyword(태풍 포함)로 게이팅
  추가 — 기존 eonet_events/satellite_snapshot과 같은 패턴. 검색
  스니펫보다 신뢰도 높은 기상청/JMA 공식 소스를 메인 채팅에서도
  쓸 수 있게 한다.
- news_search: DISASTER_PATTERN(prompt-gates.ts, isDisasterRequest와
  동일 소스 — 두 파일이 서로 다른 재해 키워드 목록을 들고 있다가
  어긋나는 걸 막기 위해 재사용)으로 게이팅 추가. "뉴스"라는 단어가
  없어도 재해 키워드만으로 피해/대피 상황 검색이 가능해진다.

handle-chat.ts의 사전 리마인더도 "위치/경로는 weather_map_screenshot,
피해상황은 news_search"로 역할을 나눠 안내하도록 확장했다.

이전 커밋(재해→news_search 라우팅 프롬프트 지침)만으로는 실사용
재검증에서 모델이 여전히 예전 쿼리를 그대로 썼다 — 프롬프트 지시만
믿지 않고 도구 자체를 실제로 쓸 수 있게 여는 쪽으로 보강했다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 15:56:42 +09:00
kimandClaude Opus 5 c5c3c0449b fix: 재해 질문을 news_search로 유도 — web_search 기본 쿼리가 실제 사건을 놓침
실제 사고(2026-08-10): "태풍 돌핀 지금 어디 있지?"에 web_search가 자체
생성한 "태풍 돌핀 현재 위치 실시간 정보" 쿼리로 검색해 일반적인/내용없는
결과 3개(기상청 빈 포털, 오래된 지도 SPA, 무관한 결과 1개)만 받았다.
모델은 정직하게(좌표를 지어내지 않고) "검색결과가 없으니 이미 소멸했을
가능성이 높다"고 추론했다 — 추론 자체는 멀쩡했지만 전제(검색해도 안
나온다)가 검색 실패 때문에 거짓이었다. 실제로는 돌핀이 막 중국에
상륙해 500mm 폭우 경보, 100만명 이상 대피 중이었고, 같은 사건을 다룬
국내 언론사 기사가 6개 이상 있었다 — "태풍 돌핀 중국 상륙 피해"처럼
상황어를 넣은 쿼리로는 바로 찾아졌다.

news_search(NewsData.io, 실제 발행일 보장)가 정확히 이런 "지금 벌어지고
있는 일" 질문을 위한 도구인데, 재해 질문은 뉴스로 분류되지 않아
(isNewsRequest는 "뉴스/속보" 키워드가 있어야 걸림) 이 라우팅을 안 탔다.

isLiveDataRequest 안에 있던 재해 정규식을 isDisasterRequest로 분리해
handle-chat.ts의 사전 리마인더와 AUTO-RECOVER 재시도 리마인더 양쪽에
재사용했다 — 이 파일 헤더의 원칙대로, 검증을 강제하는 게이트끼리
어긋나면 한쪽만 고친 게 무의미해진다. 재해 질문엔 news_search를
우선 권하고, web_search를 쓰더라도 "현재 위치/실시간 정보" 같은
일반 쿼리 대신 "...상륙 피해", "...대피" 같은 상황어를 쓰도록
안내한다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 15:42:31 +09:00
kimandClaude Opus 5 f972b184d6 revert: 지서버 서버측 keep-alive 제거 — 지서버 자체 10초 하트비트로 대체
효과가 불확실했다. 로그로 보면 2분 핑이 도는 동안에도 한 번은 실제로
잠들었고("방금 또 잠들었던거 같으네"), 다른 구간에선 40분 연속 안
잤는데 그게 핑 덕분인지 그 시간 내내 실사용 채팅이 있었기 때문인지
분리할 수 없었다.

사용자가 지서버 쪽에 직접 10초 간격 하트비트를 새로 걸었다 — 원격에서
2분마다 찔러보는 것보다 훨씬 촘촘하고, 무엇보다 지서버 자체에서 도는
메커니즘이라 신뢰도를 판단하기 쉽다. 서버 쪽 것은 이제 중복이라
사용자 결정으로 제거.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 15:19:13 +09:00
kimandClaude Opus 5 6a4e333822 fix: 죽은 링크 취소선 처리가 마크다운 경계를 넘어 텍스트를 뒤섞던 버그
실사용 사고(2026-08-10, "16호 태풍" 답변): 모델이 URL을 그 자체로 링크
텍스트로 쓰는 흔한 패턴 `[https://x.com](https://x.com)`에서, 죽은 링크
탐지 정규식(BARE_RE)이 `]`/`[`에서 멈추지 않아 라벨의 `]`, href의 `(`를
넘어 마크다운 링크 전체를 하나의 "URL"로 삼켜버렸다. 그 결과 사용자가
실제로 본 출력은 `~~[~~url](url~~ ⚠️ (링크 끊김)))~~ ⚠️ (링크 끊김)`
같은 뒤섞인 텍스트였다.

BARE_RE의 부정 lookbehind와 문자클래스 양쪽에 `]`/`[` 제외를 추가해
마크다운 링크 경계를 넘지 못하게 했다. LINK_RE(먼저 실행됨)가 이미
"라벨이 URL 자체인" 경우를 포함해 [label](url) 쌍 전체를 처리하므로
BARE_RE는 그 경계 안으로 들어갈 필요가 없다.

rewriteDeadLinks()를 validateLinksInText()에서 분리해 순수 문자열
변환만 네트워크 모킹 없이 테스트 가능하게 했다 — 회귀 테스트 8개
추가, 실제 사고 사례를 그대로 재현해 고정.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 14:34:42 +09:00
kimandClaude Opus 5 82283c4dc2 fix: 메인 채팅 WebSocket에 Wake Lock 적용 — 외부 접속 시 잦은 끊김 대응
사용자가 "어제 집에서는 괜찮았는데 지금 외부에서 연결하니깐 자꾸
끊어지네"라고 보고. 서버는 이미 30초마다 ping을 보내 프록시/OS 유휴
타임아웃(92초 패턴)을 피하는 방어 로직이 있고(server.ts wsPingInterval),
60초 무응답이면 서버가 직접 연결을 끊는다.

메인 채팅 페이지엔 이 ping에 응답할 JS 타이머를 살려두는 장치가 없었다.
음성통화 페이지(voice-call.js)는 정확히 같은 증상(폰 화면 꺼짐 → 탭
백그라운드 스로틀링 → 소켓이 서버 ping에 응답 못 함 → 끊김)을 07-09에
Wake Lock으로 이미 고쳤는데, 메인 채팅 쪽엔 이 조치가 빠져 있었다 —
집에서는 화면이 잘 안 꺼지니 덜 겪고, 외부에서 폰으로 쓸 때 화면이
자주 잠기니 더 자주 겪는 것과도 맞아떨어진다.

voice-call.js와 같은 패턴을 그대로 적용: WS가 열릴 때 Wake Lock을
요청하고, visibilitychange로 탭이 다시 보일 때 재요청(Wake Lock은 탭이
숨겨지면 자동 해제되므로), WS가 닫히면 명시적으로 해제.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 14:19:40 +09:00
kimandClaude Opus 5 5d88d34b33 feat: 지서버가 활성 모델일 때 절전 안 하도록 keep-alive 신호 추가
08-09에 이미 조사했던 "지서버가 30분 설정보다 훨씬 빨리 잔다" 문제는
사용자가 전기료 절약 관점에서 선호하는 동작으로 확정하고 조사를 접었던
건이다([[project_wol_gate]]). 그런데 오늘(08-10) 외부에서 실제로 사용
중일 때 이 빠른 절전이 매번 ~23초 기상 지연으로 이어져 불편해졌고,
사용자가 "지서버가 안자게 신호를 계속 보내자"고 명시적으로 요청했다 —
원인 재조사가 아니라 능동적 keep-alive를 원하는 것으로 확인.

원인 후보였던 "Windows 유휴 타이머가 마지막 사용자 입력 기준이라
API 트래픽으로는 리셋 안 됨"이 맞다면, 주기적으로 실제 HTTP 요청을
보내는 것 자체가 대응책이 된다.

`llm.provider === 'ollama_local'`이고(getProvider()의 단일 캐시 구조상
어차피 이 조건에서만 실제 채팅 트래픽도 지서버로 간다) WS 클라이언트가
최소 1개 연결돼 있을 때만(앱이 실제로 열려 있을 때만) 2분마다 직결
엔드포인트에 가벼운 /api/tags 요청을 보낸다. 게이트가 아니라 직결로만
보내므로, 이미 잠들어 있으면 그냥 조용히 실패한다 — 깨우는 신호가
아니라 "깨어있는 동안 재우지 않는" 신호라, 나머지 시간대의 절전
이점(사용자가 이미 선택한)을 훼손하지 않는다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 14:19:40 +09:00
kimandClaude Opus 5 aa74d4b710 feat: 모델별 도구-스킵(AUTO-RECOVER) 발생 로그 추가
사용자가 "지서버 젬마(gemma4:26b)가 도구를 잘 안 부르는 경향이 있다"고
직접 사용 경험을 보고했다. 이건 mistral-large-3 프로필이 이미 대응하는
것과 같은 실패 클래스(확인 가능한 질문에 도구 없이 답함)인데, 젬마
쪽은 뒷받침할 로그 증거가 없었다 — 있는 거라곤 사용자의 기억뿐이었다.

[[feedback_calibrate_on_real_data]]에 이미 세 번 적힌 교훈대로("가상
케이스로 임계값 잡으면 틀린다"), 기억만으로 프롬프트 경고를 새로
박아넣는 대신 실제 신호를 기록하고 나중에 데이터로 판단하기로 했다.

decideAutoRecover()가 이미 정확히 필요한 이벤트("도구가 필요한 질문에
관련 도구 없이 답함")를 계산하고 있었는데, 그 결과가 닿는 곳이 로테이션
되는 콘솔 로그 한 줄뿐이었고 거기엔 모델 이름도 없었다. usage-log.ts와
같은 형태(같은 디렉터리, 같은 JSONL append 패턴)로 tool-skip.jsonl을
추가해 모델별 집계가 가능하게 했다.

effectiveModel이 try 블록 안에서만 스코프였고 AUTO-RECOVER 체크는 그
catch 이후라서, try 블록 진입 시점에 resolvedModelThisRound에 미리
복사해 바깥에서도 참조 가능하게 했다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 12:57:46 +09:00
kimandClaude Opus 5 d4e8750400 refactor: VERIFY CONCRETE FACTS를 web_search 보유 턴에만 붙도록 도구 게이팅
이 문장은 245토큰으로 무조건 블록 중 단일 최대 항목인데, 내용이 통째로
"web_search를 먼저 불러라"다. 그 도구가 이번 턴 스키마에 없으면 실행할 수
없는 지시이고, 무엇보다 executeTool()이 스키마 멤버십을 확인하지 않고
이름으로만 디스패치하기 때문에 프롬프트가 도구 이름을 언급하는 것만으로도
모델이 파라미터 문서 없이 그 도구를 호출해버린다. meteorologist 스킬을 끈
사용자에게 weather_kma가 계속 돌던 그 경로다.

조건이 규칙의 실행 가능성과 정확히 일치하므로, 이건 이 파일 규칙 1이 금지하는
메시지 게이팅이 아니라 정상적인 도구 게이팅이다. ANTI-HALLUCINATION 앞부분
(도구 출력을 그대로 옮겨 적으라는 지침)은 도구와 무관하므로 무조건 유지된다.

토큰 절감은 없다. 처음엔 절감을 기대했으나, web_search가 어떤 스킬 게이트에도
속하지 않아 사실상 모든 일반 턴에 스키마에 들어간다는 걸 뒤늦게 확인했다.
실제로 줄어드는 건 부팅 턴(coder_list_files/coder_read_file만 있는 턴)뿐이고,
1687 → 1442 토큰이다. 그 턴에서 스키마에 없는 web_search를 프롬프트가 부르던
누출 벡터가 사라진 것이 이 커밋의 실질적 이득이다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 12:41:27 +09:00
kimandClaude Opus 5 305f93d08b fix: 검증 강제 가드의 오탐 3종 — 사용자가 준 숫자, NHC/EONET, 붙여넣기 본문
실사용 로그와 세션 104건을 가드에 직접 돌려서 찾은 오탐들이다.

1) 사용자가 질문에 직접 적은 숫자로 AUTO-RECOVER가 걸렸다.

   로그 실례: "북위 27.7도, 동경 124.8도 여기가 어디쯤이지?" — 그 좌표 자체가
   한 턴 전 web_search 결과였고 사용자가 그대로 옮겨 물은 것이다. 모델이
   정답("동중국해")을 냈는데 답에 좌표를 되풀이했다는 이유로 COORDINATE_CLAIM이
   걸려 재생성 + "latitude 27.7 longitude 124.8 location" 검색을 태웠고,
   돌아온 건 "농림통계연보(2005).hwp"였다. 그러고서 같은 답을 다시 썼다.

   이 가드가 잡으려는 건 "모델이 지어낸 숫자"인데, 사용자가 적어 넣은 숫자는
   정의상 모델이 만든 값이 아니다. looksLikeUnverifiedSpecClaim에 userMessage를
   받아 스캔 전에 지운다. 모델이 새로 덧붙인 수치는 그대로 남는다. 한 자리 수는
   지우지 않는다 — 메시지의 "2" 하나로 답변의 모든 "2"가 지워지면 가드가 통째로
   무력화된다(회귀 테스트 포함).

2) nhc_active_storms/eonet_events가 GROUNDING_TOOL_PATTERN에 없었다.

   둘 다 권위 있는 원본(미 국립허리케인센터, NASA EONET) 직결이고 태풍 질문이
   실제로 도는 도구인데, 08-09에 추가한 MEASUREMENT_UNITS/COORDINATE_CLAIM이
   바로 그 도구들이 돌려주는 값(hPa·m/s·북위)에 반응한다. 그래서 NHC 데이터로만
   답한 턴이 "근거 도구 미호출"로 분류돼, 방금 읽은 권위 있는 출처를 의심하라고
   web_search 재시도를 걸었다. memory_stats 때와 같은 구멍이다.

3) 긴 붙여넣기 문서 본문의 우연한 키워드로 강제검색이 걸렸다.

   7,583자짜리 고소장 작성 요청이 본문 한가운데(2578자 지점)의 "시세"(장물
   시세) 때문에 isLiveDataRequest에 걸렸다. 문서를 써 달라는 턴에 "메모리에서
   답하지 말고 검색부터 하라"는 리마인더가 붙는 건 토큰 낭비를 넘어 작업 방향을
   반대로 민다.

   처음엔 "길다 + isExecutionLikeRequest"로 막으려 했는데 회귀 테스트가 그게
   틀렸음을 잡아냈다 — 그 메시지가 실행형으로 분류된 근거가 본문의 "화면" 한
   단어였다. 막으려는 문제와 똑같은 우연 위에 얹은 수정이었다. 대신 위치를
   본다: 사람은 지시를 자료의 앞이나 뒤에 쓰지 한가운데 묻지 않는다.
   600자 초과 메시지는 앞 400자 + 뒤 300자만 스캔한다(instructionZone).
   검증 강제 게이트끼리 예외가 어긋나면 안 되므로 isFactualInfoRequest에도
   같이 적용했다.

실사용 104건 대조: isLiveDataRequest 오탐 3건 제거하고 정당한 25건은 전부
유지, 출력측 가드는 로그의 그 좌표 케이스가 정확히 하나 걸러졌다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 12:40:50 +09:00
kimandClaude Opus 5 fe69cc0126 perf: CHEMISTRY NOTATION을 화학 맥락일 때만 붙도록 게이팅 (매 턴 -174토큰)
이 블록은 470토큰까지 커져 무조건 블록 중 두 번째로 큰 항목이었는데,
실제로 모델이 \ce{}나 ```smiles를 내보낸 건 로그 83일 중 3일(~4%)뿐이었다.
나머지 96%의 턴에 값을 치르는 쪽이 더 나쁜 거래다.

이 파일 규칙 1("도구로만 게이팅, 메시지 문구로는 절대 금지")에 대한 의도적
예외다. 출력 형식 제약이라 키로 삼을 도구가 애초에 없고, 무엇보다 대가의
크기가 다르다 — image_edit 규칙이 빠지면 위험한 도구 호출이 나가지만, 이건
놓쳐봐야 깨진 ASCII 다이어그램 하나다. 파일 헤더의 규칙 2도 "무조건 유지"에서
이 예외를 설명하는 쪽으로 고쳐 적었다.

키워드는 일부러 넓게 잡았다. 애초에 무조건 유지했던 이유가 "치과/의료 질문은
화학 키워드 없이도 구조를 그리게 될 수 있다"였고 그게 이 사용자의 실제
도메인이므로, 성분·약물·마취·불소·아말감·레진·모노머·resin·anesthe 등을
함께 넣었다.

실측: 비화학 턴 시스템 프롬프트 1708 → 1533 토큰.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 12:40:11 +09:00
kimandClaude Opus 5 96989373f5 fix: 검색결과의 캐시 날짜·표 오독을 의심하도록 TEMPORAL CONSISTENCY 보강
"지금 비오는 곳이 있나?"에 web_search를 부르긴 했는데, 돌아온 게 URL에
tm=2024.12.13.20:00이 박힌 2024년 캐시 페이지였다. 모델은 그걸 "현재"라며
그대로 소개했고, 표를 옮겨 적으면서 열까지 잘못 읽어 동두천의 강수량을
파주 것으로, 양평의 풍속을 강수량으로 뒤바꿨다.

기존 가드는 "근거 도구를 불렀는가"만 본다(hasGroundingToolCall). 불렀다는
사실만으로는 결과가 최신인지, 옮겨 쓴 값이 맞는지 전혀 검증되지 않는다.
그래서 결과 자체를 의심하라는 지침을 프롬프트에 넣었다:

- 검색결과가 자체 타임스탬프(URL의 날짜, dateline, "as of ...")를 갖고
  있는지 확인하고 현재 날짜와 대조할 것. 하루이틀 넘게 묵었으면 "지금"이
  아니므로, 옛 수치를 현재처럼 내놓지 말고 실시간 데이터를 못 찾았다고
  말하고 찾은 날짜를 밝힐 것.
- 표는 위치가 아니라 행 레이블로 읽을 것. 한 행의 수치가 옆 행으로
  밀리면 "못 찾음"보다 나쁘다 — 틀렸는데 권위 있어 보이기 때문이다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 12:38:56 +09:00
kimandClaude Opus 5 22a8c1a11c fix: 시점 표현("최근/지금/현재")을 화제 무관 라이브데이터 신호로 일반화
세션 papa@ca71bc1c(로그 12804행): "마두로 미국에 잡혀가지 않았나?"에 세 턴
연속 도구 호출 없이 "체포되지 않았다"고 확신에 차 정정했는데, 실제로는
사용자가 맞았다(이미 미국에 체포·압송된 상태). 직전 커밋에서 체포/사임 등
개별 어휘를 isLiveDataRequest에 추가해 이 사고는 막았지만, 그 방식 자체가
화이트홀이었다 — 다음번엔 태풍도 체포도 아닌 제3의 화제에서 또 뚫린다.

화제별 어휘 목록 대신 "최근/지금/현재"가 들어가면 화제를 안 가리고 걸리도록
일반화했다. 근거는 실사용 로그 403건 전수조사: 이 세 표현이 들어간 66건
(16.4%) 중 오탐 0건 — 비트코인 시세·현직 대통령·최신 모델 버전 등 전부
정당한 실시간 조회였다. 새 레이어가 추가로 잡아낸 32건 중 31건도 마찬가지로
필요한 검색이었고, "지금 몇 시야?"만 이미 시스템 프롬프트에 있는 시각을
다시 검색시키는 사소한 낭비였다(오답은 아님). "요즘"은 실사용 예가 없어
후보에서 뺐다 — "요즘 어떻게 지내?" 같은 잡담과 부딪힐 위험을 검증 없이
감수하지 않는다.

넓히는 과정에서 진짜 버그 두 개가 드러났다:
- isLiveDataRequest가 isExemptFromVerification(지서버/클로서버 등 개인
  인프라 예외)을 애초에 한 번도 거치지 않고 있었다. 파일 상단 주석은 "모든
  검증 가드가 이 예외를 자동으로 적용받는다"고 주장했지만 사실이 아니었다.
  시점 표현을 넓게 걸자 "지서버 최근에 왜 이렇게 느려"가 웹검색 리마인더에
  걸릴 뻔하면서 드러났다.
- "시어엔진"(SearXNG 별칭)이 예외 목록에 없었다. 실사용 로그의 "현재
  시어엔진 상태 어때?"는 웹검색으로 검증 불가능한 로컬 인프라 질문이다.

둘 다 함께 고쳤다. 부수 효과로, 오타("잡ㅎ간") 때문에 이전 커밋의 체포 어휘
목록이 못 잡던 마두로 사건의 첫 턴도 "현재" 표현으로 이제 잡힌다.

코딩/실행형 요청(isExecutionLikeRequest)과 인사말(isGreetingLikeMessage)은
계속 제외한다 — "지금 이 코드 확인해줘"에 검색 리마인더가 끼어들면 안 된다.

검증: tests/prompt-gates.test.ts 6개 추가, 196개 전부 통과. 실제 채팅에서
"비트코인 현재 가격이 얼마야?" → web_search 호출 확인, "지서버 지금 몇 개
모델 로드돼있나" → 웹검색 없이 로컬 처리 확인.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 13:29:01 +09:00
kimandClaude Opus 5 12666f363b fix: 사이드바 모델 박스에서 무의미한 "Online" 문구 제거
지서버는 provider가 바뀌어도 항상 backendOnline=true(클라우드는 하드코딩,
ollama_local은 연결 성공 시)라서, 사이드바가 늘 "ollama_local Online" /
"OpenAI Online"을 표시했다. 초록 점이 이미 온라인을 뜻하는데 그 옆에 같은
정보를 글자로 반복했고, 길어서 좁은 사이드바에서 줄바꿈까지 일으켰다.

온라인일 때는 provider 이름만 남기고, 절전 중/오프라인처럼 실제로 알려줄
정보가 있을 때만 상태 단어를 붙인다. 어떤 provider든 동일한 조건문을 타므로
클라우드에도 자동 적용된다(브라우저에서 "OpenAI"로 확인).

설정 탭의 런타임 정보 행(#r-ollama)은 라벨-값 쌍의 전용 상태 표시라 손대지
않았다 — 값이 비면 어색해진다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 13:28:36 +09:00
kimandClaude Opus 5 89cc0a51d7 fix: 사용자별 vault.key/vault.enc를 git 추적에서 제거
세 계정(papa/cherry/jasmine)의 vault.key와 vault.enc가 저장소에 함께 추적되고
있었다. vault.key는 vault.enc를 여는 마스터 키라 둘이 같은 저장소에 있으면
암호화가 무의미하다. 복호화해보니 담긴 값이 메인 vault와 해시까지 동일한
현재 사용 중인 자격증명이었다 — 이메일 비밀번호 3개와 Unsplash/Pexels 키.

원인은 .gitignore의 `.smallclaw/vault/`가 저장소 루트에 앵커된 패턴이라
한 단계 아래의 사용자별 vault를 못 잡은 것. 아래쪽 `.smallclaw/users/*/workspace/`
규칙이 지금은 이 경로를 덮지만 gitignore는 이미 추적 중인 파일에는 적용되지
않으므로, 그 규칙이 추가되기 전에 커밋된 이 파일들은 계속 남아 있었다.

- git rm --cached (디스크 파일은 유지 — 서비스가 그대로 읽는다)
- `**/vault/vault.key|vault.enc|vault-audit.log` 추가. 누구도 예상 못 한 경로에
  새 vault가 생겨도 막히도록 깊이에 무관하게 매칭한다.

메인 vault(API 키·NVR·텔레그램 등 40개)는 기존 규칙에 걸려 유출되지 않았다.

주의: 히스토리에는 그대로 남아 있다. 이 커밋은 앞으로의 추가를 막을 뿐이며,
저장소가 2026-05-05부터 96일간 공개 상태였으므로 노출된 값은 이미 유출된 것으로
간주하고 회전해야 한다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 10:46:59 +09:00
kimandClaude Opus 5 ce684a6cf3 feat: 설정 자격증명 탭에 키 발급 사이트 카드 추가
Vault에 저장된 키는 이름만 opaque 문자열로 남아 있고 어디서 발급받았는지가 UI
어디에도 없어, 갱신·재발급 때마다 해당 콘솔을 찾아 헤매야 했다. data.go.kr은
특히 하나의 키라도 서비스별로 활용신청이 따로 필요한데(에어코리아 측정소정보가
같은 키로 403 난 이유), 그 사실이 기록된 곳이 없었다.

- 서비스 19곳의 발급 페이지 링크. /api/credentials/status로 실제 저장 여부를 읽어
  ✓(저장됨)/·(미저장)로 표시하므로 하드코딩된 목록이 아니다.
- 공공데이터포털 항목에 "서비스별로 활용신청 필요"를 명시했다.
- 전 링크 https + target=_blank + rel=noopener noreferrer, 값은 escHtml 처리.
- Vault 조회에 실패해도 링크는 그대로 보이고 경고만 덧붙인다. 저장 여부를 잘못
  표시하는 것보다 모른다고 하는 편이 낫다.

브라우저에서 렌더 확인: 19개 중 17개 ✓, 미저장으로 표시된 Brave·OpenAI는
실제 Vault 내용과 일치. 콘솔 에러 없음.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 10:40:22 +09:00
kimandClaude Opus 5 c830c9fbf6 feat: 지서버 절전 상태를 표시하고 모델 선택 시 자동으로 깨우기
지서버는 유휴 시 절전에 들어간다. 그동안 UI가 이를 "Offline"(빨강)으로만 보여줘서
고장난 것처럼 읽혔고, 실제로는 메시지를 보내면 자동으로 깨어나 정상 응답한다.
사용자가 붉은 표시를 보고 원인을 찾아 나선 뒤 발견했다.

- /api/status가 sleeping을 함께 반환한다. 게이트를 찔러 확인하지 않고 wake_url
  설정 유무로 판별하는데, 상태 표시는 10초마다 갱신되고 게이트를 찌르는 행위가
  곧 매직패킷 발사라 확인하러 가면 지서버를 영영 못 자게 만든다.
- 표시를 셋으로 분리: Online(초록) / 절전 중(노랑) / Offline(빨강).
  노랑엔 "메시지를 보내면 자동으로 깨어납니다" 툴팁을 붙였다.

또한 설정에서 provider를 고르면 그 자리에서 깨우도록 했다. UI는 이미 선택 즉시
/api/models/test로 모델 목록을 가져오는데, 지서버가 자면 여기서 "모델 없음 —
서버가 실행 중인가요?"로 실패했다. 모델 선택은 쓰겠다는 의사가 분명하므로 이를
기상 트리거로 삼는다. 새 엔드포인트 없이 기존 동작에 얹었고, "자면 모델 목록이
빈다"는 문제도 같이 해결된다. 잠든 지서버로 실측 시 7초 만에 모델 5개를 받았고,
깨어있을 때는 0초로 통과해 불필요한 패킷을 보내지 않는다. wake_url이 없는
provider(클로서버 등)는 기존 동작 그대로다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 10:40:07 +09:00
kimandClaude Opus 5 3594d26047 fix: 단위를 감싼 LaTeX를 평문으로 정규화
모델이 미세먼지 단위를 $\mu\text{g/m}^3$처럼 LaTeX로 쓴다. 단위는 수식이 아닌데
이 표기가 세 곳에서 깨진다: index.html은 KaTeX가 렌더하지만 복사하면 MathML까지
딸려와 "μ g/m 3 μg/m 3"처럼 중복돼 보이고, 나머지 채팅 페이지 8개(weather/doctor/
lawyer/detective 등)는 KaTeX 자체가 없어 생 LaTeX가 그대로 노출되며, TTS는
백슬래시를 소리 내어 읽는다.

단위만 담긴 수식 span만 유니코드로 바꾸고, 연산자·분수·변수가 있으면 그대로 둔다:
  $\mu\text{g/m}^3$ → μg/m³      $\text{m/s}$ → m/s
  $E = mc^2$ / $x + y$ / $\frac{a}{b}$ → 유지

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 10:39:20 +09:00
kimandClaude Opus 5 05bb3b6d95 fix: 재해 현황·관측 수치가 할루시네이션 가드를 그냥 통과하던 문제
"지금 태풍 돌핀 어디 있지?"에 도구를 하나도 부르지 않고 좌표·기압·풍속을
단언했다(로그 12065행, TOOL 호출 0건). 같은 수치가 일주일 전 답변(9119행,
8월 2일)과 글자까지 동일해서, 검색이 아니라 대화 기억에서 나온 값임이 드러났다.
실제로는 돌핀이 이미 오키나와를 통과한 뒤였다.

가드 세 개가 전부 통과시켰다:
  isLiveDataRequest       false
  isFactualInfoRequest    false
  looksLikeUnverifiedSpec false

- 입력측: 라이브 데이터 패턴이 날씨/기온/환율/주가뿐이라 태풍이 없었다.
  태풍·호우·폭우·폭설·한파·폭염·지진·해일·산불·홍수·가뭄·열대저압부 추가.
- 출력측: 스펙 단위 목록이 GB/TFLOPS/원/% 계열뿐이라 hPa·m/s·위경도가 어디에도
  없었다. 검증할 대상 자체를 못 본 것. 관측 단위(hPa/m/s/kt/km/h/mm/cm/℃)와
  좌표(북위·동경 N.N°)를 추가했다.

관측 단위를 넣으면 정상 날씨 답변까지 되던질 위험이 있는데, 재검색 조건에
!hasGroundingToolCall이 있고 weather_가 grounding으로 인정되므로 도구로 받아온
31.5°C·2mm는 영향받지 않는다. 그 반대 방향을 테스트로 못박았다 — 이쪽이 깨지면
날씨 기능이 무한 재검색에 빠진다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 10:38:52 +09:00
kimandClaude Opus 5 e5a781b301 fix: 날씨 도구가 도시 단위 대기질을 정확히 조회하도록 수정
"오늘 구미 미세먼지"에 포항 측정소(우현동/장흥동/장량동/대도동) 수치가 구미로
제시됐다. 지적 후 재시도했더니 이번엔 영주·경산 측정소를 구미라고 답했다.

원인은 실시간 대기질 API가 시/도 단위 목록만 주고 응답에 sidoName만 있다는 것.
측정소 이름에는 소속 시/군이 없어("우현동"은 포항, "중방동"은 경산) 모델이
도 전체 목록에서 임의로 골라 특정 도시 값으로 제시할 수밖에 없었다.

- 측정소정보 서비스(MsrstnInfoInqireSvc) 연동. 주소 필드로 시/군을 매칭해
  해당 도시 측정소만 반환한다(구미 4곳, 포항 13곳 확인). 목록은 하루 캐시.
  이 서비스는 data.go.kr에서 별도 활용신청이 필요하며 2026-08-09 승인됐다.
- 필터가 실패하면 필터된 척하지 않고 "필터 실패, 전체 표시"로 밝히고 경고를 남긴다.
- 실시간 조회 numOfRows 20 → 200. 경북만 53개 측정소라 20행에서는 포항이 잘려
  도시 필터가 조용히 빈 결과를 냈다.
- 504 SERVICETIMEOUT에 4회 재시도 추가. 측정 결과 업스트림이 무작위로 실패하며
  (연속 8회 중 7성공 / 8초 간격 6회 중 4성공) 호출 간격과 무관해 쿨다운이 아니다.
  단발 호출이던 기존 코드는 그때마다 도구가 통째로 실패했다. 4xx는 재시도 안 함.

또한 6개 날씨 도구가 공유하던 "위치를 찾을 수 없습니다"를 실행 가능한 에러로
교체했다. 도시 비교 요청에 모델이 "London, Berlin, Paris, Rome, Madrid"처럼
묶어 넘기면 지오코딩이 실패하는데, 기존 메시지엔 고칠 방법이 없어 같은 인자로
무한 재시도했다. 이제 도시 목록과 재호출 예시, 재시도가 무의미함을 함께 알린다.
첫 두 조각이 각각 지오코딩될 때만 목록으로 판정하므로 "Seoul, Korea"·"London,GB"·
좌표는 영향받지 않는다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 10:38:36 +09:00
kimandClaude Opus 5 bd64e0fc0d fix: 짧은 답변을 빈 응답으로 오판정하던 문제
handleChat()의 빈 응답 판정이 finalText.length < 5였다. 응답이 "없는 것"과
"짧은 것"을 뭉뚱그린 규칙이라, "2"·"네"·"서울" 같은 완결된 답변이 버려지고
"죄송합니다, 응답을 생성하지 못했습니다"로 대체됐다. 모델은 제대로 답했는데
사용자에게는 실패했다고 표시되는, 가장 눈에 띄는 형태의 실패였다.

진짜 신호는 길이가 아니라 문자/숫자의 유무다. sanitizeFinalReply()가 pptx
링크나 메타 문구를 걷어내면 구두점만 남을 수 있고, 폴백 메시지는 원래 그런
잔여물을 위한 것이다.

replyLooksEmpty()를 reply-content.ts로 분리했다(handle-chat.ts는 테스트에서
import가 불가능할 만큼 크다 — tool-scope/system-prompt를 빼낸 것과 같은 방향).
유니코드 property escape를 써서 모든 문자 체계를 커버하고, 덤으로 한글 뒤에서
\b가 매칭되지 않는 함정도 회피한다.

검증: 신규 테스트 10개 포함 180개 통과. 실제 채팅에서
1+1 → "2", 3x7 → "21", 수도 → "서울", 네/아니오 → "네." 정상 반환.
빈 문자열·공백·"..."·"**"·이모지만 있는 응답은 여전히 폴백 처리.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 01:04:43 +09:00
kimandClaude Opus 5 cb8d93e03f feat: 지서버 직결 + wol-gate 폴백 (하이브리드 연결)
지서버 트래픽이 전부 wol-gate(TrueNAS) 프록시를 거치고 있었다. 게이트는 자는
타깃에 HTTP 200 + "깨우는 중" HTML을 반환하는데, ollama 클라이언트가 그 HTML을
JSON.parse해서 "Unexpected token '<'"로 죽었다.

endpoint를 지서버 직결(192.168.0.8:11434)로 두고, wake_url을 새로 받아
게이트는 직결이 안 될 때만 쓴다.

- _isAwake(host): 200이어도 본문이 HTML이면 잠든 것으로 판정
- 깨어있으면 즉시 통과(fast path). 기존 코드는 매 호출마다 /api/tags를 찍고
  무조건 5초를 기다린 뒤 재확인했다.
- 게이트를 초인종이 아니라 폴백 경로로 사용. 직결과 게이트를 같은 주기로 확인해
  둘 중 되는 쪽으로 이번 호출을 넘긴다. 08-09 00:52 지서버는 살아있고 게이트는
  정상 프록시 중인데 직결만 타임아웃인 상황이 실제로 관찰됐고, 직결만 폴링하면
  작동하는 경로를 두고 90초를 버린 뒤 실패한다.
- getModelCtx()도 _activeHost를 따라가게 수정. 폴백 중에 num_ctx 계산용
  /api/show만 따로 실패하는 것을 막는다.

wake_url이 없는 provider(클로서버 등)는 기존 동작 그대로다.

검증: 직결을 죽은 주소로 바꿔 강제 재현 → 게이트 경유로 정상 응답(33초),
로그에 routing via wol-gate 기록. 원복 후 직결 fast path 9초.
실제로 잠든 지서버에 채팅 → 23초 만에 기상 후 응답.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 01:04:28 +09:00
kimandClaude Opus 5 1146b59199 fix: 컨텍스트 윈도우가 이전 모델 값에 고정되던 문제
게이지가 8192로 표시됐으나 지서버 gemma4:26b의 실제 값은 262144, 32배 차이.
원인이 세 겹이었다.

1. resolveNumCtx()의 _cachedNumCtx가 아무 키 없이 프로세스 수명 내내 유지됐다.
   6곳에서 설정되는데 무효화하는 코드가 어디에도 없어, 모델/provider를 바꿔도
   이전 값을 계속 반환했다(클로서버로 바꿨는데 지서버의 262k가 계속 보이던 증상).
   → (provider, models.primary, provider별 model, llm.num_ctx) 키에 연동

2. 활성 provider가 아니라 localhost:11434를 하드코딩해 조회했다.
   provider가 ollama_local(지서버)일 때 클로서버 로컬 Ollama에 물어보니
   "model not found" → 최후 폴백 8192로 떨어졌다.
   → activeOllamaEndpoint() 추가, /api/model-context의 per-model 조회에도 적용

3. 프론트엔드 _fetchAppCtxMax()가 페이지 로드 시 한 번만 조회했다.
   → force 인자 추가, 설정 저장 직후 재조회

검증: 게이지 8192 → 262144(지서버 실제값 일치), 반복 호출 5회 안정.
캐시 키는 모델 전환 시 무효화되고 설정 동일 시 적중.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 01:04:12 +09:00
kimandClaude Opus 5 de738c3abd fix: config.json 원자적 쓰기 + 동시 저장 직렬화
updateConfig()는 fire-and-forget이고 호출 지점이 41곳(대부분 HTTP 라우트)인데
saveConfig()가 평범한 writeFile을 써서, 두 저장이 겹치면 파일이 깨졌다.
writeFile은 truncate 후 청크 단위로 쓰기 때문에 짧은 쪽이 온전히 기록된 뒤
긴 쪽의 남은 청크가 자기 fd 오프셋에 덧붙어, 유효한 JSON 뒤에 "}}" 조각이
붙은 형태가 된다. 재현 시 200회 중 196회 손상, 실제 손상 파일과 꼬리 패턴 일치.
2026-08-08 밤부터 08-09 새벽까지 세 번 발생했다.

- temp 파일에 쓰고 fsync 후 rename (같은 파일시스템 내 원자적 교체)
- writeQueue 프라미스 체인으로 동시 저장 직렬화
- 파싱 실패 시 config.json.corrupt-<timestamp>로 백업.
  DEFAULT_CONFIG 폴백 상태에서 updateConfig가 한 번만 돌아도 기본값이
  파일을 덮어써 29개 섹션(NVR/collabora/email/telegram 등)이 영구 소실된다.

검증: 재현 테스트 196/200 → 0/200, 임시 파일 누수 없음.
라이브 API 동시 저장 6건에도 config 정상.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 01:03:59 +09:00
kimandClaude Sonnet 5 1071f7205d fix: wol-gate 헬스체크를 TCP 연결 확인에서 실제 HTTP 응답 확인으로 변경
TCP 연결만 확인하면, 대상이 부팅 중이라 포트는 열렸지만 실제 서비스가
아직 요청을 못 받는 순간에도 "살아있다"고 오판해서 실제 프록시를
시도하다 자체 타임아웃으로 "Bad gateway: read ETIMEDOUT"를 반환하는
문제가 있었음(HTML 깨우는 페이지 대신 이 502가 나가면 호출부가 재시도
로직을 못 탐) — 지서버 wol-gate에서 실제로 반복 관측됨(2026-08-08).
실제 HTTP GET으로 응답을 받는지 확인하도록 바꿔서, 상태코드 상관없이
진짜 HTTP 응답이 와야만 "살아있다"고 판단하게 수정. 두 인스턴스(클로서버
8099, 지서버 8100) 모두 재빌드·재배포 후 정상 동작 확인.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-08 23:43:56 +09:00
kimandClaude Sonnet 5 6a4b3f8d68 fix: wol-gate로 깨우는 중인 Ollama 대상에 재시도 대기 로직 추가
지서버처럼 wol-gate 프록시 뒤에 있는 Ollama 엔드포인트가 잠들어 있으면,
프록시가 실제 API 응답 대신 "깨우는 중..." HTML 페이지를 돌려주는데
ollama 클라이언트 라이브러리가 이걸 JSON으로 파싱하려다 매번
"Unexpected token '<'" 에러로 죽는 문제 발견(실제 채팅에서 반복 확인,
2026-08-08). chat/generate/chatStream 진입 시점에 /api/tags를 먼저
가볍게 찔러보고, HTML(깨우는 중) 응답이면 최대 90초까지 5초 간격으로
재시도 대기한 뒤 실제 호출을 진행하도록 수정 — 이미 깨어있는 경우엔
응답이 JSON이라 즉시 통과, 지연 거의 없음(실측 확인).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-08 23:29:11 +09:00
kimandClaude Sonnet 5 b8ee2e1be9 fix: 모델 드롭다운에서 현재 선택된 모델이 맨 위에 오도록 정렬
Ollama /api/tags가 반환하는 순서(풀받은 순서 등)를 그대로 써서, 저장된
현재 모델(예: gemma4:31b-cloud)이 목록 중간에 있으면 드롭다운 열 때
엉뚱한 모델(예: nemotron)이 맨 위에 보여 실수로 클릭하기 쉬웠음
(2026-08-08 보고). 현재/저장된 값을 목록 맨 앞으로 옮겨서 선택값과
시각적 첫 항목이 항상 일치하도록 수정.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-08 20:46:39 +09:00
kimandClaude Sonnet 5 b8c6a769be feat: 지서버용 별도 Ollama provider(ollama_local) 추가
기존 "ollama" provider(클로서버 localhost, 주로 *:cloud 모델용)는 그대로
두고, LAN의 다른 로컬 GPU 머신(지서버 등)을 가리키는 두 번째 독립 Ollama
provider "ollama_local"을 신설. OllamaAdapter가 id를 생성자 인자로 받게
바꿔서(기존 'ollama' 하드코딩 → 'ollama'|'ollama_local' 선택 가능) 두
인스턴스를 provider 캐시/설정에서 구분함. 설정화면에 "Ollama
(클로서버/클라우드)" / "Ollama (지서버/로컬)"로 라벨 분리, 엔드포인트·모델
입력 필드 추가.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-08 19:42:31 +09:00
kimandClaude Sonnet 5 d9680fc96f feat: web_search에 검색결과 핵심답변 추출 단계 추가
메인 채팅 모델이 web_search 원본 스니펫에서 구체적 수치를 못 찾고
"검색결과에 없음"으로 답하는 사례 발견(2026-08-08, 실제 대화에서 확인) —
같은 검색결과를 ollama_web_search로 재질의하면 정확히 찾아냈음. 원인은
검색엔진 차이가 아니라(둘 다 동일한 폴백체인 사용) ollama_web_search가
"이 결과만 근거로 답하라"는 좁고 집중된 프롬프트로 별도 모델 호출을
한 번 더 거치기 때문이었음 — 메인 모델은 긴 시스템프롬프트+대화이력
속에서 검색결과를 곁다리로 처리하다 보니 놓친 것으로 추정.

web_search에도 같은 패턴(executeWebSearchWithExtraction)을 적용 —
원본 검색결과는 그대로 유지(출처 인용용)하고 앞에 "[핵심 답변]" 추출
섹션을 붙임. 추출 실패/타임아웃(15초) 시 원본 결과만 반환해 기존 동작을
깨지 않도록 함. 실제 쿼리로 테스트 완료(Gemma 4 31B VRAM 요구사양 정확히
추출됨).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-08 19:22:04 +09:00
kimandClaude Sonnet 5 bb8599574b fix: TTS가 온도 범위(28~35°C)를 자연스럽게 못 읽던 버그 수정
voice.js는 "~"를 기호제거 정규식이 °C보다 먼저 먹어버려 앞쪽 숫자가
단위를 잃었고("28 섭씨 35도"), voice-call.js는 "~" 제거 자체가 없어서
숫자 한글변환 단계까지 물결표가 그대로 살아남았음. 두 파일 다 범위 전용
정규식을 °C/°F 처리보다 먼저 넣어서 "섭씨 28도에서 35도"로 자연스럽게
읽도록 수정.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-08 19:21:52 +09:00
kimandClaude Sonnet 5 968aab1faa fix: 비전 없는 모델이 이미지 못 본다는 걸 명시 (환각 방지)
weather_map_screenshot 같은 도구의 stdout에는 "위 이미지에서 실제로 보이는
것만 설명하라" 같은 비전 모델 대상 지시문이 섞여 있는 경우가 있는데, 이걸
비전 없는 모델이 받으면 응답 안 된 명령으로 읽고 색상·좌표·아이콘 같은
시각적 디테일을 그럴듯하게 지어내는 문제가 있었음. 경로 힌트 뒤에 "이 모델은
이미지를 볼 수 없다"는 명시적 안내를 덧붙여 추측 대신 솔직히 모른다고
답하도록 함.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-07 13:16:43 +09:00
kimandClaude Sonnet 5 9e0520d069 feat: weather_map_screenshot에 JMA(RSMC 도쿄) 태풍 전체 진로도 추가
KMA는 한국에 영향 줄 태풍만, NHC는 대서양/동태평양만 보여줘서 대만·필리핀·
괌·일본 근해에 있지만 아직 한국 영향권 밖인 태풍은 두 소스 모두에서 안 보임.
JMA(서태평양 전역 담당 지역특별기상센터)의 typhoon=all 지도를 캡처 대상에
추가해 그 사각지대를 메움.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-07 13:16:13 +09:00
kimandClaude Sonnet 5 d51c40484a feat: 의료 어시스턴트 앱 + Home Assistant 임베드 앱 추가
법률 사건관리 앱과 같은 패턴으로 의료 사건(진료기록) 관리 앱 추가
(routes-doctor-cases.ts + doctor-app.html + WOPI 라우트). Home Assistant를
iframe으로 띄우는 얇은 래퍼 앱 추가(ha.applecherry.net, 2026-08-01 통합
완료된 것 URL만 새로 노출). weather_openmeteo의 요청 성형/리포트 포맷팅
로직을 weather-openmeteo-format.ts로 분리(순수함수라 테스트 가능,
2026-07-29 있었던 타임존/롤링윈도우/daily-in-hourly 버그 3건 회귀 방지
테스트 포함) + geocode 재시도 테스트.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-07 13:16:01 +09:00
kimandClaude Sonnet 5 02247e4ee3 feat: 스튜디오 앱 사진→동영상(LTX Image-to-Video)
studio-app.html에 시작 이미지 업로드/제거 UI + strength 슬라이더 추가,
imagegen.ts에 ltxImageToVideoWorkflow (ComfyUI 공식 ltxv_image_to_video.json
템플릿 기반 LoadImage→LTXVImgToVideo→LTXVConditioning 그래프) 추가.
실제 생성 테스트로 프레임0(원본 이미지 일치)·프레임15(모션 발생) 확인 완료된
작업을 커밋만 안 하고 남겨뒀던 것.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-07 13:13:58 +09:00
kimandClaude Sonnet 5 61885cb923 fix: 코드에디터 iframe 안 에뮬레이터 모바일 레이아웃 오판 + 리셋 버튼
arduino-emulator.html이 코드에디터 안 iframe으로 뜰 때, 미디어쿼리가 창
폭이 아니라 iframe 폭으로 평가되어 넓은 화면에서도 모바일 서랍 레이아웃이
나오던 버그 수정 (브레이크포인트 1024px→760px, JS 판정도 matchMedia로
통일해 CSS와 어긋나지 않게 함). 코드에디터에 에뮬레이터 세션 강제
재시작 버튼 추가, 패널 높이를 고정값→75vh로.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-07 13:13:28 +09:00
kimandClaude Sonnet 5 3ae9b986e9 feat: 안드로이드 에뮬레이터 관리콘솔/ADB 쉘 + 컨테이너 정리
세션 영속화, 관리콘솔 WS 프록시(attachAndroidConsoleWsProxy), ADB 쉘
엔드포인트, 고아 컨테이너 정리 로직 추가. 2026-07-31 실사용 검증 완료된
작업을 커밋만 안 하고 남겨뒀던 것.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-07 13:13:15 +09:00
kimandClaude Sonnet 5 b02fe0a184 feat: 상단 네비게이션 드롭다운으로 재구성
앱 버튼이 늘어나면서 상단 바가 너무 길어져, 창작/전문직/법률·탐정/홈으로
그룹화한 드롭다운 메뉴로 재구성.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-07 13:12:23 +09:00
kimandClaude Sonnet 5 b001cd9318 feat: OpenAI 모델 목록 조회 라우트 추가, LM Studio thinking 비활성화 스위치
프론트엔드가 예전부터 호출하던 POST /api/openai/models가 백엔드에 구현이
안 되어 있어 항상 404였던 버그 수정 - OpenAICompatAdapter로 실제 OpenAI
API에 모델 목록을 조회하도록 구현.

지서버(LM Studio)의 gemma-4-26b-a4b가 응답당 완성 토큰을 2.5배 더 많이
쓰는 것을 벤치마크로 확인(322 vs 132 토큰/응답, thinking 오버헤드로 추정) -
chat_template_kwargs.enable_thinking=false를 기본 적용하되, 복잡한 질문에서
품질이 떨어질 수 있어 설정 화면(모델 → LM Studio)에 체크박스로 켜고 끌 수
있게 구현. 기본값은 비활성화(현재 상태 유지).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-07 12:57:22 +09:00
kimandClaude Sonnet 5 ae6e347339 fix: 지서버 미세먼지 도구 오류 원인 규명 및 강수확률 하드코딩 방지
weather_airkorea가 data.go.kr에서 계속 403(SERVICE_KEY_IS_NOT_REGISTERED_ERROR)을
내던 원인을 규명 - 기상청 키는 살아있지만 에어코리아 대기오염정보 조회서비스는
별도 활용신청 승인이 필요함. 에러 메시지를 명확히 하고 모델이 무한 루프 대신
바로 web_search로 넘어가도록 안내.

weather_openmeteo 기본 variables에 precipitation_probability가 빠져있어 모델이
강수확률을 임의로 지어내던 문제 수정 - 기본값에 추가하고, precipitation(mm)은
확률이 아니라는 점을 도구 설명에 명시.

weather_kma에 미세먼지 데이터 없음 안내 추가, 시/군 이름→시도 자동변환
(구미→경북 등), 시/군 조회 시 해당 측정소를 결과 상단에 노출.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-07 01:23:55 +09:00
kimandClaude Sonnet 5 b0345e24d4 GROUNDING_TOOL_PATTERN에 pubmed/law/case-search/sqlite_query 추가
memory_stats 버그와 같은 클래스의 문제를 전체 도구 목록에서 재점검.
pubmed_search/fetch/fulltext, openalex_search는 도구 설명 자체가 "웹검색
대신 이걸 써라"인데도 AUTO-RECOVER가 그 지시를 무시하고 웹검색을 강제하고
있었음. korean_law_search/fetch, us_case_search, sqlite_query도 조작이
아닌 실제 조회값이라 같은 이유로 추가.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-06 15:56:47 +09:00
kimandClaude Sonnet 5 0c03581fcf memory_stats 도구 추가 + AUTO-RECOVER가 도구검증된 답변에 오발동하는 버그 수정
챗봇이 "얼마나 기억하냐"는 질문에 스스로 답할 수 있게 memory_stats 도구
신규 추가(user_facts/daily_extracts 문서수 + 벡터DB 디스크 용량). 추가
과정에서 실사용 테스트로 발견한 버그: 답변에 "21.9MB" 같은 스펙형 숫자가
있으면 이미 도구로 검증됐어도 GROUNDING_TOOL_PATTERN에 없는 도구라
무조건 웹검색 재시도(AUTO-RECOVER)를 강제해서, 로컬 시스템 정보를 인터넷
에서 찾으려다 실패하고 답을 얼버무리는 문제가 있었음. memory_stats를
그 패턴에 추가해서 해결.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-06 15:50:36 +09:00
kimandClaude Sonnet 5 571c684dad SearXNG 별칭·로봇청소기 IP 사실을 homeclaw RAG 메모리에 백필
시어엔진(SearXNG)/로봇청소기 IP가 Claude Code 자신의 프로젝트 메모리에만
있고 homeclaw 챗봇의 memory_write 경로에는 한 번도 안 들어갔던 걸 RAG
업그레이드 조사 중 발견. memory_write와 동일한 경로(USER.md + 벡터스토어)로
백필.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-06 15:33:59 +09:00
kimandClaude Sonnet 5 70c1e1c23f RAG 기억 검색에 하이브리드 검색(벡터+키워드) + 재랭킹 추가
Chroma 벡터검색만으로는 IP·모델명 같은 정확한 용어를 놓치는 경우가 있어
SQLite FTS5(trigram) 키워드검색을 병합하고, 애매한 경우(예: 클로서버 vs
지서버 GPU 스펙 혼동)만 LLM 재랭킹으로 오답을 걸러내도록 함. 재랭킹은
후보가 없거나 확실한 단일매치일 때는 건너뛰어 대부분의 대화에서는 지연시간
증가가 거의 없음. 기존 570개 기록은 scripts/backfill-fts.ts로 백필.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-06 15:12:50 +09:00
kimandClaude Sonnet 5 6a61c3f4af 스튜디오 앱에 프롬프트 작성 도우미 채팅 + 생성 진행률 바 추가
이미지/동영상/사진편집/이모티콘세트 탭과 ComfyUI 탭 양쪽에 AI 프롬프트
도우미를 붙여, 한국어로 설명하면 SDXL/FLUX/LTX용 영어 프롬프트를 만들어줌.
생성 중에는 예상 소요시간 기준 진행률 바를 표시.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-06 12:27:39 +09:00
kimandClaude Sonnet 5 6014e51ba3 이미지스튜디오를 ComfyUI 백엔드로 전환, 원본 UI 임베드, LTX 비디오 고화질 옵션 추가
diffusers venv 스크립트 방식 대신 상시구동 ComfyUI(systemd, GPU1)를 통해
SDXL/FLUX/PuLID 스타일변환/LTX 비디오를 생성하도록 전환. ComfyUI 자체 웹
UI를 리버스 프록시로 스튜디오 앱에 같은 도메인으로 임베드(routes-comfyui.ts)
하고, LTX-Video에 non-distilled "dev" 체크포인트 기반 고화질 옵션을 추가.
날씨 앱에는 Windy 제트기류/태풍 마커, 게이트웨이에는 wol-gate 웨이크 로그
조회 API도 함께 반영.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-05 17:21:36 +09:00
kimandClaude Sonnet 5 675ee75de5 wol-gate: 매직패킷 발송을 국내(KR) IP로 제한
해외 클라우드/스캐너 IP가 루트 경로 한 번 찔러보는 것만으로 매직패킷이
발사되는 사례(143.198.239.149 등)가 반복돼, 요청 IP가 APNIC KR 대역
(ipv4/ipv6, kr-cidrs.json)에 속할 때만 실제로 깨우도록 제한. LAN/루프백
대역은 홈 NAT 헤어핀으로 사설 IP로 잡히는 경우가 있어 항상 허용 유지.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-05 10:18:50 +09:00
kimandClaude Sonnet 5 bb7c6a2105 wol-gate: 매직패킷 발송 시 트리거한 요청 출처 로깅
깨우기 원인이 실사용자 접속인지 봇/스캐너 트래픽인지 구분할 수 있도록
IP, User-Agent, 요청 경로를 로그에 남김.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-04 10:37:43 +09:00
kimandClaude Opus 5 dd7a1d0ca1 v4.3.41: 법률 앱 사건 목록에서 이름 바로 편집
사이드바 사건 목록에서 ✎ 버튼으로 바로 이름 변경 가능하게 추가 (헤더까지
안 들어가도 됨). 처음엔 더블클릭 방식으로 짰다가 실제 클릭으로 테스트하니
안 먹혀서 원인을 봤더니 — 더블클릭은 실제로 click·click·dblclick 순으로
발생하는데, 앞의 두 click이 부모(사건 선택)로 버블링되면서 목록을
통째로 다시 그려버려 dblclick 핸들러가 붙어있던 엘리먼트가 바뀌는
경쟁 상태였음. 더블클릭 대신 별도 버튼(단일 클릭 + stopPropagation)으로
바꿔서 해결.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 19:02:52 +09:00
kimandClaude Opus 5 9d76a0d7e9 v4.3.40: 법률 앱 사건 관리 버그 2건 수정 (첨부파일 위치·자동 이름짓기)
실사용 중 발견된 버그:
- 첨부파일 위치: PDF/DOCX 등 문서 파일을 업로드하면 어느 탭 버튼을 눌렀든
  무조건 "제출서류" 탭으로만 가버려서, "첨부자료" 탭에서 업로드한 파일이
  사라진 것처럼 보였음(탐정 앱의 증거/서류 분류 로직을 그대로 가져온 부작용).
  파일 확장자가 아니라 실제 업로드한 탭(attachments/submissions 폴더)
  기준으로 분리하도록 수정 — 기존에 폴더 구분 없이 올라간 파일은 첨부자료
  탭에 계속 보이게 하위호환 유지.
- 사건 자동 이름짓기: 메모 탭에 직접 타이핑해야만 작동했는데, 실사용자는
  메모 대신 채팅으로만 사건을 상담해서 한 번도 안 뜬 것이었음. 채팅 한 턴이
  끝날 때도(사건 선택 상태라면) 그 메시지로 이름짓기를 시도하도록 추가.

실제 사용자 데이터로 재현·검증 완료 (전세보증금 반환 사건 채팅 → "새 사건"이
"전세금반환소송"으로 자동 변경 확인).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 18:00:08 +09:00
kimandClaude Opus 5 2be697aff8 v4.3.39: 법률 앱 사건 자동 이름짓기
메모를 처음 작성하고 포커스를 벗어나면(blur), 제목이 아직 기본값 "새 사건"인
경우에 한해 AI가 메모 내용을 보고 8자 이내 사건명을 지어 채워줌. 채팅창에는
안 보이는 조용한 호출(quietChatCall)로 처리 — 이미 만든 generateChecklist용
callChatRaw(화면에 타이핑 표시)와 달리 UI에 아무 흔적을 안 남김. 사용자가 이미
직접 이름을 바꿨거나 다른 사건으로 넘어간 경우는 덮어쓰지 않음.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 17:41:58 +09:00
kimandClaude Opus 5 1c2c5e23bc v4.3.38: 법률 어시스턴트 앱에 사건(사건 기록) 관리 기능 추가
- 채팅창 파일 첨부 버튼 추가 (PDF/TXT, 회계사 앱과 같은 /api/upload/file 재사용,
  pdf_read로 계약서 분석 — 스킬 프롬프트엔 이미 방법론이 있었는데 버튼만 없었음)
- 탐정 앱의 "사건" 관리를 법률 앱에 이식: 사건 목록(검색/정렬) + 사건별 메모·
  체크리스트·타임라인·첨부자료·제출서류 탭 + 사건별 독립 AI 대화 세션
  (다른 사건 컨텍스트가 안 섞임, 탐정 앱과 동일 패턴)
- PDF 편집기(Stirling-PDF)·Collabora 문서 편집 연동도 그대로 이식 — 사건 폴더에
  자동 저장됨
- 백엔드는 탐정 앱 기존 코드를 건드리지 않고 새 공용 모듈(case-storage.ts)로
  일반화 — appType 파라미터로 detective/lawyer 둘 다 같은 로직 재사용,
  탐정 앱 데이터·동작은 그대로 유지(회귀 테스트로 확인)
- 사진 자동 날짜인식(탐정 앱 전용, 카톡 캡처 등에서 날짜 추출) 등 법률 사건과
  무관한 기능은 이식 범위에서 제외

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 17:34:52 +09:00
kimandClaude Opus 5 b61fcd1889 v4.3.37: 날씨 지도 블랙스크린·스트리밍 레이스 수정, 태풍 예상진로(KMA/NHC) 지도 추가
- Windy iframe이 실제 로딩보다 훨씬 먼저 스피너를 걷어 몇 초간 완전 블랙스크린으로
  보이던 문제 수정 (반투명 오버레이 + 고정 타이머)
- 스트리밍 중 Windy URL이 아직 다 도착 안 한 상태에서 잘린 좌표로 지도를
  이동시키고 잠가버리던 레이스 컨디션 수정
- Windy는 태풍 예상 진로(향후 경로)를 그리지 않는다는 걸 명확히 하고, 실제로
  경로를 그리는 기상청 태풍 페이지 + NOAA/NHC 페이지를 지도에 임베드하는 기능 추가
  (nhc_active_storms 신규 도구로 이름→공식 forecast-track URL 조회, 대양/슬롯
  번호를 모델이 추측하지 않도록 함)
- 채팅창에 원문 도구호출(web_search{...} 등)이 그대로 노출되던 누출 수정 —
  기존 정리 로직은 메시지 전체/끝부분만 잡아서 중간에 낀 누출은 못 걸렀음
- Windy/NHC URL 정규식이 마크다운 괄호·별표까지 삼켜 URL을 깨뜨리던 과매칭 버그 수정
- 모델이 NHC URL의 슬롯 번호를 잘못 베끼는 경우에 대비해, 도구가 찾은 검증된
  URL을 직접 스트림에 실어보내 지도가 항상 정확한 폭풍을 가리키도록 함

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 13:02:56 +09:00
kimandClaude Opus 5 52a42e26f0 v4.3.36: 거대 HTML 3개의 인라인 JS 외부화 + 아두이노 페이지의 기존 버그 2건 수정
index.html(인라인 0%)과 code.html이 이미 쓰던 방식을 나머지 큰 페이지에 적용.
각 파일이 거대 인라인 <script> 하나씩이라 순수 이동으로 처리됨.

  pptx-wizard.html       3,988 →   820줄   (/js/pptx/pptx-wizard.js       3,174줄)
  music-app.html         4,549 → 1,197줄   (/js/music/music-app.js        3,360줄)
  arduino-emulator.html  8,026 →   849줄   (/js/arduino/arduino-emulator.js 7,186줄)

보존한 것:
- arduino의 type="module" 유지. 모듈 스코프에서는 최상위 선언이 전역이 되지
  않으므로, onclick 핸들러가 의존하는 window.foo= 노출 방식이 그대로 성립해야 함
- music-app의 테마 설정 스크립트는 인라인 유지 — 페인트 전에 실행돼야 하고
  외부화하면 네트워크 왕복 때문에 다크/라이트가 깜빡임
- 로드 위치는 셋 다 원래대로 </body> 직전
- document.currentScript / document.write / import.meta 사용 0건 확인
  (인라인과 외부의 의미가 달라지는 패턴 없음)

브라우저 실측 검증 중 아두이노 페이지에서 기존 버그 2건 발견 — 이동이 원인이
아니라 원래 깨져 있던 것이 파일 위치가 바뀌며 드러남:

1) 동적 import 경로가 처음부터 틀려 있었음
   import('./vendor/avr8js/index.js')는 /html/vendor/... 를 가리키는데 실제
   파일은 /vendor/... 에 있음. 서버에 SPA 캐치올이 있어 없는 경로도 200 + HTML을
   반환하는 탓에 404로 드러나지 않았고, JS 대신 HTML을 받아 import가 실패했음.
   절대경로 /vendor/... 로 수정(avr8js, rp2040js 2건).

2) 실패 핸들러가 2차 예외를 던져 모듈 전체를 중단시킴
   위 import 실패의 catch에서 showOverlayError()를 부르는데, 이 함수는 파일
   2874줄에서 window에 할당되므로 그 시점에는 아직 없음 → ReferenceError로 모듈
   실행이 통째로 멈추고, 아래쪽 window.* 노출이 전혀 일어나지 않아 onclick
   핸들러 100개가 전부 죽어 있었음. 로드 실패보다 이 2차 실패가 훨씬 치명적.
   console.error로 먼저 남기고 showOverlayError는 있을 때만 호출하도록 변경.

3) 전수 점검 중 window 노출이 누락된 핸들러 6개 발견(mobCloseBoard,
   mobToggleBoard, _wifi* 4개) — 클릭해도 ReferenceError만 나던 상태. 명시적 노출 추가.

브라우저 실측(192.168.0.5:18789):
  pptx-wizard  전역 함수 6/6, onclick 87개, 콘솔 에러 없음
  music-app    전역 함수 6/6, onclick 103개, 인라인 테마 정상(data-theme=dark)
  arduino      onclick 이름 해석 0/35 → 35/35, avr8js 로드 성공, 콘솔 깨끗

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 21:19:36 +09:00
kimandClaude Opus 5 f63e9cef74 v4.3.35: dental-agent의 getToken/authH를 나머지 11개 앱과 통일
앱 HTML 복붙 조사의 후속. getToken/authH는 12개 앱에 복사돼 있는데
dental-agent 한 곳만 다른 구현이었고, 두 가지 실제 동작 차이가 있었음.

1) getToken에 try/catch 없음
   나머지 11개는 예외를 삼키고 빈 문자열을 반환하는데 dental-agent만 그대로
   던짐. 시크릿 모드나 스토리지가 차단된 환경에서 sessionStorage.getItem이
   SecurityError를 던지면 페이지 초기화가 그 자리에서 멈춤. 실증:
     차단 상황  dental → 예외 전파 / 표준 → "" 반환

2) authH가 토큰 없을 때 입력 객체를 그대로 반환
   `return t ? {...extra, Authorization} : extra` 형태라 토큰이 없으면 호출자가
   넘긴 객체를 참조째 돌려줌. 호출자가 그 객체를 재사용하면 서로 오염될 수 있는
   구조(표준은 항상 새 객체를 만듦). dental-agent의 실제 호출부는 대부분 객체
   리터럴이라 현재 피해는 없었으나, 같은 이름의 함수가 페이지마다 다르게
   동작한다는 점이 문제.

전 앱 스캔으로 이 두 결함이 dental-agent에만 있는 것을 확인한 뒤 표준으로 교체.
이제 getToken/authH 모두 저장소 전체에서 단일 버전.

검증: dental-agent가 쓰는 네 가지 호출 형태 authH() / authH({...}) /
authH(null) / authH(opts.headers||{}) 전부 정상 동작, 스토리지 차단 시 예외
없음, 배포 후 페이지 HTTP 200 및 /api/dental/status 인증 통과 확인.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 21:06:01 +09:00
kimandClaude Opus 5 6280af3212 v4.3.34: escHtml 11개 앱 통일 — 속성 인젝션 + 0/false 소실 두 결함 수정
앱 HTML들의 복붙 현황을 조사하다 발견. 같은 이름의 escHtml이 4가지 버전으로
갈라져 있었고, 그 차이가 실제 동작 차이였음.

결함 1 — 속성 인젝션:
6개 앱(detective/investor/lawyer/mind/traffic/weather)과 2개 앱(nvr/writer)의
버전이 & < > 만 이스케이프하고 따옴표는 건드리지 않았음. 그런데 이 함수를 HTML
속성값 안에서 호출하는 곳이 8군데 있었음:
  writer-app  src="${escHtml(b.cover)}"   href="${escHtml(it.url)}"  href="${escHtml(b.link)}"
  nvr-app     name="${escHtml(...)}" x2
  lawyer-app  id=/name="${escHtml(...)}"
writer-app의 b.cover는 routes-writer.ts가 Google Books/구텐베르크/알라딘 응답을
그대로 담아 보내는 값(vi.imageLinks?.thumbnail 등) — 우리가 통제하지 않는 외부
데이터가 따옴표 미이스케이프 상태로 속성에 들어가고 있었음. 실증:
  입력  x" onerror=alert(1) y="
  이전  <img src="x" onerror=alert(1) y="">   ← 속성 탈출
  이후  <img src="x&quot; onerror=alert(1)...">

결함 2 — 숫자 0과 false가 화면에서 사라짐:
6개 앱이 String(s||'')를 쓰고 있어 escHtml(0)이 '', escHtml(false)도 ''를
반환했음. 재생수 0, 조회수 0 같은 값이 빈칸으로 렌더링되는 경로.
nvr/writer만 String(s==null?'':s)로 올바르게 처리하고 있었음.

11개 앱 전부를 language-app이 이미 쓰고 있던 가장 완전한 형태로 통일:
  String(s ?? '') + [&<>"'] 전체 이스케이프
따옴표/홑따옴표를 엔티티로 바꿔도 텍스트 문맥에서는 브라우저가 동일하게
렌더링하므로 기존 화면 변화 없음. 배포 후 7개 앱 HTTP 200 및 writer-app의 실제
렌더 경로(악의적 커버 URL)로 검증함.

남은 중복은 이번에 수정하지 않음 — 조사 결과 callChat(7종)/mobToggle(10종)/
initSessionId(7종)은 발산 버그가 아니라 앱별 정당한 차이였음(세션 접두사, 패널
구조, Windy 자동로드 같은 훅). 통합하려면 훅 설계가 필요하고 지금 고장난 곳은
없으므로 보류.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 21:03:14 +09:00
kimandClaude Opus 5 aad964b659 v4.3.33: parse_schedule_pattern 도구 삭제 — 죽은 코드 정리
execute-tool.ts에 남은 순수 로직을 조사하다 발견. I/O가 전혀 없는 유일한 긴
블록(92줄)이었지만 dormantAgentTools에 들어 있어 모델에게 노출조차 되지 않는
죽은 코드였음.

감사 로그(tool_audit.log) 전체 기간 호출 2회:
  2026-05-26 OK   "daily at 8am"
  2026-06-18 FAIL "every weekday at 9:30am"
실패 원인은 파서가 daily/weekly/cron 문자열만 지원하고 평일(weekday) 패턴이
아예 없어서. 즉 쓰이지도 않는 데다 흔한 표현에 구멍이 있는 상태였음.

삭제 범위(참조 0개 확인 후 진행):
- execute-tool.ts의 case 블록 90줄
- build-tools.ts의 도구 정의 15줄
- tool-scope.ts / code-chat.ts의 dormant·blocked 목록 항목
- tool-scope.ts의 dormant 설명 주석에서 해당 도구 언급 제거

웹UI·스킬·schedule_job 어디서도 참조하지 않는 것을 사전 확인했고, 배포 후
"예약된 스케줄 목록" 요청이 schedule_job으로 정상 처리되는 것도 확인.
도구 총 개수 84 → 83. 테스트 142개 통과.

스케줄 자연어 파싱이 다시 필요해지면 이 커밋을 되살리기보다, 평일/격주 등
실제 쓰는 패턴을 정하고 테스트와 함께 새로 만드는 편이 낫다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 20:48:25 +09:00
kimandClaude Opus 5 f2ae455458 v4.3.32: 수치 대조 가드에 날짜 검증 추가, 순위는 의도적으로 제외
단위 없는 주장(날짜·순위)이 numeric-grounding의 사각지대로 남아 있어 확장.

날짜 — 추가함. 단 형식 관용이 필수:
출처는 "Nov 10th, 2015" / "November 2015" / "2015-11-10" / "2015년 11월"을
제각각 쓰는데 답변은 한국어로 나온다. 실제로 모델이 영문 스펙 페이지를 보고
"2015년 11월 10일"이라 답했으므로, 단순 문자열 대조를 넣었다면 정답이 오탐으로
걸렸을 것. 연도 일치 + 월을 영문명/약어/ISO/한글 중 어떤 형태로든 연도 근처에서
찾는 방식으로 구현. 연도만 맞고 월이 다르면 걸린다.
날짜를 묻지 않은 질문에서는 답변에 연월이 섞여 있어도 판정하지 않음.

순위 — 넣지 않기로 결정. 근거:
순위표는 위치를 선행 맨숫자로 적는다("40 경상북도 구미시 402,726"). "위" 인접을
요구하면 정답인 답변이 오탐으로 걸리고(나무위키 실제 표로 확인), 단위를 떼고
맨숫자 "40"만 찾으면 어떤 문서에나 있어 검증이 무의미해진다. 오탐 아니면 무동작
둘 중 하나이므로 추가할 가치가 없음. 이 판단 근거를 테스트로 고정해 나중에
누가 다시 넣으려 할 때 이유가 보이도록 함.

배포 후 확인(둘 다 정답, 가드 미발동 = 오탐 없음):
- "Tesla M40 몇 년 몇 월 출시" → 2015년 11월
- "구미시 인구 전국 몇 위" → 402,726명 40위 (나무위키 원본 표와 정확히 일치,
  모델이 web_fetch 후 python_eval로 직접 스크래핑해 산출)

테스트 142개 통과(날짜 4개, 순위 제외 근거 1개 신규).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 20:26:00 +09:00
kimandClaude Opus 5 ca7e6e49af v4.3.31: 답변 수치가 실제 검색 결과에서 나왔는지 대조하는 가드 신설
기존 세 가드가 모두 엉뚱한 질문을 하고 있었음:
- AUTO-RECOVER    "근거 도구를 호출했는가?"   → 예
- EMPTY-GROUNDING "결과가 비어있는가?"        → 아니오
- looksLikeUnverifiedSpecClaim은 검색을 아예 안 했을 때만 발동
아무도 "그 숫자가 검색 결과에서 나왔는가?"를 묻지 않았음.

실사고(2026-07-29): 벤치마크가 존재하지 않는 조합(Tesla M40 2장 + Qwen3.6 35B)의
처리량을 묻자, 모델이 검색을 수행하고(이름이 비슷한 Apple M4 가이드가 걸림)
"초당 약 2~5 토큰"이라 단정. 세 가드 모두 통과시킴.

src/gateway/chat/numeric-grounding.ts 신설 — 답변의 (숫자, 단위) 주장이 이번 턴
도구 출력에 실제로 등장하는지 대조. 단위 근처(40자 이내)에 같은 숫자가 있어야
근거로 인정.

임계값을 실전에서 두 번 틀렸고, 두 번 다 실제 데이터가 잡아줌 — 둘 다 테스트로
고정:
1차 "모든 수치가 미근거일 때" → 답변이 질문의 "24GB"를 되뇐 것이 유일한 근거로
    잡혀 발동 실패. 질문에 등장한 숫자는 전제의 반복이지 모델의 주장이 아니므로
    제외하도록 수정
2차 여전히 실패 — 모델이 "157.66 tok/s on RTX 3090"을 페이지에서 정확히 인용
    했고, 그 진짜 수치 하나가 옆의 날조를 가려줌. "어딘가 근거가 있으면 통과"는
    안전한 규칙이 아니었음
3차 질문이 명시한 단위(몇 토큰/몇 GB/몇 와트/몇 퍼센트)로 범위를 좁히고, 그 안에서
    질문에 직접 답하는 수치(=선두 주장)만 검증. 비교·파생 수치는 자유롭게 허용

배포 후 확인 — 같은 질문에 대한 답변이 바뀜:
  이전: "초당 약 2~5 토큰 내외의 매우 낮은 속도가 예상됩니다"
  이후: "검색 결과에는 해당 조합의 실측 성능 수치가 없어서 단정하기 어렵습니다.
        다만 유사한 Tesla P40 1장에서 약 26.2 tokens per second ... M40은 이전
        세대이므로 이보다 낮을 것으로 보입니다"
모른다고 말하면서 실제로 찾은 인접 데이터는 제시하고 방향만 정성적으로 설명함.

테스트 137개 통과(수치 대조 15개 신규).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 19:54:37 +09:00
kimandClaude Opus 5 0c75b946d9 v4.3.30: 기본 날씨 2종을 스킬 게이트에서 해제 — 일상 질문에 스킬 토글 불필요
"내일 날씨"는 일상 질문인데 9개 날씨 도구가 전부 meteorologist 스킬 뒤에 있어,
스킬이 꺼진 계정에서는 날씨 도구가 하나도 없이 web_search로 폴백하고 있었음.

두 단계로 분리:
- 기본(키워드만 요구, 스킬 불필요): weather_kma, weather_openmeteo — 합 446토큰,
  날씨 키워드가 있을 때만 붙음. 둘이서 한국(관측 기반)과 해외(강수확률·시간별)를
  모두 커버함
- 전문(스킬+키워드 유지): ERA5/CDS/CMIP6/NASA POWER 재분석·기후 아카이브,
  대기질(airpollution/airkorea), 그리고 weather_search — openmeteo와 스키마
  비용은 같은데(290토큰) 담는 데이터는 적은 중복 폴백이라 전문 쪽에 둠

배포 후 확인(스킬 여전히 꺼진 상태):
- "내일 구미 날씨" → weather_kma, 강수확률 20% 포함
- "내일 파리 날씨" → weather_openmeteo, 16시·18시 뇌우 등 시간별 예보 반영
어제 계획했던 한국=기상청 / 해외=openmeteo 라우팅이 스킬 토글 없이 성립함.

테스트 123개 통과(날씨 게이트 3종 신규/갱신).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 19:41:44 +09:00
kimandClaude Opus 5 5f99dab941 v4.3.29: 프롬프트가 스키마에 없는 도구를 지목하던 누출 차단 + 날씨 소스 라우팅
스키마 게이팅이 도구 실행을 전혀 막지 못하고 있었음. executeTool()은 이름으로
레지스트리를 조회할 뿐 스키마 포함 여부를 검사하지 않으므로, 프롬프트가 도구
이름을 알려주면 모델이 그대로 호출하고 그대로 실행됨.

실제 관측(2026-07-29): papa는 meteorologist 스킬이 꺼져 있어 tool-scope가 모든
weather_* 스키마를 정상적으로 제외했는데도(런타임 계측으로 weatherTools=(none)
확인), weather_kma가 매 날씨 턴마다 2회씩 실행되고 있었음. 모델이 파라미터
문서 없이 이름만 보고 호출한 것 — 날씨 도구 라우팅이 설명 문구를 두 번 고쳐도
계속 불안정했던 원인이 이것이었음.

누출 지점 3곳을 모두 실제 도구 목록 기반으로 전환:
- handle-chat 사전 리마인더: 하드코딩 "weather_kma/weather_search" → 보유한
  도구만 나열
- handle-chat AUTO-RECOVER toolHint: 동일
- personality-context [TOOLS] 블록: 키워드로만 켜지고 보유 여부를 안 봐서
  날씨 단어 하나에 9개 도구 사용법이 통째로 주입되던 것이 최대 누출원.
  카테고리→도구 매핑을 두고 하나라도 보유한 경우에만 블록 포함(호출자가
  목록을 안 주면 기존 동작 유지)
- system-prompt 날씨 라우팅 문장도 보유 도구에 따라 조건부 생성

배포 후 확인: 스킬이 꺼진 상태에서 날씨 질문 시 weather_* 호출이 완전히
사라지고 web_search로 폴백 — 의도한 동작.

함께 반영한 날씨 소스 우선순위(스킬이 켜졌을 때 적용):
한국은 weather_kma 우선(관측 기반, 강수확률 포함), 3일 초과·UV·시간별은
weather_openmeteo, 해외는 openmeteo, weather_search는 폴백. 구미 실측 비교에서
세 소스가 강수확률 20% vs 0%, 최고기온 34.0 vs 36.5로 갈렸음.

전체 121개 테스트 통과.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 17:54:59 +09:00
kimandClaude Opus 5 a1c653655f v4.3.28: 날씨 답변 부실 원인 3종 수정 — 도구 라우팅 + 지오코딩 오지정
"내일 구미 날씨"가 75자 한 문장(기온+하늘상태)으로만 나오던 문제를 추적한 결과,
프롬프트가 아니라 도구 쪽에 원인이 세 개 있었음.

1) weather_search가 더 나은 도구를 밀어내고 있었음
   설명에 "ALWAYS use this for weather/forecast/temperature queries"라는 절대
   지시가 있어 모델이 매번 이걸 골랐는데, 이 도구는 기온 범위와 하늘 상태만
   반환함(강수확률·습도·풍속 없음). 강수확률을 주는 weather_openmeteo가 있는데도
   도달하지 못했음. 두 설명을 각자의 실제 능력에 맞게 정정하고 openmeteo를
   PREFERRED로 표시.

2) 시스템 프롬프트가 없는 데이터를 요구하고 있었음
   OUTPUT FORMAT이 "강수확률·평년대비를 서술하라"고 했으나 모델이 호출한 도구는
   그 값을 주지 않음 — 지시를 따르려면 지어내는 수밖에 없는 구조였음. "받지
   못했으면 지어내지도 말고 조용히 빼지도 말고 weather_openmeteo를 호출하라"로
   변경.

3) ⚠ 구미가 엉뚱한 지역으로 해석되고 있었음 (가장 심각)
   KR_CITY_COORDS에 구미가 없어 Open-Meteo 지오코더로 넘어갔는데, 한국 내 "구미"
   결과 10건 중 실제 구미시(36.12, 128.34)가 하나도 없음. 게다가 모든 항목의
   population 필드가 비어 있어 "인구 최대 선택" 로직이 results[0]으로 붕괴,
   강원도의 작은 마을(38.16)이 선택됐음. 위도 2도 차이 = 다른 기후대인데 에러는
   전혀 나지 않았음 — 사용자는 그동안 남의 동네 날씨를 받고 있었을 수 있음.
   구미 포함 중견도시 25곳의 좌표를 테이블에 직접 추가.

data.go.kr 키는 vault(kma.api_key)에 암호화 저장 — 평문 커밋 없음. 배포 후 확인:
기상청 초단기실황이 구미 기온 32.2°C/습도 60%/풍향 북북서로 정상 응답, 예보
답변도 75자 → 167자로 강수확률·UV지수 포함.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 16:55:25 +09:00
kimandClaude Opus 5 159669ec04 v4.3.27: coder_patch_file 매칭에 조건부 줄 앵커 적용 — 엉뚱한 줄 편집 차단
이전에는 순수 부분 문자열 매칭이라, 파일보다 얕게 들여쓴 SEARCH가 더 깊은 줄
안에서 매칭됐음("    deep()"가 "        deep()"에 적중). 한 줄을 노린 편집이
다른 줄에 적용될 수 있는 경로였고, 조용히 잘못된 바이트를 쓰는 fail-open이었음.

"무조건 줄 단위 일치"로 바꾸면 줄 안의 조각을 고치는 정당한 사용
("return 1" → "return 42")이 전부 깨지므로, SEARCH가 스스로 무엇을 주장하는지에
따라 앵커를 조건부로 적용:

- 선행 공백이 있거나 여러 줄  → 줄 시작 정렬 필수
  (들여쓰기/구조를 주장하고 있으므로 정렬돼야 함)
- 선행 공백 없는 한 줄        → 기존대로 부분 매칭
  (줄 안의 조각일 뿐 들여쓰기를 주장한 적 없음)

거부는 fail-closed — 찾지 못함으로 보고돼 모델이 더 정확한 컨텍스트로 재시도함.
앵커에 걸린 등장은 건너뛰고 뒤쪽의 정렬된 등장을 계속 찾으므로, 같은 텍스트가
줄 중간과 줄 시작에 모두 있으면 후자를 고침.

부수 수정 — 오프셋 치환:
빠른 경로와 폴백 경로 모두 String.replace()를 쓰고 있었는데, replace()는 위치 0
부터 다시 스캔하므로 방금 내린 앵커 판정을 무효화하고 앞쪽의 정렬 안 된 등장을
고칠 수 있었음. 이미 알고 있는 오프셋으로 splice하도록 양쪽 다 변경.

테스트 121개 통과(앵커 규칙 4개 신규). 실디스크 검증: 8칸 들여쓰기 안의
value = 1 → 99 수정 정상, 들여쓰기 불일치 케이스는 파일 무변경으로 거부.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 16:35:53 +09:00
kimandClaude Opus 5 abac4f7940 v4.3.26: coder_patch_file의 SEARCH/REPLACE 적용부 분리 + 테스트 15개
execute-tool.ts에서 파일 편집 알고리즘을 순수 모듈로 추출. 1,529 → 1,498줄.

이 함수는 툴 레이어에서 가장 위험한 순수 로직임 — 사용자의 실제 소스 파일을
다시 쓰는데, 실패해도 예외를 던지지 않고 잘못된 바이트를 쓰거나 "3건 적용"이라
보고하고 2건만 적용한다. executeTool()의 1,349줄 본문 안에서 fs.writeFileSync
바로 옆에 인라인으로 있어서 디스크를 건드리지 않고는 검증할 방법이 없었음.

테스트가 고정한 성질(피해 큰 순):
1. 적용하지 않은 편집을 성공으로 세지 않는다
2. 엉뚱한 구간을 치환하지 않는다
3. 매칭 구간 밖의 바이트(들여쓰기·후행공백)를 보존한다
4. 실패를 보고해 모델이 재시도할 수 있게 한다
+ /g 정규식 lastIndex 오염으로 두 번째 호출이 편집을 건너뛰는 회귀 방지

추출 중 확인 — 계획 정정:
당초 "early return 평탄화"를 하려 했으나 실측해보니 이 파일의 깊은 중첩은
방어적 가드 피라미드가 아니라 알고리즘 자체의 깊이였음(공백 정규화 텍스트 검색,
Promise 래핑+파싱 루프 등). 평탄화해도 나아지지 않고 테스트 없는 I/O 코드를
기계적으로 건드리는 위험만 남으므로, 오늘 효과가 확인된 방식(파묻힌 순수 로직
추출)으로 방향을 바꿈.

알려진 날카로운 모서리를 문서화(수정하지 않음):
매칭이 줄 단위 앵커가 아니라 부분 문자열이라, 파일보다 얕게 들여쓴 SEARCH가
더 깊은 줄 안에서 매칭된다("    deep()"가 "        deep()"에 적중). 대개 무해
하지만 한 줄을 노린 편집이 다른 줄에 갈 수 있는 경로임. 줄 앵커로 바꾸면 모든
호출자의 파일 편집 의미가 달라지므로 조용히 "고치지" 않고 테스트로 고정만 함.

전체 117개 테스트 통과. 실디스크 검증: coder_patch_file로 return 1 → return 42
수정이 들여쓰기·구조 보존한 채 정확히 반영됨.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 16:30:24 +09:00
kimandClaude Opus 5 7a413f2388 v4.3.25: 재시도 판단 3종을 retry-decisions.ts로 분리 + 테스트 23개
라운드 루프에 남아 있던 판단 로직 전부 추출. 세 검사 모두 "모델이 유창하게 답을
썼지만 그 답에 이르는 과정이 신뢰할 수 없다"를 잡는 같은 계열임:

- decideAutoRecover()            근거 도구 없이 검증 가능한 질문에 답한 경우
- shouldForceMessagingRetry()    전송 도구 없이 "보냈다"고 주장한 경우
- shouldForceEmptyGroundingRetry() 검색이 전부 비었는데 답한 경우

셋 다 fail-open이라 회귀해도 에러도 로그도 남지 않고 검증 안 된 답이 사실처럼
전달된다 — 정확히 이 저장소가 반복해서 겪은 실패 유형이라(07-29 로그 감사)
재시도해야 하는 경우와 하지 말아야 하는 경우를 양방향으로 고정함.

handle-chat.ts 2,908 → 2,872줄. 전체 102개 테스트 통과.

테스트 작성 중 확인한 경계(코드는 정상, 테스트가 틀렸던 케이스):
짧고 애매한 답변("음...")은 재시도 대상이 아님 — looksLikeReasoning은 300자
초과를 요구하고 거부 문구도 아니므로. 회복할 내용 자체가 없는 게 맞는 동작이라
그 경계를 별도 테스트로 남김.

배포 후 확인: "RTX 5090 VRAM 용량" 질문에서 web_search 선행 후 답변 정상.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 16:23:19 +09:00
kimandClaude Opus 5 5eb47ca08b v4.3.24: 검색 예산 판단을 search-budget.ts로 분리 + 테스트 13개
라운드 루프(1,938줄) 안의 순수 판단 로직 추출. 루프 자체는 옮기지 않음 — 판단
근거는 아래에 정리.

- src/gateway/chat/search-budget.ts (신규) — evaluateSearchBudget()가
  (이전 쿼리들, 현재 쿼리, 차단횟수) → (허용여부, 사유, 도구차단, 하드캡도달)
  을 반환하는 순수 함수. handle-chat.ts 2,936 → 2,908줄
- tests/search-budget.test.ts — 13개. 캡이 "반복에는 물리고 팬아웃에는 안
  물린다"는 양방향을 모두 고정

테스트가 찾은 약점 — 한 단어 우회:
"어떤 미등장 토큰이라도 있으면 새 주제로 본다"는 규칙이 팬아웃(도시 8곳 날씨
확인 등)을 보호하려고 일부러 느슨한데, 그 탓에 "우크라이나 전황" → "우크라이나
전황 다시"가 완전히 새로운 주제로 통과해 재검색 루프를 그대로 허용했음. 이
모듈이 막으려던 바로 그 패턴. 규칙 자체를 조이면 정당한 쿼리까지 막히므로,
변별력 없는 단어(다시/추가/자세히/더/또/again/more/detail)를 불용어에 추가하는
쪽으로 해결.

라운드 루프를 통째로 옮기지 않은 이유:
루프는 1,938줄이면서 외부 가변 변수 22개(allThinking, toolSkipForcedRetries,
toolsDisabledForRestOfTurn, fileOpOwner 등)를 변경하고, 본질적으로 I/O
(모델 스트리밍·도구 실행·SSE 전송)라 순수 함수가 될 수 없음. 옮기려면 22개를
상태 객체로 묶어 넘겨야 하는데 회귀 위험만 크고 테스트 가능성은 늘지 않음 —
줄 수만 다른 파일로 이동할 뿐임. 오늘 tool-scope/system-prompt 추출이 값어치
있었던 건 순수 함수가 되어 테스트가 붙었기 때문이지 줄 수가 줄어서가 아니므로,
같은 기준으로 루프 안의 '판단'만 뽑는 방향을 택함.

전체 79개 테스트 통과. 배포 후 뉴스 요청에서 news_search 8회 팬아웃 정상 확인.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 16:16:59 +09:00
kimandClaude Opus 5 4f31af0593 v4.3.23: 시스템 프롬프트 조립을 system-prompt.ts로 분리 + 테스트 19개
tool-scope.ts와 같은 이유로 추출. 시스템 메시지는 모델 동작을 가장 크게 좌우
하는 단일 문자열인데, 2,900줄 함수 안에서 인라인으로 조립돼 결과를 보려면 실제
채팅을 보내 SSE의 tool_overhead를 읽는 수밖에 없었음.

- src/gateway/chat/system-prompt.ts (신규) — buildChatSystemPrompt(input).
  날짜/모델프로필/성격/스킬 컨텍스트는 호출자가 해결해 넘기므로 순수 함수
- tests/system-prompt.test.ts — 19개
- handle-chat.ts 2,992 → 2,936줄 (오늘 누적 3,131 → 2,936)

테스트가 고정한 핵심 불변식: 도구를 억제하는 규칙은 그 도구가 호출 가능한 한
반드시 함께 온다. image_edit이 스키마에 있으면 "함부로 편집하지 말라"가 없을
수 없다 — 이걸 사용자 문구로 게이팅했다면 정규식이 놓친 표현에서 규칙만 조용히
사라졌을 것. 반대로 tool-scope.ts의 키워드 게이팅은 최악의 경우가 "도구가 없어
못 한다"이므로 허용된다. 두 모듈 헤더에 이 구분을 명시해둠.

동작 무변경 검증: 리팩터 전후 tool_overhead가 "안녕" 3,574 / "RTX 5090 스펙
비교" 3,724로 바이트 단위 동일. 전체 66개 테스트 통과.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 16:09:02 +09:00
kimandClaude Opus 5 6461e73a5e v4.3.22: 도구 선택 로직을 tool-scope.ts로 분리 + 테스트 17개 — 버그 3개 발견
handleChat()에서 도구 선택 로직(145줄)을 순수 모듈로 추출. 이 로직은 2,959줄
함수 안의 클로저라 테스트가 불가능했고, 게이트를 확인하려면 게이트웨이를 띄우고
실제 채팅을 보내 SSE의 토큰 수를 읽는 수밖에 없었음.

- src/gateway/chat/tool-scope.ts (신규) — I/O 없는 순수 함수.
  buildSkillToolFilter()는 스키마 객체가 아니라 도구 '이름'을 받게 해서
  테스트에서 문자열만으로 구동 가능하도록 함
- tests/tool-scope.test.ts — 17개. 비용(불필요한 도구 미노출)과
  도달성(요청한 도구는 반드시 노출) 양쪽을 모두 고정
- handle-chat.ts 3,131 → 2,992줄

추출과 동시에 테스트가 버그 3종을 찾음:

1. 한글 뒤 \b 는 절대 매칭되지 않음 (죽은 패턴 5개)
   \b는 ASCII 단어경계라 한글 음절 뒤에서는 성립하지 않음. `메일\b`가 그 예로,
   "메일 확인해줘"가 게이트를 통과하지 못해 이메일 도구가 아예 노출되지 않았음
   ("이메일"이라고 써야만 동작). 같은 죽은 패턴이 hasCodeKeyword(함수/클래스/
   버그), hasNewsKeyword(기사), hasPptxKeyword(덱)에도 있었음. 배포 후 확인:
   "메일 확인해줘"가 email_list를 정상 호출함

2. pptx 게이트 조건 반전
   `presenterEnabled && !hasPptxKeyword`라 스킬이 '꺼진' 경우 배제가 아예
   동작하지 않았음. 스킬이 꺼졌으면 더 제한해야 하는데 반대로 열려 있었던 것.
   실제 영향 범위(skills_state.json 대조): papa·cherry는 presenter=true라 무관,
   jasmine만 매 턴 ~1,600토큰(create_presentation 833은 최대 스키마)을 지고
   있었음. weather/legal과 같은 형태로 정정

3. (테스트 작성 중) 원문을 줄여 쓴 케이스가 실패 → 이전 커밋에서 수정한
   SPEC_CONTEXT_KEYWORD 구멍과 동일 계열임을 재확인

전체 47개 테스트 통과. 실측 오버헤드는 papa 기준 3,574로 동일(위 2번이
papa에겐 원래 정상 동작했으므로 예상된 결과).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 16:03:23 +09:00
kimandClaude Opus 5 2ba39bca76 v4.3.21: 테스트 인프라 복구 — npm test 실행 가능하게 + 회귀 테스트 30개
tests/ 디렉터리가 비어 있었고 package.json의 두 스크립트가 삭제된 파일을
가리킨 채 방치돼 있었음:
- test    → tests/test-v2.ts (존재하지 않음)
- gateway → src/gateway/server-v2.ts (v4.3.5에서 삭제됨)

즉 npm test가 몇 달간 실행 자체가 불가능한 상태였고, 그 대가로 프롬프트 게이트
정규식을 고칠 때마다 node -e 임시 스크립트를 손으로 짜고 버리는 일이 반복됐음
(2026-07-29 하루에만 5회). 회귀 방지는 하나도 남지 않았음.

- Node 22 내장 러너(node:test) + tsx 사용, 테스트 프레임워크 의존성 없음
- tests/prompt-gates.test.ts (25) — 모델 동작을 강제/억제하는 정규식 게이트 전반
- tests/usage-log.test.ts (5)  — Ollama 외 provider의 토큰 집계 단일 지점
- tests/README.md — 무엇을 왜 테스트하는지, 케이스 작성 규칙, 다음 확장 대상
- 전체 30개 통과, 0.6초

부수 성과 — 테스트가 실제 버그를 찾음:
케이스를 쓰면서 프로덕션 원문을 한 단어(GPU) 줄여 썼더니 실패했고, 그게 진짜
구멍이었음. "토큰 처리 속도 손실이 약 15%~25%"처럼 하드웨어 명사가 없는 처리량
날조는 SPEC_CONTEXT_KEYWORD에 걸리지 않아 그대로 통과하고 있었음 — 원문에
우연히 들어있던 "GPU" 덕에 잡히던 것. 토큰/대역폭/추론/처리속도를 키워드에
추가해 막고, 그 케이스를 테스트로 고정함(강수확률·할인율 오탐 없음 확인).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 15:29:02 +09:00
kimandClaude Opus 5 8a121457ff v4.3.20-3: ollama_web_* 도구 키워드 게이팅 — 오버헤드 214토큰 절감
ollama_web_search / ollama_web_fetch가 매 턴 무조건 스키마에 포함되고 있었음.
두 도구는 자기 description에 "뉴스·정보 검색은 반드시 web_search를 사용. 이
툴은 web_search가 없는 환경에서만 fallback으로 사용"이라고 적혀 있어서, 모델
에게 "이 도구를 쓰지 말라"고 알리는 데 214토큰을 쓰던 셈. 게다가 그 폴백
체인은 이미 executeWebSearch 내부에서 서버가 처리하므로 모델이 직접 부를
이유가 없음.

삭제가 아니라 게이팅을 택함: personality-context.ts에 "올라마로 검색" 류
표현에만 뜨는 ollama_web 힌트 블록이 이미 있어서, 도구를 제거하면 그 힌트가
존재하지 않는 도구를 설명하게 됨. 같은 표현으로 게이트해 평소엔 0토큰,
명시 요청 시엔 정상 동작하도록 함.

실측: 3,788 → 3,574. "올라마로 검색해줘" 요청에서는 4,980으로 도구가 정상
복귀하는 것도 확인.

조사 중 정정: eonet_events/satellite_snapshot과 run_command는 이미
skillToolFilter에 게이트가 있었음(각각 hasSatelliteKeyword, hasCodeKeyword).
초기 분석에서 필터 전체를 보지 못해 "무조건 켜짐"으로 잘못 판단했던 것을
바로잡음 — 실제 always-on은 8개(1,109토큰)뿐이며 07-26 스코핑 작업이
이미 대부분을 처리해둔 상태.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 15:18:06 +09:00
kimandClaude Opus 5 37257801ed v4.3.20-2: 도구 전용 프롬프트 블록 4개 조건부 전환 — 오버헤드 542토큰 절감
특정 도구를 제약하려고 존재하는 블록들이 도구 유무와 무관하게 매 턴 무조건
주입되고 있었음. 기존 BROWSER RULE이 쓰던 도구 목록 기반 게이팅을 그대로 확장.

조건부로 전환한 블록(측정 정적 비용):
- IMAGE EDITING RULE (90)      → image_edit 있을 때만
- IMAGE/VIDEO GENERATION (225) → image_generate/video_generate 있을 때만
- CODE OUTPUT (154)            → coder_* 있을 때만
- PACKAGE INSTALL (79)         → shell/run_command 있을 때만

실측: tool_overhead 4,330 → 3,788 (-542, 12.5%). 서로 다른 두 질문에서
동일하게 542 감소 확인.

메시지 문구가 아니라 도구 존재 여부로 게이트한 것이 핵심. "업로드한 사진을
함부로 편집하지 말라" 같은 억제 규칙은 그 도구가 호출 가능한 동안 항상 떠
있어야 하며, 정규식 문구 매칭으로 게이트하면 정규식이 놓친 표현에서 규칙이
사라진다 — 오늘 환각 조사에서 확인한 것과 같은 실패 모드.

CHEMISTRY NOTATION(180)은 의도적으로 무조건 유지: 도구가 아니라 출력 형식을
제약하므로 키로 삼을 도구가 없고, 치과/의학 질문은 화학 키워드 없이도 모델이
구조식을 그리게 만들 수 있어 문구 기반 게이팅이 위험함.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 15:08:48 +09:00
kimandClaude Opus 5 75b35eede1 v4.3.20-1: USER.md에 클로서버 대시보드 주소 확정 기록
모델이 "클로서버.local"이나 ":3000" 같은 존재하지 않는 주소를 지어내던 것을
막기 위해, 실제 주소(192.168.0.5:18789 / localhost:18789)와 함께 "그 값들은
실재하지 않으니 답변에 쓰지 말 것"을 USER.md에 명시.

USER.md는 벡터 메모리(Chroma)로 대체된 것이 아니라 병행 사용됨 — 벡터 쪽은
관련도 기반이라 사용자가 주소를 명시적으로 묻지 않으면 유사도 컷을 못 넘고,
Chroma 조회 실패 시엔 조용히 빈 값이 되므로, 항상 떠 있어야 하는 이런 사실은
전량 주입되는 USER.md에 있어야 한다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 14:59:20 +09:00
kimandClaude Opus 5 e85ebe6608 v4.3.20: 미커밋 자산 일괄 정리 + 유저 워크스페이스/세션토큰 gitignore
그동안 쌓여 있던 미커밋 파일들을 정리. 소스·자산만 올리고, 개인 데이터와
런타임 상태는 gitignore로 차단.

추가된 자산:
- web-ui/audiomass, web-ui/wavacity — 자체 호스팅 오디오 에디터
- web-ui/drums — 드럼 샘플
- wol-gate — WOL 프록시 컨테이너(Dockerfile + server.js)
- glm-1m-fix — GLM 1M 컨텍스트 설치 스크립트(linux/windows)
- RENODE_FIXES_SUMMARY*.md, CCTV정보_서울특별시.csv, .claude/plans

수정:
- edge_tts_synth.py: rate 인자 추가(말하기 속도 조절)
- .smallclaw/config.json: google(gemini) provider 등록 — 키는 vault 참조
- assets/SmallClaw*.png 삭제분 반영

gitignore (중요):
- .smallclaw/users/*/workspace/ 전체 차단. 기존 패턴이 memory/uploads와 일부
  확장자만 막아서 detective/(고소장·통화녹취·의사소견서·가족관계증명서),
  generated-media/, music/, pptx/ 등 앱 생성 개인 데이터 403MB가 노출돼 있었음
- .smallclaw/active-sessions.json 차단 — 살아있는 bearer 토큰과 admin 역할이
  평문으로 들어있어 커밋 시 게이트웨이 관리자 자격증명이 그대로 공개됨
- google-usage.json, config.json.bak-*, workspace/memory·weather-maps·
  task_result_*, voice/ 등 런타임 상태도 함께 차단

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 14:55:37 +09:00
kimandClaude Opus 5 8359901cac v4.3.19-26: 환각 방어 전역화 + 답변 길이 압박 완화 + 토큰 로깅 구멍 수정
실사용 로그 감사(07-29 GPU/PCIe 대화)에서 발견한 문제들 일괄 수정.

환각 방어:
- looksLikeUnverifiedSpecClaim이 놓치던 패턴 2개 추가 — 단위 없는
  "15토큰"(strict tier), 퍼센트 벤치마크 "약 5%"(ambiguous tier,
  SPEC_CONTEXT_KEYWORD 게이트로 오탐 방지)
- forceToolChoiceOnLiveData를 모델별 opt-in → 전역 기본 ON + opt-out.
  모델을 수시로 바꾸는데 profiles에 등재된 2개만 방어되던 구멍이 원인
  (프로필 없던 gemini-3.5-flash-lite가 tok/s 날조 + 없는 제품명 창작)

답변 길이:
- "Keep responses SHORT" 1-2 → 2-4문장, 대화/실행 답변으로 범위 한정.
  정보성 답변(뉴스·날씨·설명·비교)은 OUTPUT FORMAT을 따르도록 명시
- 불확실성 표현은 길이 제한 면제. 1-2문장 압박이 "모른다"고 말할 자리를
  없애 환각을 확신형 한 줄로 포장하던 것이 실측으로 확인됨
- 뉴스는 도구가 반환한 기사 전부 표기(3건 체리픽 금지, 8-10행 목표)

토큰 로깅:
- logUsage가 ollama-adapter 내부 private이라 Ollama 경유만 기록되고,
  openai-compat은 usage 필드를 추출조차 안 하고 버리던 문제. 비용 비교
  대상인 Gemini가 통째로 누락돼 공짜처럼 보이는 상태였음
- providers/usage-log.ts로 공용화 → openai-compat(chat/generate)과
  openai-codex(SSE response.completed)에서도 캡처·로깅·호출자 반환

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 14:44:21 +09:00
kimandClaude Sonnet 5 3bff465c08 v4.3.19-25: 탐정 앱 자체 WOPI 호스트로 NextCloud 없이 Collabora 직결
기존 col.applecherry.net Collabora 서버를 재사용하되, NextCloud(WebDAV
업로드/가져오기 왕복) 없이 홈클로 게이트웨이가 직접 WOPI 호스트 역할
수행 (CheckFileInfo/GetFile/PutFile). hwp는 사건 폴더 안에서 odt로
1회 변환 후 그 자리에서 바로 편집 — 타이핑하는 대로 사건 폴더에 즉시
저장되어 별도 "가져오기" 버튼이 필요 없어짐(dtFetchFromNextcloud 제거).
config.json에 collabora.baseUrl/publicUrl 추가. Express 5의
res.sendFile 이슈(이미 known) 재발 → createReadStream으로 우회.

실기기(col.applecherry.net + 실제 사건 고소장.odt)로 전체 라운드트립
검증 완료.

부수: 사이드바 사건목록이 빠른조사/법률질문 버튼 늘어나며 눌리는 문제
— 잘 안쓰는 버튼 정리(19개→11개)로 완화 + .dt-cases min-height 안전장치.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-28 14:18:18 +09:00
kimandClaude Sonnet 5 e180c3978e v4.3.19-24: 일별 메모리 추출 스크립트 깨진 import 경로 수정
memory-vector.ts가 src/gateway/memory/ 하위로 옮겨진 뒤 이 스크립트만
경로가 안 고쳐져서 MODULE_NOT_FOUND로 매일 새벽 1시 systemd 타이머가
3일 연속 즉시 실패하고 있었음(daily_extracts Chroma 컬렉션이 07-24
이후로 안 쌓이던 원인). 경로 수정 후 밀린 07-25~27 백필 확인.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-28 13:22:18 +09:00
kimandClaude Sonnet 5 8a7f6443fc v4.3.19-23: 글로벌 하트비트가 빈 워크스페이스를 보던 버그 수정
멀티유저 전환 후 USER.md/SOUL.md가 유저별 워크스페이스로 옮겨졌는데
HeartbeatRunner는 여전히 글로벌 워크스페이스(파일 없음)를 보고 있어서
30분마다 memory_read(user/soul)가 매번 "not found"로 실패하던 문제.
workspacePath를 papa 워크스페이스로, handleChat에 username도 함께
전달하도록 수정.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-28 13:22:09 +09:00
kimandClaude Sonnet 5 4c3d90c3d2 v4.3.19-22: 탐정 앱 증거자료 파일/폴더 다운로드 버튼 + 관련 사이트 링크 추가
파일별 다운로드(download 속성), 폴더 전체 zip 다운로드(archiver 신규
의존성 + /api/detective/files/download-folder 라우트) 추가. 사이드바에
법률 서식(대한법률구조공단)·법원 소송 서식(전자소송포털)·대법원 판례
검색(사법정보공개포털)·경찰청 링크 블록 신설.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-28 13:21:59 +09:00
kim e39c430485 v4.3.19-21: NextCloud 편집 버튼(🌐/⬇️)을 odt 파일에도 확장
hwp에서 가져온 odt 사본을 다시 편집하려 해도 버튼이 없었음 — 이제
.odt 파일도 hwp와 동일하게 🌐(NextCloud에서 열기)/⬇️(가져오기+PDF
변환) 대상이 됨. 백엔드는 이미 비-hwp 파일을 변환 없이 그대로
업로드/처리하도록 되어 있어서 프론트엔드 조건만 확장하면 됐음.
2026-07-27 23:43:52 +09:00
kim 667a539343 v4.3.19-20: NextCloud 가져오기에 odt→PDF 자동 변환 추가
h2orestart는 hwp 내보내기(쓰기)를 지원 안 해서 편집본을 진짜 hwp로
되돌릴 방법이 없음(리눅스에서 근본적으로 불가) — 대신 odt→PDF는
특별한 필터 없이 표준 변환이라 이걸로 실질적 요구(제출용 PDF)를 충족.

⬇️ 가져오기 시 odt와 함께 PDF도 자동 생성. 파일명은 "{이름}
(NextCloud변환).pdf"로 접미사를 붙여서, 같은 폴더에 이미 있는 원본
증거자료 PDF(예: 고소장.pdf)를 덮어쓰지 않도록 함 — 실제로 이 충돌
가능성을 발견하고 배포 직전에 수정함.
2026-07-27 23:32:43 +09:00
kim 6cfb2db5b1 v4.3.19-19: 탐정 앱 hwp 파일 NextCloud/Collabora 실시간 편집 연동 + 지도탭 누락 커밋 반영
주요 변경:
- 탐정 앱의 .hwp 파일에 🌐 버튼 추가: 서버가 자동으로 hwp→odt 변환(host의
  h2orestart 필터 사용) → 사용자 소유 NextCloud(cloud.applecherry.net)의
  homeclaw-detective/{caseId}/ 폴더로 WebDAV 업로드 → Collabora Online
  편집화면으로 리다이렉트. 타이핑하는 대로 자동 저장, 다운로드/재업로드 불필요.
- NextCloud 앱 비밀번호는 vault에 저장(nextcloud.appPassword), config.json엔
  평문 없음.
- (부수적, 별도 이슈) Stirling-PDF는 이 사건의 원본 PDF가 실제 폼필드가
  없는 인쇄용 레이아웃이라 텍스트 스트림 편집이 계속 실패해서 우회 경로로
  채택함.

추가로 이전 세션에서 커밋 안 하고 넘어간 weather-app.html의 지도탭
자동전환 fix(processWindyUrl 이동 시 mapLat/mapLon 동기화 + switchCenterTab)도
같이 반영됨.
2026-07-27 22:56:45 +09:00
kim ebb605bc24 v4.3.19-18: PDF 편집기 자동다운로드 되돌림 - 사용자가 원치 않음
작업 디렉토리에 로컬로 접근 가능해서 Stirling-PDF 업로드 대화상자에서
직접 파일을 찾아 선택하는 걸 선호함. 클릭할 때마다 자동 다운로드되는
건 불필요한 부작용이었음. 툴팁만 업로드 버튼에서 직접 선택하라고
안내하도록 수정.
2026-07-27 21:57:43 +09:00
kim ec62051251 v4.3.19-17: 탐정 앱 PDF 편집기 — 파일 자동 오픈 불가를 다운로드 보조로 완화
원인: /pdf-editor-open은 caseId만 전달하고 파일명은 전혀 안 넘김 —
Stirling-PDF(외부 PDF 편집기)의 실제 프론트엔드 번들을 직접 열어봤는데
"token" 쿼리파라미터 하나만 읽고 원격 파일 로드 기능 자체가 없음(자체
storage API는 있지만 별도 로그인 필요, 우리는 미설정). 즉 특정 파일을
자동으로 여는 건 우리 쪽에서 고칠 수 있는 버그가 아니라 외부 툴의
한계임을 확인.

대신 ✏️ 클릭 시 편집기 새창 열기와 동시에 그 파일을 다운로드시켜서,
사용자가 편집기 창에 끌어다 놓기만 하면 되도록 마찰을 최소화. 툴팁도
실제 동작을 정확히 설명하도록 수정.
2026-07-27 17:37:08 +09:00
kim bb0f44b0c3 v4.3.19-16: 탐정 앱 메모 textarea에서 Tab 키로 들여쓰기 가능하도록 수정
기본 브라우저 동작은 Tab을 누르면 포커스가 다음 요소로 넘어가버려서
메모 안에서 들여쓰기를 못 했음. keydown에서 Tab을 가로채 탭 문자를
커서 위치에 삽입하고 포커스는 textarea에 유지.
2026-07-27 15:19:37 +09:00
kim f49d7a903c v4.3.19-15: satellite_snapshot 이미지도 SSE로 직접 주입 (모델이 경로 오타내는 문제)
실사용 중 발견: 모델이 satellite_snapshot 결과의 /api/files/... 마크다운을
답변에 그대로 복사하지 않고 재작성하면서 /api/files/ 접두사를 빠뜨림 —
그러면 클라이언트 정규식이 매치 안 돼서 사진탭이 아예 안 열림.
weather_map_screenshot과 같은 패턴으로, 도구 결과의 마크다운을 그대로
SSE로 주입해서 모델의 재현 정확도에 의존하지 않게 함. 태풍 돌핀 실제
NASA 위성사진으로 재현·검증 완료.
2026-07-27 12:18:32 +09:00
kim ce9f556e84 v4.3.19-14: 위성사진 위치→지도 이동을 클라이언트에서 결정론적으로 처리
실제 테스트에서 발견: "지도 이동해줘" 후속 요청에 모델(deepseek-v4-flash)이
도구를 재호출하지 않고 이전 응답을 그대로 반복하는 경우가 많아 프롬프트
가드만으로는 신뢰도가 부족했음. 근본 해결로 사진 뷰어에 "지도에서 위치
보기" 버튼 추가 — satellite_snapshot 파일명에 이미 박혀있는 좌표를
그대로 파싱해 지도를 이동시키므로 모델 개입이 전혀 필요 없음.

부수적으로 execute-tool.ts에도 weather_map_screenshot 성공 시 그 URL을
SSE로 직접 주입하는 방어선 추가(모델이 실제로 도구를 호출했을 때 대비).
2026-07-27 12:11:44 +09:00
kim 0b9ce3d18a v4.3.19-13: Windy 지도 "이동" 후속요청 무시 버그 수정
실제 로그 분석 결과: "위치로 이동" 같은 후속 요청은 트리거 단어 목록에
없어서 모델이 Windy URL을 재출력하지 않고, 대신 weather_map_screenshot
(서빙 안 되는 모델 전용 스크린샷 도구) 결과를 [REDACTED-HE].jpg 같은
가짜 마크다운 이미지로 답변에 끼워넣음 — 실제 지도는 전혀 안 움직이고
깨진 이미지 링크만 표시됨. 후속 이동 요청도 트리거로 인정하고,
screenshot 도구 결과를 이미지로 삽입하지 말라는 가드 추가.
2026-07-27 11:54:51 +09:00
kim ced421cac9 v4.3.19-12: 스킬 프롬프트 소스 가독성 개선 — \uXXXX 이스케이프 → 실제 한글
accountant/lawyer/investor-app의 SKILL_CONTENT가 모든 한글을 6자리
유니코드 이스케이프(\uXXXX)로 저장하고 있어 소스 읽기가 사실상 불가능했음.
디코드→재인코딩 스크립트로 실제 UTF-8 텍스트로 변환, 디코드된 내용은
변환 전후 완전히 동일함을 스크립트로 검증. 기능 변화 없음, 가독성만 개선.
2026-07-27 10:54:39 +09:00
kim f615475618 v4.3.19-11: 어제 추가한 가드 텍스트 오탈자 수정
investor-app에 유니코드 이스케이프를 손으로 옮기다 "상호명이"가
"사이앨목이"로 깨졌던 것 수정, lawyer-app 조사 오류 2건 수정.
2026-07-27 10:48:10 +09:00
kim 5ac766381a v4.3.19-10: investor/lawyer/detective 스킬에 식별자 대조 가드 추가
날씨 스킬에서 태풍 번호 혼동 사고 확인 후, 같은 패턴(단일 소스에서
가져온 데이터를 다른 개체에 잘못 귀속) 위험이 큰 세 스킬에 가드 추가:
- investor: 종목코드/거래소/보통주-우선주/분기 대조
- lawyer: 법령 시행일·개정판, 판례 사건번호 전체 일치 대조
- detective: 동명이인·유사상호 — 추가 식별자 없이 귀속 금지
2026-07-27 10:38:22 +09:00
kim 72855ea77a v4.3.19-9: 날씨 스킬 프롬프트에 태풍 번호/이름 대조 가드 추가
실제 대화에서 KMA 통보문 단일 페이지만 보고 제13호 태풍 돌핀의 좌표를
제12호 태풍 노을(이미 광둥성 상륙·소멸)의 것으로 착각해 답한 사례 발견.
번호 대조 + 한/영 교차검색 지침 추가로 재발 방지.
2026-07-27 10:31:52 +09:00
kim 74af6fdfd3 v4.3.19-8: eonet_events 설명에 유럽/중동/아프리카 커버리지 부족 명시
EFFIS(유럽 산불 전용 DB) 공개 WFS를 붙여볼까 테스트했으나 GetCapabilities조차
35초+ 걸리다 500 에러 — 신뢰성 부족으로 보류. 대신 EONET이 북미 편향이라는
사실을 도구 설명에 적어 모델이 유럽 등 이벤트는 자동으로 web_search 우회하게 함.
2026-07-26 19:10:37 +09:00
kim 071e36110e v4.3.19-7: NASA EONET/Worldview 위성사진 기능 + weather-app 멀티탭 뷰어
- eonet_events/satellite_snapshot 도구 추가 (키 불필요 공개 API)
- weather-app.html: 위성사진 채팅 이미지 렌더링, 지도/사진 멀티탭 센터패널,
  확대/축소·드래그팬, localStorage+히스토리 이중 복원으로 새로고침 시
  탭 유지(하드리셋으로 localStorage가 지워져도 대화 이력에서 재구성)
2026-07-26 18:43:20 +09:00
kimandClaude Sonnet 5 a80ad9e809 v4.3.19-6: Google 일일 사용량 표시 + 한도 도달시 자동 폴백
- google-usage.ts: Gemini 무료티어 RPD(하루 500회 추정치) 대비 요청 카운트를
  날짜별로 추적(.smallclaw/google-usage.json, 자정 자동 리셋)
- openai-compat-adapter.ts: google provider일 때 post() 호출마다 카운트
- factory.ts: getProvider()/getModelForRole() 둘 다 getEffectiveProviderConfig()
  경유하도록 통일 — 카운트가 480(여유분 20 적용) 넘으면 자동으로
  ollama+deepseek-v4-flash:cloud로 전환(429 맞고 반응하는 게 아니라 사전 차단).
  provider/model 선택이 별도 함수라 따로 고치면 불일치 날 수 있어서 공유
  헬퍼로 묶음
- 메인 채팅 사이드바 모델 pill에 "N/500" 뱃지 추가(Google일 때만 표시,
  70%↑ 노랑/90%↑ 빨강), 기존 10초 폴링(checkStatus)에 얹어서 실시간 반영
- 실사용 확인: 배포 직후 실제 채팅으로 카운트 0→1 증가 확인, 480 시뮬레이션
  으로 폴백 전환/복귀 양쪽 다 확인

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-26 17:37:03 +09:00
kimandClaude Sonnet 5 814f32a63e v4.3.19-5: 컨텍스트 게이지 provider-aware하게 수정 + 캐시 버그 제거
- resolveNumCtx()가 Ollama 전용이라 primary가 Google로 바뀐 뒤 세션
  컨텍스트 게이지가 419%까지 잘못 표시되던 버그 수정 — google provider면
  Gemini API에서 inputTokenLimit을 실시간 조회(하드코딩 아님, API 실패시만
  1048576 폴백)
- 최종 폴백(8192)을 캐시하던 버그 제거 — 재시작 직후 첫 조회가 일시적으로
  실패하면 프로세스 수명 내내 8192에 고정되던 문제, 이제 재시도 가능
- /api/model-context: model 파라미터 없을 때(메인 채팅 사이드바 게이지)
  무조건 200000(클로드 가정) 반환하던 걸 resolveNumCtx() 재사용으로 교체
  — 이제 실제 활성 provider와 항상 동기화됨

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-26 17:17:14 +09:00
kimandClaude Sonnet 5 5f83803ed4 v4.3.19-4: Google(Gemini) LLM provider 추가
- LLMProvider.ts/factory.ts: 'google' ProviderID 추가, 기존 openai_compat
  어댑터를 Gemini의 OpenAI 호환 엔드포인트로 재사용(새 어댑터 클래스 없음).
  api_key는 resolveEnvKey가 아닌 getConfig().resolveSecret()로 해석 —
  vault: 참조를 실제로 못 푸는 기존 openai/anthropic 케이스의 버그를
  google 케이스에서는 피해감
- config.ts: llm.providers.google 기본값 추가(기본모델 gemini-3.6-flash),
  SECRET_FIELD_MAP에 api_key 자동 vault 이관 등록
- 설정 화면(index.html/settings-provider.js): LLM 제공자 드롭다운에
  Google 옵션 + API키/모델 선택 UI, anthropic 패턴 그대로 재사용
- openai-compat-adapter.ts: 기본 max_tokens 512→4000 — 사고형 모델
  (gemini-3.6-flash)이 내부 사고 토큰에 예산을 다 써서 빈 응답 반환하는
  버그 실측 후 수정(llama_cpp/lm_studio/openai/google 전부 공유하는 값)
- 5문항 벤치마크로 gemini-3.6-flash/3.5-flash-lite/3.1-flash-lite 비교,
  secondary 후보로 3.5-flash-lite 확정(정직함 동률 최상위+5배 빠름+저가)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-26 15:50:26 +09:00
kimandClaude Sonnet 5 f5ae095837 v4.3.19-3: 도구 스키마 컨텍스트 스코핑 + web-ui/js 서브디렉토리 정리
- handle-chat.ts: excel/coder+shell/email/kakao/sqlite_query/image_edit/
  image_read/run_command/news_search/pdf_*/schedule_job를 전용 앱 세션·
  키워드·첨부 신호로 스코핑 (기존 weather/legal/pptx/dental/mind 패턴과
  동일). 일반 대화 기준 도구 스키마 오버헤드 10,204→3,951 토큰(-61%)
- desktop_*(10개, Windows 전용) 완전 제외 — 이 리눅스 서버에서 전량
  실패 확인(tool_audit.log), process.platform 체크라 윈도우 이전시 자동 복구
- 채팅 기본 num_predict 4096→6000 (사고형 모델 빈 응답 여유분)
- ollama-adapter.ts: 턴마다 prompt/completion 토큰을 token-usage.jsonl에
  기록 (Ollama Cloud vs 종량제 API 비용 비교용)
- web-ui/js/ 25개 파일을 app/·code-editor/·voice/·music/·claude-app/
  서브디렉토리로 재배치, 전체 참조 경로 갱신
- package.json build 스크립트에 dist 클린 단계 추가

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-26 14:58:32 +09:00
kimandClaude Sonnet 5 0963231327 v4.3.19-2: 회계사 앱 스프레드시트 뷰어 수정 + helpers.js 5개 파일 분리
- coder_write_file이 xlsx/pptx/docx/pdf/zip 같은 바이너리 확장자로 파일을
  만들려 하면 차단 (텍스트 escape로 바이너리를 흉내내다 파일이 깨지던 문제)
- 레거시 HTML 리다이렉트(/xlsx-viewer.html 등)가 쿼리스트링을 버리던 버그 수정
- xlsx-viewer: "새 시트"가 존재하지 않는 __new__.xlsx를 fetch하다 404나던
  것을 빈 워크북 생성 후 저장 시 파일명 입력받는 흐름으로 교체
- xlsx-viewer: LuckyExcel 변환이 내놓는 손상된 숫자 서식("???.???")을
  Luckysheet SSF 파서가 못 읽고 죽던 문제에 sanitizer 적용
- 회계사 앱 업로드가 존재하지 않는 /api/upload를 호출하던 것을
  /api/upload/file로 수정
- web-ui/js/helpers.js(2240줄)를 helpers-core/markdown/voice/media/files.js
  5개로 분리 (기능 변경 없음, 함수 단위 1:1 이관 확인됨)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-25 21:30:05 +09:00
kimandClaude 4d0404ea0d v4.3.19-1: Helpers 섹션을 helpers.js로 분리 + escHtml 중복 제거
app.js 4728-6961(Helpers 섹션 전체)을 web-ui/js/helpers.js로 추출.
escHtml/_normalizeFileUrl/_renderLinkButton, renderMarkdown+embed 변환기,
TTS/voice(mic 포함), music/MIDI, smiles/tradingview, 파일 미리보기/업로드,
동적 로더(SheetJS/Luckysheet/PDF.js) 등 75개 함수 + midiRouter 객체.

모든 함수를 전역 function 선언으로 유지(모듈 래핑 안 함)해 inline onclick
문자열(downloadFile, openImageZoom, togglePdfEmbed, playTTSMsg, ...)이
그대로 동작. state vars(_xxxLoaded, stagedAttachments, _tts*, _appSendFn
등)도 함께 이동.

escHtml 중복(원래 3625, dead code — 5412 버전이 이김) 제거로 단일 정의.
escapeHtml(이메일 렌더링 전용, 다른 매핑)은 app.js에 유지.

index.html: voice-call → helpers → chat-sse → app 순서로 로드.
node --check 통과, 재시작 후 /js/helpers.js 200 확인.

다음(B2)에서 helpers.js를 5개 파일로 세분화 예정.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-25 20:07:32 +09:00