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
This commit is contained in:
Nyk 2026-03-13 12:32:25 +07:00
parent 626c8fabff
commit d53d93351c
1 changed files with 31 additions and 5 deletions

View File

@ -13,12 +13,25 @@ interface DispatchableTask {
workspace_id: number workspace_id: number
agent_name: string agent_name: string
agent_id: number agent_id: number
agent_config: string | null
ticket_prefix: string | null ticket_prefix: string | null
project_ticket_no: number | null project_ticket_no: number | null
project_id: number | null project_id: number | null
tags?: string[] 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 { function buildTaskPrompt(task: DispatchableTask, rejectionFeedback?: string | null): string {
const ticket = task.ticket_prefix && task.project_ticket_no const ticket = task.ticket_prefix && task.project_ticket_no
? `${task.ticket_prefix}-${String(task.project_ticket_no).padStart(3, '0')}` ? `${task.ticket_prefix}-${String(task.project_ticket_no).padStart(3, '0')}`
@ -94,11 +107,22 @@ interface ReviewableTask {
description: string | null description: string | null
resolution: string | null resolution: string | null
assigned_to: string | null assigned_to: string | null
agent_config: string | null
workspace_id: number workspace_id: number
ticket_prefix: string | null ticket_prefix: string | null
project_ticket_no: number | 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 { function buildReviewPrompt(task: ReviewableTask): string {
const ticket = task.ticket_prefix && task.project_ticket_no const ticket = task.ticket_prefix && task.project_ticket_no
? `${task.ticket_prefix}-${String(task.project_ticket_no).padStart(3, '0')}` ? `${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(` const tasks = db.prepare(`
SELECT t.id, t.title, t.description, t.resolution, t.assigned_to, t.workspace_id, 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 FROM tasks t
LEFT JOIN projects p ON p.id = t.project_id AND p.workspace_id = t.workspace_id 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' WHERE t.status = 'review'
ORDER BY t.updated_at ASC ORDER BY t.updated_at ASC
LIMIT 3 LIMIT 3
@ -181,8 +206,8 @@ export async function runAegisReviews(): Promise<{ ok: boolean; message: string
try { try {
const prompt = buildReviewPrompt(task) const prompt = buildReviewPrompt(task)
// Use the assigned agent or fall back to a default reviewer agent // Resolve the gateway agent ID from config, falling back to assigned_to or default
const reviewAgent = task.assigned_to || 'jarv' const reviewAgent = resolveGatewayAgentIdForReview(task)
const invokeParams = { const invokeParams = {
message: prompt, message: prompt,
@ -290,7 +315,7 @@ export async function dispatchAssignedTasks(): Promise<{ ok: boolean; message: s
const db = getDatabase() const db = getDatabase()
const tasks = db.prepare(` 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 p.ticket_prefix, t.project_ticket_no
FROM tasks t FROM tasks t
JOIN agents a ON a.name = t.assigned_to AND a.workspace_id = t.workspace_id 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) const prompt = buildTaskPrompt(task, rejectionFeedback)
// Step 1: Invoke via gateway // Step 1: Invoke via gateway
const gatewayAgentId = resolveGatewayAgentId(task)
const invokeParams = { const invokeParams = {
message: prompt, message: prompt,
agentId: task.agent_name, agentId: gatewayAgentId,
idempotencyKey: `task-dispatch-${task.id}-${Date.now()}`, idempotencyKey: `task-dispatch-${task.id}-${Date.now()}`,
deliver: false, deliver: false,
} }