From fe84d69e1001ddbf3da6ee903ba57349d3348f22 Mon Sep 17 00:00:00 2001 From: Haitao Pan Date: Sat, 6 Jun 2026 11:51:01 +0800 Subject: [PATCH] refactor: align OpenClaw session key state flow --- .gitignore | 3 + ...penclaw-session-key-state-and-data-flow.md | 198 ++++++++++++++++++ lib/app/app_controller_desktop_gateway.dart | 2 +- ...p_controller_desktop_settings_runtime.dart | 10 +- ...app_controller_desktop_thread_storage.dart | 4 +- ...ontroller_desktop_workspace_execution.dart | 4 +- .../assistant_page_composer_clipboard.dart | 2 +- .../assistant_page_state_actions.dart | 2 +- lib/features/mobile/mobile_settings_page.dart | 4 +- lib/features/settings/settings_page_core.dart | 6 +- lib/runtime/code_agent_node_orchestrator.dart | 4 +- lib/runtime/gateway_runtime_helpers.dart | 10 +- lib/runtime/runtime_controllers_settings.dart | 4 +- .../runtime_models_settings_snapshot.dart | 2 +- lib/runtime/runtime_models_ui_state.dart | 4 +- lib/runtime/secure_config_store.dart | 4 +- lib/runtime/settings_store.dart | 6 +- 17 files changed, 239 insertions(+), 30 deletions(-) create mode 100644 docs/architecture/openclaw-session-key-state-and-data-flow.md diff --git a/.gitignore b/.gitignore index e2320cbb..6c5bf4e0 100644 --- a/.gitignore +++ b/.gitignore @@ -70,3 +70,6 @@ app.*.map.json # Repomix — dynamically generated, not committed repomix-output.xml + +# Flutter analyze — local diagnostic dump +flutter_analyze_output.txt diff --git a/docs/architecture/openclaw-session-key-state-and-data-flow.md b/docs/architecture/openclaw-session-key-state-and-data-flow.md new file mode 100644 index 00000000..70f7b04d --- /dev/null +++ b/docs/architecture/openclaw-session-key-state-and-data-flow.md @@ -0,0 +1,198 @@ +# OpenClaw Session Key State And Data Flow + +This note is the source-of-truth for the XWorkmate App -> Bridge -> OpenClaw +session identity boundary. + +## Terms + +| Term | Owner | Example | Meaning | +| --- | --- | --- | --- | +| `TaskThread.sessionKey` | XWorkmate App | `draft:1780658097668838-1` | App-local thread identity. It owns UI state, queue state, local workspace, and persisted TaskThread data. | +| `appThreadKey` | XWorkmate typed integration metadata | `draft:1780658097668838-1` | The typed cross-repo name for the App TaskThread identity. It is currently the same value as `TaskThread.sessionKey`. | +| local thread workspace | XWorkmate App | `~/.xworkmate/threads/draft-1780658097668838-1` | Filesystem path derived from the App thread key. Path formatting is not a protocol key. | +| `openclawSessionKey` | OpenClaw native session layer | `agent:main:draft:1780658097668838-1` | OpenClaw SessionEntry key and `chat.send.sessionKey` value. | +| `runId` | OpenClaw task/runtime layer | `run-openclaw-...` | The active OpenClaw run/task id. Bridge updates to the actual OpenClaw `runId` if `chat.send` returns one different from the initial request id. | + +Rules: + +- App code may keep `sessionKey` for local TaskThread and UI session APIs. +- XWorkmate OpenClaw integration payloads must use `appThreadKey` and + `openclawSessionKey`. +- Bridge may send `sessionKey` only when calling OpenClaw native APIs that + require that field, such as `chat.send`. +- Plugin lookup must read `appThreadKey -> openclawSessionKey` from + `SessionEntry.pluginExtensions`, not from string replace or reverse parsing. +- Legacy `sessionKey` aliases are not accepted from App or Bridge as mapping + inputs. + +## State Graph + +```mermaid +stateDiagram-v2 + [*] --> AppDraftCreated + AppDraftCreated: TaskThread.sessionKey = appThreadKey + AppDraftCreated: local workspace path is derived for filesystem use + + AppDraftCreated --> Submitted: App sends session.start + Submitted: params.sessionId/threadId = App thread identity + Submitted: metadata.xworkmateTaskArtifactContract.appThreadKey + Submitted: metadata.xworkmateTaskArtifactContract.expectedArtifactDirs + + Submitted --> BridgePrepared: Bridge calls xworkmate.session.prepare + BridgePrepared: schemaVersion = 1 + BridgePrepared: appThreadKey + BridgePrepared: openclawSessionKey + BridgePrepared: expectedArtifactDirs + BridgePrepared: requestId/externalTaskId + + BridgePrepared --> MappingPersisted: Plugin patches SessionEntry.pluginExtensions + MappingPersisted: source = bridge_prepare or session_start + MappingPersisted: conflicts fail closed + + MappingPersisted --> OpenClawRunning: Bridge calls chat.send + OpenClawRunning: chat.send.sessionKey = openclawSessionKey + OpenClawRunning: idempotencyKey = requestId + + OpenClawRunning --> OpenClawRunning: agent.wait still running + OpenClawRunning --> RunIdReprepared: chat.send returns actual runId + RunIdReprepared: Bridge prepares actual run scope + RunIdReprepared --> OpenClawRunning + + OpenClawRunning --> Terminal: agent.wait or native task-registry terminal state + Terminal --> ArtifactSnapshot: collect-and-snapshot + ArtifactSnapshot --> ArtifactExport: xworkmate.artifacts.export + ArtifactExport --> AppReady: session.update or xworkmate.tasks.get result + AppReady --> [*] + + OpenClawRunning --> ReconcileAfterReconnect: App/Bridge reconnect + ReconcileAfterReconnect --> MappingPersisted: lookup mapping by appThreadKey + ReconcileAfterReconnect --> Terminal: query native task-registry by openclawSessionKey/runId +``` + +## Data Flow + +```mermaid +flowchart LR + subgraph App["xworkmate-app"] + TT["TaskThread\nsessionKey = draft:..."] + Meta["Typed metadata\nschemaVersion=1\nappThreadKey\nexpectedArtifactDirs"] + Assoc["OpenClawTaskAssociation\nappThreadKey\nopenclawSessionKey\nrunId"] + end + + subgraph Bridge["xworkmate-bridge"] + Validate["validate request"] + PrepareParams["prepare params\nschemaVersion=1\nappThreadKey\nopenclawSessionKey\nexpectedArtifactDirs\nrequestId/externalTaskId"] + NativeChat["OpenClaw native chat.send\nsessionKey = openclawSessionKey\nidempotencyKey"] + Events["normalize native events\nsession.update to App"] + end + + subgraph OpenClaw["openclaw.svc.plus"] + SessionEntry["SessionEntry\nkey = openclawSessionKey"] + NativeTask["native task-registry\nrunId/taskId/status"] + Transcript["transcript/runtime events"] + end + + subgraph Plugin["openclaw-multi-session-plugins"] + Prepare["xworkmate.session.prepare"] + Ext["pluginExtensions mapping\nxworkmate.sessionMapping"] + TasksGet["xworkmate.tasks.get\nnative registry lookup"] + Artifact["artifact resolver\nexpectedArtifactDirs"] + end + + TT --> Meta + Meta --> Validate + Validate --> PrepareParams + PrepareParams --> Prepare + Prepare --> Ext + Ext --> SessionEntry + PrepareParams --> NativeChat + NativeChat --> SessionEntry + NativeChat --> NativeTask + NativeTask --> Transcript + Transcript --> Events + Events --> Assoc + Assoc --> TasksGet + TasksGet --> NativeTask + TasksGet --> Artifact + Artifact --> Events +``` + +## Field Contract + +App `session.start`: + +```json +{ + "sessionId": "draft:1780658097668838-1", + "threadId": "draft:1780658097668838-1", + "metadata": { + "xworkmateTaskArtifactContract": { + "schemaVersion": 1, + "appThreadKey": "draft:1780658097668838-1", + "expectedArtifactDirs": [ + "artifacts/", + "reports/", + "exports/", + "assets/", + "assets/images/", + "dist/" + ] + } + } +} +``` + +Bridge `xworkmate.session.prepare`: + +```json +{ + "schemaVersion": 1, + "appThreadKey": "draft:1780658097668838-1", + "openclawSessionKey": "agent:main:draft:1780658097668838-1", + "runId": "turn-...", + "requestId": "turn-...", + "externalTaskId": "turn-...", + "expectedArtifactDirs": ["artifacts/", "reports/"] +} +``` + +Plugin writes: + +```json +{ + "schemaVersion": 1, + "appThreadKey": "draft:1780658097668838-1", + "openclawSessionKey": "agent:main:draft:1780658097668838-1", + "expectedArtifactDirs": ["artifacts/", "reports/"], + "source": "bridge_prepare", + "createdAt": "2026-06-05T00:00:00.000Z", + "updatedAt": "2026-06-05T00:00:00.000Z" +} +``` + +App `xworkmate.tasks.get` params: + +```json +{ + "appThreadKey": "draft:1780658097668838-1", + "openclawSessionKey": "agent:main:draft:1780658097668838-1", + "runId": "run-openclaw-...", + "includeArtifacts": true +} +``` + +## Final Consistency + +For the debug case: + +- App local key: `TaskThread.sessionKey = appThreadKey = draft:1780658097668838-1` +- App local path: `~/.xworkmate/threads/draft-1780658097668838-1` +- OpenClaw URL: `?session=agent%3Amain%3Adraft%3A1780658097668838-1` +- OpenClaw key: `openclawSessionKey = agent:main:draft:1780658097668838-1` +- Durable mapping: + `SessionEntry.pluginExtensions["openclaw-multi-session-plugins"]["xworkmate.sessionMapping"]` + +This keeps the visible IDs aligned for debugging while avoiding string inference +as the lookup source. The deterministic `agent:main:` creation +policy is only used before the mapping exists; after `xworkmate.session.prepare` +the durable mapping is authoritative. diff --git a/lib/app/app_controller_desktop_gateway.dart b/lib/app/app_controller_desktop_gateway.dart index 3c55bb5b..d0701b37 100644 --- a/lib/app/app_controller_desktop_gateway.dart +++ b/lib/app/app_controller_desktop_gateway.dart @@ -247,7 +247,7 @@ extension AppControllerDesktopGateway on AppController { forceRefresh: true, persistMountTargets: true, ); - } catch (_) { + } catch (e, stackTrace) { debugPrint('Error: $e\n$stackTrace'); // Keep the Gateway connect flow usable even if ACP capability refresh // trails the runtime handshake. } diff --git a/lib/app/app_controller_desktop_settings_runtime.dart b/lib/app/app_controller_desktop_settings_runtime.dart index 092f0153..67972940 100644 --- a/lib/app/app_controller_desktop_settings_runtime.dart +++ b/lib/app/app_controller_desktop_settings_runtime.dart @@ -267,7 +267,7 @@ extension AppControllerDesktopSettingsRuntime on AppController { ); try { await runtime.health(); - } catch (_) { + } catch (e, stackTrace) { debugPrint('Error: $e\n$stackTrace'); // Connectivity succeeded; health is best-effort for the test path. } final endpoint = @@ -286,14 +286,14 @@ extension AppControllerDesktopSettingsRuntime on AppController { } finally { try { await runtime.disconnect(clearDesiredProfile: false); - } catch (_) { + } catch (e, stackTrace) { debugPrint('Error: $e\n$stackTrace'); // Ignore teardown noise from temporary connectivity checks. } runtime.dispose(); temporaryStore.dispose(); try { await temporaryRoot.delete(recursive: true); - } catch (_) { + } catch (e, stackTrace) { debugPrint('Error: $e\n$stackTrace'); // Ignore cleanup noise for temporary connectivity checks. } } @@ -447,7 +447,7 @@ extension AppControllerDesktopSettingsRuntime on AppController { } try { await settingsControllerInternal.restoreAccountSession(); - } catch (_) { + } catch (e, stackTrace) { debugPrint('Error: $e\n$stackTrace'); // Keep initialization resilient when remote account restore fails. } restoreAssistantThreadsInternal(sanitizedAssistantThreads); @@ -501,7 +501,7 @@ extension AppControllerDesktopSettingsRuntime on AppController { startupTarget, ), ); - } catch (_) { + } catch (e, stackTrace) { debugPrint('Error: $e\n$stackTrace'); // Keep the shell usable when auto-connect fails. } } diff --git a/lib/app/app_controller_desktop_thread_storage.dart b/lib/app/app_controller_desktop_thread_storage.dart index db712aab..46462d90 100644 --- a/lib/app/app_controller_desktop_thread_storage.dart +++ b/lib/app/app_controller_desktop_thread_storage.dart @@ -77,7 +77,7 @@ extension AppControllerDesktopThreadStorage on AppController { } try { await syncAiGatewayCatalog(snapshot.aiGateway, apiKeyOverride: apiKey); - } catch (_) { + } catch (e, stackTrace) { debugPrint('Error: $e\n$stackTrace'); // Keep the saved draft applied even if model sync fails immediately. } } @@ -564,7 +564,7 @@ extension AppControllerDesktopThreadStorage on AppController { normalizedRecord.workspacePath.trim().isNotEmpty) { try { Directory(normalizedRecord.workspacePath).createSync(recursive: true); - } catch (_) { + } catch (e, stackTrace) { debugPrint('Error: $e\n$stackTrace'); // Best effort only. The thread should still restore even when the // directory cannot be recreated immediately. } diff --git a/lib/app/app_controller_desktop_workspace_execution.dart b/lib/app/app_controller_desktop_workspace_execution.dart index 7a65cae7..0da3626c 100644 --- a/lib/app/app_controller_desktop_workspace_execution.dart +++ b/lib/app/app_controller_desktop_workspace_execution.dart @@ -238,7 +238,7 @@ extension AppControllerDesktopWorkspaceExecution on AppController { resolvedTarget, ), ); - } catch (_) { + } catch (e, stackTrace) { debugPrint('Error: $e\n$stackTrace'); // Keep the selected execution target even when the immediate reconnect // fails so the user can retry or adjust gateway settings manually. } @@ -441,7 +441,7 @@ extension AppControllerDesktopWorkspaceExecution on AppController { sessionId: normalizedSessionKey, threadId: normalizedSessionKey, ); - } catch (_) { + } catch (e, stackTrace) { debugPrint('Error: $e\n$stackTrace'); // Best effort only. } }).catchError((_) {}), diff --git a/lib/features/assistant/assistant_page_composer_clipboard.dart b/lib/features/assistant/assistant_page_composer_clipboard.dart index c1ee2782..6a58f973 100644 --- a/lib/features/assistant/assistant_page_composer_clipboard.dart +++ b/lib/features/assistant/assistant_page_composer_clipboard.dart @@ -170,7 +170,7 @@ Future resolveClipboardAttachmentTempDirectoryInternal() async { Directory rootDirectory; try { rootDirectory = await getTemporaryDirectory(); - } catch (_) { + } catch (e, stackTrace) { debugPrint('Error: $e\n$stackTrace'); rootDirectory = Directory.systemTemp; } final clipboardDirectory = Directory( diff --git a/lib/features/assistant/assistant_page_state_actions.dart b/lib/features/assistant/assistant_page_state_actions.dart index 9d3c18d0..835265c3 100644 --- a/lib/features/assistant/assistant_page_state_actions.dart +++ b/lib/features/assistant/assistant_page_state_actions.dart @@ -359,7 +359,7 @@ extension AssistantPageStateActionsInternal on AssistantPageStateInternal { Future continueCurrentTaskInternal(String sessionKey) async { try { await widget.controller.continueAssistantTaskInternal(sessionKey); - } catch (_) { + } catch (e, stackTrace) { debugPrint('Error: $e\n$stackTrace'); focusComposerInternal(); } } diff --git a/lib/features/mobile/mobile_settings_page.dart b/lib/features/mobile/mobile_settings_page.dart index ce87270b..7bb904c8 100644 --- a/lib/features/mobile/mobile_settings_page.dart +++ b/lib/features/mobile/mobile_settings_page.dart @@ -185,12 +185,12 @@ class _MobileSettingsPageState extends State { await controller.refreshSingleAgentCapabilitiesInternal( forceRefresh: true, ); - } catch (_) { + } catch (e, stackTrace) { debugPrint('Error: $e\n$stackTrace'); // Account login should not fail only because runtime refresh is transient. } try { await controller.refreshAcpCapabilitiesInternal(forceRefresh: true); - } catch (_) { + } catch (e, stackTrace) { debugPrint('Error: $e\n$stackTrace'); // Runtime capabilities can be refreshed again from Assistant. } } diff --git a/lib/features/settings/settings_page_core.dart b/lib/features/settings/settings_page_core.dart index ee9487ed..d135c1e9 100644 --- a/lib/features/settings/settings_page_core.dart +++ b/lib/features/settings/settings_page_core.dart @@ -86,7 +86,7 @@ Future> loadBridgeMetadataForSettingsAbout({ if (decoded is Map) { return decoded.cast(); } - } catch (_) { + } catch (e, stackTrace) { debugPrint('Error: $e\n$stackTrace'); return const { 'status': 'unavailable', 'version': '', @@ -296,13 +296,13 @@ class _SettingsPageState extends State { await controller.refreshSingleAgentCapabilitiesInternal( forceRefresh: true, ); - } catch (_) { + } catch (e, stackTrace) { debugPrint('Error: $e\n$stackTrace'); // Best effort only. Account sync should still succeed if runtime refresh // is temporarily unavailable. } try { await controller.refreshAcpCapabilitiesInternal(forceRefresh: true); - } catch (_) { + } catch (e, stackTrace) { debugPrint('Error: $e\n$stackTrace'); // Best effort only. Runtime capabilities can be retried later. } } diff --git a/lib/runtime/code_agent_node_orchestrator.dart b/lib/runtime/code_agent_node_orchestrator.dart index 50dcc436..74239675 100644 --- a/lib/runtime/code_agent_node_orchestrator.dart +++ b/lib/runtime/code_agent_node_orchestrator.dart @@ -1,3 +1,5 @@ +import 'package:flutter/foundation.dart'; + import '../app/app_metadata.dart'; import 'runtime_coordinator.dart'; import 'runtime_models.dart'; @@ -68,7 +70,7 @@ class CodeAgentNodeOrchestrator { metadata: resolution.metadata, ); } - } catch (_) { + } catch (e, stackTrace) { debugPrint('Error: $e\n$stackTrace'); // Dispatch metadata is advisory; task execution still carries routing. } } diff --git a/lib/runtime/gateway_runtime_helpers.dart b/lib/runtime/gateway_runtime_helpers.dart index b29d62d7..721dfd58 100644 --- a/lib/runtime/gateway_runtime_helpers.dart +++ b/lib/runtime/gateway_runtime_helpers.dart @@ -487,7 +487,7 @@ mixin GatewayRuntimeHelpersInternal on ChangeNotifier { version: info.version, buildNumber: info.buildNumber, ); - } catch (_) { + } catch (e, stackTrace) { debugPrint('Error: $e\n$stackTrace'); return const RuntimePackageInfo( appName: kSystemAppName, packageName: 'plus.svc.xworkmate', @@ -546,7 +546,7 @@ mixin GatewayRuntimeHelpersInternal on ChangeNotifier { modelIdentifier: info.machineId ?? 'linux', ); } - } catch (_) { + } catch (e, stackTrace) { debugPrint('Error: $e\n$stackTrace'); // Fall through to generic info. } return RuntimeDeviceInfo( @@ -589,7 +589,7 @@ GatewaySetupPayload? decodeGatewaySetupCode(String rawInput) { final padded = normalized + '=' * ((4 - normalized.length % 4) % 4); final decoded = utf8.decode(base64.decode(padded)); return decodeSetupPayloadJsonInternal(decoded); - } catch (_) { + } catch (e, stackTrace) { debugPrint('Error: $e\n$stackTrace'); return null; } } @@ -614,7 +614,7 @@ GatewaySetupPayload? decodeSetupPayloadJsonInternal(String raw) { token: stringValue(json['token']) ?? '', password: stringValue(json['password']) ?? '', ); - } catch (_) { + } catch (e, stackTrace) { debugPrint('Error: $e\n$stackTrace'); return null; } } @@ -625,7 +625,7 @@ String resolveSetupCodeCandidateInternal(String raw) { if (decoded is Map) { return stringValue(decoded['setupCode']) ?? raw; } - } catch (_) { + } catch (e, stackTrace) { debugPrint('Error: $e\n$stackTrace'); // Leave raw as-is. } return raw; diff --git a/lib/runtime/runtime_controllers_settings.dart b/lib/runtime/runtime_controllers_settings.dart index e2998618..0f5f7e71 100644 --- a/lib/runtime/runtime_controllers_settings.dart +++ b/lib/runtime/runtime_controllers_settings.dart @@ -535,7 +535,7 @@ class SettingsController extends ChangeNotifier { }), ); } - } catch (_) { + } catch (e, stackTrace) { debugPrint('Error: $e\n$stackTrace'); // Best effort only. If file watching fails, directory watching may still work. } } @@ -549,7 +549,7 @@ class SettingsController extends ChangeNotifier { scheduleReload(); }), ); - } catch (_) { + } catch (e, stackTrace) { debugPrint('Error: $e\n$stackTrace'); // Best effort only. Missing watch support should not block runtime. } } diff --git a/lib/runtime/runtime_models_settings_snapshot.dart b/lib/runtime/runtime_models_settings_snapshot.dart index 076757d2..81beea04 100644 --- a/lib/runtime/runtime_models_settings_snapshot.dart +++ b/lib/runtime/runtime_models_settings_snapshot.dart @@ -339,7 +339,7 @@ class SettingsSnapshot { try { final decoded = jsonDecode(raw) as Map; return SettingsSnapshot.fromJson(decoded); - } catch (_) { + } catch (e, stackTrace) { debugPrint('Error: $e\n$stackTrace'); return SettingsSnapshot.defaults(); } } diff --git a/lib/runtime/runtime_models_ui_state.dart b/lib/runtime/runtime_models_ui_state.dart index d4affe33..19250848 100644 --- a/lib/runtime/runtime_models_ui_state.dart +++ b/lib/runtime/runtime_models_ui_state.dart @@ -1,3 +1,5 @@ +import 'package:flutter/foundation.dart'; + import 'dart:convert'; import '../models/app_models.dart'; @@ -97,7 +99,7 @@ class AppUiState { return AppUiState.defaults(); } return AppUiState.fromJson(decoded); - } catch (_) { + } catch (e, stackTrace) { debugPrint('Error: $e\n$stackTrace'); return AppUiState.defaults(); } } diff --git a/lib/runtime/secure_config_store.dart b/lib/runtime/secure_config_store.dart index 76e07416..e6cb86fb 100644 --- a/lib/runtime/secure_config_store.dart +++ b/lib/runtime/secure_config_store.dart @@ -1,3 +1,5 @@ +import 'package:flutter/foundation.dart'; + import 'dart:convert'; import 'dart:io'; @@ -134,7 +136,7 @@ class SecureConfigStore { } try { return AppUiState.fromJson(payload); - } catch (_) { + } catch (e, stackTrace) { debugPrint('Error: $e\n$stackTrace'); return AppUiState.defaults(); } } diff --git a/lib/runtime/settings_store.dart b/lib/runtime/settings_store.dart index 20382392..673befe6 100644 --- a/lib/runtime/settings_store.dart +++ b/lib/runtime/settings_store.dart @@ -1,3 +1,5 @@ +import 'package:flutter/foundation.dart'; + import 'dart:async'; import 'dart:convert'; import 'dart:io'; @@ -122,7 +124,7 @@ class SettingsStore { final layout = await _layoutResolver.resolve(); await deleteIfExists(File('${layout.tasksDirectory.path}/threads.json')); await deleteIfExists(File('${layout.configDirectory.path}/settings.yaml')); - } catch (_) { + } catch (e, stackTrace) { debugPrint('Error: $e\n$stackTrace'); // Ignore errors for secondary persistence. } } @@ -138,7 +140,7 @@ class SettingsStore { return decoded.map((e) => SecretAuditEntry.fromJson(e)).toList(); } } - } catch (_) { + } catch (e, stackTrace) { debugPrint('Error: $e\n$stackTrace'); // Ignore errors for secondary persistence. } return const [];