@elvatis_com/openclaw-cli-bridge-elvatis 3.3.0 → 3.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,187 @@
1
+ # Handover: CLI Session Resume Pattern
2
+
3
+ ## Problem Solved
4
+
5
+ Spawning fresh CLI processes (`claude -p`, `gemini -p`, `codex exec`) for every request forces the model to re-process the entire conversation history (20KB+) from scratch. This causes:
6
+ - **Silent hangs** — Sonnet goes completely silent (zero stdout) ~50% of the time on large prompts
7
+ - **Slow responses** — 80-120s per request instead of 5-10s
8
+ - **Wasted tokens** — the full history is re-tokenized on every call
9
+
10
+ ## Solution: Session Resume
11
+
12
+ Instead of one-shot processes, maintain persistent sessions per model. First request creates a session, subsequent requests resume it — the CLI keeps the full conversation context.
13
+
14
+ ## Implementation by CLI Tool
15
+
16
+ ### Claude Code (`claude`)
17
+
18
+ ```bash
19
+ # First request — create session
20
+ echo "user prompt" | claude -p \
21
+ --session-id "550e8400-e29b-41d4-a716-446655440000" \
22
+ --model claude-sonnet-4-6 \
23
+ --output-format text \
24
+ --permission-mode bypassPermissions \
25
+ --dangerously-skip-permissions
26
+
27
+ # Subsequent requests — resume (Claude has full context, only new message needed)
28
+ echo "follow-up prompt" | claude -p \
29
+ --resume "550e8400-e29b-41d4-a716-446655440000" \
30
+ --model claude-sonnet-4-6 \
31
+ --output-format text \
32
+ --permission-mode bypassPermissions \
33
+ --dangerously-skip-permissions
34
+ ```
35
+
36
+ **Key flags:**
37
+ - `--session-id <uuid>` — creates a new session with this ID (first request)
38
+ - `--resume <uuid>` — resumes an existing session (subsequent requests)
39
+ - Both work with `-p` (print/headless mode)
40
+ - Session files stored by Claude CLI internally (~/.claude/projects/)
41
+
42
+ ### Gemini CLI (`gemini`)
43
+
44
+ ```bash
45
+ # First request — auto-creates session
46
+ echo "user prompt" | gemini -m gemini-2.5-flash -p "" --approval-mode yolo
47
+
48
+ # Subsequent requests — resume by UUID
49
+ echo "follow-up" | gemini -m gemini-2.5-flash -p "" --resume "ad79893c-4e3d-40e6-83e7-400e49dba0d6" --approval-mode yolo
50
+ ```
51
+
52
+ **Key flags:**
53
+ - `--resume <uuid>` — resume by session UUID
54
+ - `--list-sessions` — list available sessions
55
+ - Session UUID is visible in `--list-sessions` output
56
+
57
+ **Note:** Gemini doesn't have a `--session-id` flag to create a specific UUID. The session is auto-created and the UUID is extracted from `--list-sessions` or from the output. For the bridge, we generate a UUID and pass it as `--resume` — Gemini creates a new session if the UUID doesn't exist.
58
+
59
+ ### OpenAI Codex (`codex`)
60
+
61
+ ```bash
62
+ # First request — auto-creates session
63
+ echo "user prompt" | codex exec --model gpt-5.3-codex --full-auto
64
+
65
+ # Subsequent requests — resume subcommand
66
+ echo "follow-up" | codex exec resume "550e8400-xxxx" --model gpt-5.3-codex --full-auto
67
+ ```
68
+
69
+ **Key flags:**
70
+ - `codex exec resume <session-id>` — resume subcommand (not a flag)
71
+ - `--ephemeral` — skip session persistence (opposite of what we want)
72
+ - Session ID is a UUID
73
+
74
+ ## Session Registry Pattern (TypeScript)
75
+
76
+ ```typescript
77
+ interface CliSessionEntry {
78
+ sessionId: string; // UUID
79
+ provider: string; // "claude" | "gemini" | "codex"
80
+ model: string; // e.g. "claude-sonnet-4-6"
81
+ createdAt: number; // epoch ms
82
+ lastUsedAt: number; // epoch ms
83
+ requestCount: number; // total requests in this session
84
+ }
85
+
86
+ // Persist to JSON file
87
+ const SESSIONS_FILE = "~/.openclaw/cli-bridge/cli-sessions.json";
88
+
89
+ // Session lifecycle
90
+ function getOrCreateSession(provider: string, model: string): CliSessionEntry {
91
+ const existing = sessions.get(model);
92
+
93
+ // Reuse if fresh enough
94
+ const TTL = 2 * 60 * 60 * 1000; // 2 hours
95
+ const MAX_REQUESTS = 50; // context rotation
96
+ if (existing &&
97
+ (Date.now() - existing.lastUsedAt) < TTL &&
98
+ existing.requestCount < MAX_REQUESTS) {
99
+ return existing;
100
+ }
101
+
102
+ // Create fresh session
103
+ return { sessionId: randomUUID(), provider, model, ... };
104
+ }
105
+
106
+ // After successful response
107
+ function recordSuccess(model: string): void {
108
+ session.requestCount++;
109
+ session.lastUsedAt = Date.now();
110
+ saveToDisk();
111
+ }
112
+
113
+ // On session error (corrupted, expired, not found)
114
+ function invalidate(model: string): void {
115
+ sessions.delete(model);
116
+ saveToDisk();
117
+ // Next request will auto-create a fresh session
118
+ }
119
+ ```
120
+
121
+ ## Session Expiry Strategy
122
+
123
+ | Condition | Action | Why |
124
+ |-----------|--------|-----|
125
+ | `lastUsedAt > 2 hours` | Create new session | Context may be stale |
126
+ | `requestCount >= 50` | Create new session | Prevent context bloat |
127
+ | CLI returns "session not found" | Invalidate + retry | Session file was cleaned up |
128
+ | CLI returns auth error | Refresh token + retry | OAuth token expired |
129
+ | CLI timeout (exit 143) | Keep session alive | Session is valid, API was slow |
130
+
131
+ ## Performance Impact (measured on openclaw-cli-bridge)
132
+
133
+ | Metric | Before (one-shot) | After (session resume) |
134
+ |--------|-------------------|----------------------|
135
+ | Prompt size per request | 18-25 KB | < 1 KB (new message only) |
136
+ | Sonnet response time | 80-120s (50% hang rate) | 5-10s |
137
+ | Haiku response time | 5-15s | 3-5s |
138
+ | Silent hang rate | ~50% | Near 0% |
139
+
140
+ ## Stream-JSON Mode (Future Enhancement)
141
+
142
+ Claude CLI supports bidirectional streaming via `--input-format stream-json --output-format stream-json --verbose`. This enables:
143
+ - **Persistent process** — don't spawn/kill per request, keep one running
144
+ - **Real-time streaming** — token-by-token output via SSE
145
+ - **Native tool calls** — Claude's own tools (Bash, Read, Write, Edit, Grep)
146
+ - **Rate limit visibility** — `rate_limit_event` messages show quota state
147
+ - **Cost tracking** — per-request cost in USD
148
+
149
+ ```bash
150
+ # Bidirectional streaming session
151
+ echo '{"type":"user","message":{"role":"user","content":"hello"}}' | \
152
+ claude -p \
153
+ --model claude-sonnet-4-6 \
154
+ --input-format stream-json \
155
+ --output-format stream-json \
156
+ --verbose \
157
+ --permission-mode bypassPermissions \
158
+ --dangerously-skip-permissions
159
+ ```
160
+
161
+ Response includes `session_id`, tool list, model info, thinking blocks, and full usage metrics. This is the path to a fully persistent agent process.
162
+
163
+ ## Files Reference (openclaw-cli-bridge-elvatis)
164
+
165
+ | File | What it does |
166
+ |------|-------------|
167
+ | `src/cli-runner.ts` | Session registry + `runClaude()`, `runGemini()`, `runCodex()` with resume |
168
+ | `src/config.ts` | `STALE_OUTPUT_TIMEOUT_MS = 30_000` (kill silent processes fast) |
169
+ | `src/tool-protocol.ts` | Tool schema injection + JSON response parsing |
170
+ | `src/proxy-server.ts` | Cross-provider fallback chains, empty-response detection |
171
+ | `src/debug-log.ts` | File-based debug log + SSE streaming |
172
+ | `~/.openclaw/cli-bridge/cli-sessions.json` | Persisted session registry |
173
+ | `~/.openclaw/cli-bridge/debug.log` | Real-time request lifecycle log |
174
+
175
+ ## Key Learnings
176
+
177
+ 1. **Claude Sonnet hangs silently** on large prompts (~50% of the time). NOT RAM (28GB free). Likely API-side rate limiting. Session resume fixes it by keeping prompts small.
178
+
179
+ 2. **Exit code 143 = SIGTERM**, not OOM. Our stale-output detector sends it when the CLI produces zero stdout for 30 seconds.
180
+
181
+ 3. **Haiku ignores JSON tool format** in long conversations — returns conversational text instead of `{"tool_calls":[...]}`. Fix: JSON reminder at the END of the prompt + reject text responses during tool loops.
182
+
183
+ 4. **Empty responses (0 bytes) must trigger fallback**, not be treated as success. The model exits 0 but produces nothing useful.
184
+
185
+ 5. **Cross-provider fallback chains** are essential: `Sonnet → Haiku → Gemini Flash → Codex`. Each provider has different failure modes.
186
+
187
+ 6. **The gateway loads plugins from `~/.openclaw/extensions/`**, NOT from the workspace. Must rsync + `openclaw gateway restart` after every change.
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  > OpenClaw plugin that bridges locally installed AI CLIs (Codex, Gemini, Claude Code, OpenCode, Pi) as model providers — with slash commands for instant model switching, restore, health testing, and model listing.
4
4
 
5
- **Current version:** `3.3.0`
5
+ **Current version:** `3.3.1`
6
6
 
7
7
  ---
8
8
 
@@ -406,6 +406,10 @@ npm run ci # lint + typecheck + test
406
406
 
407
407
  ## Changelog
408
408
 
409
+ ### v3.3.1
410
+ - **fix:** test requests no longer pollute `debug.log` — test instances (port 0) now skip file logging
411
+ - **fix:** Codex test updated for session resume args
412
+
409
413
  ### v3.3.0
410
414
  - **feat:** session resume for ALL CLI providers — Claude, Gemini, and Codex all now use persistent sessions with `--resume`. Unified session registry at `~/.openclaw/cli-bridge/cli-sessions.json`.
411
415
  - **feat:** auto-rotation: sessions expire after 2 hours OR 50 requests (whichever first) to prevent context bloat
package/SKILL.md CHANGED
@@ -68,4 +68,4 @@ On gateway restart, if any session has expired, a **WhatsApp alert** is sent aut
68
68
 
69
69
  See `README.md` for full configuration reference and architecture diagram.
70
70
 
71
- **Version:** 3.3.0
71
+ **Version:** 3.3.1
@@ -2,7 +2,7 @@
2
2
  "id": "openclaw-cli-bridge-elvatis",
3
3
  "slug": "openclaw-cli-bridge-elvatis",
4
4
  "name": "OpenClaw CLI Bridge",
5
- "version": "3.3.0",
5
+ "version": "3.3.1",
6
6
  "license": "MIT",
7
7
  "description": "Phase 1: openai-codex auth bridge. Phase 2: local HTTP proxy routing model calls through gemini/claude CLIs (vllm provider).",
8
8
  "providers": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elvatis_com/openclaw-cli-bridge-elvatis",
3
- "version": "3.3.0",
3
+ "version": "3.3.1",
4
4
  "description": "Bridges gemini, claude, and codex CLI tools as OpenClaw model providers. Reads existing CLI auth without re-login.",
5
5
  "type": "module",
6
6
  "openclaw": {
package/src/debug-log.ts CHANGED
@@ -38,11 +38,19 @@ function ts(): string {
38
38
  return new Date().toISOString();
39
39
  }
40
40
 
41
+ /**
42
+ * Suppress logging in test mode (vitest sets NODE_ENV or uses port 0).
43
+ * Without this, every test run pollutes the production debug log with 43+ fake requests.
44
+ */
45
+ let _enabled = true;
46
+ export function setDebugLogEnabled(enabled: boolean): void { _enabled = enabled; }
47
+
41
48
  /**
42
49
  * Append a debug line to the log file.
43
50
  * Non-blocking, never throws — logging must not crash the bridge.
44
51
  */
45
52
  export function debugLog(category: string, message: string, data?: Record<string, unknown>): void {
53
+ if (!_enabled) return;
46
54
  try {
47
55
  ensureDir();
48
56
  rotate();
@@ -34,7 +34,7 @@ import {
34
34
  DEFAULT_MODEL_TIMEOUTS,
35
35
  TOOL_ROUTING_THRESHOLD,
36
36
  } from "./config.js";
37
- import { debugLog, DEBUG_LOG_PATH, getLogTail, watchLogFile } from "./debug-log.js";
37
+ import { debugLog, DEBUG_LOG_PATH, getLogTail, watchLogFile, setDebugLogEnabled } from "./debug-log.js";
38
38
 
39
39
  // ── Active request tracking ─────────────────────────────────────────────────
40
40
 
@@ -212,6 +212,9 @@ export function startProxyServer(opts: ProxyServerOptions): Promise<http.Server>
212
212
  reject(err);
213
213
  }
214
214
  });
215
+ // Disable debug file logging for test instances (port 0) to avoid polluting production logs
216
+ if (opts.port === 0) setDebugLogEnabled(false);
217
+
215
218
  server.listen(opts.port, "127.0.0.1", () => {
216
219
  opts.log(
217
220
  `[cli-bridge] proxy listening on :${opts.port}`
@@ -87,7 +87,7 @@ describe("runCodex()", () => {
87
87
  expect(result).toBe("codex result");
88
88
  expect(mockSpawn).toHaveBeenCalledWith(
89
89
  "codex",
90
- ["exec", "--model", "gpt-5.3-codex", "--full-auto"],
90
+ expect.arrayContaining(["exec", "--model", "gpt-5.3-codex", "--full-auto"]),
91
91
  expect.any(Object)
92
92
  );
93
93
  });