메카넘휠 이동로봇 프로젝트(파이5+RPLidar, 추후 팔 추가)의 부품리스트/
단계별 작업/예산을 관리하는 웹앱. doctor-app의 GET/PUT 단일문서 패턴을
재사용하되 케이스 목록 없이 단일 프로젝트 문서로 단순화했고, 첫 로드시
project_robot_dog.md 메모 기준 실제 진행상황을 기본 시드로 반환한다.
- src/gateway/routes/routes-robot.ts: GET/PUT /api/robot/project,
사용자별 workspace/.smallclaw/robot-project.json에 원자적 저장
- web-ui/html/robot-app.html: 부품/작업/메모 3탭, 실시간 예산 요약,
디바운스 자동저장
- index.html 🏠 홈 드롭다운에 🤖 로봇 링크 추가
장비연동/전용챗봇/외부데이터연동/CAD플러그인은 다음 단계로 보류.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
스킬 프롬프트 전면 점검 중 발견한 버그 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
사용자 제보: "지서버 모델 연결됐는데 컨텍스트가 세션창에서만 8킬로", 그리고
"웹을 리로드하면 256킬로가 된다".
서버 쪽 마지막 폴백(8192)은 이미 캐시하지 않게 돼 있어서 매번 다시 조회한다
(그 사고를 겪고 고친 주석이 남아 있다). 문제는 브라우저였다 — app.js가
if (_appCtxMax > 0 && !force) return; // 한 번 받으면 끝
로 한 번 받은 값을 페이지 수명 내내 들고 있어서, 지서버가 자고 있을 때 열면
8192가 그대로 박혔다. 리로드가 고친 게 아니라 리로드로 캐시가 비워진 것.
메인 채팅 게이지는 SSE usage로 라이브 값을 받아서 멀쩡했고 세션창만 틀렸던
이유가 이것 — 두 화면이 서로 다른 경로로 같은 숫자를 구하고 있었다.
어댑터 쪽(bfbf04e)과 같은 원칙으로 고친다: **추측을 측정처럼 다루지 않는다.**
- resolveNumCtxInfo()가 value와 known(진짜 해석 결과인지)을 함께 반환
- /api/model-context가 provisional 플래그를 실어 보낸다. /api/show 실패나
마지막 폴백이면 true
- app.js: provisional이면 화면에는 보여주되 _appCtxMax에 저장하지 않아 다음
호출에서 다시 묻는다
- code.js: provisional이면 모델별 1회 래치(_codeAiCtxMaxFetchedFor)를 풀어
다시 조회하게 한다
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
08-10에 만들었던 2분 간격 버전을 효과가 불확실하다며 지웠는데(그
시점엔 지서버 쪽에 사용자가 직접 건 30초 하트비트가 병행되고 있어
어느 쪽 효과인지 분리가 안 됐음), 오늘 실제 실험으로 인과관계가
확인됐다: 지서버 쪽 30초 폴링을 끄자 몇 분 안에 Windows가 실제로
절전 상태에 들어갔다(게이트가 "깨우는 중" HTML 반환, ARP FAILED,
RDP·ping 전부 무응답 — 전부 실측 확인).
즉 주기적 폴링이 절전을 막는 효과 자체는 실재하고, 남은 건 그걸
지서버 쪽 별도 스크립트로 둘지 게이트웨이가 직접 할지의 문제였다.
사용자가 게이트웨이 쪽으로 다시 넣기로 결정 — 간격은 이전 2분보다
촘촘한 1분으로.
게이팅 로직은 이전과 동일: llm.provider === 'ollama_local'이고
WS 클라이언트가 최소 1개 연결돼 있을 때만(앱이 실제로 열려있고
지서버가 활성 모델일 때만) 직결 엔드포인트에 가벼운 요청을 보낸다.
게이트가 아니라 직결로만 보내므로 이미 잠들어 있으면 조용히
실패한다 — 깨우는 신호가 아니라 "깨어있는 동안 재우지 않는" 신호.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
사용자 관찰("크기가 바뀔 때마다 올라마가 모델을 다시 로드하네")을
직접 재현·측정: 같은 num_ctx로 두 번 호출하면 load_duration 1.3초,
num_ctx만 바꿔 호출하면 18.8초. ollama-adapter.ts의 _resolveCtx()가
매 턴 프롬프트 크기에 맞춰 2의 거듭제곱으로 동적으로 재계산하는데,
그 값이 바뀔 때마다 Ollama가 모델을 통째로 재로드하는 것 — 대화
도중 설명 안 되는 수 초짜리 멈춤으로 나타났다.
동적 사이징 자체는 VRAM이 빠듯한 배포를 위해 존재하는데, 지서버는
그 경우가 아니다: gemma4:26b를 native 최대 컨텍스트(262144)로
로드해도 32GB 중 18.3GB만 쓰고 13.7GB가 남는다(실측, size_vram).
아낄 필요가 없는데 아끼다가 재로드 비용만 치르고 있었다.
models.profiles[<model>].fixedNumCtx로 모델별로 동적 사이징을
끄고 항상 고정값을 요청하게 하는 옵션을 추가했다(getModelProfileForceToolChoice와
같은 패턴). gemma4:26b는 262144로 설정 — 한 번 로드되면 대화가
아무리 길어져도 재로드가 없다. 다른 모델/제공자는 기존 동적
사이징 그대로 유지된다(opt-in이라 이 모델 외엔 영향 없음).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
효과가 불확실했다. 로그로 보면 2분 핑이 도는 동안에도 한 번은 실제로
잠들었고("방금 또 잠들었던거 같으네"), 다른 구간에선 40분 연속 안
잤는데 그게 핑 덕분인지 그 시간 내내 실사용 채팅이 있었기 때문인지
분리할 수 없었다.
사용자가 지서버 쪽에 직접 10초 간격 하트비트를 새로 걸었다 — 원격에서
2분마다 찔러보는 것보다 훨씬 촘촘하고, 무엇보다 지서버 자체에서 도는
메커니즘이라 신뢰도를 판단하기 쉽다. 서버 쪽 것은 이제 중복이라
사용자 결정으로 제거.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
08-09에 이미 조사했던 "지서버가 30분 설정보다 훨씬 빨리 잔다" 문제는
사용자가 전기료 절약 관점에서 선호하는 동작으로 확정하고 조사를 접었던
건이다([[project_wol_gate]]). 그런데 오늘(08-10) 외부에서 실제로 사용
중일 때 이 빠른 절전이 매번 ~23초 기상 지연으로 이어져 불편해졌고,
사용자가 "지서버가 안자게 신호를 계속 보내자"고 명시적으로 요청했다 —
원인 재조사가 아니라 능동적 keep-alive를 원하는 것으로 확인.
원인 후보였던 "Windows 유휴 타이머가 마지막 사용자 입력 기준이라
API 트래픽으로는 리셋 안 됨"이 맞다면, 주기적으로 실제 HTTP 요청을
보내는 것 자체가 대응책이 된다.
`llm.provider === 'ollama_local'`이고(getProvider()의 단일 캐시 구조상
어차피 이 조건에서만 실제 채팅 트래픽도 지서버로 간다) WS 클라이언트가
최소 1개 연결돼 있을 때만(앱이 실제로 열려 있을 때만) 2분마다 직결
엔드포인트에 가벼운 /api/tags 요청을 보낸다. 게이트가 아니라 직결로만
보내므로, 이미 잠들어 있으면 그냥 조용히 실패한다 — 깨우는 신호가
아니라 "깨어있는 동안 재우지 않는" 신호라, 나머지 시간대의 절전
이점(사용자가 이미 선택한)을 훼손하지 않는다.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
지서버는 유휴 시 절전에 들어간다. 그동안 UI가 이를 "Offline"(빨강)으로만 보여줘서
고장난 것처럼 읽혔고, 실제로는 메시지를 보내면 자동으로 깨어나 정상 응답한다.
사용자가 붉은 표시를 보고 원인을 찾아 나선 뒤 발견했다.
- /api/status가 sleeping을 함께 반환한다. 게이트를 찔러 확인하지 않고 wake_url
설정 유무로 판별하는데, 상태 표시는 10초마다 갱신되고 게이트를 찌르는 행위가
곧 매직패킷 발사라 확인하러 가면 지서버를 영영 못 자게 만든다.
- 표시를 셋으로 분리: Online(초록) / 절전 중(노랑) / Offline(빨강).
노랑엔 "메시지를 보내면 자동으로 깨어납니다" 툴팁을 붙였다.
또한 설정에서 provider를 고르면 그 자리에서 깨우도록 했다. UI는 이미 선택 즉시
/api/models/test로 모델 목록을 가져오는데, 지서버가 자면 여기서 "모델 없음 —
서버가 실행 중인가요?"로 실패했다. 모델 선택은 쓰겠다는 의사가 분명하므로 이를
기상 트리거로 삼는다. 새 엔드포인트 없이 기존 동작에 얹었고, "자면 모델 목록이
빈다"는 문제도 같이 해결된다. 잠든 지서버로 실측 시 7초 만에 모델 5개를 받았고,
깨어있을 때는 0초로 통과해 불필요한 패킷을 보내지 않는다. wake_url이 없는
provider(클로서버 등)는 기존 동작 그대로다.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
모델이 미세먼지 단위를 $\mu\text{g/m}^3$처럼 LaTeX로 쓴다. 단위는 수식이 아닌데
이 표기가 세 곳에서 깨진다: index.html은 KaTeX가 렌더하지만 복사하면 MathML까지
딸려와 "μ g/m 3 μg/m 3"처럼 중복돼 보이고, 나머지 채팅 페이지 8개(weather/doctor/
lawyer/detective 등)는 KaTeX 자체가 없어 생 LaTeX가 그대로 노출되며, TTS는
백슬래시를 소리 내어 읽는다.
단위만 담긴 수식 span만 유니코드로 바꾸고, 연산자·분수·변수가 있으면 그대로 둔다:
$\mu\text{g/m}^3$ → μg/m³ $\text{m/s}$ → m/s
$E = mc^2$ / $x + y$ / $\frac{a}{b}$ → 유지
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
게이지가 8192로 표시됐으나 지서버 gemma4:26b의 실제 값은 262144, 32배 차이.
원인이 세 겹이었다.
1. resolveNumCtx()의 _cachedNumCtx가 아무 키 없이 프로세스 수명 내내 유지됐다.
6곳에서 설정되는데 무효화하는 코드가 어디에도 없어, 모델/provider를 바꿔도
이전 값을 계속 반환했다(클로서버로 바꿨는데 지서버의 262k가 계속 보이던 증상).
→ (provider, models.primary, provider별 model, llm.num_ctx) 키에 연동
2. 활성 provider가 아니라 localhost:11434를 하드코딩해 조회했다.
provider가 ollama_local(지서버)일 때 클로서버 로컬 Ollama에 물어보니
"model not found" → 최후 폴백 8192로 떨어졌다.
→ activeOllamaEndpoint() 추가, /api/model-context의 per-model 조회에도 적용
3. 프론트엔드 _fetchAppCtxMax()가 페이지 로드 시 한 번만 조회했다.
→ force 인자 추가, 설정 저장 직후 재조회
검증: 게이지 8192 → 262144(지서버 실제값 일치), 반복 호출 5회 안정.
캐시 키는 모델 전환 시 무효화되고 설정 동일 시 적중.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
weather_map_screenshot 같은 도구의 stdout에는 "위 이미지에서 실제로 보이는
것만 설명하라" 같은 비전 모델 대상 지시문이 섞여 있는 경우가 있는데, 이걸
비전 없는 모델이 받으면 응답 안 된 명령으로 읽고 색상·좌표·아이콘 같은
시각적 디테일을 그럴듯하게 지어내는 문제가 있었음. 경로 힌트 뒤에 "이 모델은
이미지를 볼 수 없다"는 명시적 안내를 덧붙여 추측 대신 솔직히 모른다고
답하도록 함.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
법률 사건관리 앱과 같은 패턴으로 의료 사건(진료기록) 관리 앱 추가
(routes-doctor-cases.ts + doctor-app.html + WOPI 라우트). Home Assistant를
iframe으로 띄우는 얇은 래퍼 앱 추가(ha.applecherry.net, 2026-08-01 통합
완료된 것 URL만 새로 노출). weather_openmeteo의 요청 성형/리포트 포맷팅
로직을 weather-openmeteo-format.ts로 분리(순수함수라 테스트 가능,
2026-07-29 있었던 타임존/롤링윈도우/daily-in-hourly 버그 3건 회귀 방지
테스트 포함) + geocode 재시도 테스트.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
프론트엔드가 예전부터 호출하던 POST /api/openai/models가 백엔드에 구현이
안 되어 있어 항상 404였던 버그 수정 - OpenAICompatAdapter로 실제 OpenAI
API에 모델 목록을 조회하도록 구현.
지서버(LM Studio)의 gemma-4-26b-a4b가 응답당 완성 토큰을 2.5배 더 많이
쓰는 것을 벤치마크로 확인(322 vs 132 토큰/응답, thinking 오버헤드로 추정) -
chat_template_kwargs.enable_thinking=false를 기본 적용하되, 복잡한 질문에서
품질이 떨어질 수 있어 설정 화면(모델 → LM Studio)에 체크박스로 켜고 끌 수
있게 구현. 기본값은 비활성화(현재 상태 유지).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
diffusers venv 스크립트 방식 대신 상시구동 ComfyUI(systemd, GPU1)를 통해
SDXL/FLUX/PuLID 스타일변환/LTX 비디오를 생성하도록 전환. ComfyUI 자체 웹
UI를 리버스 프록시로 스튜디오 앱에 같은 도메인으로 임베드(routes-comfyui.ts)
하고, LTX-Video에 non-distilled "dev" 체크포인트 기반 고화질 옵션을 추가.
날씨 앱에는 Windy 제트기류/태풍 마커, 게이트웨이에는 wol-gate 웨이크 로그
조회 API도 함께 반영.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- 채팅창 파일 첨부 버튼 추가 (PDF/TXT, 회계사 앱과 같은 /api/upload/file 재사용,
pdf_read로 계약서 분석 — 스킬 프롬프트엔 이미 방법론이 있었는데 버튼만 없었음)
- 탐정 앱의 "사건" 관리를 법률 앱에 이식: 사건 목록(검색/정렬) + 사건별 메모·
체크리스트·타임라인·첨부자료·제출서류 탭 + 사건별 독립 AI 대화 세션
(다른 사건 컨텍스트가 안 섞임, 탐정 앱과 동일 패턴)
- PDF 편집기(Stirling-PDF)·Collabora 문서 편집 연동도 그대로 이식 — 사건 폴더에
자동 저장됨
- 백엔드는 탐정 앱 기존 코드를 건드리지 않고 새 공용 모듈(case-storage.ts)로
일반화 — appType 파라미터로 detective/lawyer 둘 다 같은 로직 재사용,
탐정 앱 데이터·동작은 그대로 유지(회귀 테스트로 확인)
- 사진 자동 날짜인식(탐정 앱 전용, 카톡 캡처 등에서 날짜 추출) 등 법률 사건과
무관한 기능은 이식 범위에서 제외
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
실사용 로그 감사(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>
멀티유저 전환 후 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>
- 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>
- 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>
정의만 있고 호출부 없는 함수들. 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>