Commit Graph
530 Commits
Author SHA1 Message Date
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
kimandClaude Opus 5 1811f62a18 feat: 법률앱 검색 UI — 판례 기본 + 결과 내 좁히기 + 결정례 드롭다운
- 판례 탭을 맨 왼쪽으로 옮기고 기본 검색 대상으로 바꿨다. 법률앱에서 가장
  자주 쓰는 게 판례다.
- 결과 목록 위에 좁히기 입력칸. 사건명·법원·사건번호에서 찾는다. 서버를 다시
  부르지 않고 이미 받아온 결과를 숨김/표시로 거르므로 즉시 반응하고 law.go.kr
  쿼터도 안 쓴다. 새 검색이 오면 조건을 비운다(안 그러면 결과가 있는데 빈
  화면으로 보인다). 1건이면 좁힐 게 없으므로 입력칸을 숨긴다.
- 결정례 4종(헌재·법령해석·행정심판·심판)은 드롭다운으로 묶었다. 탭이 8개가
  되면 좁은 화면에서 줄바꿈이 생긴다. 드롭다운 항목을 고르면 탭 하이라이트를
  끄고 드롭다운이 현재 선택을 나타낸다.
- 키가 없어 검색 자체가 불가능한 경우(미국 판례)를 "결과 없음"과 구분해
  안내를 띄운다. 예전엔 "다른 키워드로 검색해보세요"가 떠서 헛되이 재시도했다.

부트스트랩에서 setSearchType()을 부른다. 이 함수는 탭 클릭 때만 돌아서 최초
로드에는 안 걸렸고, 그래서 기본값이 판례인데 placeholder는 법령용이었다 —
📁 버튼이 안 보이던 것과 같은 종류의 누락이라 이번엔 같이 잡았다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 10:19:50 +09:00
kimandClaude Opus 5 f79ba166e4 feat: 법령 검색에 결정례 4종 추가 + 자치법규·미국판례·판례상세 수정
키를 꽂고 나니 여러 문제가 한꺼번에 드러났다.

1) 자치법규가 늘 실패했다. target 코드가 `ordinance`가 아니라 `ordin`이다.
   틀린 코드를 보내면 에러가 아니라 **빈 응답(길이 0)** 이 와서 JSON 파싱
   단계에서 "Unexpected end of JSON input"으로 터진다 — 원인이 target이라는
   게 전혀 안 드러난다. 별칭 정규화를 넣고, 빈 본문은 명확한 메시지로 갈랐다.

2) 미국 판례(CourtListener)는 인증 헤더를 아예 안 보내고 있었다. 예전엔
   무인증으로 열렸지만 지금은 403이다. legal.courtlistener_api_key 지원을
   넣고, 키가 없으면 날것의 403 대신 발급 안내와 직접 검색 URL을 준다
   (korean_law_search의 키 미설정 경로와 같은 모양).

3) 판례 상세가 부실해 보였던 이유:
   - 값이 없는 항목도 "### 판시사항" 헤딩이 찍혀서 자료가 빈약해 보였다
   - 참조판례를 통째로 버리고 있었다
   - 전문이 4000자에서 조용히 잘렸다 → 8000자로 늘리고 잘릴 때 밝힌다
   - 선고일자가 `19860701` 원본 그대로였다 → 1986.07.01
   구조화 데이터도 같이 반환한다(예전엔 case_name 하나뿐이었다).

4) 성격이 다른 DB 4종을 추가했다: 헌재결정례(detc)·법령해석례(expc)·
   행정심판례(decc)·심판례(ppc). law.go.kr은 **대상마다 응답 구조가 전부
   다르다** — 래퍼 이름, 항목 배열 키, 필드명까지. 하나라도 어긋나면 에러가
   아니라 0건으로 조용히 나오므로, 실제 응답을 찍어 확인한 값을 TARGET_SPECS
   표 하나에 모았다. 다음 대상은 그 표만 채우면 된다.

SECRET_FIELD_MAP에 legal 키 둘을 추가해 평문이 들어와도 vault로 이관되게 했다.

검증: 판례 8,748 / 법령 13 / 자치법규 884(주차장) / 헌재 19 / 해석례 13 /
행정심판 13 / 심판례 6 — 전부 정상 응답.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 10:19:33 +09:00
kimandClaude Opus 5 38695fb4fb fix: 판례 ID 필드명 수정 + legal 키를 vault 이관 대상에 추가
law.go.kr API 키(OC 값 `ailawyer`)를 vault에 넣고 나니 드러난 버그.

  코드   item.판례정보일련번호
  API    판례일련번호            ← 실제 필드명

ID가 undefined로 나와서 korean_law_fetch로 본문을 가져올 수 없었다. 검색
결과는 멀쩡히 보이고 상세만 안 되는 모양이라 눈에 잘 안 띈다. 옛 이름도
폴백으로 남겨 응답 스키마가 바뀌었을 때를 대비했다.

SECRET_FIELD_MAP에 legal.law_go_kr_api_key가 빠져 있어서 평문으로 들어와도
vault로 자동 이관되지 않았다. 추가했다 — config.json에는 vault: 참조만 남는다.

확인: 판례 검색 17건 → 본문 조회에서 판시사항·판결요지·참조조문 정상 수신.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 11:54:25 +09:00
kimandClaude Opus 5 4ac9919449 feat: 날씨에 도로 안개 추가 (CCTV 기반 도로날씨) — 안개 있을 때만 표시
안개는 ASOS·AWS·Open-Meteo 어디에도 없는 값이라 도로 CCTV 영상 분석이 유일한
소스다. 전국 593개 도로 지점에서 안개/비/눈 코드가 온다.

**앞선 판단 정정**: 08-15~18에 이 API가 0행이라 서비스 종료를 의심했는데,
소급 반영이 늦었던 것이었다. 지금 조회하면 08/17이 83,478행으로 채워져 있고
08/14도 57,247 → 67,937행으로 늘었다. "지금 0행"으로 종료를 단정하면 안 된다.

안개가 있을 때만 한 줄 붙인다 — 매번 "안개 없음"을 쓰면 소음이 된다:
  도로 안개: 보통 (CCTV 양재 관측, 13km · 11:20 기준)

어느 지점에서 몇 km 떨어진 값인지 밝혀 대표성 판단은 사용자에게 남긴다.
30km 밖이면 표시하지 않는다(도로변에만 있어 성기게 깔려 있다).

성능: 처음엔 응답이 7초 → 11초로 느려졌다. 조회 창을 90분에서 30분으로 줄이고
(지점 커버리지 584/593로 사실상 동일, 591KB → 87KB) 전국 한 벌을 5분 캐시해
구미 기준 7.1s → 2.7s로 되돌렸다.

같은 지점이 몇 분 간격으로 반복 보고되므로 지점별 최신 행만 남긴다.
야간에는 영상 분석이 없어 데이터가 안 나온다 — 안개 줄이 없는 게 정상이다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 11:54:10 +09:00
kimandClaude Opus 5 a9040b7222 feat: 법률앱 — 일반 대화를 사건으로 전환
상담하듯 일반 대화로 물어보다가 사건으로 승격하고 싶을 때 쓴다. 채팅 헤더의
📁 버튼(사건 미선택일 때만 노출).

요약만 옮기면 사건 안에서 이어 물을 때 맥락이 끊기므로 **대화 세션 파일 자체를**
사건 세션(`lw_case_<id>`)으로 옮긴다. 그래서 서버에 세션 이동 API를 새로 만들었다
(POST /api/chat/sessions/:id/move). 파일명으로 그대로 쓰이므로 세션 ID는
[A-Za-z0-9_-]만 허용하고, 목적지가 이미 있으면 409로 거부한다 — 덮어쓰면 다른
사건의 대화가 날아간다.

실패 처리는 되돌릴 수 없는 쪽을 기준으로 잡았다:
 - 제목/개요 추출이 실패해도 진행한다. 첫 질문을 제목으로 쓴다. 이름은 나중에
   고치면 되지만 대화는 못 되찾는다.
 - 세션 이동이 실패하면 사건을 만들지 않는다. 사건만 생기고 대화가 안 따라오면
   일반창에 그대로 남아 중복이 된다.

버튼 표시 토글을 updateToCaseBtn()으로 빼서 부트스트랩에서도 부른다.
처음엔 ensureChatContextForCase 안에만 넣었는데, 그 함수는 모드 전환·사건
선택·대화 지우기에서만 돌아서 최초 로드에는 안 걸렸다 — 버튼이 계속 숨어 있었다.

가드 검증: 없는 세션 404 / 경로조작 from·to 400 / 인증 없음 401.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 11:53:51 +09:00
kimandClaude Opus 5 d60019f004 fix: 앱 페이지 7개에 수식 렌더러 배선 — 애초에 KaTeX가 없었다
법률앱에서 `$\rightarrow$`가 날것으로 보인다는 신고. 통화 `$` 문제(f06955f)를
먼저 의심했지만 원인은 더 단순했다 — **앱 페이지들은 KaTeX를 아예 로드하지
않고 있었다.**

  index.html        katex O
  lawyer / detective / doctor / investor / accountant / mind / weather   전부 X

메인 채팅에만 chat-render.js의 renderKatexIn이 있었고, 앱 7개는 각자 자기
renderMd를 갖고 있어 수식이 통과할 길이 없었다. 공용 js/app/katex-inline.js로
빼서 전부에 붙였다(통화 `$` 보호 로직 포함).

addMsg에서 DOM 삽입 직후 호출한다. innerHTML은 <script>를 실행하지 않으므로
삽입 시점마다 명시적으로 불러야 한다. TTS 버튼이 innerText를 읽어가므로
그보다 먼저 렌더링되게 배치했다 — 안 그러면 음성이 "달러 백슬래시 rightarrow
달러"를 읽는다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 11:53:35 +09:00
kimandClaude Opus 5 f06955f967 fix: 금액의 $가 수식 여는 기호로 먹혀 뒤의 수식이 깨지던 문제
KaTeX auto-render는 `$`를 만나면 무조건 인라인 수식의 시작으로 보고 다음
`$`까지를 짝으로 묶는다. 그래서 답변에 금액이 하나라도 섞이면 그 뒤의
진짜 수식이 통째로 어긋난다.

브라우저에서 재현 확인:
  "감액 비율 50% $\rightarrow$ 10~20%"           → 화살표 정상
  "월 $100 절감 … 50% $\rightarrow$ 10~20%"      → "100 절감. 감액 비율 50"이
                                                   수식으로 잡히고 뒤는 날것

숫자가 바로 뒤따르는 `$`만 렌더링 직전에 사설영역 문자로 감췄다가 끝난 뒤
되돌린다. 수식이라면 `$\ce{...}$`, `$\rightarrow$`처럼 백슬래시나 문자로
시작하므로 숫자가 오는 건 사실상 전부 통화다. 렌더링이 실패해도 finally에서
복구한다 — 안 그러면 화면에 빈 네모가 남는다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 11:53:21 +09:00
kimandClaude Opus 5 4ea87f1f7a feat: 파이썬 삼중따옴표 안의 HTML을 HTML로 하이라이팅
sprinkler_backend.py의 `DASHBOARD_HTML = """<!DOCTYPE html>…"""` 174줄이
통째로 단색으로 보였다. Monaco의 파이썬 문법(Monarch)이 삼중따옴표 내부를
`string` 토큰 하나로 처리하기 때문이고, VS Code의 TextMate 문법과 달리
Monarch에는 언어 주입이 기본으로 없다.

다만 Monarch에도 nextEmbedded가 있어서 구간을 다른 언어 토크나이저에
넘길 수 있다(Monaco 자체 HTML 문법이 <script>/<style>을 이렇게 처리한다).
그걸 파이썬에 얹었다. HTML로 넘기면 그 안의 <style>/<script>까지 연쇄로
살아난다.

판별: 삼중따옴표 뒤 첫 비어있지 않은 내용이 `<`로 시작하면 HTML로 본다.
Monarch는 줄 단위라 여는 줄에서 바로 판단할 수 없어 대기 상태를 하나 뒀다.
SQL 독스트링(conn.execute("""CREATE TABLE…))과 산문 독스트링은 `<`로
시작하지 않으므로 종전대로 문자열 색을 유지한다. 빗나가도 색만 이상해질 뿐
편집·저장에는 영향이 없다.

구현 중 밟은 것 둘:
 - 빈 줄에서 `/^\s*$/`는 폭 0으로 매칭돼 같은 상태에 머물며 무한 루프가
   된다(Monarch가 "진행 없음"으로 예외를 던진다). `/\s+$/`로 최소 한 글자를
   요구하면 빈 줄은 아예 매칭되지 않고 다음 줄로 넘어가며 상태만 유지된다.
 - HTML 언어를 미리 로드해야 한다. Monaco는 언어를 지연 로딩해서, 파이썬
   파일만 열면 HTML 토크나이저가 없어 임베딩이 조용히 실패한다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 15:19:14 +09:00
kimandClaude Opus 5 ae4439efba fix: 가드 재프롬프트에 예고만 하고 끝나는 답변을 한 번 밀어준다
"계산하다가 멈춘다"는 신고. 생성이 끊긴 게 아니었다 — 로그에는 답변이
끝까지 있었고, 그 답변의 내용이 예고문뿐이었다.

  USER: 우크라이나 전쟁 미사일 총 CO2?
  1. AUTO-RECOVER (1/2)        검증 안 된 수치 1070자 → 재프롬프트
  2. web_search 호출 → 결과 받음
  3. NUMERIC-GROUNDING (1/1)   "존재하지 않는다"가 검색결과에 없음 → 재프롬프트
  4. 모델: "죄송합니다… 다시 정밀하게 검색해 보겠습니다."  (도구 호출 없음)

3번 재프롬프트는 "다시 검색해서 그 결과로 답하라"고 지시했는데 모델은
예고만 했다. (1/1)이라 재시도 예산이 바닥이어서 그 예고문이 그대로 최종
답변으로 나갔다. 사용자 화면에는 계산하다 멈춘 것으로 보인다.

이걸 잡는 가드는 있었지만 상수 false로 꺼져 있었다:
  // DISABLED: Force continuation was causing excessive unnecessary tool calls
너무 광범위해서 멀쩡한 답변까지 재시도로 끌고 갔던 것이다.

그래서 **가드 재프롬프트 직후로만** 좁혀 되살렸다. 그 상황은 "실행하라고
지시했는데 예고만 함"이라 오탐 여지가 거의 없고, 평상시 답변은 건드리지
않는다. 한 턴에 한 번만 개입한다.

isIntentOnlyReply 판정도 좁다 — 400자 이내 + 미래형 예고 + 실질 내용 없음.
숫자·목록·표·코드·링크가 하나라도 있으면 실질 답변으로 본다:
  "계산해 보겠습니다. 총 배출량은 50,000톤입니다." → 통과
  "정리해 드리겠습니다.\n- 첫째\n- 둘째"          → 통과
  "다시 정밀하게 검색해 보겠습니다."               → 걸림

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 15:17:00 +09:00
kimandClaude Opus 5 1f4c0ae0ee fix: 코드앱 전체 점검 — 조용한 실패 3건 + 이스케이프 2층 누락
전체 점검에서 나온 것들. 앞선 커밋들과 달리 서버 모드 전용 문제가 아니라
로컬 모드에도 있던 버그다.

1) Write/Edit 툴이 쓰기 결과를 확인하지 않고 "저장 완료"를 돌려줬다.
   실패해도 모델은 파일이 생긴 줄 알고 다음 단계로 넘어간다 — 디스크에
   없는 파일을 Edit하려다 엉키거나, 다 끝났다고 보고해버린다.

2) 이름 변경이 파일을 잃을 수 있었다. 복사 후 삭제 구조인데 복사 성공을
   안 보고 원본을 지웠다. 새 경로에 못 썼는데 원본이 사라지면 파일이
   없어진다. 디렉토리 쪽은 삭제가 재귀라 더 나빴다. 이제 전부 옮겨진 게
   확인돼야만 원본을 지운다.

3) deleteActiveProject가 localStorage를 먼저 지우고 폴더 삭제를 나중에
   했다. 삭제가 실패하면 파일은 디스크에 남았는데 앱에서는 접근할 길이
   없는 유령 폴더가 된다. 순서를 뒤집어 성공했을 때만 정리한다.

4) `onclick="fn('${...}')"`은 HTML 속성과 JS 문자열 **두 층**을 순서대로
   통과하는데(브라우저가 엔티티를 먼저 풀고 그 결과를 JS가 읽는다),
   기존 헬퍼 둘이 각각 반쪽이었다:
     _csbEsc → `'`를 `&#39;`로 바꾸지만 HTML이 도로 `'`로 풀어 JS 문자열이 끊김
     safe    → `\'`로 JS는 막지만 `"`가 그대로라 속성이 조기 종료
   escAttrJs()로 JS 이스케이프 → HTML 이스케이프 순서를 지킨다. 따옴표가
   든 파일명(it's.py, say"hi".py)은 리눅스에서 합법이라 실제로 만들어진다.
   5개 케이스가 두 층을 통과해 원본으로 복원되는 것과, 서버 쪽 쓰기·목록·
   읽기가 정상인 것을 확인했다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 11:45:13 +09:00