Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
45878f4
spec: session pooling TRD rev 2 — resolve all 14 sub-agent findings
Mar 21, 2026
4115d3f
spec: session pooling TRD Rev 3 — resolve 7 findings from second review
Mar 21, 2026
ab9dff0
spec: session pooling TRD Rev 4 — resolve 7 findings from third Opus …
Mar 21, 2026
c1c4d66
spec: session pooling TRD Rev 5 — resolve 4 findings from fourth Opus…
Mar 21, 2026
2aac92b
feat: session pooling — M1 router, M2 route integration, M3 startup/s…
Mar 21, 2026
d55f0e5
chore: M4 prototype cleanup — remove pool.ts, standalone-pool.ts, upd…
Mar 21, 2026
b26598d
feat: session pooling — locked process pool with per-session serializ…
Mar 21, 2026
c9f18ac
fix: M1-21 clearSessionLock canonical path, M1-22 recycle kill+respaw…
Mar 21, 2026
8886e88
fix: route sentinel cleanup through clearSessionLock (M1-21 complete)
Mar 21, 2026
3a63c75
feat: session pooling — locked process pool with per-session serializ…
sterling-prog Mar 21, 2026
ebdcdfb
fix: fall back to body sessionId when session-key header absent
Mar 22, 2026
4ef1787
feat: add unhandledRejection and uncaughtException handlers (proxy-cr…
Apr 8, 2026
99da3ca
fix: guard all 4 emit("error") sites with listenerCount check (proxy-…
Apr 8, 2026
1e21843
[P-safe-emit] Add listenerCount guard to all error emit sites (proxy …
Apr 8, 2026
f55bdb7
[P130] Safe EventEmitter error emission guards (proxy crash fix)
Apr 8, 2026
dd20a82
Refactor safeEmitter to factory pattern, apply to all creation sites
Apr 8, 2026
58f0a87
fix: simplify safeEmitter to minimal permanent error listener
Apr 8, 2026
74a765d
Merge branch 'build/proxy-crash-handler' — unhandledRejection + uncau…
Apr 8, 2026
ff6db97
Merge branch 'build/proxy-safe-emit' — safeEmitter permanent error li…
Apr 8, 2026
052ffe4
[P?] Safe EventEmitter error guards (router)
Apr 8, 2026
09f6179
fix: safeListenerCount() + targeted listener removal
Apr 9, 2026
6e587f3
feat: add request_received + request_routed logging
Apr 9, 2026
faef78c
fix: release process on client disconnect (cancelRequest)
Apr 9, 2026
59a7188
fix: SSE keep-alive to prevent gateway timeout on slow first-token
Apr 9, 2026
dfe9daa
revert: remove SSE keep-alive (broke gateway stream parsing)
Apr 9, 2026
3d27b77
fix: clear session lock in cancelRequest before kill
Apr 9, 2026
02b622b
[Psession-pool-context-fix] fix: eliminate quadratic context growth i…
Apr 14, 2026
f288a00
fix: remove dead listenerCount guards and unused PendingRequest.resolve
Apr 14, 2026
3b528a7
fix: address Indent CODE_QUALITY — listener cleanup and extractText g…
Apr 14, 2026
96a8eb2
[P0] Detect gateway session resets via message count drop (router)
Apr 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 34 additions & 5 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,39 @@ launchctl list com.openclaw.claude-max-proxy

## Architecture

- `src/types/claude-cli.ts` - Claude CLI JSON streaming types and type guards
- `src/types/openai.ts` - OpenAI-compatible API types
The proxy uses a session-aware process pool to eliminate per-request spawn overhead
(3–10s) and prevent cross-agent context contamination.

### Key files

- `src/subprocess/router.ts` - **SessionPoolRouter**: session-key locking, per-model warm pools, orphan reclamation, context accumulation recycling, nightly sweep
- `src/subprocess/manager.ts` - **ClaudeSubprocess**: single-request subprocess fallback (retained for headerless requests and non-pooled models)
- `src/server/routes.ts` - Express route handlers; routes pool requests via `x-openclaw-session-key` header; falls back to ClaudeSubprocess when header is absent
- `src/server/standalone.ts` - Server entry point; initializes pool, schedules 3 AM ET sweep via node-cron, handles graceful shutdown
- `src/adapter/openai-to-cli.ts` - Converts OpenAI requests to CLI input
- `src/adapter/cli-to-openai.ts` - Converts CLI output to OpenAI responses
- `src/subprocess/manager.ts` - Spawns and manages Claude CLI subprocesses
- `src/server/routes.ts` - Express route handlers (streaming + non-streaming)
- `src/server/standalone.js` - Server entry point
- `src/types/claude-cli.ts` - Claude CLI JSON streaming types and type guards
- `src/types/openai.ts` - OpenAI-compatible API types

### Request routing

```
POST /v1/chat/completions
x-openclaw-session-key present AND model is opus/sonnet
→ SessionPoolRouter.execute() → locked warm process (33% faster)
header absent OR model is haiku
→ ClaudeSubprocess (subprocess-per-request, original behavior)
```

### Pool env vars

| Var | Default | Description |
|-----|---------|-------------|
| POOL_OPUS_SIZE | 6 | Warm opus processes |
| POOL_SONNET_SIZE | 4 | Warm sonnet processes |
| MAX_TOTAL_PROCESSES | 30 | Hard cap (locked + warm) |
| POOL_MAX_REQUESTS_PER_PROCESS | 50 | Context accumulation threshold |
| POOL_REQUEST_QUEUE_DEPTH | 3 | Per-process queue depth before 429 |
| POOL_REQUEST_TIMEOUT_MS | 300000 | Per-request timeout (5 min) |
| SWEEP_IDLE_THRESHOLD_MS | 7200000 | Idle time before sweep recycles (2 hr) |
| SWEEP_HOUR | 3 | Hour in ET for nightly sweep |
17 changes: 17 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@
},
"homepage": "https://github.com/atalovesyou/claude-max-api-proxy#readme",
"dependencies": {
"@types/node-cron": "^3.0.11",
"express": "^4.21.2",
"node-cron": "^4.2.1",
"uuid": "^11.0.5"
},
"devDependencies": {
Expand Down
658 changes: 658 additions & 0 deletions specs/session-pooling.spec.md

Large diffs are not rendered by default.

26 changes: 24 additions & 2 deletions src/adapter/openai-to-cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import type { OpenAIChatRequest, OpenAIContentBlock } from "../types/openai.js";
export type ClaudeModel = "opus" | "sonnet" | "haiku";

export interface CliInput {
prompt: string;
prompt: string; // Full prompt (system + history + user) — for first turn
latestPrompt: string; // Latest user message only — for subsequent turns
model: ClaudeModel;
sessionId?: string;
}
Expand Down Expand Up @@ -61,7 +62,11 @@ function extractText(content: string | OpenAIContentBlock[]): string {
}
if (Array.isArray(content)) {
return content
.filter((block) => block.type === "text" || block.type === "input_text")
.filter(
(block) =>
(block.type === "text" || block.type === "input_text") &&
block.text != null
)
.map((block) => block.text)
.join("\n");
}
Expand Down Expand Up @@ -132,12 +137,29 @@ export function messagesToPrompt(
return parts.join("\n").trim();
}

/**
* Extract only the latest user message from the messages array.
* Used by pooled processes on subsequent turns (requestCount > 0)
* where the CLI already has system context and prior turns in memory.
*/
export function latestUserMessage(
messages: OpenAIChatRequest["messages"]
): string {
for (let i = messages.length - 1; i >= 0; i--) {
if (messages[i].role === "user") {
return extractText(messages[i].content);
}
}
return "";
}

/**
* Convert OpenAI chat request to CLI input format
*/
export function openaiToCli(request: OpenAIChatRequest): CliInput {
return {
prompt: messagesToPrompt(request.messages),
latestPrompt: latestUserMessage(request.messages),
model: extractModel(request.model),
sessionId: request.user, // Use OpenAI's user field for session mapping
};
Expand Down
Loading