From 60ed369df7c63204a6e635325d428df5a8a410a5 Mon Sep 17 00:00:00 2001 From: Cowork 3P Date: Thu, 4 Jun 2026 22:38:09 +0800 Subject: [PATCH] refactor: remove stale runtime fallbacks --- lib/app/app_controller_desktop_core.dart | 1 - ...ntroller_desktop_external_acp_routing.dart | 1 - lib/app/app_controller_desktop_gateway.dart | 1 - .../app_controller_desktop_navigation.dart | 1 - ...ler_desktop_runtime_coordination_impl.dart | 1 - ...pp_controller_desktop_runtime_helpers.dart | 1 - lib/app/app_controller_desktop_settings.dart | 1 - ...p_controller_desktop_settings_runtime.dart | 1 - ..._controller_desktop_skill_permissions.dart | 1 - ...app_controller_desktop_thread_actions.dart | 64 +++++++- ...app_controller_desktop_thread_binding.dart | 1 - ...pp_controller_desktop_thread_sessions.dart | 1 - ...op_thread_sessions_collaboration_impl.dart | 4 +- ...app_controller_desktop_thread_storage.dart | 1 - ...ontroller_desktop_workspace_execution.dart | 1 - .../assistant_page_state_actions.dart | 3 +- lib/features/desktop/desktop_client.dart | 43 +++-- lib/runtime/codex_runtime.dart | 65 -------- lib/runtime/embedded_agent_launch_policy.dart | 11 -- lib/runtime/file_store_support.dart | 24 +-- lib/runtime/go_core.dart | 36 ---- lib/runtime/go_task_service_client.dart | 87 +++++++++- lib/runtime/runtime_controllers_settings.dart | 2 - .../runtime_controllers_settings_account.dart | 5 - ...ontrollers_settings_connectivity_impl.dart | 6 - ...ime_controllers_settings_secrets_impl.dart | 10 +- lib/runtime/secret_store.dart | 17 +- .../features/desktop/desktop_client_test.dart | 155 ++++++++++++++++++ .../assistant_execution_target_test.dart | 104 +++++++++++- 29 files changed, 459 insertions(+), 190 deletions(-) delete mode 100644 lib/runtime/go_core.dart diff --git a/lib/app/app_controller_desktop_core.dart b/lib/app/app_controller_desktop_core.dart index fd5e6bcb..1596cf3b 100644 --- a/lib/app/app_controller_desktop_core.dart +++ b/lib/app/app_controller_desktop_core.dart @@ -12,7 +12,6 @@ import '../i18n/app_language.dart'; import '../models/app_models.dart'; import '../runtime/device_identity_store.dart'; import '../runtime/file_store_support.dart'; -import '../runtime/go_core.dart'; import '../runtime/runtime_bootstrap.dart'; import '../runtime/desktop_platform_service.dart'; import '../runtime/gateway_runtime.dart'; diff --git a/lib/app/app_controller_desktop_external_acp_routing.dart b/lib/app/app_controller_desktop_external_acp_routing.dart index afc06697..4fe13b47 100644 --- a/lib/app/app_controller_desktop_external_acp_routing.dart +++ b/lib/app/app_controller_desktop_external_acp_routing.dart @@ -12,7 +12,6 @@ import '../i18n/app_language.dart'; import '../models/app_models.dart'; import '../runtime/device_identity_store.dart'; -import '../runtime/go_core.dart'; import '../runtime/runtime_bootstrap.dart'; import '../runtime/desktop_platform_service.dart'; import '../runtime/gateway_runtime.dart'; diff --git a/lib/app/app_controller_desktop_gateway.dart b/lib/app/app_controller_desktop_gateway.dart index 33b16bf9..3c55bb5b 100644 --- a/lib/app/app_controller_desktop_gateway.dart +++ b/lib/app/app_controller_desktop_gateway.dart @@ -12,7 +12,6 @@ import '../i18n/app_language.dart'; import '../models/app_models.dart'; import '../runtime/device_identity_store.dart'; -import '../runtime/go_core.dart'; import '../runtime/runtime_bootstrap.dart'; import '../runtime/desktop_platform_service.dart'; import '../runtime/gateway_runtime.dart'; diff --git a/lib/app/app_controller_desktop_navigation.dart b/lib/app/app_controller_desktop_navigation.dart index 062e7f20..c763793d 100644 --- a/lib/app/app_controller_desktop_navigation.dart +++ b/lib/app/app_controller_desktop_navigation.dart @@ -12,7 +12,6 @@ import '../i18n/app_language.dart'; import '../models/app_models.dart'; import '../runtime/device_identity_store.dart'; -import '../runtime/go_core.dart'; import '../runtime/runtime_bootstrap.dart'; import '../runtime/desktop_platform_service.dart'; import '../runtime/gateway_runtime.dart'; diff --git a/lib/app/app_controller_desktop_runtime_coordination_impl.dart b/lib/app/app_controller_desktop_runtime_coordination_impl.dart index 491c1f42..36373740 100644 --- a/lib/app/app_controller_desktop_runtime_coordination_impl.dart +++ b/lib/app/app_controller_desktop_runtime_coordination_impl.dart @@ -12,7 +12,6 @@ import '../i18n/app_language.dart'; import '../models/app_models.dart'; import '../runtime/device_identity_store.dart'; -import '../runtime/go_core.dart'; import '../runtime/runtime_bootstrap.dart'; import '../runtime/desktop_platform_service.dart'; import '../runtime/gateway_runtime.dart'; diff --git a/lib/app/app_controller_desktop_runtime_helpers.dart b/lib/app/app_controller_desktop_runtime_helpers.dart index 6183c1f8..da476f72 100644 --- a/lib/app/app_controller_desktop_runtime_helpers.dart +++ b/lib/app/app_controller_desktop_runtime_helpers.dart @@ -14,7 +14,6 @@ import '../i18n/app_language.dart'; import '../models/app_models.dart'; import '../runtime/device_identity_store.dart'; -import '../runtime/go_core.dart'; import '../runtime/acp_endpoint_paths.dart'; import '../runtime/runtime_bootstrap.dart'; import '../runtime/desktop_platform_service.dart'; diff --git a/lib/app/app_controller_desktop_settings.dart b/lib/app/app_controller_desktop_settings.dart index ef8fd2fc..47548c5b 100644 --- a/lib/app/app_controller_desktop_settings.dart +++ b/lib/app/app_controller_desktop_settings.dart @@ -12,7 +12,6 @@ import '../i18n/app_language.dart'; import '../models/app_models.dart'; import '../runtime/device_identity_store.dart'; -import '../runtime/go_core.dart'; import '../runtime/runtime_bootstrap.dart'; import '../runtime/desktop_platform_service.dart'; import '../runtime/gateway_runtime.dart'; diff --git a/lib/app/app_controller_desktop_settings_runtime.dart b/lib/app/app_controller_desktop_settings_runtime.dart index cfa81005..092f0153 100644 --- a/lib/app/app_controller_desktop_settings_runtime.dart +++ b/lib/app/app_controller_desktop_settings_runtime.dart @@ -12,7 +12,6 @@ import '../i18n/app_language.dart'; import '../models/app_models.dart'; import '../runtime/device_identity_store.dart'; -import '../runtime/go_core.dart'; import '../runtime/runtime_bootstrap.dart'; import '../runtime/desktop_platform_service.dart'; import '../runtime/gateway_runtime.dart'; diff --git a/lib/app/app_controller_desktop_skill_permissions.dart b/lib/app/app_controller_desktop_skill_permissions.dart index 5ed7c8d3..bb4d1726 100644 --- a/lib/app/app_controller_desktop_skill_permissions.dart +++ b/lib/app/app_controller_desktop_skill_permissions.dart @@ -12,7 +12,6 @@ import '../i18n/app_language.dart'; import '../models/app_models.dart'; import '../runtime/device_identity_store.dart'; -import '../runtime/go_core.dart'; import '../runtime/runtime_bootstrap.dart'; import '../runtime/desktop_platform_service.dart'; import '../runtime/gateway_runtime.dart'; diff --git a/lib/app/app_controller_desktop_thread_actions.dart b/lib/app/app_controller_desktop_thread_actions.dart index e3a2d481..b7926eec 100644 --- a/lib/app/app_controller_desktop_thread_actions.dart +++ b/lib/app/app_controller_desktop_thread_actions.dart @@ -13,7 +13,6 @@ import '../i18n/app_language.dart'; import '../models/app_models.dart'; import '../runtime/device_identity_store.dart'; -import '../runtime/go_core.dart'; import '../runtime/runtime_bootstrap.dart'; import '../runtime/desktop_platform_service.dart'; import '../runtime/gateway_runtime.dart'; @@ -97,10 +96,14 @@ extension AppControllerDesktopThreadActions on AppController { } try { await runtimeInternal.health(); - } catch (_) {} + } catch (error) { + debugPrint('Gateway health refresh failed: $error'); + } try { await runtimeInternal.status(); - } catch (_) {} + } catch (error) { + debugPrint('Gateway status refresh failed: $error'); + } notifyListeners(); } @@ -465,12 +468,20 @@ extension AppControllerDesktopThreadActions on AppController { final capturedLocalAttachments = List.unmodifiable( localAttachments, ); - final taskMetadata = Map.unmodifiable(dispatch.metadata); final executionWorkingDirectory = gatewayExecutionWorkingDirectoryInternal( target: currentTarget, workingDirectory: workingDirectory, remoteWorkingDirectoryHint: remoteWorkingDirectoryHint, ); + final taskMetadata = Map.unmodifiable( + gatewayTaskMetadataWithArtifactContractInternal( + baseMetadata: dispatch.metadata, + sessionKey: normalizedSessionKey, + localWorkingDirectory: workingDirectory, + executionWorkingDirectory: executionWorkingDirectory, + remoteWorkingDirectoryHint: remoteWorkingDirectoryHint, + ), + ); if (usesOpenClawGatewayQueueInternal(currentTarget, provider)) { await enqueueOpenClawGatewayTurnInternal( OpenClawGatewayQueuedTurnInternal( @@ -924,12 +935,57 @@ extension AppControllerDesktopThreadActions on AppController { '7. Files listed in taskInputAttachments already belong to this TaskThread; reuse them from the task context and do not ask the user to upload them again.', ) ..writeln(); + if (target.isGateway) { + buffer + ..writeln('XWorkmate task artifact contract:') + ..writeln( + '- The remote runtime owns final-deliverable detection; do not rely on local task classification.', + ) + ..writeln( + '- If this request needs files, export the final deliverables through the current XWorkmate task artifact scope before final response.', + ) + ..writeln( + '- A textual download/path claim is not a deliverable unless the file has been exported into the current task artifact scope.', + ) + ..writeln( + '- Do not reuse artifacts from previous sessions, previous runs, or global OpenClaw workspaces.', + ) + ..writeln(); + } buffer ..writeln('User request:') ..write(requestText); return buffer.toString(); } + Map gatewayTaskMetadataWithArtifactContractInternal({ + required Map baseMetadata, + required String sessionKey, + required String localWorkingDirectory, + required String executionWorkingDirectory, + required String remoteWorkingDirectoryHint, + }) { + final localWorkspace = localWorkingDirectory.trim(); + final executionWorkspace = executionWorkingDirectory.trim(); + final remoteHint = remoteWorkingDirectoryHint.trim(); + return { + ...baseMetadata, + 'xworkmateTaskArtifactContract': { + 'version': 1, + 'sessionKey': sessionKey, + 'scopeKind': 'task', + 'finalDeliverableDetection': 'remote-runtime', + 'requiresExportBeforeFinalResponse': true, + 'rejectTextOnlyFileClaims': true, + 'currentTaskWorkspace': executionWorkspace.isNotEmpty + ? executionWorkspace + : (remoteHint.isNotEmpty ? remoteHint : localWorkspace), + if (localWorkspace.isNotEmpty) 'localWorkspace': localWorkspace, + if (remoteHint.isNotEmpty) 'remoteWorkspaceHint': remoteHint, + }, + }; + } + bool usesOpenClawGatewayQueueInternal( AssistantExecutionTarget target, SingleAgentProvider provider, diff --git a/lib/app/app_controller_desktop_thread_binding.dart b/lib/app/app_controller_desktop_thread_binding.dart index f8b1fe2f..3a9c5d97 100644 --- a/lib/app/app_controller_desktop_thread_binding.dart +++ b/lib/app/app_controller_desktop_thread_binding.dart @@ -12,7 +12,6 @@ import '../i18n/app_language.dart'; import '../models/app_models.dart'; import '../runtime/device_identity_store.dart'; -import '../runtime/go_core.dart'; import '../runtime/runtime_bootstrap.dart'; import '../runtime/desktop_platform_service.dart'; import '../runtime/gateway_runtime.dart'; diff --git a/lib/app/app_controller_desktop_thread_sessions.dart b/lib/app/app_controller_desktop_thread_sessions.dart index 3b7a9119..c5ba0ed4 100644 --- a/lib/app/app_controller_desktop_thread_sessions.dart +++ b/lib/app/app_controller_desktop_thread_sessions.dart @@ -12,7 +12,6 @@ import '../i18n/app_language.dart'; import '../models/app_models.dart'; import '../runtime/device_identity_store.dart'; -import '../runtime/go_core.dart'; import '../runtime/runtime_bootstrap.dart'; import '../runtime/desktop_platform_service.dart'; import '../runtime/gateway_runtime.dart'; diff --git a/lib/app/app_controller_desktop_thread_sessions_collaboration_impl.dart b/lib/app/app_controller_desktop_thread_sessions_collaboration_impl.dart index c1554bab..25558e73 100644 --- a/lib/app/app_controller_desktop_thread_sessions_collaboration_impl.dart +++ b/lib/app/app_controller_desktop_thread_sessions_collaboration_impl.dart @@ -12,7 +12,6 @@ import '../i18n/app_language.dart'; import '../models/app_models.dart'; import '../runtime/device_identity_store.dart'; -import '../runtime/go_core.dart'; import '../runtime/runtime_bootstrap.dart'; import '../runtime/desktop_platform_service.dart'; import '../runtime/gateway_runtime.dart'; @@ -66,7 +65,8 @@ Future openOnlineWorkspaceThreadSessionInternal( if (Platform.isLinux) { await Process.run('xdg-open', [url]); } - } catch (_) { + } catch (error) { + debugPrint('Open collaboration URL failed: $error'); // Best effort only. Do not surface a blocking error from a convenience link. } } diff --git a/lib/app/app_controller_desktop_thread_storage.dart b/lib/app/app_controller_desktop_thread_storage.dart index 7dee75b6..588e0c8e 100644 --- a/lib/app/app_controller_desktop_thread_storage.dart +++ b/lib/app/app_controller_desktop_thread_storage.dart @@ -12,7 +12,6 @@ import '../i18n/app_language.dart'; import '../models/app_models.dart'; import '../runtime/device_identity_store.dart'; -import '../runtime/go_core.dart'; import '../runtime/runtime_bootstrap.dart'; import '../runtime/desktop_platform_service.dart'; import '../runtime/gateway_runtime.dart'; diff --git a/lib/app/app_controller_desktop_workspace_execution.dart b/lib/app/app_controller_desktop_workspace_execution.dart index ac92dac8..7a65cae7 100644 --- a/lib/app/app_controller_desktop_workspace_execution.dart +++ b/lib/app/app_controller_desktop_workspace_execution.dart @@ -12,7 +12,6 @@ import '../i18n/app_language.dart'; import '../models/app_models.dart'; import '../runtime/device_identity_store.dart'; -import '../runtime/go_core.dart'; import '../runtime/runtime_bootstrap.dart'; import '../runtime/desktop_platform_service.dart'; import '../runtime/gateway_runtime.dart'; diff --git a/lib/features/assistant/assistant_page_state_actions.dart b/lib/features/assistant/assistant_page_state_actions.dart index e995d4d1..9d3c18d0 100644 --- a/lib/features/assistant/assistant_page_state_actions.dart +++ b/lib/features/assistant/assistant_page_state_actions.dart @@ -156,7 +156,8 @@ extension AssistantPageStateActionsInternal on AssistantPageStateInternal { selectedSkillLabels: selectedSkillLabels, ); clearComposerDraftForSessionInternal(submittedSessionKey); - } catch (_) { + } catch (error) { + debugPrint('Assistant task submission failed: $error'); if (!mounted) { rethrow; } diff --git a/lib/features/desktop/desktop_client.dart b/lib/features/desktop/desktop_client.dart index de760059..1a4d8ec5 100644 --- a/lib/features/desktop/desktop_client.dart +++ b/lib/features/desktop/desktop_client.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:convert'; +import 'package:flutter/foundation.dart'; import 'package:flutter_webrtc/flutter_webrtc.dart'; import '../../app/app_controller.dart'; @@ -30,6 +31,22 @@ Map desktopOfferParams({ }; } +Future desktopRemoteVideoStreamForTrack( + RTCTrackEvent event, { + required Future Function(String label) createFallbackStream, +}) async { + if (event.track.kind != 'video') { + return null; + } + if (event.streams.isNotEmpty) { + return event.streams.first; + } + + final stream = await createFallbackStream('xworkmate-remote-desktop'); + await stream.addTrack(event.track, addToNative: false); + return stream; +} + class DesktopClient { DesktopClient({required this.controller, required this.sessionId}); @@ -38,7 +55,6 @@ class DesktopClient { RTCPeerConnection? _peerConnection; RTCDataChannel? _dataChannel; - MediaStream? _remoteStream; final StreamController _streamController = StreamController.broadcast(); @@ -76,14 +92,18 @@ class DesktopClient { _peerConnection = await createPeerConnection(config); - // Listen for remote streams + // Listen for remote video tracks. Some Unified Plan servers send + // streamless tracks, so synthesize a stream for RTCVideoView when needed. _peerConnection!.onTrack = (event) { - if (event.track.kind == 'video') { - if (event.streams.isNotEmpty) { - _remoteStream = event.streams.first; - _streamController.add(_remoteStream!); + unawaited(() async { + final stream = await desktopRemoteVideoStreamForTrack( + event, + createFallbackStream: createLocalMediaStream, + ); + if (stream != null) { + _streamController.add(stream); } - } + }()); }; _peerConnection!.onConnectionState = (state) { @@ -187,7 +207,9 @@ class DesktopClient { }, }, ); - } catch (_) {} + } catch (error) { + debugPrint('Desktop ICE candidate send failed: $error'); + } } void sendInput(Map event) { @@ -205,13 +227,14 @@ class DesktopClient { method: 'xworkmate.desktop.close', params: {'sessionId': sessionId}, ); - } catch (_) {} + } catch (error) { + debugPrint('Desktop close request failed: $error'); + } await _dataChannel?.close(); await _peerConnection?.close(); _dataChannel = null; _peerConnection = null; - _remoteStream = null; _stateController.add('disconnected'); } } diff --git a/lib/runtime/codex_runtime.dart b/lib/runtime/codex_runtime.dart index 87a2210e..4291a55c 100644 --- a/lib/runtime/codex_runtime.dart +++ b/lib/runtime/codex_runtime.dart @@ -1,4 +1,3 @@ -import 'dart:async'; /// Codex sandbox mode for controlling file system access. enum CodexSandboxMode { @@ -274,70 +273,6 @@ enum CodexConnectionState { /// Codex App Server RPC client. class CodexRuntime {} -List> _decodeModelListResponse( - Map result, -) { - final rawModels = [ - ...switch (result['models']) { - final List items => items, - _ => const [], - }, - if (switch (result['models']) { - final List items => items.isEmpty, - _ => true, - }) - ...switch (result['data']) { - final List items => items, - _ => const [], - }, - ]; - final seen = {}; - final items = >[]; - for (final item in rawModels) { - if (item is! Map) { - continue; - } - final model = item.cast(); - final rawId = model['id'] ?? model['name']; - final id = rawId is String ? rawId.trim() : ''; - if (id.isEmpty || !seen.add(id)) { - continue; - } - items.add(model); - } - return items; -} - -Object _normalizeModelListError(Object error) { - if (error is TimeoutException) { - return TimeoutException('Codex model refresh timed out'); - } - if (error is CodexRpcError) { - final message = error.message.trim(); - final lower = message.toLowerCase(); - if (lower.contains('cloudflare') || lower.contains('403 forbidden')) { - return CodexRpcError( - code: error.code, - message: 'Codex model refresh blocked by Cloudflare (403)', - data: error.data, - ); - } - if (lower.contains('timeout waiting for child process to exit')) { - return TimeoutException( - 'Codex model refresh timed out waiting for child process exit', - ); - } - if (lower.contains('missing field `models`')) { - return CodexRpcError( - code: error.code, - message: 'Codex model list payload used an unsupported schema', - data: error.data, - ); - } - } - return error; -} - class CodexLaunchConfiguration { const CodexLaunchConfiguration({ required this.executable, diff --git a/lib/runtime/embedded_agent_launch_policy.dart b/lib/runtime/embedded_agent_launch_policy.dart index dbaae35d..8176d721 100644 --- a/lib/runtime/embedded_agent_launch_policy.dart +++ b/lib/runtime/embedded_agent_launch_policy.dart @@ -14,14 +14,3 @@ bool shouldBlockEmbeddedAgentLaunch({ enabled: enabled, ); } - -/// Helper for Go core launch blocking check. -bool shouldBlockGoCoreLaunch({ - required bool isAppleHost, - bool? enabled, -}) { - return shouldBlockEmbeddedAgentLaunch( - isAppleHost: isAppleHost, - enabled: enabled, - ); -} diff --git a/lib/runtime/file_store_support.dart b/lib/runtime/file_store_support.dart index 95b81708..74be957b 100644 --- a/lib/runtime/file_store_support.dart +++ b/lib/runtime/file_store_support.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'dart:io'; +import 'package:flutter/foundation.dart'; import 'package:path_provider/path_provider.dart'; import 'package:yaml/yaml.dart'; @@ -143,19 +144,7 @@ class StoreLayoutResolver { await _resolvePath(_supportRootPathResolver) ?? await _defaultSupportRootPath(); if (supportRootPath == null) { - // Fallback to a temporary directory instead of failing fast with an error. - // This ensures the app remains usable in "memory-only" or "ephemeral" mode. - final tempDir = await Directory.systemTemp.createTemp( - 'xworkmate-fallback-', - ); - final layout = StoreLayout( - rootDirectory: tempDir, - configDirectory: await ensureDirectory('${tempDir.path}/config'), - tasksDirectory: await ensureDirectory('${tempDir.path}/tasks'), - secretDirectory: await ensureDirectory('${tempDir.path}/secrets'), - ); - _cached = layout; - return layout; + throw StateError('Persistent support root is unavailable.'); } final appDataRootPath = await _resolvePath(_appDataRootPathResolver) ?? supportRootPath; @@ -197,7 +186,8 @@ class StoreLayoutResolver { try { final supportDirectory = await getApplicationSupportDirectory(); return '${supportDirectory.path}/xworkmate'; - } catch (_) { + } catch (error) { + debugPrint('Application support directory lookup failed: $error'); return defaultUserSettingsRootPath(); } } @@ -213,7 +203,8 @@ class StoreLayoutResolver { return null; } return normalizeStoreDirectoryPath(trimmed); - } catch (_) { + } catch (error) { + debugPrint('Store layout path resolver failed: $error'); return null; } } @@ -302,7 +293,8 @@ Object? decodeYamlDocument(String raw) { } try { return _yamlToObject(loadYaml(trimmed)); - } catch (_) { + } catch (error) { + debugPrint('YAML decode failed: $error'); return null; } } diff --git a/lib/runtime/go_core.dart b/lib/runtime/go_core.dart deleted file mode 100644 index 27acfd66..00000000 --- a/lib/runtime/go_core.dart +++ /dev/null @@ -1,36 +0,0 @@ -import 'dart:async'; - -/// DEPRECATED: Local Go core execution is disabled. -enum GoCoreLaunchSource { buildArtifact } - -/// DEPRECATED: Local Go core execution is disabled. -class GoCoreLaunch { - const GoCoreLaunch({ - required this.executable, - required this.source, - this.arguments = const [], - this.workingDirectory, - }); - - final String executable; - final GoCoreLaunchSource source; - final List arguments; - final String? workingDirectory; -} - -typedef GoCoreBinaryExistsResolver = Future Function(String command); - -/// DEPRECATED: Local Go core locator is disabled. -class GoCoreLocator { - GoCoreLocator({ - GoCoreBinaryExistsResolver? binaryExistsResolver, - String? workspaceRoot, - String Function()? resolvedExecutableResolver, - }); - - /// Always returns null as local execution is disabled. - Future locate() async => null; - - /// Always returns false as local execution is disabled. - Future isAvailable() async => false; -} diff --git a/lib/runtime/go_task_service_client.dart b/lib/runtime/go_task_service_client.dart index 8da3724a..892c3d23 100644 --- a/lib/runtime/go_task_service_client.dart +++ b/lib/runtime/go_task_service_client.dart @@ -496,9 +496,28 @@ class GoTaskServiceResult { ? raw['resultSummary'].toString().trim() : raw['summary']?.toString().trim() ?? ''; - String get status => raw['status']?.toString().trim() ?? ''; + String get status => _firstNestedGoTaskString( + raw, + const >[ + ['status'], + ['error', 'status'], + ['details', 'status'], + ['payload', 'status'], + ['result', 'status'], + ], + ); - String get code => raw['code']?.toString().trim() ?? ''; + String get code => _firstNestedGoTaskString( + raw, + const >[ + ['code'], + ['error', 'code'], + ['error', 'details', 'code'], + ['details', 'code'], + ['payload', 'code'], + ['result', 'code'], + ], + ); bool get isOpenClawRunningTaskHandle { final normalizedStatus = status.trim().toLowerCase(); @@ -793,7 +812,8 @@ GoTaskServiceResult goTaskServiceResultFromAcpResponse( .map((item) => item['id']?.toString().trim() ?? '') .where((item) => item.isNotEmpty) .toList(growable: false); - final success = _boolValue(result['success']) ?? true; + final success = + _boolValue(result['success']) ?? _inferGoTaskSuccess(result); final fallbackFailureText = () { if (success) { return ''; @@ -821,7 +841,7 @@ GoTaskServiceResult goTaskServiceResultFromAcpResponse( ? streamedText.trim() : '') .trim(); - final directErrorMessage = result['error']?.toString().trim() ?? ''; + final directErrorMessage = _extractGoTaskDisplayText(result['error']); final effectiveErrorMessage = success ? directErrorMessage : fallbackFailureText.isNotEmpty @@ -843,6 +863,44 @@ GoTaskServiceResult goTaskServiceResultFromAcpResponse( ); } +bool _inferGoTaskSuccess(Map result) { + if (result.containsKey('error')) { + return false; + } + final status = _firstNestedGoTaskString( + result, + const >[ + ['status'], + ['details', 'status'], + ['payload', 'status'], + ['result', 'status'], + ], + ).toLowerCase(); + if (status == 'failed' || + status == 'error' || + status == 'artifact_missing' || + status == 'cancelled' || + status == 'canceled') { + return false; + } + final code = _firstNestedGoTaskString( + result, + const >[ + ['code'], + ['details', 'code'], + ['payload', 'code'], + ['result', 'code'], + ], + ).toUpperCase(); + if (code == 'OPENCLAW_REQUIRED_ARTIFACT_MISSING' || + code == 'OPENCLAW_ARTIFACT_MISSING' || + code == 'OPENCLAW_NO_EXPORTED_ARTIFACTS' || + code == 'ARTIFACT_MISSING') { + return false; + } + return true; +} + String _firstGoTaskFailureText(Map result) { for (final key in const [ 'error', @@ -1061,6 +1119,27 @@ List> _castMapList(Object? raw) { return raw.map(_castMap).toList(growable: false); } +String _firstNestedGoTaskString( + Map source, + List> paths, +) { + for (final path in paths) { + Object? current = source; + for (final key in path) { + if (current is! Map) { + current = null; + break; + } + current = current[key]; + } + final value = current?.toString().trim() ?? ''; + if (value.isNotEmpty) { + return value; + } + } + return ''; +} + List _castList(Object? raw) { if (raw is List) { return raw; diff --git a/lib/runtime/runtime_controllers_settings.dart b/lib/runtime/runtime_controllers_settings.dart index 742e9f61..e2998618 100644 --- a/lib/runtime/runtime_controllers_settings.dart +++ b/lib/runtime/runtime_controllers_settings.dart @@ -212,7 +212,6 @@ class SettingsController extends ChangeNotifier { Future resolveSecretValueInternal({ String explicitValue = '', String refName = '', - String fallbackRefName = '', String accountTarget = '', bool allowVaultLookup = true, bool persistExplicitValue = true, @@ -220,7 +219,6 @@ class SettingsController extends ChangeNotifier { this, explicitValue: explicitValue, refName: refName, - fallbackRefName: fallbackRefName, accountTarget: accountTarget, allowVaultLookup: allowVaultLookup, persistExplicitValue: persistExplicitValue, diff --git a/lib/runtime/runtime_controllers_settings_account.dart b/lib/runtime/runtime_controllers_settings_account.dart index 8aaee25b..1064d37d 100644 --- a/lib/runtime/runtime_controllers_settings_account.dart +++ b/lib/runtime/runtime_controllers_settings_account.dart @@ -14,8 +14,6 @@ extension SettingsControllerAccountExtension on SettingsController { pendingAccountMfaTicketInternal.trim().isNotEmpty && !accountSignedIn; bool get hasEffectiveAiGatewayApiKey => secureRefsInternal.containsKey(aiGatewayApiKeyRefInternal()) || - (aiGatewayApiKeyRefInternal() == 'ai_gateway_api_key' && - secureRefsInternal.containsKey('ai_gateway_api_key')) || secureRefsInternal.containsKey( kAccountManagedSecretTargetAIGatewayAccessToken, ); @@ -43,7 +41,6 @@ extension SettingsControllerAccountExtension on SettingsController { Future loadEffectiveAiGatewayApiKey() async { return resolveSecretValueInternal( refName: snapshotInternal.aiGateway.apiKeyRef, - fallbackRefName: 'ai_gateway_api_key', accountTarget: kAccountManagedSecretTargetAIGatewayAccessToken, ); } @@ -64,7 +61,6 @@ extension SettingsControllerAccountExtension on SettingsController { return resolveSecretValueInternal( refName: gatewayTokenRefForProfileInternal(resolvedProfileIndex), - fallbackRefName: SecretStore.gatewayTokenRefKey(resolvedProfileIndex), accountTarget: resolvedProfileIndex == kGatewayRemoteProfileIndex ? kAccountManagedSecretTargetBridgeAuthToken : '', @@ -77,7 +73,6 @@ extension SettingsControllerAccountExtension on SettingsController { return resolveSecretValueInternal( refName: gatewayPasswordRefForProfileInternal(resolvedProfileIndex), - fallbackRefName: SecretStore.gatewayPasswordRefKey(resolvedProfileIndex), allowVaultLookup: true, ); } diff --git a/lib/runtime/runtime_controllers_settings_connectivity_impl.dart b/lib/runtime/runtime_controllers_settings_connectivity_impl.dart index a832594a..4040b291 100644 --- a/lib/runtime/runtime_controllers_settings_connectivity_impl.dart +++ b/lib/runtime/runtime_controllers_settings_connectivity_impl.dart @@ -149,14 +149,12 @@ Future syncAiGatewayCatalogSettingsInternal( ? await controller.resolveSecretValueInternal( explicitValue: apiKeyOverride, refName: profile.apiKeyRef, - fallbackRefName: 'ai_gateway_api_key', accountTarget: kAccountManagedSecretTargetAIGatewayAccessToken, allowVaultLookup: false, persistExplicitValue: false, ) : await controller.resolveSecretValueInternal( refName: profile.apiKeyRef, - fallbackRefName: 'ai_gateway_api_key', accountTarget: kAccountManagedSecretTargetAIGatewayAccessToken, ); if (apiKey.isEmpty && !_allowsAnonymousAiGatewayInternal(normalizedBaseUrl)) { @@ -254,14 +252,12 @@ Future testAiGatewayConnectionSettingsInternal( ? await controller.resolveSecretValueInternal( explicitValue: apiKeyOverride, refName: profile.apiKeyRef, - fallbackRefName: 'ai_gateway_api_key', accountTarget: kAccountManagedSecretTargetAIGatewayAccessToken, allowVaultLookup: false, persistExplicitValue: false, ) : await controller.resolveSecretValueInternal( refName: profile.apiKeyRef, - fallbackRefName: 'ai_gateway_api_key', accountTarget: kAccountManagedSecretTargetAIGatewayAccessToken, ); final endpoint = controller @@ -320,14 +316,12 @@ Future> loadAiGatewayModelsSettingsInternal( ? await controller.resolveSecretValueInternal( explicitValue: apiKeyOverride, refName: activeProfile.apiKeyRef, - fallbackRefName: 'ai_gateway_api_key', accountTarget: kAccountManagedSecretTargetAIGatewayAccessToken, allowVaultLookup: false, persistExplicitValue: false, ) : await controller.resolveSecretValueInternal( refName: activeProfile.apiKeyRef, - fallbackRefName: 'ai_gateway_api_key', accountTarget: kAccountManagedSecretTargetAIGatewayAccessToken, ); if (apiKey.isEmpty && !_allowsAnonymousAiGatewayInternal(normalizedBaseUrl)) { diff --git a/lib/runtime/runtime_controllers_settings_secrets_impl.dart b/lib/runtime/runtime_controllers_settings_secrets_impl.dart index 9506ae3f..a9f5ceb4 100644 --- a/lib/runtime/runtime_controllers_settings_secrets_impl.dart +++ b/lib/runtime/runtime_controllers_settings_secrets_impl.dart @@ -424,15 +424,12 @@ Future resolveSecretValueSettingsInternal( SettingsController controller, { String explicitValue = '', String refName = '', - String fallbackRefName = '', String accountTarget = '', bool allowVaultLookup = true, bool persistExplicitValue = true, }) async { final trimmedExplicit = explicitValue.trim(); - final normalizedRef = refName.trim().isNotEmpty - ? refName.trim() - : fallbackRefName.trim(); + final normalizedRef = refName.trim(); if (trimmedExplicit.isNotEmpty) { if (persistExplicitValue && normalizedRef.isNotEmpty) { await controller.storeInternal.saveSecretValueByRef( @@ -463,8 +460,9 @@ Future resolveSecretValueSettingsInternal( ); return vaultValue; } - } catch (_) { - // Keep account-managed fallback available even when Vault lookup fails. + } catch (error) { + debugPrint('Vault secret lookup failed for $normalizedRef: $error'); + // Keep account-managed secret resolution available even when Vault lookup fails. } } } diff --git a/lib/runtime/secret_store.dart b/lib/runtime/secret_store.dart index e4da38c1..60180949 100644 --- a/lib/runtime/secret_store.dart +++ b/lib/runtime/secret_store.dart @@ -1,6 +1,8 @@ import 'dart:convert'; import 'dart:io'; +import 'package:flutter/foundation.dart'; + import 'file_store_support.dart'; import 'runtime_models.dart'; @@ -127,7 +129,8 @@ class SecretStore { _secureStorage = FileSecureStorageClient( () async => _layout?.secretDirectory, ); - } catch (_) { + } catch (error) { + debugPrint('Secret store initialization failed: $error'); _layout = null; _secureStorage = null; } @@ -231,7 +234,8 @@ class SecretStore { return AccountSessionSummary.fromJson( (jsonDecode(raw!) as Map).cast(), ); - } catch (_) { + } catch (error) { + debugPrint('Account session summary decode failed: $error'); return null; } } @@ -251,7 +255,8 @@ class SecretStore { return AccountSyncState.fromJson( (jsonDecode(raw!) as Map).cast(), ); - } catch (_) { + } catch (error) { + debugPrint('Account sync state decode failed: $error'); return null; } } @@ -532,7 +537,8 @@ class SecretStore { .map((item) => item.toString().trim()) .where((item) => item.isNotEmpty) .toSet(); - } catch (_) { + } catch (error) { + debugPrint('Custom secret ref registry decode failed: $error'); return {}; } } @@ -561,7 +567,8 @@ class SecretStore { _memorySecure[key] = value; return value; } - } catch (_) { + } catch (error) { + debugPrint('Secure read failed for $key: $error'); // Fall back to memory only when the secret path is unavailable. } } diff --git a/test/features/desktop/desktop_client_test.dart b/test/features/desktop/desktop_client_test.dart index 40f6722c..2236057b 100644 --- a/test/features/desktop/desktop_client_test.dart +++ b/test/features/desktop/desktop_client_test.dart @@ -1,7 +1,105 @@ +import 'dart:typed_data'; + import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_webrtc/flutter_webrtc.dart'; import 'package:xworkmate/features/desktop/desktop_client.dart'; +class FakeMediaStream extends MediaStream { + FakeMediaStream(String id) : super(id, 'test'); + + final List tracks = []; + final List addToNativeValues = []; + + @override + bool? get active => true; + + @override + Future addTrack( + MediaStreamTrack track, { + bool addToNative = true, + }) async { + tracks.add(track); + addToNativeValues.add(addToNative); + } + + @override + Future getMediaTracks() async {} + + @override + List getAudioTracks() => + tracks.where((track) => track.kind == 'audio').toList(); + + @override + MediaStreamTrack? getTrackById(String trackId) { + for (final track in tracks) { + if (track.id == trackId) { + return track; + } + } + return null; + } + + @override + List getTracks() => List.unmodifiable(tracks); + + @override + List getVideoTracks() => + tracks.where((track) => track.kind == 'video').toList(); + + @override + Future removeTrack( + MediaStreamTrack track, { + bool removeFromNative = true, + }) async { + tracks.remove(track); + } +} + +class FakeMediaStreamTrack extends MediaStreamTrack { + FakeMediaStreamTrack({required this.trackId, required this.trackKind}); + + final String trackId; + final String trackKind; + bool _enabled = true; + + @override + Future captureFrame() { + throw UnimplementedError(); + } + + @override + Future dispose() async {} + + @override + bool get enabled => _enabled; + + @override + set enabled(bool b) { + _enabled = b; + } + + @override + Future hasTorch() async => false; + + @override + String? get id => trackId; + + @override + String? get kind => trackKind; + + @override + String? get label => trackKind; + + @override + bool? get muted => false; + + @override + Future setTorch(bool torch) async {} + + @override + Future stop() async {} +} + void main() { group('DesktopClient protocol helpers', () { test('normalizes WebRTC connection states for view gating', () { @@ -42,5 +140,62 @@ void main() { expect(params['width'], 1280); expect(params['height'], 720); }); + + test('uses bridge-provided remote stream when present', () async { + var fallbackCreated = false; + final providedStream = FakeMediaStream('provided-stream'); + final track = FakeMediaStreamTrack( + trackId: 'video-track-1', + trackKind: 'video', + ); + + final stream = await desktopRemoteVideoStreamForTrack( + RTCTrackEvent(streams: [providedStream], track: track), + createFallbackStream: (label) async { + fallbackCreated = true; + return FakeMediaStream(label); + }, + ); + + expect(stream, same(providedStream)); + expect(fallbackCreated, isFalse); + expect(providedStream.tracks, isEmpty); + }); + + test('synthesizes stream for streamless remote video track', () async { + final track = FakeMediaStreamTrack( + trackId: 'video-track-1', + trackKind: 'video', + ); + final fallbackStream = FakeMediaStream('fallback-stream'); + + final stream = await desktopRemoteVideoStreamForTrack( + RTCTrackEvent(streams: const [], track: track), + createFallbackStream: (label) async => fallbackStream, + ); + + expect(stream, same(fallbackStream)); + expect(fallbackStream.tracks, [same(track)]); + expect(fallbackStream.addToNativeValues, [isFalse]); + }); + + test('ignores streamless non-video tracks', () async { + var fallbackCreated = false; + final track = FakeMediaStreamTrack( + trackId: 'audio-track-1', + trackKind: 'audio', + ); + + final stream = await desktopRemoteVideoStreamForTrack( + RTCTrackEvent(streams: const [], track: track), + createFallbackStream: (label) async { + fallbackCreated = true; + return FakeMediaStream(label); + }, + ); + + expect(stream, isNull); + expect(fallbackCreated, isFalse); + }); }); } diff --git a/test/runtime/assistant_execution_target_test.dart b/test/runtime/assistant_execution_target_test.dart index 80dd3a6d..d13c1b22 100644 --- a/test/runtime/assistant_execution_target_test.dart +++ b/test/runtime/assistant_execution_target_test.dart @@ -1307,6 +1307,13 @@ void main() { final request = fakeGoTaskService.requests.single; expect(request.metadata, isNot(contains('taskLoadClass'))); expect(request.metadata, isNot(contains('expectedArtifactExtensions'))); + expect(request.metadata, contains('xworkmateTaskArtifactContract')); + final artifactContract = + (request.metadata['xworkmateTaskArtifactContract'] as Map) + .cast(); + expect(artifactContract['finalDeliverableDetection'], 'remote-runtime'); + expect(artifactContract['requiresExportBeforeFinalResponse'], isTrue); + expect(artifactContract, isNot(contains('expectedArtifactExtensions'))); expect(request.prompt, isNot(contains('Task load classification:'))); expect( request.prompt, @@ -1345,7 +1352,22 @@ void main() { final request = fakeGoTaskService.requests.single; expect(request.metadata, isNot(contains('taskLoadClass'))); expect(request.metadata, isNot(contains('expectedArtifactExtensions'))); + expect(request.metadata, contains('xworkmateTaskArtifactContract')); + final artifactContract = + (request.metadata['xworkmateTaskArtifactContract'] as Map) + .cast(); + expect(artifactContract['scopeKind'], 'task'); + expect(artifactContract['rejectTextOnlyFileClaims'], isTrue); + expect( + artifactContract['currentTaskWorkspace'], + request.workingDirectory, + ); expect(request.prompt, isNot(contains('Required final artifact'))); + expect(request.prompt, contains('XWorkmate task artifact contract:')); + expect( + request.prompt, + contains('export the final deliverables through the current XWorkmate task artifact scope'), + ); expect(request.prompt, contains('最后 输出 PDF文件')); }, ); @@ -1931,6 +1953,68 @@ void main() { }, ); + test( + 'sendChatMessage treats nested OpenClaw artifact errors as terminal failures', + () async { + final fakeGoTaskService = _RecordingGoTaskServiceClient() + ..outcomes.add( + goTaskServiceResultFromAcpResponse( + const { + 'jsonrpc': '2.0', + 'id': 'nested-openclaw-artifact-error', + 'result': { + 'status': 'failed', + 'error': { + 'code': 'OPENCLAW_REQUIRED_ARTIFACT_MISSING', + 'message': + 'openclaw returned partial artifacts without required final deliverables', + }, + }, + }, + route: GoTaskServiceRoute.externalAcpSingle, + ), + ) + ..outcomes.add( + const GoTaskServiceResult( + success: true, + message: 'new OpenClaw session delivered final artifacts', + turnId: 'turn-2', + raw: {}, + errorMessage: '', + resolvedModel: '', + route: GoTaskServiceRoute.externalAcpSingle, + ), + ); + final controller = _connectedController(fakeGoTaskService); + addTearDown(controller.dispose); + + await controller.sessionsController.switchSession( + 'unit-fixture-task-a', + ); + + await controller.sendChatMessage('first turn'); + + expect(fakeGoTaskService.requests, hasLength(1)); + final failedThread = controller.taskThreadForSessionInternal( + 'unit-fixture-task-a', + ); + expect( + failedThread?.lifecycleState.lastResultCode, + 'OPENCLAW_REQUIRED_ARTIFACT_MISSING', + ); + expect(failedThread?.lastArtifactSyncStatus, 'failed'); + + await controller.sendChatMessage('retry final artifact'); + + expect(fakeGoTaskService.requests, hasLength(2)); + expect(fakeGoTaskService.requests.last.resumeSession, isFalse); + await _waitForLastChatMessageText( + controller, + 'new OpenClaw session delivered final artifacts', + ); + }, + ); + test( 'sendChatMessage hides OpenClaw artifact guard text from failed results and streaming', () async { @@ -4355,6 +4439,21 @@ UiFeatureManifest _defaultDesktopManifest() { ); } +Future _resilientDelete(Directory dir) async { + if (!await dir.exists()) { + return; + } + for (var attempt = 0; attempt < 8; attempt++) { + try { + await dir.delete(recursive: true); + return; + } catch (_) { + await Future.delayed(const Duration(milliseconds: 50)); + } + } + await dir.delete(recursive: true); +} + AppController _sandboxController({ SecureConfigStore? store, RuntimeCoordinator? runtimeCoordinator, @@ -4371,10 +4470,7 @@ AppController _sandboxController({ final actualHome = homeDir ?? Directory.systemTemp.createTempSync('xworkmate-sandbox-home-').path; if (homeDir == null) { addTearDown(() async { - final dir = Directory(actualHome); - if (await dir.exists()) { - await dir.delete(recursive: true); - } + await _resilientDelete(Directory(actualHome)); }); } return AppController(