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>
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>
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>
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>
특정 도구를 제약하려고 존재하는 블록들이 도구 유무와 무관하게 매 턴 무조건
주입되고 있었음. 기존 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>
모델이 "클로서버.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>
그동안 쌓여 있던 미커밋 파일들을 정리. 소스·자산만 올리고, 개인 데이터와
런타임 상태는 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>
실사용 로그 감사(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>
기존 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>
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>
멀티유저 전환 후 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>
파일별 다운로드(download 속성), 폴더 전체 zip 다운로드(archiver 신규
의존성 + /api/detective/files/download-folder 라우트) 추가. 사이드바에
법률 서식(대한법률구조공단)·법원 소송 서식(전자소송포털)·대법원 판례
검색(사법정보공개포털)·경찰청 링크 블록 신설.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
hwp에서 가져온 odt 사본을 다시 편집하려 해도 버튼이 없었음 — 이제
.odt 파일도 hwp와 동일하게 🌐(NextCloud에서 열기)/⬇️(가져오기+PDF
변환) 대상이 됨. 백엔드는 이미 비-hwp 파일을 변환 없이 그대로
업로드/처리하도록 되어 있어서 프론트엔드 조건만 확장하면 됐음.
h2orestart는 hwp 내보내기(쓰기)를 지원 안 해서 편집본을 진짜 hwp로
되돌릴 방법이 없음(리눅스에서 근본적으로 불가) — 대신 odt→PDF는
특별한 필터 없이 표준 변환이라 이걸로 실질적 요구(제출용 PDF)를 충족.
⬇️ 가져오기 시 odt와 함께 PDF도 자동 생성. 파일명은 "{이름}
(NextCloud변환).pdf"로 접미사를 붙여서, 같은 폴더에 이미 있는 원본
증거자료 PDF(예: 고소장.pdf)를 덮어쓰지 않도록 함 — 실제로 이 충돌
가능성을 발견하고 배포 직전에 수정함.
주요 변경:
- 탐정 앱의 .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)도
같이 반영됨.
원인: /pdf-editor-open은 caseId만 전달하고 파일명은 전혀 안 넘김 —
Stirling-PDF(외부 PDF 편집기)의 실제 프론트엔드 번들을 직접 열어봤는데
"token" 쿼리파라미터 하나만 읽고 원격 파일 로드 기능 자체가 없음(자체
storage API는 있지만 별도 로그인 필요, 우리는 미설정). 즉 특정 파일을
자동으로 여는 건 우리 쪽에서 고칠 수 있는 버그가 아니라 외부 툴의
한계임을 확인.
대신 ✏️ 클릭 시 편집기 새창 열기와 동시에 그 파일을 다운로드시켜서,
사용자가 편집기 창에 끌어다 놓기만 하면 되도록 마찰을 최소화. 툴팁도
실제 동작을 정확히 설명하도록 수정.
실사용 중 발견: 모델이 satellite_snapshot 결과의 /api/files/... 마크다운을
답변에 그대로 복사하지 않고 재작성하면서 /api/files/ 접두사를 빠뜨림 —
그러면 클라이언트 정규식이 매치 안 돼서 사진탭이 아예 안 열림.
weather_map_screenshot과 같은 패턴으로, 도구 결과의 마크다운을 그대로
SSE로 주입해서 모델의 재현 정확도에 의존하지 않게 함. 태풍 돌핀 실제
NASA 위성사진으로 재현·검증 완료.
실제 테스트에서 발견: "지도 이동해줘" 후속 요청에 모델(deepseek-v4-flash)이
도구를 재호출하지 않고 이전 응답을 그대로 반복하는 경우가 많아 프롬프트
가드만으로는 신뢰도가 부족했음. 근본 해결로 사진 뷰어에 "지도에서 위치
보기" 버튼 추가 — satellite_snapshot 파일명에 이미 박혀있는 좌표를
그대로 파싱해 지도를 이동시키므로 모델 개입이 전혀 필요 없음.
부수적으로 execute-tool.ts에도 weather_map_screenshot 성공 시 그 URL을
SSE로 직접 주입하는 방어선 추가(모델이 실제로 도구를 호출했을 때 대비).
실제 로그 분석 결과: "위치로 이동" 같은 후속 요청은 트리거 단어 목록에
없어서 모델이 Windy URL을 재출력하지 않고, 대신 weather_map_screenshot
(서빙 안 되는 모델 전용 스크린샷 도구) 결과를 [REDACTED-HE].jpg 같은
가짜 마크다운 이미지로 답변에 끼워넣음 — 실제 지도는 전혀 안 움직이고
깨진 이미지 링크만 표시됨. 후속 이동 요청도 트리거로 인정하고,
screenshot 도구 결과를 이미지로 삽입하지 말라는 가드 추가.
accountant/lawyer/investor-app의 SKILL_CONTENT가 모든 한글을 6자리
유니코드 이스케이프(\uXXXX)로 저장하고 있어 소스 읽기가 사실상 불가능했음.
디코드→재인코딩 스크립트로 실제 UTF-8 텍스트로 변환, 디코드된 내용은
변환 전후 완전히 동일함을 스크립트로 검증. 기능 변화 없음, 가독성만 개선.
날씨 스킬에서 태풍 번호 혼동 사고 확인 후, 같은 패턴(단일 소스에서
가져온 데이터를 다른 개체에 잘못 귀속) 위험이 큰 세 스킬에 가드 추가:
- investor: 종목코드/거래소/보통주-우선주/분기 대조
- lawyer: 법령 시행일·개정판, 판례 사건번호 전체 일치 대조
- detective: 동명이인·유사상호 — 추가 식별자 없이 귀속 금지
EFFIS(유럽 산불 전용 DB) 공개 WFS를 붙여볼까 테스트했으나 GetCapabilities조차
35초+ 걸리다 500 에러 — 신뢰성 부족으로 보류. 대신 EONET이 북미 편향이라는
사실을 도구 설명에 적어 모델이 유럽 등 이벤트는 자동으로 web_search 우회하게 함.
- eonet_events/satellite_snapshot 도구 추가 (키 불필요 공개 API)
- weather-app.html: 위성사진 채팅 이미지 렌더링, 지도/사진 멀티탭 센터패널,
확대/축소·드래그팬, localStorage+히스토리 이중 복원으로 새로고침 시
탭 유지(하드리셋으로 localStorage가 지워져도 대화 이력에서 재구성)
- 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>
- 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>
- 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>
- 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>
- 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>
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>
app.js 1411-2092의 async function sendChat을 web-ui/js/chat-sse.js로
추출. 범위 내 top-level 선언은 sendChat 하나뿝이라 외부 노출 대상은
sendChat 단일. 끝에 window.sendChat 할당 추가. app.js에서 제거 후
index.html에서 app.js 직전에 로드.
공유 상태(currentAbortController/isThinking)와 외부 헬퍼
(authHeaders/renderChatMessages/addProcessEntry 등)는 전역 그대로
참조. node --check 통과, 재시작 후 /js/chat-sse.js 200 확인.
Co-Authored-By: Claude <noreply@anthropic.com>
- handle-chat.ts에서 handleCodeChat(180줄) + _overlapLen 헬퍼를 별도 파일로 이동
(handle-chat.ts 3,222→3,033줄, 189줄 감소)
- code-chat.ts는 createCodeChat 팩토리로 정의, handle-chat.ts에서 필요한 의존성만 주입
- server.ts는 기존 createHandleChat 팩토리 사용 그대로 유지, handleCodeChat은
handle-chat.ts 내부에서 code-chat.ts 팩토리로 생성
- 빌드 성공 + 재시작 후 일반 채팅/code_ai 채팅(python_eval) 모두 정상 동작 확인
- handle-chat.ts에서 preflight advisor 블록(1092-1284줄, 약 195줄)과
replaceCurrentUserPromptWithAdvisorObjective 헬퍼를 별도 파일로 이동
(handle-chat.ts 3,400→3,222줄, 178줄 감소)
- preflightRoute/preflightReasonForTurn은 getter/setter 뷰 객체로 연결하여
메인 루프의 sanitizeFinalReply 및 route 조건에서 계속 사용 가능
- cron 스케줄러(server.ts)의 callSecondaryPreflight 사용은 그대로 유지
- 빌드 성공 + 재시작 후 일반 채팅/code_ai 채팅 모두 정상 동작 확인
- handle-chat.ts에서 maybeRunBrowserAdvisorPass(430줄) + maybeRunDesktopAdvisorPass(140줄)
+ 헬퍼 4개를 별도 파일로 이동 (handle-chat.ts 3,900→3,400줄, 500줄 감소)
- 메인 루프의 가변 상태는 그대로 두고, getter/setter 기반 advisorCtx 뷰 객체를 통해
새 파일과 공유 → 메인 루프 변수명/조건식은 모두 유지
- 현재 배포에선 dormantBrowserTools / Linux desktop 제한으로 여전히 도달 불가능한
코드이나, 향후 브라우저/데스크톱 자동화 복구 시 이 파일만 다시 연결하면 됨
- 빌드 성공 + 재시작 후 일반 채팅/code_ai 채팅 모두 정상 동작 확인
정의만 있고 호출부 없는 함수들. readDailyMemoryContext/readMemoryCategories는
Chroma 벡터 메모리(2026-07-24 도입, buildPersonalityContext의 [RECALLED_FACTS])가
관련성 기반 회상으로 대체하면서 이미 실질적으로 무용해진 상태였음.
3자 순환 의존성(executeTool→handleTaskControlAction→handleChat→executeTool) 발견:
- inferTaskChannelFromSession/findBlockedTaskForSession/parseTaskIdFromText는 실제로는
handleChat에 의존하지 않는 순수 쿼리 헬퍼임을 확인하고 팩토리 밖으로 빼서 일반 export로
전환 → handle-chat.ts가 deps 주입 없이 직접 import하도록 변경 (HandleChatDeps 2개 필드 제거)
- handleTaskControlAction 자체만 진짜 순환: launchBackgroundTaskRunner가 handleChat을 필요로
하는데 handleChat이 executeTool을, executeTool이 handleTaskControlAction을 필요로 함
→ lazy binding으로 해결: executeTool 인스턴스화 이전에 hoisted wrapper 함수
(let _handleTaskControlActionImpl + function handleTaskControlAction)를 선언해 dep로 넘기고,
handleChat 생성 후 실제 구현을 대입
실제 백그라운드 태스크 생성→조회→삭제 왕복으로 순환 경로 전체 검증 완료.
detectToolCategories/TOOL_BLOCKS/readMemorySnippets 등 관련 헬퍼 클러스터를
createPersonalityContext(resolvePromptPath) 팩토리로 통째 이동. 외부 의존은
resolvePromptPath 하나뿐이라 순환 의존성 없이 분리 가능했음.
오늘 분석에서 찾은 3대 핵심 엔진 함수(buildTools/executeTool/handleChat,
합쳐서 원래 6043줄=51%) 중 가장 크고 마지막 조각. handleChat 바로 앞에
정의된 형제 함수 handleCodeChat(코드 에디터 /api/chat 전용 경로)이 정확히
같은 의존성 집합을 공유하는 걸 확인하고 함께 이동 — 둘 다 buildTools/
executeTool/normalizeToolArgs/logToolCall/ToolResult를 그대로 씀.
app/config/server로 잡혔던 "외부 심볼 참조"는 buildTools 때와 동일하게
전부 도구 설명/주석 안 우연한 단어 오탐으로 확인. 실제 dep는 34개
(밖에서도 쓰이는 telegramChannel/skillsManager/broadcastWS/codeAiLocalSessions/
buildTools/executeTool 등 + handleChat 전용이지만 재귀적 의존성 파악 부담을
피하기 위해 dep로 남긴 로컬 헬퍼 30개), createHandleChat(deps) 팩토리로 주입.
MAX_TOOL_ROUNDS/ExecutionMode/SubagentProfile+TOOL_PROFILES(죽은 코드)는
server.ts에서 정리.
server.ts: 9167 → 5455줄. handle-chat.ts는 3903줄.
검증: 빌드 1차 시도에서 에러 4종류(HandleChatResult 미export, getModelForRole/
fs/path 미import)만 발생 — 나머지 30여개 dep는 전부 정확히 맞았음. 재시작 후
실제 채팅으로 (1) 일반 텍스트 (2) 도구 호출 (3) handleCodeChat 코드에디터 경로
(4) web_search (5) 백그라운드 태스크(BackgroundTaskRunner 경유 handleChat) —
5개 시나리오 전부 실제 왕복 검증. 백그라운드 태스크가 needs_assistance로 끝난
건 handleChat 문제가 아니라 이 환경의 오케스트레이션 disabled 설정 때문임을
daily memory 로그("1+1은 2입니다" 3회 정상 응답 확인)로 교차검증 후 확인.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
executeTool 전용으로만 쓰이던 헬퍼(SAFE_COMMANDS, buildUrlOpenCommand,
buildBrowserLaunchCommand, hasUriScheme, quoteShellArg, BLOCKED_PATTERNS,
lastFilenameUsed, normalizeScheduleJobAction, summarizeCronJob,
normalizeDeliveryChannel, ToolResult 타입)도 함께 이동 — 전부 executeTool
밖에서 전혀 참조되지 않는 것을 grep으로 확인 후 판단. 죽은 코드였던
isLinux(정의만 있고 미사용)도 이 김에 삭제.
밖에서도 쓰이는 10개(isPathInsideDir/cronScheduler/resolvePromptPath/
webSearch/webFetch/imageSearch/IMAGE_TYPES/codeAiLocalSessions/
handleTaskControlAction/broadcastWS)는 createExecuteTool(deps) 팩토리로 주입,
호출부 시그니처는 그대로 유지.
server.ts: 10584 → 9167줄. 빌드/재시작 후 실제 채팅으로 shell/coder_write_file/
web_search 3개 도구 실행 왕복 검증(생성 파일 확인 후 정리 완료), 기존
라우트 8개 재확인.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
3개 핵심 엔진 함수(buildTools/executeTool/handleChat, 합쳐서 6043줄=51%) 중
가장 독립적인 것부터 분리 시작. 실제 참조하는 외부 심볼은
isOrchestrationSkillEnabled 단 하나뿐 — app/config/server는 텍스트 검색에서
도구 설명 문자열("node app.js" 등) 안 우연한 단어와 오탐이었음. 캐시(_toolsCache)
도 함수 스코프 안에서만 쓰여 통째로 이동. createBuildTools(dep) 팩토리 패턴으로
감싸 호출부 시그니처는 그대로 유지.
server.ts: 11785 → 10584줄. 빌드/재시작 후 실제 채팅 메시지로 coder_list_files
도구 호출 왕복까지 검증(HTTP 200 체크만으론 부족 — 실제 도구 실행까지 확인).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
routes-*.ts를 routes/ 서브디렉토리로 옮기면서 import 경로는 스크립트로
재계산했지만, __dirname 기반 런타임 파일 경로(디렉토리 한 단계 더 깊어짐)는
놓쳤음. dental_dict.db·web-ui/schematics·scripts/pptx_preview.py·
edge_tts_synth.py(voice/writer/language 3곳) 전부 ../를 하나씩 추가.
실제 DB 통계(4809건)·검색 API로 재검증 완료.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
export {} 뿐인 빈 껍데기, 아무 곳에서도 참조 안 됨(reactor.ts +
orchestration/multi-agent.ts로 대체된 지 오래). 빌드/재시작 확인 완료.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- 7개 라우트 클러스터 추가 추출 (12,785 → 11,785줄)
- detective는 2곳에 흩어진 사례(9761~)와 네트워크도구(11581~)를 하나로 통합,
/pdf-editor 프록시가 쓰는 detectiveFilesDir/resolveDetectiveUploadPath는 export
- users는 CRUD/이메일계정/텔레그램봇 3곳을 통합, 생성→조회→삭제 전체 사이클 실제
테스트로 검증(테스트 계정 정리 완료)
- 핵심 로그인/세션(/api/auth/status·setup·login·logout·change-password)과
handleChat에 직결된 거대 POST /api/chat 루프는 위험도 높아 이번에도 제외
(기존 /api/code/round 제외와 동일한 이유)
- 오늘 세 차례 분리 누적: 14,913 → 11,785줄 (-3,128줄, -21%)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>