From d53d93351cbfa0084b679ab49aa829c0a6d4e058 Mon Sep 17 00:00:00 2001 From: Nyk <0xnykcd@googlemail.com> Date: Fri, 13 Mar 2026 12:32:25 +0700 Subject: [PATCH 1/2] fix(tasks): use gateway agent ID instead of display name for dispatch Task dispatch and Aegis review were sending `agents.name` (the display name) as `agentId` to the gateway. When `identity.name` differs from the gateway agent `id`, the gateway rejects the call with "unknown agent id". Now extracts `openclawId` from the agent's config JSON (set during agent sync) and uses that for gateway invocations, falling back to the display name for backwards compatibility. Closes #310 --- src/lib/task-dispatch.ts | 36 +++++++++++++++++++++++++++++++----- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/src/lib/task-dispatch.ts b/src/lib/task-dispatch.ts index c3e4aa2..e52825b 100644 --- a/src/lib/task-dispatch.ts +++ b/src/lib/task-dispatch.ts @@ -13,12 +13,25 @@ interface DispatchableTask { workspace_id: number agent_name: string agent_id: number + agent_config: string | null ticket_prefix: string | null project_ticket_no: number | null project_id: number | null tags?: string[] } +/** Extract the gateway agent identifier from the agent's config JSON. + * Falls back to agent_name (display name) if openclawId is not set. */ +function resolveGatewayAgentId(task: DispatchableTask): string { + if (task.agent_config) { + try { + const cfg = JSON.parse(task.agent_config) + if (typeof cfg.openclawId === 'string' && cfg.openclawId) return cfg.openclawId + } catch { /* ignore */ } + } + return task.agent_name +} + function buildTaskPrompt(task: DispatchableTask, rejectionFeedback?: string | null): string { const ticket = task.ticket_prefix && task.project_ticket_no ? `${task.ticket_prefix}-${String(task.project_ticket_no).padStart(3, '0')}` @@ -94,11 +107,22 @@ interface ReviewableTask { description: string | null resolution: string | null assigned_to: string | null + agent_config: string | null workspace_id: number ticket_prefix: string | null project_ticket_no: number | null } +function resolveGatewayAgentIdForReview(task: ReviewableTask): string { + if (task.agent_config) { + try { + const cfg = JSON.parse(task.agent_config) + if (typeof cfg.openclawId === 'string' && cfg.openclawId) return cfg.openclawId + } catch { /* ignore */ } + } + return task.assigned_to || 'jarv' +} + function buildReviewPrompt(task: ReviewableTask): string { const ticket = task.ticket_prefix && task.project_ticket_no ? `${task.ticket_prefix}-${String(task.project_ticket_no).padStart(3, '0')}` @@ -154,9 +178,10 @@ export async function runAegisReviews(): Promise<{ ok: boolean; message: string const tasks = db.prepare(` SELECT t.id, t.title, t.description, t.resolution, t.assigned_to, t.workspace_id, - p.ticket_prefix, t.project_ticket_no + p.ticket_prefix, t.project_ticket_no, a.config as agent_config FROM tasks t LEFT JOIN projects p ON p.id = t.project_id AND p.workspace_id = t.workspace_id + LEFT JOIN agents a ON a.name = t.assigned_to AND a.workspace_id = t.workspace_id WHERE t.status = 'review' ORDER BY t.updated_at ASC LIMIT 3 @@ -181,8 +206,8 @@ export async function runAegisReviews(): Promise<{ ok: boolean; message: string try { const prompt = buildReviewPrompt(task) - // Use the assigned agent or fall back to a default reviewer agent - const reviewAgent = task.assigned_to || 'jarv' + // Resolve the gateway agent ID from config, falling back to assigned_to or default + const reviewAgent = resolveGatewayAgentIdForReview(task) const invokeParams = { message: prompt, @@ -290,7 +315,7 @@ export async function dispatchAssignedTasks(): Promise<{ ok: boolean; message: s const db = getDatabase() const tasks = db.prepare(` - SELECT t.*, a.name as agent_name, a.id as agent_id, + SELECT t.*, a.name as agent_name, a.id as agent_id, a.config as agent_config, p.ticket_prefix, t.project_ticket_no FROM tasks t JOIN agents a ON a.name = t.assigned_to AND a.workspace_id = t.workspace_id @@ -350,9 +375,10 @@ export async function dispatchAssignedTasks(): Promise<{ ok: boolean; message: s const prompt = buildTaskPrompt(task, rejectionFeedback) // Step 1: Invoke via gateway + const gatewayAgentId = resolveGatewayAgentId(task) const invokeParams = { message: prompt, - agentId: task.agent_name, + agentId: gatewayAgentId, idempotencyKey: `task-dispatch-${task.id}-${Date.now()}`, deliver: false, } From 2cada89ee961f328e2c4530d96bf5e1ef361748f Mon Sep 17 00:00:00 2001 From: Nyk <0xnykcd@googlemail.com> Date: Fri, 13 Mar 2026 12:50:15 +0700 Subject: [PATCH 2/2] fix(runtime): allow all Node versions >= 22 The version check was an allowlist of specific majors (22, 24). Changed to a >= 22 floor so future Node releases work without code changes. --- package.json | 2 +- scripts/check-node-version.mjs | 9 ++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 0939a69..b1c0442 100644 --- a/package.json +++ b/package.json @@ -66,7 +66,7 @@ "vitest": "^2.1.5" }, "engines": { - "node": "22.x || 24.x" + "node": ">=22" }, "keywords": [ "openclaw", diff --git a/scripts/check-node-version.mjs b/scripts/check-node-version.mjs index 3fe8a04..8ccdc7e 100644 --- a/scripts/check-node-version.mjs +++ b/scripts/check-node-version.mjs @@ -1,16 +1,15 @@ #!/usr/bin/env node -const SUPPORTED_NODE_MAJORS = [22, 24] +const MIN_NODE_MAJOR = 22 const current = process.versions.node const currentMajor = Number.parseInt(current.split('.')[0] || '', 10) -if (!SUPPORTED_NODE_MAJORS.includes(currentMajor)) { - const supported = SUPPORTED_NODE_MAJORS.map((major) => `${major}.x`).join(' or ') +if (currentMajor < MIN_NODE_MAJOR) { console.error( [ - `error: Mission Control supports Node ${supported}, but found ${current}.`, - 'use `nvm use 22` (recommended LTS) or `nvm use 24` before installing, building, or starting the app.', + `error: Mission Control requires Node ${MIN_NODE_MAJOR} or later, but found ${current}.`, + 'use `nvm use 22` (recommended LTS) or any later version before installing, building, or starting the app.', ].join('\n') ) process.exit(1)