fix: end-to-end working after laptop integration test
Discovered during smoke-testing on hzhang's laptop: 1. `--channels server:X --dangerously-load-development-channels server:X` makes Claude Code list the channel twice and the second copy never inherits dev-mode, leaving "server: entries need --dangerously-load-development-channels" stuck in the status panel. Fix: pass channel ONLY via --dangerously-load-development-channels. 2. Without a controlling TTY, Claude Code's dev-mode confirmation dialog blocks forever waiting for keystrokes that never arrive. Fix: spawn claude wrapped in `script -q -c CMD PTYLOG` so it gets a PTY, then write "\r" to stdin at several timeouts (cheap to over-send). 3. process-manager.markReady was matching on PID, but the PID in the bridge hello frame is the ClaudePlugin (bun) process's pid, not the script-wrapped claude process's pid we tracked. Fix: match on openclaw-session-key, which is consistent on both sides. 4. First spawn for a new session can't use --resume (no transcript exists yet) — claude errors out. Fix: probe ~/.claude/projects/<workspace-slug>/<uuid>.jsonl for existence and use --session-id on fresh sessions, --resume after a process restart. 5. Add --debug-file per session so future debugging has the gating logs. 6. Local definePluginEntry shim (no openclaw runtime dependency) so `bun index.ts` works standalone for laptop smoke tests. End-to-end verified twice on laptop: curl POST -> SSE delta with the exact reply text. Average cold-start ~10s, hot path 2-3s.
This commit is contained in:
@@ -70,7 +70,7 @@ export function startBridgeServer(deps: BridgeDeps): { httpServer: http.Server;
|
||||
const prev = byKey.get(conn.openclawSessionKey)
|
||||
if (prev && prev.ws !== ws) try { prev.ws.close(4002, 'replaced by reconnect') } catch {}
|
||||
byKey.set(conn.openclawSessionKey, conn)
|
||||
processManager.markReady(conn.pid)
|
||||
processManager.markReady(conn.openclawSessionKey)
|
||||
log.info(`hello session=${conn.openclawSessionKey} pid=${conn.pid}`)
|
||||
ws.send(JSON.stringify({ type: 'hello_ack' }))
|
||||
return
|
||||
@@ -123,6 +123,7 @@ export function startBridgeServer(deps: BridgeDeps): { httpServer: http.Server;
|
||||
|
||||
// ── HTTP server (OpenAI-compatible /v1/chat/completions) ────────────────────
|
||||
const httpServer = http.createServer(async (req, res) => {
|
||||
log.info(`[trace] http req method=${req.method} url=${req.url}`)
|
||||
if (req.method === 'GET' && req.url === '/health') {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' })
|
||||
res.end(JSON.stringify({ ok: true, processes: processManager.list().length, conns: byKey.size }))
|
||||
@@ -132,14 +133,24 @@ export function startBridgeServer(deps: BridgeDeps): { httpServer: http.Server;
|
||||
res.writeHead(404); res.end(); return
|
||||
}
|
||||
|
||||
let bodyRaw = ''
|
||||
req.on('data', c => { bodyRaw += c })
|
||||
req.on('end', async () => {
|
||||
let body: any
|
||||
try { body = JSON.parse(bodyRaw) } catch {
|
||||
res.writeHead(400, { 'Content-Type': 'application/json' })
|
||||
res.end(JSON.stringify({ error: 'invalid_json' })); return
|
||||
}
|
||||
// Read the body synchronously (Promise-wrapped) so the outer `async`
|
||||
// handler holds the response open until we explicitly end it. Returning
|
||||
// from the outer fn before res.end() causes Bun's http server to close
|
||||
// the connection.
|
||||
const bodyRaw = await new Promise<string>(resolve => {
|
||||
let buf = ''
|
||||
req.on('data', c => { buf += c })
|
||||
req.on('end', () => resolve(buf))
|
||||
})
|
||||
|
||||
log.info(`[trace] body received bytes=${bodyRaw.length}`)
|
||||
let body: any
|
||||
try { body = JSON.parse(bodyRaw); log.info(`[trace] body parsed`) } catch (e) {
|
||||
log.warn(`[trace] body parse FAILED: ${e}`)
|
||||
res.writeHead(400, { 'Content-Type': 'application/json' })
|
||||
res.end(JSON.stringify({ error: 'invalid_json' })); return
|
||||
}
|
||||
{
|
||||
|
||||
// Minimal session-key extraction for v1 — use headers where OpenClaw
|
||||
// doesn't natively send them. Production OpenClaw runtime injects
|
||||
@@ -159,12 +170,19 @@ export function startBridgeServer(deps: BridgeDeps): { httpServer: http.Server;
|
||||
}
|
||||
|
||||
const completionId = `chatcmpl-synthesis-${randomUUID().slice(0, 8)}`
|
||||
res.writeHead(200, {
|
||||
'Content-Type': 'text/event-stream',
|
||||
'Cache-Control': 'no-cache',
|
||||
'Connection': 'keep-alive',
|
||||
'Transfer-Encoding': 'chunked',
|
||||
})
|
||||
log.info(`[trace] before writeHead session=${sessionKey} user=${latestUser.substring(0,50)}`)
|
||||
try {
|
||||
res.writeHead(200, {
|
||||
'Content-Type': 'text/event-stream',
|
||||
'Cache-Control': 'no-cache',
|
||||
'Connection': 'keep-alive',
|
||||
'Transfer-Encoding': 'chunked',
|
||||
})
|
||||
log.info(`[trace] after writeHead`)
|
||||
} catch (e) {
|
||||
log.warn(`[trace] writeHead FAILED: ${e}`)
|
||||
return
|
||||
}
|
||||
|
||||
// SSE heartbeat: empty content delta every HEARTBEAT_MS to keep
|
||||
// OpenClaw's LLM idle watchdog from firing during long quiet turns.
|
||||
@@ -215,7 +233,7 @@ export function startBridgeServer(deps: BridgeDeps): { httpServer: http.Server;
|
||||
clearInterval(sseHeartbeat)
|
||||
releaseSlot()
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
httpServer.listen(config.bridgePort, '127.0.0.1', () => {
|
||||
|
||||
Reference in New Issue
Block a user