Compare commits

...
10 Commits
Author SHA1 Message Date
taodengandClaude Opus 4.6 b6b33d2623 chore: add MIT LICENSE file
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-10 20:37:11 +10:00
taodengandClaude Opus 4.6 8796a5fc19 docs: update README version to v3.3.1
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 07:49:54 +10:00
taodengandClaude Opus 4.6 609ceb28b0 fix: use stable node symlink path in macOS plist, avoid Cellar breakage
setup.mjs now resolves /opt/homebrew/Cellar/node/X.Y.Z/ to
/opt/homebrew/opt/node/ so the LaunchAgent plist survives node upgrades.
Bump to v3.3.1.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-04 10:01:54 +10:00
taodengandClaude Opus 4.6 3843ec8fe6 refactor: simplify timeout to single CLAUDE_TIMEOUT (default 600s)
Remove firstByteTimeout, adaptive tier algorithm, and MODEL_TIMEOUT_TIERS.
Claude tool-use causes 30s-5min pauses in token streams, making fine-grained
timeouts unreliable — they repeatedly killed valid requests. A single generous
timeout (10 min, matching LiteLLM/OpenAI SDK defaults) is simpler and correct.

Breaking: CLAUDE_FIRST_BYTE_TIMEOUT env var removed, default timeout changed
from 300s to 600s. Bump to v3.3.0.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-04 09:46:12 +10:00
taodengandClaude Opus 4.6 afe0c6e1be fix: computeFirstByteTimeout now respects CLAUDE_FIRST_BYTE_TIMEOUT env var
The adaptive timeout calculation ignored BASE_FIRST_BYTE_TIMEOUT (set via
CLAUDE_FIRST_BYTE_TIMEOUT env), always using the tier base (120s for Sonnet).
Now uses whichever is larger: adaptive or env override. Bump to v3.2.2.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-04 09:26:01 +10:00
taodengandClaude Opus 4.6 a6007393a3 fix: auto-configure idleTimeoutSeconds=0 to prevent tool-call timeouts
setup.mjs now sets agents.defaults.llm.idleTimeoutSeconds=0 in
openclaw.json during installation. Without this, OpenClaw's default
60s idle timeout kills Claude connections during tool use (Bash, Read,
etc.), causing exit 143 errors and stuck sessions.

Also adds troubleshooting section to README. Bump to v3.2.1.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-03 08:50:19 +10:00
taodengandClaude Opus 4.6 8592150f7a docs: update README for v3.2.0 — add update command, upgrade notes
- Document ocp update / ocp update --check
- Add Self-Update section
- Add Upgrading from v3.0.x migration notes
- Remove Known Issues (resolved upstream)
- Note neutral service names in OpenClaw Integration

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-03 08:22:55 +10:00
taodengandClaude Opus 4.6 eb76971ffc fix: resolve symlinks in ocp update/restart for linked installs
BASH_SOURCE returns the symlink path, not the target. This broke
ocp update on systems where ocp is installed as a symlink
(e.g. ~/.local/bin/ocp → repo/ocp).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-03 08:20:26 +10:00
taodengandClaude Opus 4.6 d54e73ef89 fix: prevent git fetch failure from aborting ocp update --check
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-03 08:19:41 +10:00
taodengandClaude Opus 4.6 3f10b459f5 feat: add ocp update command + fix restart for new service names
- `ocp update`: pulls latest from GitHub, syncs plugin, restarts proxy
- `ocp update --check`: shows current vs latest version without applying
- `ocp restart`: now handles both dev.ocp.proxy and legacy ai.openclaw.proxy
  service names on macOS/Linux
- Bumps to v3.2.0

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-03 08:18:41 +10:00
8 changed files with 244 additions and 101 deletions
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Tao Deng
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+63 -18
View File
@@ -1,6 +1,6 @@
# OCP — Open Claude Proxy
> **Status: Stable (v3.0.0)** — Feature-complete. Bug fixes only.
> **Status: Stable (v3.3.1)** — Feature-complete. Bug fixes only.
> **Already paying for Claude Pro/Max? Use your subscription as an OpenAI-compatible API — $0 extra cost.**
@@ -95,7 +95,8 @@ ocp sessions Active sessions
ocp clear Clear all sessions
ocp restart Restart proxy
ocp restart gateway Restart gateway
ocp version Show version
ocp update Update to latest version
ocp update --check Check for updates without applying
ocp --help Command reference
```
@@ -111,6 +112,16 @@ ocp --help
> **Cloud/Linux servers:** If `ocp: command not found`, the binary isn't in PATH. Full path: `~/.openclaw/projects/ocp/ocp`
### Self-Update
```bash
# Check if a new version is available
ocp update --check
# Pull latest, sync plugin, restart proxy — one command
ocp update
```
### Runtime Settings (No Restart Needed)
```
@@ -157,6 +168,7 @@ OCP was originally built for [OpenClaw](https://github.com/openclaw/openclaw) an
- **`setup.mjs`** auto-configures the `claude-local` provider in `openclaw.json`
- **Gateway plugin** registers `/ocp` as a native slash command in Telegram/Discord
- **Multi-agent** — 8 concurrent requests sharing one subscription
- **No conflicts** — uses neutral service names (`dev.ocp.proxy` / `ocp-proxy`) that don't trigger OpenClaw's gateway-like service detection
### Install the Gateway Plugin
@@ -176,14 +188,61 @@ Add to `~/.openclaw/openclaw.json`:
Restart: `openclaw gateway restart`
## Troubleshooting
### Requests fail with exit 143 / SIGTERM after ~60 seconds
**Symptom:** Claude returns errors or stops responding after about 60 seconds, especially during tool use (Bash, Read, etc.).
**Cause:** OpenClaw's gateway has a default `idleTimeoutSeconds` of 60 seconds. When Claude calls tools, the token stream pauses while the tool executes — if that takes longer than 60s, the gateway kills the connection.
**Fix:** `setup.mjs` (v3.2.1+) sets this automatically. If you installed an older version, add this to `~/.openclaw/openclaw.json`:
```json
{
"agents": {
"defaults": {
"llm": {
"idleTimeoutSeconds": 0
}
}
}
}
```
Then restart: `openclaw gateway restart`
### Agents stuck in "typing" but never respond
Usually caused by stuck sessions from previous timeout errors. Fix:
```bash
# Clear all sessions
ocp clear
# Restart both services
ocp restart
openclaw gateway restart
```
If that doesn't help, manually clear the session store:
```bash
# Find and reset stuck Telegram sessions
cat ~/.openclaw/agents/main/sessions/sessions.json
# Remove entries with "telegram" channel, then restart gateway
```
## Upgrading from v3.0.x
If you installed OCP before v3.1.0, the auto-start service used names that OpenClaw's gateway detected as conflicting (`ai.openclaw.proxy` on macOS, `openclaw-proxy` on Linux). Running `node setup.mjs` or `ocp update` will automatically migrate to the new neutral names.
## Environment Variables
| Variable | Default | Description |
|----------|---------|-------------|
| `CLAUDE_PROXY_PORT` | `3456` | Listen port |
| `CLAUDE_BIN` | *(auto-detect)* | Path to claude binary |
| `CLAUDE_TIMEOUT` | `300000` | Overall request timeout (ms) |
| `CLAUDE_FIRST_BYTE_TIMEOUT` | `90000` | Base first-byte timeout (ms) |
| `CLAUDE_TIMEOUT` | `600000` | Request timeout (ms, default: 10 min) |
| `CLAUDE_MAX_CONCURRENT` | `8` | Max concurrent claude processes |
| `CLAUDE_MAX_PROMPT_CHARS` | `150000` | Prompt truncation limit (chars) |
| `CLAUDE_SESSION_TTL` | `3600000` | Session expiry (ms, default: 1 hour) |
@@ -198,20 +257,6 @@ Restart: `openclaw gateway restart`
- **No API keys needed** — authentication goes through Claude CLI's OAuth session
- **Auto-start** — launchd (macOS) / systemd (Linux)
## Known Issues
### `/ocp` command returns "Unknown skill: ocp" (OpenClaw only)
The `/ocp` plugin command may intermittently stop working in Telegram/Discord. This is caused by an OpenClaw gateway bug ([openclaw/openclaw#26895](https://github.com/openclaw/openclaw/issues/26895)).
**Workaround:**
```
/new
/ocp restart
```
**Important:** Do not add `ocp` to agent `skills` lists or place a `SKILL.md` in workspace skills — this creates a routing conflict.
## License
MIT
+119 -5
View File
@@ -231,12 +231,27 @@ cmd_restart() {
openclaw gateway restart 2>&1
else
echo "Restarting proxy..."
launchctl kickstart -k gui/501/ai.openclaw.proxy 2>/dev/null || {
echo "launchctl failed, trying kill + restart..."
pkill -f "claude-proxy/server.mjs" 2>/dev/null || true
# Try current service name, then legacy, then manual restart
local uid
uid=$(id -u)
if launchctl kickstart -k "gui/$uid/dev.ocp.proxy" 2>/dev/null; then
true
elif launchctl kickstart -k "gui/$uid/ai.openclaw.proxy" 2>/dev/null; then
true
elif systemctl --user restart ocp-proxy 2>/dev/null; then
true
elif systemctl --user restart openclaw-proxy 2>/dev/null; then
true
else
echo "Service restart failed, trying kill + restart..."
pkill -f "server.mjs.*CLAUDE_PROXY" 2>/dev/null || pkill -f "claude-proxy/server.mjs" 2>/dev/null || true
sleep 1
cd "$HOME/.openclaw/projects/claude-proxy" && nohup node server.mjs >> "$HOME/.openclaw/logs/proxy.log" 2>&1 &
}
local self_r script_dir
self_r="${BASH_SOURCE[0]}"
while [[ -L "$self_r" ]]; do self_r="$(readlink "$self_r")"; done
script_dir="$(cd "$(dirname "$self_r")" && pwd)"
nohup node "$script_dir/server.mjs" >> "$HOME/.ocp/logs/proxy.log" 2>&1 &
fi
sleep 3
if curl -sf --max-time 5 "$PROXY/health" > /dev/null 2>&1; then
echo "✓ Proxy restarted successfully."
@@ -323,6 +338,102 @@ for e in d.get('errors', []):
fi
}
# ── update ──────────────────────────────────────────────────────────────
cmd_update_help() {
cat <<'EOF'
ocp update — Update OCP to the latest version
Pulls the latest code from GitHub, restarts the proxy service,
and optionally syncs the plugin to the OpenClaw extensions directory.
Usage:
ocp update Pull latest and restart
ocp update --check Check for updates without applying
EOF
}
cmd_update() {
local script_dir self
self="${BASH_SOURCE[0]}"
# Resolve symlinks (e.g. ~/.local/bin/ocp → real location)
while [[ -L "$self" ]]; do self="$(readlink "$self")"; done
script_dir="$(cd "$(dirname "$self")" && pwd)"
# Check-only mode
if [[ "${1:-}" == "--check" ]]; then
cd "$script_dir"
git fetch origin main --quiet 2>/dev/null || true
local local_ver remote_ver
local_ver=$(python3 -c "import json; print(json.load(open('package.json'))['version'])" 2>/dev/null || echo "?")
remote_ver=$(git show origin/main:package.json 2>/dev/null | python3 -c "import sys,json; print(json.load(sys.stdin)['version'])" 2>/dev/null || echo "?")
local behind
behind=$(git rev-list HEAD..origin/main --count 2>/dev/null || echo "?")
echo "OCP Update Check"
echo "─────────────────────────────────────"
echo " Current: v$local_ver"
echo " Latest: v$remote_ver"
if [[ "$behind" == "0" ]]; then
echo " Status: ✓ Up to date"
else
echo " Status: $behind commit(s) behind"
echo ""
echo " Run 'ocp update' to apply."
fi
return 0
fi
echo "Updating OCP..."
echo ""
# 1. Pull latest
cd "$script_dir"
local old_ver
old_ver=$(python3 -c "import json; print(json.load(open('package.json'))['version'])" 2>/dev/null || echo "?")
echo " Pulling latest from GitHub..."
if ! git pull origin main --ff-only 2>&1 | sed 's/^/ /'; then
echo " ✗ Git pull failed. Resolve conflicts manually in: $script_dir"
return 1
fi
local new_ver
new_ver=$(python3 -c "import json; print(json.load(open('package.json'))['version'])" 2>/dev/null || echo "?")
if [[ "$old_ver" == "$new_ver" ]]; then
echo " ✓ Already at latest (v$new_ver)"
else
echo " ✓ Updated v$old_ver → v$new_ver"
fi
# 2. Sync plugin to extensions dir
local ext_dir="$HOME/.openclaw/extensions/ocp"
if [[ -d "$ext_dir" && -d "$script_dir/ocp-plugin" ]]; then
echo ""
echo " Syncing OCP plugin..."
cp "$script_dir/ocp-plugin/index.js" "$ext_dir/index.js" 2>/dev/null
cp "$script_dir/ocp-plugin/package.json" "$ext_dir/package.json" 2>/dev/null
cp "$script_dir/ocp-plugin/openclaw.plugin.json" "$ext_dir/openclaw.plugin.json" 2>/dev/null
echo " ✓ Plugin synced to $ext_dir"
fi
# 3. Restart proxy
echo ""
echo " Restarting proxy..."
cmd_restart > /dev/null 2>&1
sleep 2
if curl -sf --max-time 5 "$PROXY/health" > /dev/null 2>&1; then
local running_ver
running_ver=$(curl -sf --max-time 5 "$PROXY/health" | python3 -c "import sys,json; print(json.loads(sys.stdin.read())['version'])" 2>/dev/null || echo "?")
echo " ✓ Proxy running (v$running_ver)"
else
echo " ⚠ Proxy not responding — check: ocp health"
fi
echo ""
echo "Done."
}
# ── help ─────────────────────────────────────────────────────────────────
cmd_help() {
cat <<'EOF'
@@ -341,6 +452,8 @@ Commands:
clear Clear all sessions
restart Restart proxy
restart gateway Restart gateway
update Update OCP to latest version
update --check Check for updates
Run 'ocp <command> --help' for details on a specific command.
EOF
@@ -380,5 +493,6 @@ case "$subcmd" in
sessions) cmd_sessions ;;
clear) cmd_clear ;;
restart) cmd_restart "${1:-}" ;;
update) cmd_update "${1:-}" ;;
*) echo "Unknown command: $subcmd"; echo ""; cmd_help; exit 1 ;;
esac
+1 -1
View File
@@ -2,7 +2,7 @@
"id": "ocp",
"name": "OCP Commands",
"description": "Slash commands for the OpenClaw Proxy — /ocp usage, /ocp settings, /ocp health, etc.",
"version": "3.1.0",
"version": "3.3.1",
"configSchema": {
"type": "object",
"additionalProperties": false,
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "ocp",
"version": "3.1.0",
"version": "3.3.1",
"description": "Slash commands for the OpenClaw Proxy",
"main": "index.js",
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "openclaw-claude-proxy",
"version": "3.1.0",
"version": "3.3.1",
"description": "OCP (Open Claude Proxy) — use your Claude Pro/Max subscription as an OpenAI-compatible API for any IDE. Works with Cline, OpenCode, Aider, Continue.dev, OpenClaw, and more.",
"type": "module",
"bin": {
+15 -74
View File
@@ -1,30 +1,19 @@
#!/usr/bin/env node
/**
* openclaw-claude-proxy v2.5.0 — OpenAI-compatible proxy for Claude CLI
* openclaw-claude-proxy — OpenAI-compatible proxy for Claude CLI
*
* Translates OpenAI chat/completions requests into `claude -p` CLI calls,
* letting you use your Claude Pro/Max subscription as an OpenClaw model provider.
*
* v2.5.0:
* - Sliding-window circuit breaker: uses time-windowed failure rate instead of
* consecutive-count, preventing multi-agent burst scenarios from tripping the
* breaker too aggressively. Half-open state allows configurable probe requests.
* - Graduated backoff: cooldown doubles on each re-open (capped at 5min),
* resets fully on success.
* - Health endpoint now exposes per-model breaker state and sliding window stats.
* - Increased default timeout tiers for Opus/Sonnet to handle large agent prompts.
*
* v2.4.0:
* - Per-model circuit breaker: consecutive timeouts temporarily mark a model as degraded
* - Adaptive first-byte timeout: scales by model tier + prompt size
* - Structured JSON logging for key events (easier to parse/alert on)
* - On-demand spawning (no pool), session management, full tool access
* Timeout design: single CLAUDE_TIMEOUT (default 600s / 10 min).
* No separate first-byte or idle timeout — Claude tool-use causes long pauses
* in the token stream (30s-5min) that make fine-grained timeouts unreliable.
* This matches LiteLLM, OpenAI SDK, and other major LLM proxies.
*
* Env vars:
* CLAUDE_PROXY_PORT — listen port (default: 3456)
* CLAUDE_BIN — path to claude binary (default: auto-detect)
* CLAUDE_TIMEOUT — per-request timeout in ms (default: 300000)
* CLAUDE_FIRST_BYTE_TIMEOUT — base first-byte timeout in ms (default: 90000)
* CLAUDE_TIMEOUT — per-request timeout in ms (default: 600000)
* CLAUDE_ALLOWED_TOOLS — comma-separated tools to allow (default: expanded set)
* CLAUDE_SKIP_PERMISSIONS — "true" to bypass all permission checks (default: false)
* CLAUDE_SYSTEM_PROMPT — system prompt appended to all requests
@@ -89,8 +78,7 @@ function resolveClaude() {
// Settings marked with `let` can be changed at runtime via PATCH /settings.
const PORT = parseInt(process.env.CLAUDE_PROXY_PORT || "3456", 10);
const CLAUDE = resolveClaude();
let TIMEOUT = parseInt(process.env.CLAUDE_TIMEOUT || "300000", 10);
let BASE_FIRST_BYTE_TIMEOUT = parseInt(process.env.CLAUDE_FIRST_BYTE_TIMEOUT || "90000", 10);
let TIMEOUT = parseInt(process.env.CLAUDE_TIMEOUT || "600000", 10);
const PROXY_API_KEY = process.env.PROXY_API_KEY || "";
const SKIP_PERMISSIONS = process.env.CLAUDE_SKIP_PERMISSIONS === "true";
const ALLOWED_TOOLS = (process.env.CLAUDE_ALLOWED_TOOLS ||
@@ -358,26 +346,13 @@ function messagesToPrompt(messages) {
return result;
}
// Model tier multipliers for first-byte timeout.
// Opus is much slower to produce first token, especially with large contexts.
let MODEL_TIMEOUT_TIERS = {
"opus": { base: 150000, perPromptChar: 0.00050 }, // 150s base + ~50s per 100k chars
"sonnet": { base: 120000, perPromptChar: 0.00050 }, // 120s base + ~50s per 100k chars
"haiku": { base: 45000, perPromptChar: 0.00010 }, // 45s base + ~10s per 100k chars
};
// Model tier — used for logging only (no timeout logic).
function getModelTier(cliModel) {
if (cliModel.includes("opus")) return "opus";
if (cliModel.includes("haiku")) return "haiku";
return "sonnet";
}
function computeFirstByteTimeout(cliModel, promptLength) {
const tier = MODEL_TIMEOUT_TIERS[getModelTier(cliModel)];
const timeout = tier.base + Math.floor(promptLength * tier.perPromptChar);
return Math.min(timeout, Math.max(TIMEOUT - 5000, 10000));
}
// ── Spawn claude CLI (shared setup) ─────────────────────────────────────
// Resolves session logic, builds CLI args, spawns the process, and sets up
// timeouts. Returns context object or throws synchronously.
@@ -437,7 +412,6 @@ function spawnClaudeProcess(model, messages, conversationId) {
activeProcesses.add(proc);
const t0 = Date.now();
const firstByteTimeoutMs = computeFirstByteTimeout(cliModel, prompt.length);
let gotFirstByte = false;
let cleaned = false;
@@ -445,7 +419,6 @@ function spawnClaudeProcess(model, messages, conversationId) {
if (cleaned) return;
cleaned = true;
clearTimeout(overallTimer);
clearTimeout(firstByteTimer);
stats.activeRequests--;
}
@@ -459,7 +432,6 @@ function spawnClaudeProcess(model, messages, conversationId) {
function markFirstByte() {
if (!gotFirstByte) {
gotFirstByte = true;
clearTimeout(firstByteTimer);
console.log(`[claude] first-byte model=${cliModel} elapsed=${Date.now() - t0}ms`);
}
}
@@ -469,27 +441,17 @@ function spawnClaudeProcess(model, messages, conversationId) {
proc.stdin.end();
recordModelRequest(cliModel, prompt.length);
logEvent("info", "claude_spawned", { model: cliModel, promptChars: prompt.length, firstByteTimeout: firstByteTimeoutMs, tier: getModelTier(cliModel), session: conversationId ? conversationId.slice(0, 12) + "..." : "none" });
logEvent("info", "claude_spawned", { model: cliModel, promptChars: prompt.length, timeout: TIMEOUT, tier: getModelTier(cliModel), session: conversationId ? conversationId.slice(0, 12) + "..." : "none" });
// First-byte timeout
const firstByteTimer = setTimeout(() => {
if (!gotFirstByte && !cleaned) {
stats.timeouts++;
recordModelError(cliModel, true);
breakerRecordTimeout(cliModel);
logEvent("error", "first_byte_timeout", { model: cliModel, timeoutMs: firstByteTimeoutMs, promptChars: prompt.length });
try { proc.kill("SIGTERM"); } catch {}
setTimeout(() => { try { proc.kill("SIGKILL"); } catch {} }, 5000);
}
}, firstByteTimeoutMs);
// Overall request timeout
// Single request timeout — no separate first-byte timer.
// Claude tool-use causes long pauses in the token stream (30s-5min),
// making first-byte/idle timeouts unreliable. One generous timeout is simpler and correct.
const overallTimer = setTimeout(() => {
if (!cleaned) {
stats.timeouts++;
recordModelError(cliModel, true);
breakerRecordTimeout(cliModel);
logEvent("error", "request_timeout", { model: cliModel, timeoutMs: TIMEOUT });
logEvent("error", "request_timeout", { model: cliModel, timeoutMs: TIMEOUT, elapsed: Date.now() - t0 });
try { proc.kill("SIGTERM"); } catch {}
setTimeout(() => { try { proc.kill("SIGKILL"); } catch {} }, 5000);
}
@@ -929,31 +891,18 @@ async function handleStatus(_req, res) {
//
// Tunable keys and their types/ranges:
const SETTINGS_SCHEMA = {
timeout: { type: "number", min: 30000, max: 600000, unit: "ms", desc: "Overall request timeout" },
firstByteTimeout: { type: "number", min: 15000, max: 300000, unit: "ms", desc: "Base first-byte timeout" },
timeout: { type: "number", min: 30000, max: 1800000, unit: "ms", desc: "Request timeout (default: 600s)" },
maxConcurrent: { type: "number", min: 1, max: 32, unit: "", desc: "Max concurrent claude processes" },
sessionTTL: { type: "number", min: 60000, max: 86400000, unit: "ms", desc: "Session idle expiry" },
maxPromptChars: { type: "number", min: 10000, max: 1000000, unit: "chars", desc: "Prompt truncation limit" },
"tiers.opus.base": { type: "number", min: 30000, max: 600000, unit: "ms", desc: "Opus base first-byte timeout" },
"tiers.opus.perChar": { type: "number", min: 0, max: 0.01, unit: "ms/char", desc: "Opus per-char timeout addition" },
"tiers.sonnet.base": { type: "number", min: 30000, max: 600000, unit: "ms", desc: "Sonnet base first-byte timeout" },
"tiers.sonnet.perChar": { type: "number", min: 0, max: 0.01, unit: "ms/char", desc: "Sonnet per-char timeout addition" },
"tiers.haiku.base": { type: "number", min: 15000, max: 300000, unit: "ms", desc: "Haiku base first-byte timeout" },
"tiers.haiku.perChar": { type: "number", min: 0, max: 0.01, unit: "ms/char", desc: "Haiku per-char timeout addition" },
};
function getSettings() {
return {
timeout: { value: TIMEOUT, ...SETTINGS_SCHEMA.timeout },
firstByteTimeout: { value: BASE_FIRST_BYTE_TIMEOUT, ...SETTINGS_SCHEMA.firstByteTimeout },
maxConcurrent: { value: MAX_CONCURRENT, ...SETTINGS_SCHEMA.maxConcurrent },
sessionTTL: { value: SESSION_TTL, ...SETTINGS_SCHEMA.sessionTTL },
maxPromptChars: { value: MAX_PROMPT_CHARS, ...SETTINGS_SCHEMA.maxPromptChars },
tiers: {
opus: { base: MODEL_TIMEOUT_TIERS.opus.base, perPromptChar: MODEL_TIMEOUT_TIERS.opus.perPromptChar },
sonnet: { base: MODEL_TIMEOUT_TIERS.sonnet.base, perPromptChar: MODEL_TIMEOUT_TIERS.sonnet.perPromptChar },
haiku: { base: MODEL_TIMEOUT_TIERS.haiku.base, perPromptChar: MODEL_TIMEOUT_TIERS.haiku.perPromptChar },
},
};
}
@@ -965,16 +914,9 @@ function applySettingUpdate(key, value) {
switch (key) {
case "timeout": TIMEOUT = value; break;
case "firstByteTimeout": BASE_FIRST_BYTE_TIMEOUT = value; break;
case "maxConcurrent": MAX_CONCURRENT = value; break;
case "sessionTTL": SESSION_TTL = value; break;
case "maxPromptChars": MAX_PROMPT_CHARS = value; break;
case "tiers.opus.base": MODEL_TIMEOUT_TIERS.opus.base = value; break;
case "tiers.opus.perChar": MODEL_TIMEOUT_TIERS.opus.perPromptChar = value; break;
case "tiers.sonnet.base": MODEL_TIMEOUT_TIERS.sonnet.base = value; break;
case "tiers.sonnet.perChar": MODEL_TIMEOUT_TIERS.sonnet.perPromptChar = value; break;
case "tiers.haiku.base": MODEL_TIMEOUT_TIERS.haiku.base = value; break;
case "tiers.haiku.perChar": MODEL_TIMEOUT_TIERS.haiku.perPromptChar = value; break;
default: return `${key}: not implemented`;
}
logEvent("info", "setting_changed", { key, value });
@@ -1136,7 +1078,6 @@ const server = createServer(async (req, res) => {
auth: authStatus,
config: {
timeout: TIMEOUT,
firstByteTimeout: BASE_FIRST_BYTE_TIMEOUT,
maxConcurrent: MAX_CONCURRENT,
sessionTTL: SESSION_TTL,
circuitBreaker: "disabled",
@@ -1250,7 +1191,7 @@ server.listen(PORT, "127.0.0.1", () => {
console.log(`Architecture: on-demand spawning (no pool)`);
console.log(`Models: ${MODELS.map((m) => m.id).join(", ")}`);
console.log(`Claude binary: ${CLAUDE}`);
console.log(`Timeout: ${TIMEOUT}ms (base first-byte: ${BASE_FIRST_BYTE_TIMEOUT}ms, adaptive by model/prompt) | Max concurrent: ${MAX_CONCURRENT}`);
console.log(`Timeout: ${TIMEOUT / 1000}s | Max concurrent: ${MAX_CONCURRENT}`);
console.log(`Circuit breaker: disabled`);
console.log(`Tools: ${SKIP_PERMISSIONS ? "all (skip-permissions)" : ALLOWED_TOOLS.join(", ")}`);
console.log(`Sessions: TTL=${SESSION_TTL / 1000}s`);
+23 -1
View File
@@ -169,6 +169,19 @@ for (const [key, val] of Object.entries(MODEL_ALIASES)) {
}
log(`Model aliases added to agents.defaults.models`);
// Set idleTimeoutSeconds to 0 — critical for Claude tool-use.
// When Claude calls tools (Bash, Read, etc.), the token stream pauses for 30-120s.
// OpenClaw's default idleTimeoutSeconds (60s) kills the connection mid-tool-call,
// causing exit 143 (SIGTERM) and stuck sessions. Setting to 0 disables the idle timer.
if (!config.agents.defaults.llm) config.agents.defaults.llm = {};
if (config.agents.defaults.llm.idleTimeoutSeconds === undefined ||
config.agents.defaults.llm.idleTimeoutSeconds > 0) {
config.agents.defaults.llm.idleTimeoutSeconds = 0;
log(`Set agents.defaults.llm.idleTimeoutSeconds = 0 (prevents tool-call timeouts)`);
} else {
log(`idleTimeoutSeconds already configured: ${config.agents.defaults.llm.idleTimeoutSeconds}`);
}
writeJSON(CONFIG_PATH, config);
log(`Config saved`);
@@ -285,7 +298,16 @@ if (!DRY_RUN) {
console.log("\n🔄 Installing auto-start on login...\n");
const platform = process.platform;
const nodeBin = process.execPath;
// Use stable symlink path instead of versioned Cellar path (e.g. /opt/homebrew/opt/node/bin/node
// instead of /opt/homebrew/Cellar/node/25.8.0/bin/node) so the plist survives node upgrades.
let nodeBin = process.execPath;
if (platform === "darwin" && nodeBin.includes("/Cellar/")) {
const stable = nodeBin.replace(/\/Cellar\/[^/]+\/[^/]+\//, "/opt/");
if (existsSync(stable)) {
nodeBin = stable;
log(`Using stable node path: ${nodeBin}`);
}
}
// Ensure logs dir exists
const logsDir = join(OPENCLAW_DIR, "logs");