From c1c10e5494e1576a6a2752aee343991cafdf6215 Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Mon, 15 Jun 2026 16:46:22 +0600 Subject: [PATCH] =?UTF-8?q?[2026-06-15]=20claude-proxy-node-internals=20?= =?UTF-8?q?=E2=80=94=20architecture,=20orphaned=20processes,=20no=20CLI=20?= =?UTF-8?q?streaming,=20isolated=20shell?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- personal/projects/{ => balda}/balda.md | 0 .../claude-proxy-node-internals.md | 121 ++++++++++++++++++ .../personal-os}/claude-python-cli-proxy.md | 0 .../{ => personal-os}/eagle-dashboard.md | 0 4 files changed, 121 insertions(+) rename personal/projects/{ => balda}/balda.md (100%) create mode 100644 personal/projects/personal-os/claude-proxy-node-internals.md rename {family/how-to => personal/projects/personal-os}/claude-python-cli-proxy.md (100%) rename personal/projects/{ => personal-os}/eagle-dashboard.md (100%) diff --git a/personal/projects/balda.md b/personal/projects/balda/balda.md similarity index 100% rename from personal/projects/balda.md rename to personal/projects/balda/balda.md diff --git a/personal/projects/personal-os/claude-proxy-node-internals.md b/personal/projects/personal-os/claude-proxy-node-internals.md new file mode 100644 index 00000000..ba1abdd8 --- /dev/null +++ b/personal/projects/personal-os/claude-proxy-node-internals.md @@ -0,0 +1,121 @@ +--- +title: openclaw-claude-proxy (Node.js) — внутреннее устройство и проблемы +type: reference +namespace: personal +tags: + - hermes + - mac + - eagle + - claude-proxy + - nodejs + - llm-backend + - pitfalls + - docker +created: '2026-06-15' +updated: '2026-06-15' +confidence: 0.9 +--- +# openclaw-claude-proxy (Node.js) — внутреннее устройство и проблемы + +## Обзор + +**npm пакет:** `openclaw-claude-proxy` v1.0.8+ +**Порт:** 3456 +**Язык:** TypeScript (компилируется в JS) +**Репозиторий:** [mehdic/openclaw-claude-proxy](https://github.com/mehdic/openclaw-claude-proxy) +**Форк:** `mnemon-dev/claude-max-api-proxy` + OpenClaw compatibility PR +**Текущий статус:** fallback (активен Python wrapper на порту 8090) + +## Архитектура + +``` +Hermes → POST /v1/chat/completions (порт 3456, Node.js Express) + └── proxy преобразует OpenAI-формат → stream-json протокол + └── спавнит claude CLI как child process + └── claude --output-format stream-json --verbose + └── читает/пишет JSON через stdin/stdout pipe +``` + +### Runtime-модели (CLAUDE_PROXY_RUNTIME) + +**stream-json** (default): +- init pool: несколько долгоживущих `claude` subprocess'ов +- session pool: переиспользование сессий между запросами +- промпт-кэш между вызовами сохраняется +- использует `--output-format stream-json --input-format stream-json` + +**print** (fallback): +- на каждый запрос spawn'ит свежий `claude --print` +- медленнее, но изолированно +- не требует совместимости stream-json протокола + +## Контейнерный сетап + +`~/Docker/claude-proxy-node/Dockerfile` — образ на node:20-alpine: +``` +FROM node:20-alpine +RUN npm install -g openclaw-claude-proxy @anthropic-ai/claude-code +EXPOSE 3456 +CMD ["claude-proxy", "3456"] +``` + +Образ не собран — в работе. + +## Проблемы + +### 1. Orphaned claude subprocess'ы + +**Механизм:** `stream-json` runtime держит пул долгоживущих `claude` child process'ов через `child_process.spawn()`. Proxy управляет их жизненным циклом: при нормальном shutdown (SIGTERM) должен завершить pool. Но: + +- **SIGKILL** родительского proxy (kill -9, docker stop --time=0, launchd force quit) — Node.js не успевает выполнить cleanup +- **SIGTERM с таймаутом** — если graceful shutdown превышает лимит времени, Node.js процесс убивается до завершения pool +- **launchd KeepAlive** — при падении proxy по любой причине launchd убивает и перезапускает, но старые `claude` процессы с PPID 1 остаются в системе +- **docker restart** — если контейнер перезапускается без `--time` (grace period), старые процессы переживают контейнер + +На macOS orphan'ы выглядят как `claude` с PPID=1. Их количество растёт со временем. + +### 2. Нет живого вывода claude CLI + +**Причина:** proxy читает stdout `claude` исключительно как JSON pipe. Человеческий вывод markdown/thinking/прогресса `claude` пишет в **stderr**, но proxy его не форвардит. + +По умолчанию stderr `claude` либо: +- не пипруется вообще (pipe не открывается) — stderr уходит в /dev/null контейнера +- пипруется и выбрасывается + +В режиме `print` проблема та же — proxy ждёт завершения `claude --print` и возвращает результат целиком, не транслируя промежуточный вывод. + +### 3. claude spawn'ит свой bash без контекста Hermes + +**Механизм:** proxy запускает `claude` через `child_process.spawn('claude', args, {env: process.env})`. Ниже по цепочке: + +``` +launchd → claude-proxy-start.sh → claude-proxy (node) → claude CLI (subprocess) + └── bash -c "..." +``` + +Claude Code CLI для выполнения инструментов (Bash, Write, Edit) внутри себя спавнит `bash -c "..."`. Этот bash: +- наследует env от `claude` процесса +- НЕ имеет `HERMES_HOME`, кастомного `PATH`, `SHELL=/bin/zsh` с профилем Hermes +- использует системный `/bin/bash` или `/bin/sh` + +В контейнере (alpine) bash вообще может отсутствовать — claude CLI использует `/bin/sh`. + +## Предложенные подходы + +### Против orphaned процессов + +1. **Docker `--init` флаг:** запускать контейнер с `init: true` в docker-compose (или `docker run --init`). Использует `tini` как PID 1 — корректно форвардит SIGTERM и перезахоранивает orphan'ов. +2. **SIGTERM handler в Node.js:** добавить `process.on('SIGTERM', ...)` который форсированно убивает весь child process pool перед exit. +3. **Health check + restart policy:** не `always`, а `unless-stopped` с `--time=30` grace period. + +### Против отсутствия вывода CLI + +1. **Проброс stderr:** pipe'ить stderr `claude` в stderr proxy — видно в `docker logs` или launchd логах. +2. **Режим `print`:** переключить `CLAUDE_PROXY_RUNTIME=print` — теряется производительность stream-json, но логов больше. +3. **Node.js `stream-json` мониторинг:** логировать каждую N-ную JSON строку из stdout claude для отладки. + +### Против изоляции shell + +1. **В Docker:** установить bash в контейнер (`apk add bash`), установить `SHELL=/bin/bash` в env. +2. **Передача `HERMES_HOME`:** смонтировать `~/.hermes` в контейнер и передать `HERMES_HOME` в env claude CLI. +3. **wrapper-скрипт для claude:** заменить бинарник `claude` в контейнере на shell wrapper, который ставит профиль Hermes перед вызовом реального `claude`. diff --git a/family/how-to/claude-python-cli-proxy.md b/personal/projects/personal-os/claude-python-cli-proxy.md similarity index 100% rename from family/how-to/claude-python-cli-proxy.md rename to personal/projects/personal-os/claude-python-cli-proxy.md diff --git a/personal/projects/eagle-dashboard.md b/personal/projects/personal-os/eagle-dashboard.md similarity index 100% rename from personal/projects/eagle-dashboard.md rename to personal/projects/personal-os/eagle-dashboard.md