From 5d7b05b4f6d47416e4e08b6e2805e1e26d352e95 Mon Sep 17 00:00:00 2001 From: Tom Watts Date: Sat, 14 Mar 2026 09:38:55 +0000 Subject: [PATCH] fix(tasks): use --expect-final for agent dispatch and Aegis review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The two-step agent → agent.wait pattern used in dispatchAssignedTasks and runAegisReviews only returns lifecycle metadata (runId, status, timestamps). The agent's actual response text is only available via --expect-final on the initial agent call, which blocks until completion and returns the full payload including result.payloads[0].text. Without this fix: - Task resolution is stored as the raw wait JSON instead of the agent's response - Aegis cannot parse a VERDICT from the resolution, so it always defaults to rejected - Tasks are permanently stuck in a reject/retry loop and never complete Fix: replace the two-call pattern with a single --expect-final call in both dispatchAssignedTasks and runAegisReviews. Also improve sessionId extraction to use the agentMeta path from the final payload. Co-authored-by: Tom Watts --- src/lib/task-dispatch.ts | 49 ++++++++++++++++------------------------ 1 file changed, 19 insertions(+), 30 deletions(-) diff --git a/src/lib/task-dispatch.ts b/src/lib/task-dispatch.ts index e52825b..7aee2ff 100644 --- a/src/lib/task-dispatch.ts +++ b/src/lib/task-dispatch.ts @@ -215,22 +215,18 @@ export async function runAegisReviews(): Promise<{ ok: boolean; message: string idempotencyKey: `aegis-review-${task.id}-${Date.now()}`, deliver: false, } - const invokeResult = await runOpenClaw( - ['gateway', 'call', 'agent', '--timeout', '10000', '--params', JSON.stringify(invokeParams), '--json'], - { timeoutMs: 12_000 } - ) - const acceptedPayload = parseGatewayJson(invokeResult.stdout) - ?? parseGatewayJson(String((invokeResult as any)?.stderr || '')) - const runId = acceptedPayload?.runId - if (!runId) throw new Error('Gateway did not return a runId for Aegis review') - - const waitResult = await runOpenClaw( - ['gateway', 'call', 'agent.wait', '--timeout', '120000', '--params', JSON.stringify({ runId, timeoutMs: 115_000 }), '--json'], + // Use --expect-final to block until the agent completes and returns the full + // response payload (payloads[0].text). The two-step agent → agent.wait pattern + // only returns lifecycle metadata (runId/status/timestamps) and never includes + // the agent's actual text, so Aegis could never parse a verdict. + const finalResult = await runOpenClaw( + ['gateway', 'call', 'agent', '--expect-final', '--timeout', '120000', '--params', JSON.stringify(invokeParams), '--json'], { timeoutMs: 125_000 } ) - const waitPayload = parseGatewayJson(waitResult.stdout) + const finalPayload = parseGatewayJson(finalResult.stdout) + ?? parseGatewayJson(String((finalResult as any)?.stderr || '')) const agentResponse = parseAgentResponse( - waitPayload?.result ? JSON.stringify(waitPayload.result) : waitResult.stdout + finalPayload?.result ? JSON.stringify(finalPayload.result) : finalResult.stdout ) if (!agentResponse.text) { throw new Error('Aegis review returned empty response') @@ -382,28 +378,21 @@ export async function dispatchAssignedTasks(): Promise<{ ok: boolean; message: s idempotencyKey: `task-dispatch-${task.id}-${Date.now()}`, deliver: false, } - const invokeResult = await runOpenClaw( - ['gateway', 'call', 'agent', '--timeout', '10000', '--params', JSON.stringify(invokeParams), '--json'], - { timeoutMs: 12_000 } - ) - const acceptedPayload = parseGatewayJson(invokeResult.stdout) - ?? parseGatewayJson(String((invokeResult as any)?.stderr || '')) - const runId = acceptedPayload?.runId - if (!runId) throw new Error('Gateway did not return a runId for task dispatch') - - // Step 2: Wait for completion - const waitResult = await runOpenClaw( - ['gateway', 'call', 'agent.wait', '--timeout', '120000', '--params', JSON.stringify({ runId, timeoutMs: 115_000 }), '--json'], + // Use --expect-final to block until the agent completes and returns the full + // response payload (result.payloads[0].text). The two-step agent → agent.wait + // pattern only returns lifecycle metadata and never includes the agent's text. + const finalResult = await runOpenClaw( + ['gateway', 'call', 'agent', '--expect-final', '--timeout', '120000', '--params', JSON.stringify(invokeParams), '--json'], { timeoutMs: 125_000 } ) - const waitPayload = parseGatewayJson(waitResult.stdout) + const finalPayload = parseGatewayJson(finalResult.stdout) + ?? parseGatewayJson(String((finalResult as any)?.stderr || '')) const agentResponse = parseAgentResponse( - waitPayload?.result ? JSON.stringify(waitPayload.result) : waitResult.stdout + finalPayload?.result ? JSON.stringify(finalPayload.result) : finalResult.stdout ) - // Capture sessionId from the wait payload if not in the parsed response - if (!agentResponse.sessionId && waitPayload?.sessionId) { - agentResponse.sessionId = waitPayload.sessionId + if (!agentResponse.sessionId && finalPayload?.result?.meta?.agentMeta?.sessionId) { + agentResponse.sessionId = finalPayload.result.meta.agentMeta.sessionId } if (!agentResponse.text) {