tsx 4.23.13 pulls its own esbuild 0.28.1; allowScripts only listed 0.27.2 (tsup/vite), so npm warned about the blocked postinstall. Also records the marked<16 / typescript-7 / wrap-ansi-10 constraints in the memo so a future upgrade pass doesn't have to rediscover them. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
7.0 KiB
7.0 KiB
locode 설계 메모
프로젝트 개요
locode — Claude Code의 설계 철학을 가져와 구현한 에이전트 코딩 CLI. TypeScript + Ink(React-for-CLI) 기반. 백엔드는 OpenAI 호환 /v1/chat/completions 엔드포인트 사용 (Ollama, LM Studio, 클라우드 API 모두 지원).
- 핵심 철학: "신뢰할 수 없고 느리고 비전/툴콜 지원이 불확실한 모델"이라는 현실에 맞춰 모든 가정을 비관적으로 재단. 로컬/클라우드 자동 감지로 프롬프트 분기.
- Claude Code 플러그인 포맷을 직접 소비하는 하위호환 브리지 (
.claude-plugin/plugin.json, commands/agents/skills/hooks/MCP)
아키텍처 결정 이유
Normal Screen + <Static> (alternate screen 폐지)
이전에는 alternate screen buffer + 인앱 가상 스크롤 + 마우스 트래킹을 직접 구현했으나 완전히 폐지. 이유:
- 코드 복잡도: 마우스 SGR-1006 파싱, 선택 영역, 스크롤 상태 관리가 App.tsx의 절반을 차지
- 터미널 호환성: alternate screen은 SSH, tmux, Windows Terminal 등에서 파편화 심함
- 버그 발생률: 마우스 크래시, 선택 텍스트 깨짐 등 이슈가 끝없이 발생
- 현재 방식: Ink
<Static>으로 완료된 히스토리를 한 번만 렌더링 → 터미널 스크롤백의 영구 부분. 재렌더링 없음. 마우스는 터미널 네이티브에 의존.
로컬/클라우드 모델 감지: isSmallLocalModel()
- 이전:
isLocalBackendURL(baseURL)— URL이 localhost면 무조건 "로컬 모델" - 문제: Ollama가 클라우드 라우팅 모델(
glm-5.2:cloud,qwen3.5:397b-cloud)도 같은 localhost에서 서비스함. 이 모델들은 컨텍스트 128K~1M이고 툴콜도 안정적인데 로컬용 보수적 프롬프트가 적용됨. - 해결:
isSmallLocalModel(baseURL, model)=isLocalBackendURL(baseURL) && !isCloudRoutedModelName(model)— 모델명의:cloud/:-cloud태그로 구분. - 기본값:
isLocal의 기본값을true에서false(클라우드)로 변경. 명시적 지정이 없으면 보수적이 아닌 기본 프롬프트 사용.
컨텍스트 윈도우 기본값 분리
- 이전:
DEFAULT_CONTEXT_WINDOW = 8192(단일, 로컬 모델 기준) - 현재:
DEFAULT_CONTEXT_WINDOW_LOCAL = 8192,DEFAULT_CONTEXT_WINDOW_CLOUD = 131072— 클라우드/Ollama 클라우드 라우팅 모델은 128K~1M 컨텍스트를 가지므로 8192는 과도하게 보수적.
번인레이트(🔥) 계산
- 이전:
outputTokens / 세션 경과 시간— 사용자가 방치하면 번인레이트가 0에 수렴해서 의미 없음 - 현재:
outputTokens / modelTimeMs— 실제 모델 응답 시간으로 계산. "이 모델이 얼마나 빠르게 토큰을 뿜는가"를 정확히 반영.
파일 인코딩: CRLF/LF 혼재
- Windows 환경에서는 CRLF, Unix에서는 LF가 섞여 있음.
edit_file/multi_edit은 매칭 전 LF로 정규화하고, 쓰기 전 원래 EOL을 복원. 이것 없이는 Windows에서 거의 모든 edit_file이 실패함.
핵심 파일 맵
src/ui/ink/index.tsx— 진입점. alternate screen 없이 Ink render. cleanup 시 flush + 종료.src/ui/ink/App.tsx— 메인 UI 컴포넌트.<Static>+ 라이브 영역. 상태: starting→connecting→loading-models→model-select/session-select→input.src/ui/ink/ChatInput.tsx— 커스텀 multiline 입력. Shift+Enter 줄바꿈, bracket paste, @멘션 fuzzy picker, IME 커서.src/agent/loop.ts— 메인 에이전트 루프. 턴/스트리밍/툴콜/컴팩션/서브에이전트/병렬 툴 배치/반복 루프 감지.src/agent/session.ts— Session 객체, 통계, 상태, mutation gate.src/agent/systemPrompt.ts— 시스템 프롬프트 빌더 (isSmallLocalModel기반 로컬/클라우드 분기).src/config/defaults.ts— 모든 기본값.isSmallLocalModel(),isCloudRoutedModelName(),DEFAULT_CONTEXT_WINDOW_LOCAL/CLOUD등.src/toolcalling/— native 어댑터, fallback 파서/프롬프트, partialJson 복구, resolve (Ollama 빈키 복구 포함).
설정 기본값
| 설정 | 기본값 | 비고 |
|---|---|---|
DEFAULT_CONTEXT_WINDOW_LOCAL |
8192 | 작은 로컬 모델 폴백 |
DEFAULT_CONTEXT_WINDOW_CLOUD |
131072 | 클라우드/클라우드 라우팅 폴백 |
DEFAULT_MAX_ITERATIONS |
300 | 50→100→300 상향 |
DEFAULT_MAX_OUTPUT_TOKENS |
131072 | 128K. GLM 등 1M 컨텍스트 모델 대응 |
DEFAULT_MAX_RETRIES |
0 | SDK 지수 백오프 |
DEFAULT_AUTO_COMPACT_THRESHOLD |
0.85 | |
DEFAULT_REQUEST_TIMEOUT_MS |
180,000 | 3분 |
DEFAULT_SUBAGENT_TIMEOUT_MS |
600,000 | 10분 |
MAX_EMPTY_RESPONSE_RETRIES |
3 | 1→3 상향 |
MAX_SUBAGENT_DEPTH |
1 | 서브에이전트 중첩 금지 |
MAX_PRESERVED_TAIL_MESSAGES |
8 | 컴팩션 시 보존 |
MAX_PRESERVED_TAIL_FRACTION |
0.3 | 컴팩션 시 보존 비율 |
MAX_RETAINED_IMAGES |
2 | 히스토리 이미지 보존 |
트러블슈팅 힌트
- "자꾸 에러": 주요 원인은 max_tokens 잘림 → malformed 툴콜. 동적 max_tokens + CRLF 제어문자 이스케이프로 해결됨.
- CRLF edit_file 매칭 버그: LF 정규화 공간에서 매칭, 쓰기 전 원래 EOL 복원으로 해결됨.
- "Paused after N steps":
locode config set maxIterations <number>(기본 300) - 클라우드 모델 빈 응답:
MAX_EMPTY_RESPONSE_RETRIES=3으로 재시도 - 로컬/클라우드 프롬프트 분기:
isSmallLocalModel(baseURL, model)— Ollama 클라우드 라우팅 모델(:cloud태그)은 localhost여도 클라우드 프롬프트 사용 - 반복 루프 감지:
detectRepetitionLoop()— 스트리밍 텍스트에서 짧은 반복 패턴 감지 시 중단 - 번인레이트(🔥):
outputTokens ÷ modelTimeMs기준 (세션 경과 시간이 아닌 실제 모델 응답 시간) - IPv6 localhost:
isLocalBackendURL()은[::1]형식(WHATWG URL 직렬화)도 인식
의존성 제약
- marked는 15에 고정.
marked-terminal@7.3.0의 peer가marked >=1 <16이고 marked-terminal 업데이트가 없음. marked 16+로 올리려면 marked-terminal을 교체하거나 peer를 강제해야 함. - typescript는 7.x (네이티브 컴파일러 포팅). 프로젝트는
tscCLI로 타입체크만 하고 programmatic API를 안 씀 → 네이티브 포트로 안전하게 이전. 빌드는 tsup/esbuild라 tsc와 무관. 플랫폼별@typescript/typescript-*바이너리가 optional dep으로 붙음. - wrap-ansi는 10.x (ink와 동일).
renderMarkdown이 히스토리 출력을 터미널 폭으로 하드랩할 때 사용 — ink 내부 래핑과 같은 string-width v8을 공유해야 폭 계산이 어긋나지 않음. v10은 타입 내장(앰비언트 선언 불필요). allowScripts에 트리에 실제로 존재하는 esbuild 버전을 모두 나열해야 함 (현재0.27.2= tsup/vite,0.28.1= tsx). 빠지면 postinstall 경고.
TODO
- LSP 실서버 통합 테스트 (실제 tsserver/pyright 띄워서 검증)
- task store 영속화 (세션에 task 저장)