diff --git a/lib/app/app_controller_desktop_thread_actions.dart b/lib/app/app_controller_desktop_thread_actions.dart index b7926eec..6a9b2500 100644 --- a/lib/app/app_controller_desktop_thread_actions.dart +++ b/lib/app/app_controller_desktop_thread_actions.dart @@ -1324,11 +1324,15 @@ extension AppControllerDesktopThreadActions on AppController { return; } if (!result.success) { - clearGatewayTaskArtifactStateInternal( - sessionKey, - completedAtMs: completedAtMs, - syncStatus: 'failed', - ); + if (hasCurrentRunArtifacts) { + await persistGoTaskArtifactsForSessionInternal(sessionKey, result); + } else { + clearGatewayTaskArtifactStateInternal( + sessionKey, + completedAtMs: completedAtMs, + syncStatus: 'failed', + ); + } appendLocalSessionMessageInternal( sessionKey, assistantErrorMessageInternal( diff --git a/lib/runtime/go_task_service_client.dart b/lib/runtime/go_task_service_client.dart index 892c3d23..e801b65c 100644 --- a/lib/runtime/go_task_service_client.dart +++ b/lib/runtime/go_task_service_client.dart @@ -621,15 +621,19 @@ Object? _firstGoTaskArtifactList(Map result) { final artifacts = []; for (final candidate in [ result['artifacts'], + result['finalArtifacts'], result['files'], result['attachments'], _castMap(result['payload'])['artifacts'], + _castMap(result['payload'])['finalArtifacts'], _castMap(result['payload'])['files'], _castMap(result['payload'])['attachments'], _castMap(result['result'])['artifacts'], + _castMap(result['result'])['finalArtifacts'], _castMap(result['result'])['files'], _castMap(result['result'])['attachments'], _castMap(result['data'])['artifacts'], + _castMap(result['data'])['finalArtifacts'], _castMap(result['data'])['files'], _castMap(result['data'])['attachments'], ]) { diff --git a/test/runtime/assistant_execution_target_test.dart b/test/runtime/assistant_execution_target_test.dart index d13c1b22..c82e22f1 100644 --- a/test/runtime/assistant_execution_target_test.dart +++ b/test/runtime/assistant_execution_target_test.dart @@ -1953,6 +1953,55 @@ void main() { }, ); + test( + 'sendChatMessage keeps partial OpenClaw artifacts on terminal artifact failure', + () async { + final fakeGoTaskService = _RecordingGoTaskServiceClient() + ..outcomes.add( + const GoTaskServiceResult( + success: false, + message: 'OpenClaw completed without required final artifacts.', + turnId: 'turn-1', + raw: { + 'status': 'failed', + 'code': 'OPENCLAW_REQUIRED_ARTIFACT_MISSING', + 'artifacts': >[ + { + 'relativePath': 'stages/chapter.md', + 'content': 'partial chapter', + 'contentType': 'text/markdown', + }, + ], + }, + errorMessage: + 'openclaw returned partial artifacts without required final deliverables', + resolvedModel: '', + route: GoTaskServiceRoute.externalAcpSingle, + ), + ); + final controller = _connectedController(fakeGoTaskService); + addTearDown(controller.dispose); + + await controller.sessionsController.switchSession( + 'unit-fixture-task-a', + ); + + await controller.sendChatMessage('first turn'); + + final failedThread = controller.taskThreadForSessionInternal( + 'unit-fixture-task-a', + ); + expect( + failedThread?.lifecycleState.lastResultCode, + 'OPENCLAW_REQUIRED_ARTIFACT_MISSING', + ); + expect(failedThread?.lastArtifactSyncStatus, 'synced'); + expect(failedThread?.lastTaskArtifactRelativePaths, [ + 'stages/chapter.md', + ]); + }, + ); + test( 'sendChatMessage treats nested OpenClaw artifact errors as terminal failures', () async { diff --git a/test/runtime/gateway_acp_client_auth_test.dart b/test/runtime/gateway_acp_client_auth_test.dart index dc9be44b..1b9e191c 100644 --- a/test/runtime/gateway_acp_client_auth_test.dart +++ b/test/runtime/gateway_acp_client_auth_test.dart @@ -251,6 +251,33 @@ void main() { 'https://xworkmate-bridge.svc.plus/artifacts/summary.pdf', ); }); + + test('uses OpenClaw finalArtifacts as required deliverables', () { + final result = goTaskServiceResultFromAcpResponse({ + 'jsonrpc': '2.0', + 'id': 'request-id', + 'result': { + 'success': true, + 'message': 'created final deliverable', + 'finalArtifacts': >[ + { + 'relativePath': 'exports/final.pdf', + 'downloadUrl': + 'https://xworkmate-bridge.svc.plus/artifacts/final.pdf', + 'contentType': 'application/pdf', + }, + ], + }, + }, route: GoTaskServiceRoute.externalAcpSingle); + + expect(result.success, isTrue); + expect(result.artifacts, hasLength(1)); + expect(result.artifacts.single.relativePath, 'exports/final.pdf'); + expect( + result.artifacts.single.downloadUrl, + 'https://xworkmate-bridge.svc.plus/artifacts/final.pdf', + ); + }); }); group('GatewayAcpClient authorization', () { @@ -2128,8 +2155,6 @@ void main() { ); }, ); - - }); }