Commit Graph
540 Commits
Author SHA1 Message Date
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
kimandClaude Sonnet 5 797de9b03f fix: 스킬 컨텍스트가 direct 호출에서 버려지고 8000자에서 잘리던 문제
스킬 프롬프트 전면 점검 중 발견한 버그 2건.

1) callerContext가 'no_preflight' 제어 센티널·BOOT.md 마커·호출자 스킬 텍스트를
   한 필드에 섞어 쓰면서 삼항으로 골랐다 → direct:true와 skillContext를 같이 보내면
   스킬이 조용히 버려짐. pptx-wizard.js의 아웃라인 경로(line 798)가 정확히 그 조합이라
   프레젠터 스킬이 한 번도 전달되지 않았다. 둘 다 싣도록 변경.

2) 캡이 8,000자인데 위저드가 보내는 프레젠터 스킬은 9,861자 → 매 호출 뒤 1,861자 손실.
   잘려나간 것이 [필수] source_files 규칙과 **금지 사항 블록 전체**(파이썬으로 PPTX 직접
   생성 금지, 1회 호출 원칙, 수정은 edit_presentation, spawn_agent 금지)였다. 스킬에서
   가장 값어치 있는 제약들이 모델에 도달한 적이 없다. 캡 12,000으로 올리고 초과 시 경고 로그.

검증: direct 유무 양쪽 경로에서 금지사항·source_files 포함 확인.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XZX9JHFSLZRuK9rR4sCVBs
2026-09-02 15:02:13 +09:00
kimandClaude Sonnet 5 99d3d387bc fix: 법령 "100분의 N" 표기를 %와 동일하게 인정 (오탐 재측정 후속)
과거 NUMERIC-GROUNDING 발동 20건을 현재 코드로 리플레이한 결과:
- 날씨 4건(87%/55%/65%/70~90%, weather_openmeteo 표) → 전부 통과로 전환 ✅
- 진짜 날조 1건(weather_kma에 없는 "예보 불확실성 40%") → 여전히 발동 ✅
- 나머지 15건 중 7건이 하나의 연금 감액률 스레드였는데, 한국 법령·행정규칙은
  퍼센트를 "%"가 아니라 "100분의 N"으로 쓴다 → 답변 "50%"가 출처 "100분의 50"과
  대조되지 못해 매번 오탐. unitVariants의 % 목록에 "100분의" 추가.

출처에 없는 비율은 여전히 발동함을 테스트로 고정. tests +2, 463개 전부 통과.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XZX9JHFSLZRuK9rR4sCVBs
2026-09-02 14:42:22 +09:00
kimandClaude Sonnet 5 58f77152a8 perf: 프롬프트/도구 스키마 고정비 12% 절감 (효율 감사 ①②③)
실측: LLM 호출 3,409회 중 47%가 출력 60토큰 미만(도구 라운드/가드 재프롬프트)이고
매 라운드가 프리픽스를 통째로 재전송 → 고정비 절감이 라운드 수만큼 곱해짐.

① system-prompt: TEMPORAL CONSISTENCY(2,463자)와 OUTPUT FORMAT(1,460자)이 무조건
   블록이었는데 문장 대부분이 "검색 결과를 어떻게 읽고 표시할지"라 그라운딩 도구가
   없는 턴엔 사문. searchReadingRule / newsFormatRule / weatherFormatRule로 분리해
   도구 목록으로 게이팅. 날짜=ground truth와 "3개 이상이면 표"는 무조건 유지.
② tool-scope: ERA5/CDS/CMIP6/NASA POWER는 기후 재분석·시나리오 아카이브인데 평범한
   날씨 키워드에 실려 "오늘 날씨"마다 820토큰 낭비 → climateToolNames + 기후 키워드
   게이트 신설. 일상 예보(kma/openmeteo/search/airkorea)는 그대로.
③ news_search query 파라미터 설명 1,900자→640자. 사고 경위 서술을 규칙만 남기고 압축
   (경위는 git 이력과 project_search_query_routing에). 스키마 2,978→2,089자.
④ 죽은 가드 7종은 제거하지 않음 — browser/desktop/messaging은 의도적 휴면이고
   EMPTY-GROUNDING은 유닛테스트로 정상 동작 확인(검색이 늘 뭔가 반환해서 드물 뿐).

절감 실측(스키마+프롬프트 합):
  인사/일반 3,213→2,847 (-11%) | 날씨 5,254→4,279 (-19%) | 뉴스 4,174→3,594 (-14%)
  전체 평균 -12%

tests +2 (게이팅 계약을 테스트로 고정), 461개 전부 통과.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XZX9JHFSLZRuK9rR4sCVBs
2026-09-02 14:36:33 +09:00
kimandClaude Sonnet 5 885bab088b fix: NUMERIC-GROUNDING 가드 전면 점검 — 철자단위·표형출처·숫자경계
프롬프트 가드 전체 감사 중 numeric-grounding에서 오탐 3종 발견·수정:

1. 철자 단위 불일치: 출처가 "25 percent"/"24 gigabytes"인데 답변이 "25%"/"24GB"면
   근접매칭이 실패 → 오탐. unitVariants()로 %↔percent↔퍼센트, GB↔gigabyte↔기가바이트,
   토큰↔tok, tok/s↔tps↔초당 등 동의 표기 허용.
2. 표형 출처(어제 커밋의 weather 범례) 보강 + 정직한 주석: 큰 표는 사실상
   fabrication 체크 면제됨(실패모드는 wrong-row인데 그건 원래 못 잡음).
3. 숫자 부분일치: "5"가 "157.66" 안에서 매칭돼 날조를 가려주던 문제 →
   숫자 매칭에 경계 적용 (앞: 숫자/점 금지, 뒤: 숫자 금지·점은 허용해 반올림 수용).

감사 결과 나머지 가드(AUTO-RECOVER, EMPTY/SEARCH-ABANDONMENT, MESSAGING,
INTENT-ONLY, 재시도예산)는 정상. tests +2, 전체 459 통과.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XZX9JHFSLZRuK9rR4sCVBs
2026-09-02 14:21:29 +09:00
kimandClaude Sonnet 5 61cfb180f1 fix: 표형 도구출력에서 NUMERIC-GROUNDING 오탐 → gemma "죄송합니다" 루프
weather_openmeteo 등은 단위를 헤더 범례에만 쓰고("precipitation_probability=%")
값은 열에 맨숫자로 넣는다. checkNumericGrounding의 근접매칭(40자)이 "55"와 "%"를
멀다고 판단 → 정확한 강수확률·습도 답변마다 NUMERIC-GROUNDING POST-CHECK 재프롬프트가
걸렸고, 재프롬프트 지시문이 "죄송합니다, 임의의 수치를 사용했습니다"류 답변을 유도했다.
사용자 신고: "메인챗에서 젬마가 찾은 결과 안 보여주고 죄송합니다만 함".

- numeric-grounding.ts: hasUnitLegend() 추가 — 출처가 "<name>=<unit>" 또는 "단위:" 범례를
  쓰는 표면, 근접매칭 실패 시 "숫자가 독립 토큰으로 출처에 존재"만 확인(열 값 매칭).
  표에 아예 없는 수치는 여전히 잡힘.
- handle-chat.ts: NUMERIC-GROUNDING 재프롬프트에 "죄송합니다로 시작하지 말고 결과에
  뭐가 있었는지 명시하라" 추가.
- tests +2. 전체 458 통과.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XZX9JHFSLZRuK9rR4sCVBs
2026-09-02 13:48:40 +09:00
kimandClaude Sonnet 5 88ba78c537 fix: 메인챗 두 가지 — 검색어 세대 추측 + "잠시만 기다려" 후 정지
2026-09-01 맥미니 M5/M6 세션 사후분석에서 나온 두 버그.

1. 세대 추측 검색어 오염: 사용자가 "맥미니 신형"(칩 미지정)이라 물었는데
   gemma4가 학습시점 지식으로 web_search("M4 Mac mini …")를 던졌고, SearXNG는
   시킨 대로 M4(2024) 기사를 정확히 반환 → 답 전체가 구세대 기준으로 틀어짐.
   - system-prompt TEMPORAL CONSISTENCY에 "신형/최신인데 세대 미지정이면
     쿼리에 기억 속 세대를 넣지 말고 현재 연도로 검색" 규칙
   - handle-chat: hasUnversionedNewestIntent + assumedGenerationTokenInQuery로
     감지해 1회 재프롬프트(오염된 검색 실행 전 차단)

2. "잠시만 기다려 주세요" 후 턴 종료: 사용자가 오류 지적하자 모델이
   "다시 확인해 보겠습니다. 잠시만 기다려 주세요."만 내고 도구 0개로 정지.
   isIntentOnlyReply는 답 속 "2026년 9월"을 실질내용으로 오인, lastRoundWas
   GuardReprompt도 false라 기존 넛지 미발동.
   - isDeferralPromiseReply 신규(좁은 "wait for me" 패턴), 가드 재프롬프트
     선행 없이도 1회 강제 continuation

tests/intent-only-reply.test.ts +12 케이스. 전체 456 통과.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XZX9JHFSLZRuK9rR4sCVBs
2026-09-01 17:29:19 +09:00
kimandClaude Sonnet 5 82ca2999fc feat: cms_hospital_compare — CMS(메디케어) 병원 성과 데이터 조회/비교 도구
data.cms.gov "Hospital General Information" 공개 데이터셋(키 불필요)에서
소유형태(영리/비영리/정부)·CMS 종합별점(1~5)·사망률/안전/재입원 측정치의
전국평균 대비 better/worse 개수를 조회. compare=true면 소유형태별로 집계 —
"영리 vs 비영리 병원 성과" 질문에 근거 데이터 제공.

- registry.ts / build-tools.ts 양쪽에 등록
- tool-scope.ts: 병원 데이터 키워드 게이트 추가(상시 로드 안 함)
- retry-decisions.ts: GROUNDING_TOOL_PATTERN에 추가(검색 강제 재시도 방지)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XZX9JHFSLZRuK9rR4sCVBs
2026-09-01 11:01:18 +09:00
kimandClaude Sonnet 5 434dadf038 feat: 논문검색 소스 박스에 주제별 라우팅 안내 추가
각 소스 설명을 "적합 주제" 중심으로 재작성하고, 임상=PubMed /
보건정책·경제=OpenAlex+Semantic / 자연어문장=Semantic / vs 빼기
힌트 줄을 추가. ptitle "PubMed 논문 검색" → "논문 검색".

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XZX9JHFSLZRuK9rR4sCVBs
2026-09-01 10:30:41 +09:00
kimandClaude Sonnet 5 ec2dac9e1e fix: PubMed 검색어의 단독 vs/versus 연결어 제거
"For-profit vs Non-profit hospital performance" 같은 자연어 검색어를
PubMed에 그대로 넘기면 자동 용어매핑이 "vs"[All Fields]를 AND로 강제해
제목에 "A versus B"가 든 임상 RCT를 대거 끌어왔다. 필드태그/따옴표가
없는 평문 검색어일 때만 vs/versus를 공백으로 치환한다.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XZX9JHFSLZRuK9rR4sCVBs
2026-09-01 10:26:53 +09:00
kimandClaude Sonnet 5 9e0d047eb9 feat: 작가 앱에 AI 원고 작성(compose) 기능 — 이어쓰기/새 장면/선택부분 다시쓰기
검토만 하던 작가 앱에 실제 원고 문장을 써주는 기능 추가.

- 원고 뷰 하단에 프롬프트 입력창 + [이어쓰기][새 장면][선택부분 다시쓰기]
- POST /api/writer/compose: 장르·요소·설정노트를 프롬프트에 실어 프로즈 생성
  (temp 0.85로 검토보다 창작적, "알겠습니다" 머리말 없이 본문만)
- 생성물은 "AI 초안" 미리보기 → [원고에 넣기]/[다시]/[버리기]. 이어쓰기·새 장면은
  원고 끝에 append, 다시쓰기는 선택 범위를 치환
- 요소가 자동 반영됨을 알리는 힌트("반영 중인 요소: 주제, 문체, 배경 …")

브라우저에서 새 장면·이어쓰기·삽입까지 재현·검증 완료.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UBDYbeeazVwgQ7RruCXRgY
2026-08-31 17:49:35 +09:00
kimandClaude Sonnet 5 d47d2ef138 feat: 작가 앱 요소를 AI 즉시호출 버튼 → 작가가 채우는 설정칸 + 전체 검토로 재설계
기존: 요소(주제/문체/구성/배경/인물) = 누르면 바로 AI 비평이 도는 전문가 7명.
요소를 입력할 자리가 없어 "요소 다 채우고 검토받기"라는 자연스러운 흐름이 불가능.

- 요소 = 작품에 저장되는 입력칸 5개 (works.json에 elements 객체, 옛 작품은 읽을 때 정규화)
- 원고 ↔ 요소 뷰 토글, 사이드바 요소 버튼은 해당 입력칸으로 이동
- "📋 전체 검토": expertType='review' — 총괄 편집자가 요소별 의도 vs 원고 실행의
  간극·강점·우선순위를 종합 분석
- 개별 "💬 이 요소 검토": 그 요소의 작가 의도를 프롬프트에 주입해 의도-실행 대조
- saveWork/onWorkSelect가 elements를 함께 저장·복원

브라우저에서 전체 흐름 재현·검증 완료.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UBDYbeeazVwgQ7RruCXRgY
2026-08-31 17:37:26 +09:00
kimandClaude Sonnet 5 940f78199e fix: 작가 앱 전문가 전환 시 앞선 요청 응답이 새 전문가 대화창에 들어가던 문제
주제→문체 등으로 빠르게 전환하면 selectExpert가 sendChat(true)를 부르는데
chatBusy 가드에 걸려 새 전문가 요청은 안 나가고, 먼저 나간 요청 결과가
돌아올 때 activeExpert가 바뀐 걸 확인 안 해서 새 전문가 대화창에 그대로
push+render 됐다("모델이 가로챔").

AbortController + 세대 카운터(chatGen)로 수정: 전문가 전환·새 메시지마다
진행 중 요청을 abort하고 세대를 올리며, 응답이 돌아오면 자기 세대가
아직 최신인지 확인 후에만 반영. 늦게 끝난 구세대 요청은 UI를 안 건드림.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UBDYbeeazVwgQ7RruCXRgY
2026-08-31 17:27:51 +09:00
kimandClaude Sonnet 5 493a37eb9e feat: 위치/장소 질문에 구글맵 인라인 임베드 유도하는 MAPS 시스템프롬프트 블록
markdown.js는 이미 독립된 줄의 google.com/maps URL을 keyless 임베드
iframe으로 렌더하고 handle-chat.ts는 그 URL의 browser_open을 가로챈다.
빠져 있던 건 "언제 그 URL을 내놓을지" 모델에게 알려주는 지시뿐이었음.

CHEMISTRY NOTATION과 같은 메시지 게이팅 예외로 추가 — 출력 형식 제약이라
키로 삼을 도구가 없고, 키워드를 놓쳐도 대가가 지도 하나 안 뜨는 것뿐.
/maps/search·/maps/place 형식만 유도(마크다운 변환기가 /maps/dir 미지원).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UBDYbeeazVwgQ7RruCXRgY
2026-08-31 11:31:21 +09:00
kimandClaude Sonnet 5 63bbc36c6e fix: 임베딩 모델 콜드로드로 벡터메모리 회상이 타임아웃되던 문제
Ollama 재시작(예: 버전 업그레이드) 후 embeddinggemma가 언로드되면
다음 채팅의 buildPersonalityContext에서 콜드 로드(~30s)가
15초 타임아웃을 넘겨 "vector memory query failed ... aborted due to
timeout" 경고가 났다. 답변엔 영향 없지만 회상이 빠지고 첫 응답이
15초 지연됐다.

- EMBED_KEEP_ALIVE '24h' → -1 (영구 상주). embeddinggemma가 유일한
  로컬 모델이고 ~0.6GB라 안전. ollama.service의 OLLAMA_KEEP_ALIVE=-1
  drop-in과 이중 방어.
- warmupEmbedding() 추가 — 게이트웨이 부팅 시 백그라운드 예열해서
  첫 채팅이 콜드 로드를 안 기다리게.
- embedText에 timeoutMs 파라미터 추가(기본 15s 유지, 워밍업만 60s).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gh45CPB94UFQFiHov2CQe7
2026-08-27 13:11:44 +09:00
kimandClaude Sonnet 5 38320a9162 fix: 업로드 파일 도구 전반에서 /api/files/ 채팅링크를 경로로 착각하는 문제
pdf_read에서 발견한 것과 동일한 패턴이 6개 파일 9곳에 더 있었음 —
path.isAbsolute()가 /api/files/... URL을 절대경로로 오판해 워크스페이스
상대경로 변환을 건너뛰고 실패함(있으면 "escapes workspace", 없으면
"File not found"). 각 execute() 진입점에서 /api/files/ 접두사를
제거하고, LLM에 보내는 도구 설명에도 부정 예시를 명시함.

- files.ts: resolveWorkspacePath() 헬퍼 하나 수정으로 coder_* 도구
  13곳 전체 커버
- audio-transcribe.ts, excel.ts(read/write), image.ts(read/preview/
  info/edit), imagegen.ts(style_transform/video_generate 소스 이미지)
- pdf-extract.ts: pdf_extract_images/pdf_extract_tables도 같은 방식
  으로 재정리(설명 문구 통일)

실제 업로드 파일로 /api/files/ 경로를 넘겨 재현 테스트, 정상 동작 확인.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SUjjAwB2Cmjf4YFRZpobec
2026-08-25 11:33:10 +09:00
kimandClaude Sonnet 5 c67a23f739 fix: pdf_read가 /api/files/ 채팅링크를 경로로 착각해 실패하던 문제
모델이 업로드 링크 URL을 그대로 path로 넘기면 path.isAbsolute()가
true로 판정돼 워크스페이스 상대경로 변환을 안 타고 "path escapes
workspace"로 실패했음. 도구 설명에 부정 예시 명시 + execute()에서
/api/files/ 접두사 자동 제거로 이중 보강.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SUjjAwB2Cmjf4YFRZpobec
2026-08-25 11:16:53 +09:00
kimandClaude Opus 5 7ac143652a fix: 논문 도구가 발표자료 키워드에만 묶여 있어 일반 채팅에서 안 켜지던 문제
pubmed_search·pubmed_fetch·pubmed_fulltext·openalex_search·semantic_search
다섯 개가 hasPptxKeyword에만 걸려 있었다. "논문 찾아줘"로는 스키마가 아예
안 실려서 모델이 web_search로 때웠다 — 실사용 로그에서 확인했다.

학술 의도 자체를 게이트로 추가했다. 학술 검색은 web_search보다 확실히 낫다:
openalex_search는 키 없이 2억 건에서 제목·저자·연도·저널·DOI·인용수·오픈액세스
여부를 구조화해 준다.

정규식에서 한 번 걸렸다 — `\bpaper\b`가 복수형 "papers"를 못 잡는다(뒤의 \b가
s 앞에서 끊긴다). papers?·citations?·journals?로 고쳤다. 한글 뒤에는 \b를 쓰지
않는다(ASCII 경계라 절대 매칭 안 됨 — 같은 파일의 hasEmailKeyword 주석 참고).

기존 pptx 경로는 그대로 열려 있다(논문 수집 → create_presentation).

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