From 17bf0761f5effa40a7e459a4acbc1c06c8f5859e Mon Sep 17 00:00:00 2001 From: nyk <93952610+0xNyk@users.noreply.github.com> Date: Mon, 16 Mar 2026 12:18:12 +0700 Subject: [PATCH] fix: migrate clawdbot CLI calls to gateway RPC and improve session labels (#407) Replace all `runClawdbot(['-c', ...])` invocations with `callOpenClawGateway()` RPC calls in session control, spawn, and delete routes. This eliminates the deprecated CLI dependency and uses the structured gateway protocol instead. Also add `formatSessionLabel()` helper and `useAgentSessions()` hook to task-board-panel for better session dropdown display labels. Closes #401, closes #406 --- src/app/api/sessions/[id]/control/route.ts | 20 ++++------ src/app/api/sessions/route.ts | 28 +++++++------- src/app/api/spawn/route.ts | 43 +++++----------------- src/components/panels/task-board-panel.tsx | 33 ++++++++++++++--- 4 files changed, 59 insertions(+), 65 deletions(-) diff --git a/src/app/api/sessions/[id]/control/route.ts b/src/app/api/sessions/[id]/control/route.ts index 2500fb8..4446d70 100644 --- a/src/app/api/sessions/[id]/control/route.ts +++ b/src/app/api/sessions/[id]/control/route.ts @@ -1,6 +1,6 @@ import { NextRequest, NextResponse } from 'next/server' import { requireRole } from '@/lib/auth' -import { runClawdbot } from '@/lib/command' +import { callOpenClawGateway } from '@/lib/openclaw-gateway' import { db_helpers } from '@/lib/db' import { mutationLimiter } from '@/lib/rate-limit' import { logger } from '@/lib/logger' @@ -36,20 +36,14 @@ export async function POST( ) } - let result + let result: unknown if (action === 'terminate') { - result = await runClawdbot( - ['-c', `sessions_kill("${id}")`], - { timeoutMs: 10000 } - ) + result = await callOpenClawGateway('sessions_kill', { sessionKey: id }, 10_000) } else { const message = action === 'monitor' - ? JSON.stringify({ type: 'control', action: 'monitor' }) - : JSON.stringify({ type: 'control', action: 'pause' }) - result = await runClawdbot( - ['-c', `sessions_send("${id}", ${JSON.stringify(message)})`], - { timeoutMs: 10000 } - ) + ? { type: 'control', action: 'monitor' } + : { type: 'control', action: 'pause' } + result = await callOpenClawGateway('sessions_send', { sessionKey: id, message }, 10_000) } db_helpers.logActivity( @@ -65,7 +59,7 @@ export async function POST( success: true, action, session: id, - stdout: result.stdout.trim(), + result, }) } catch (error: any) { logger.error({ err: error }, 'Session control error') diff --git a/src/app/api/sessions/route.ts b/src/app/api/sessions/route.ts index 5094dca..2e05693 100644 --- a/src/app/api/sessions/route.ts +++ b/src/app/api/sessions/route.ts @@ -5,7 +5,7 @@ import { scanCodexSessions } from '@/lib/codex-sessions' import { scanHermesSessions } from '@/lib/hermes-sessions' import { getDatabase, db_helpers } from '@/lib/db' import { requireRole } from '@/lib/auth' -import { runClawdbot } from '@/lib/command' +import { callOpenClawGateway } from '@/lib/openclaw-gateway' import { mutationLimiter } from '@/lib/rate-limit' import { logger } from '@/lib/logger' @@ -60,7 +60,8 @@ export async function POST(request: NextRequest) { return NextResponse.json({ error: 'Invalid session key' }, { status: 400 }) } - let rpcFn: string + let rpcMethod: string + let rpcParams: Record let logDetail: string switch (action) { @@ -69,7 +70,8 @@ export async function POST(request: NextRequest) { if (!VALID_THINKING_LEVELS.includes(level)) { return NextResponse.json({ error: `Invalid thinking level. Must be: ${VALID_THINKING_LEVELS.join(', ')}` }, { status: 400 }) } - rpcFn = `session_setThinking("${sessionKey}", "${level}")` + rpcMethod = 'session_setThinking' + rpcParams = { sessionKey, level } logDetail = `Set thinking=${level} on ${sessionKey}` break } @@ -78,7 +80,8 @@ export async function POST(request: NextRequest) { if (!VALID_VERBOSE_LEVELS.includes(level)) { return NextResponse.json({ error: `Invalid verbose level. Must be: ${VALID_VERBOSE_LEVELS.join(', ')}` }, { status: 400 }) } - rpcFn = `session_setVerbose("${sessionKey}", "${level}")` + rpcMethod = 'session_setVerbose' + rpcParams = { sessionKey, level } logDetail = `Set verbose=${level} on ${sessionKey}` break } @@ -87,7 +90,8 @@ export async function POST(request: NextRequest) { if (!VALID_REASONING_LEVELS.includes(level)) { return NextResponse.json({ error: `Invalid reasoning level. Must be: ${VALID_REASONING_LEVELS.join(', ')}` }, { status: 400 }) } - rpcFn = `session_setReasoning("${sessionKey}", "${level}")` + rpcMethod = 'session_setReasoning' + rpcParams = { sessionKey, level } logDetail = `Set reasoning=${level} on ${sessionKey}` break } @@ -96,7 +100,8 @@ export async function POST(request: NextRequest) { if (typeof label !== 'string' || label.length > 100) { return NextResponse.json({ error: 'Label must be a string up to 100 characters' }, { status: 400 }) } - rpcFn = `session_setLabel("${sessionKey}", ${JSON.stringify(label)})` + rpcMethod = 'session_setLabel' + rpcParams = { sessionKey, label } logDetail = `Set label="${label}" on ${sessionKey}` break } @@ -104,7 +109,7 @@ export async function POST(request: NextRequest) { return NextResponse.json({ error: 'Invalid action. Must be: set-thinking, set-verbose, set-reasoning, set-label' }, { status: 400 }) } - const result = await runClawdbot(['-c', rpcFn], { timeoutMs: 10000 }) + const result = await callOpenClawGateway(rpcMethod, rpcParams, 10_000) db_helpers.logActivity( 'session_control', @@ -115,7 +120,7 @@ export async function POST(request: NextRequest) { { session_key: sessionKey, action } ) - return NextResponse.json({ success: true, action, sessionKey, stdout: result.stdout.trim() }) + return NextResponse.json({ success: true, action, sessionKey, result }) } catch (error: any) { logger.error({ err: error }, 'Session POST error') return NextResponse.json({ error: error.message || 'Session action failed' }, { status: 500 }) @@ -137,10 +142,7 @@ export async function DELETE(request: NextRequest) { return NextResponse.json({ error: 'Invalid session key' }, { status: 400 }) } - const result = await runClawdbot( - ['-c', `session_delete("${sessionKey}")`], - { timeoutMs: 10000 } - ) + const result = await callOpenClawGateway('session_delete', { sessionKey }, 10_000) db_helpers.logActivity( 'session_control', @@ -151,7 +153,7 @@ export async function DELETE(request: NextRequest) { { session_key: sessionKey, action: 'delete' } ) - return NextResponse.json({ success: true, sessionKey, stdout: result.stdout.trim() }) + return NextResponse.json({ success: true, sessionKey, result }) } catch (error: any) { logger.error({ err: error }, 'Session DELETE error') return NextResponse.json({ error: error.message || 'Session deletion failed' }, { status: 500 }) diff --git a/src/app/api/spawn/route.ts b/src/app/api/spawn/route.ts index 68106f9..3c8ec64 100644 --- a/src/app/api/spawn/route.ts +++ b/src/app/api/spawn/route.ts @@ -1,6 +1,6 @@ import { NextRequest, NextResponse } from 'next/server' -import { runClawdbot } from '@/lib/command' import { requireRole } from '@/lib/auth' +import { callOpenClawGateway } from '@/lib/openclaw-gateway' import { config } from '@/lib/config' import { readdir, readFile, stat } from 'fs/promises' import { join } from 'path' @@ -14,11 +14,6 @@ function getPreferredToolsProfile(): string { return String(process.env.OPENCLAW_TOOLS_PROFILE || 'coding').trim() || 'coding' } -async function runSpawnWithCompatibility(spawnPayload: Record) { - const commandArg = `sessions_spawn(${JSON.stringify(spawnPayload)})` - return runClawdbot(['-c', commandArg], { timeoutMs: 10000 }) -} - export async function POST(request: NextRequest) { const auth = requireRole(request, 'operator') if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status }) @@ -68,19 +63,14 @@ export async function POST(request: NextRequest) { } try { - // Execute the spawn command (OpenClaw 2026.3.2+ defaults tools.profile to messaging). - let stdout = '' - let stderr = '' + // Call gateway sessions_spawn directly. Try with tools.profile first, + // fall back without it for older gateways that don't support the field. + let result: any let compatibilityFallbackUsed = false try { - const result = await runSpawnWithCompatibility(spawnPayload) - stdout = result.stdout - stderr = result.stderr + result = await callOpenClawGateway('sessions_spawn', spawnPayload, 15_000) } catch (firstError: any) { - const rawErr = String(firstError?.stderr || firstError?.message || '').toLowerCase() - // Only retry without tools.profile when the error specifically indicates the - // gateway doesn't recognize the tools/profile fields. Other errors (auth, - // network, model not found, etc.) should propagate immediately. + const rawErr = String(firstError?.message || '').toLowerCase() const isToolsSchemaError = (rawErr.includes('unknown field') || rawErr.includes('unknown key') || rawErr.includes('invalid argument')) && (rawErr.includes('tools') || rawErr.includes('profile')) @@ -88,23 +78,11 @@ export async function POST(request: NextRequest) { const fallbackPayload = { ...spawnPayload } delete (fallbackPayload as any).tools - const fallback = await runSpawnWithCompatibility(fallbackPayload) - stdout = fallback.stdout - stderr = fallback.stderr + result = await callOpenClawGateway('sessions_spawn', fallbackPayload, 15_000) compatibilityFallbackUsed = true } - // Parse the response to extract session info - let sessionInfo = null - try { - // Look for session information in stdout - const sessionMatch = stdout.match(/Session created: (.+)/) - if (sessionMatch) { - sessionInfo = sessionMatch[1] - } - } catch (parseError) { - logger.error({ err: parseError }, 'Failed to parse session info') - } + const sessionInfo = result?.sessionId || result?.session_id || null const ipAddress = request.headers.get('x-forwarded-for') || request.headers.get('x-real-ip') || 'unknown' logAuditEvent({ @@ -131,8 +109,7 @@ export async function POST(request: NextRequest) { label, timeoutSeconds: timeout, createdAt: Date.now(), - stdout: stdout.trim(), - stderr: stderr.trim(), + result, compatibility: { toolsProfile: getPreferredToolsProfile(), fallbackUsed: compatibilityFallbackUsed, @@ -141,7 +118,7 @@ export async function POST(request: NextRequest) { } catch (execError: any) { logger.error({ err: execError }, 'Spawn execution error') - + return NextResponse.json({ success: false, spawnId, diff --git a/src/components/panels/task-board-panel.tsx b/src/components/panels/task-board-panel.tsx index c9ab387..5367f1e 100644 --- a/src/components/panels/task-board-panel.tsx +++ b/src/components/panels/task-board-panel.tsx @@ -95,22 +95,43 @@ const STATUS_COLUMN_KEYS = [ { key: 'done', titleKey: 'colDone', color: 'bg-green-500/20 text-green-400' }, ] +/** Build a human-readable label for a session key like "agent:nefes:telegram-group-123" */ +function formatSessionLabel(s: { key: string; channel?: string; kind?: string; label?: string }): string { + if (s.label) return s.label + // Extract the identifier part after the last colon: "agent:name:main" → "main" + const parts = (s.key || '').split(':') + const identifier = parts.length > 2 ? parts.slice(2).join(':') : s.key + const channel = s.channel || '' + if (channel && identifier !== 'main') { + return `${channel} (${identifier})` + } + if (channel) return `${channel} (${s.kind || 'default'})` + return identifier || s.key +} + /** Fetch active gateway sessions for a given agent name. */ function useAgentSessions(agentName: string | undefined) { - const [sessions, setSessions] = useState>([]) + const [sessions, setSessions] = useState>([]) useEffect(() => { if (!agentName) { setSessions([]); return } let cancelled = false - fetch('/api/sessions?include_local=1') + fetch('/api/sessions') .then(r => r.json()) .then(data => { if (cancelled) return - const all = (data.sessions || []) as Array<{ key: string; id: string; agent?: string; channel?: string; label?: string; active?: boolean }> + const all = (data.sessions || []) as Array<{ key: string; id: string; agent?: string; channel?: string; kind?: string; label?: string; active?: boolean }> const filtered = all.filter(s => s.agent?.toLowerCase() === agentName.toLowerCase() || s.key?.toLowerCase().includes(agentName.toLowerCase()) ) - setSessions(filtered.map(s => ({ key: s.key, id: s.id, channel: s.channel, label: s.label }))) + setSessions(filtered.map(s => ({ + key: s.key, + id: s.id, + channel: s.channel, + kind: s.kind, + label: s.label, + displayLabel: formatSessionLabel(s), + }))) }) .catch(() => { if (!cancelled) setSessions([]) }) return () => { cancelled = true } @@ -2052,7 +2073,7 @@ function CreateTaskModal({ {agentSessions.map(s => ( ))} @@ -2305,7 +2326,7 @@ function EditTaskModal({ {agentSessions.map(s => ( ))}