From cbd8e478c2d076d9fcd80302c29720a740b736de Mon Sep 17 00:00:00 2001 From: Haitao Pan Date: Fri, 22 May 2026 11:20:46 +0800 Subject: [PATCH] fix: recover openclaw interrupted results --- ...rnal_code_agent_acp_desktop_transport.dart | 106 +++++++++++++++++- lib/runtime/go_task_service_client.dart | 7 +- .../runtime/gateway_acp_client_auth_test.dart | 38 ++++++- 3 files changed, 144 insertions(+), 7 deletions(-) diff --git a/lib/runtime/external_code_agent_acp_desktop_transport.dart b/lib/runtime/external_code_agent_acp_desktop_transport.dart index 019ca26c..10041813 100644 --- a/lib/runtime/external_code_agent_acp_desktop_transport.dart +++ b/lib/runtime/external_code_agent_acp_desktop_transport.dart @@ -220,7 +220,7 @@ class ExternalCodeAgentAcpDesktopTransport .toString() .trim() .toLowerCase(); - final result = _castMap(snapshot['result']); + final result = _recoveredResultFromSessionSnapshot(snapshot); if (result.isNotEmpty && (status == 'completed' || status == 'failed' || @@ -307,7 +307,7 @@ class ExternalCodeAgentAcpDesktopTransport snapshot.remove('result'); snapshot['turnId'] = update.turnId; snapshot['success'] = !update.error; - final text = _firstNonEmptyString(snapshot, const [ + final text = _firstNonEmptyDisplayText(snapshot, const [ 'output', 'message', 'summary', @@ -322,9 +322,58 @@ class ExternalCodeAgentAcpDesktopTransport return snapshot; } - String _firstNonEmptyString(Map values, List keys) { + Map _recoveredResultFromSessionSnapshot( + Map snapshot, + ) { + final result = {..._castMap(snapshot['result'])}; + final artifactRecord = _castMap(snapshot['artifacts']); + final artifactItems = _listValue(artifactRecord['items']); + if (artifactItems.isNotEmpty && !_hasArtifactList(result)) { + result['artifacts'] = artifactItems; + } + for (final entry in { + 'remoteWorkingDirectory': 'remoteWorkingDirectory', + 'remoteWorkspaceRefKind': 'remoteWorkspaceRefKind', + 'resultSummary': 'summary', + }.entries) { + final value = artifactRecord[entry.key]?.toString().trim() ?? ''; + if (value.isNotEmpty && + (result[entry.value]?.toString().trim().isEmpty ?? true)) { + result[entry.value] = value; + } + } + return result; + } + + bool _hasArtifactList(Map result) { + for (final key in const ['artifacts', 'files', 'attachments']) { + if (_listValue(result[key]).isNotEmpty) { + return true; + } + final recordItems = _listValue(_castMap(result[key])['items']); + if (recordItems.isNotEmpty) { + return true; + } + } + for (final key in const ['payload', 'result', 'data']) { + final nested = _castMap(result[key]); + if (nested.isNotEmpty && _hasArtifactList(nested)) { + return true; + } + } + return false; + } + + List _listValue(Object? value) { + return value is List ? value : const []; + } + + String _firstNonEmptyDisplayText( + Map values, + List keys, + ) { for (final key in keys) { - final value = values[key]?.toString().trim() ?? ''; + final value = _displayText(values[key]).trim(); if (value.isNotEmpty) { return value; } @@ -332,6 +381,55 @@ class ExternalCodeAgentAcpDesktopTransport return ''; } + String _displayText(Object? value, [Set? visited]) { + final seen = visited ?? {}; + if (value == null) { + return ''; + } + if (value is String) { + return value.trim(); + } + if (value is Map) { + if (!seen.add(value)) { + return ''; + } + final map = value.cast(); + for (final key in const [ + 'output', + 'summary', + 'resultSummary', + 'message', + 'content', + 'text', + 'delta', + 'output_text', + ]) { + final extracted = _displayText(map[key], seen); + if (extracted.isNotEmpty) { + return extracted; + } + } + for (final key in const ['result', 'payload', 'data']) { + final extracted = _displayText(map[key], seen); + if (extracted.isNotEmpty) { + return extracted; + } + } + return ''; + } + if (value is List) { + if (!seen.add(value)) { + return ''; + } + return value + .map((item) => _displayText(item, seen)) + .where((item) => item.isNotEmpty) + .join('\n') + .trim(); + } + return value.toString().trim(); + } + Map _castMap(Object? value) { if (value is Map) { return value; diff --git a/lib/runtime/go_task_service_client.dart b/lib/runtime/go_task_service_client.dart index 5a649960..ab6791fc 100644 --- a/lib/runtime/go_task_service_client.dart +++ b/lib/runtime/go_task_service_client.dart @@ -589,6 +589,11 @@ Object? _firstGoTaskArtifactList(Map result) { ]) { if (candidate is List) { artifacts.addAll(candidate); + } else if (candidate is Map) { + final items = _castList(candidate.cast()['items']); + if (items.isNotEmpty) { + artifacts.addAll(items); + } } } return artifacts.isEmpty ? null : artifacts; @@ -723,7 +728,7 @@ GoTaskServiceUpdate? goTaskServiceUpdateFromAcpNotification( payload['text']?.toString() ?? _castMap(payload['message'])['content']?.toString() ?? '', - message: payload['message']?.toString() ?? '', + message: _extractGoTaskDisplayText(payload['message']), pending: _boolValue(payload['pending']) ?? false, error: _boolValue(payload['error']) ?? false, route: diff --git a/test/runtime/gateway_acp_client_auth_test.dart b/test/runtime/gateway_acp_client_auth_test.dart index 841d92d2..60ce909d 100644 --- a/test/runtime/gateway_acp_client_auth_test.dart +++ b/test/runtime/gateway_acp_client_auth_test.dart @@ -184,6 +184,31 @@ void main() { expect(result.artifacts.single.content, 'nested artifact body'); }); + test('uses bridge artifact record items as artifacts', () { + final result = goTaskServiceResultFromAcpResponse({ + 'jsonrpc': '2.0', + 'id': 'request-id', + 'result': { + 'success': true, + 'message': 'created from snapshot record', + 'artifacts': { + 'items': >[ + { + 'relativePath': 'exports/report.md', + 'content': 'snapshot artifact body', + 'contentType': 'text/markdown', + }, + ], + }, + }, + }, route: GoTaskServiceRoute.externalAcpSingle); + + expect(result.message, 'created from snapshot record'); + expect(result.artifacts, hasLength(1)); + expect(result.artifacts.single.relativePath, 'exports/report.md'); + expect(result.artifacts.single.content, 'snapshot artifact body'); + }); + test('uses bridge files and attachments aliases as artifacts', () { final result = goTaskServiceResultFromAcpResponse({ 'jsonrpc': '2.0', @@ -582,7 +607,9 @@ void main() { 'event': 'completed', 'pending': false, 'error': false, - 'message': 'stable completed output', + 'message': { + 'content': 'stable completed output', + }, 'result': { 'success': true, 'output': 'stable completed output', @@ -692,7 +719,9 @@ void main() { 'success': true, 'output': 'recovered from bridge session snapshot', 'turnId': 'turn-recovered', - 'artifacts': >[ + }, + 'artifacts': { + 'items': >[ { 'relativePath': 'exports/snapshot.md', 'downloadUrl': @@ -702,6 +731,9 @@ void main() { 'sizeBytes': 64, }, ], + 'remoteWorkingDirectory': '/remote/openclaw/workspace', + 'remoteWorkspaceRefKind': 'remotePath', + 'resultSummary': 'recovered from top-level artifacts', }, }, }), @@ -743,6 +775,8 @@ void main() { expect(result.success, isTrue); expect(result.message, 'recovered from bridge session snapshot'); expect(result.artifacts.single.relativePath, 'exports/snapshot.md'); + expect(result.remoteWorkingDirectory, '/remote/openclaw/workspace'); + expect(result.remoteWorkspaceRefKind, WorkspaceRefKind.remotePath); expect( requestPaths, containsAll(['/gateway/openclaw', '/acp/rpc']),