Files
locode/locode-upgrade-memo.md
kimandClaude Sonnet 5 13e6540cdf chore: allow esbuild@0.28.1 install script, note dep constraints
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>
2026-09-10 15:31:50 +09:00

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 (네이티브 컴파일러 포팅). 프로젝트는 tsc CLI로 타입체크만 하고 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 저장)