[2026-06-15] claude-proxy-node-internals — architecture, orphaned processes, no CLI streaming, isolated shell

This commit is contained in:
Alexey Martemyanov
2026-06-15 16:46:22 +06:00
parent 0d1ed50db0
commit c1c10e5494
4 changed files with 121 additions and 0 deletions
@@ -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`.