From c16704b052006e4a4eaa80c0cb8c66d8f873efe0 Mon Sep 17 00:00:00 2001 From: Haitao Pan Date: Sat, 6 Jun 2026 07:56:32 +0800 Subject: [PATCH] Refactor OpenClaw task integration as thin adapter --- docs/ai-context/chain-map.md | 7 +- .../chain-map-artifact-lifecycle.md | 30 +++++--- .../chain-map-session-recovery.md | 6 +- docs/architecture/chain-map-task-execution.md | 26 ++++--- .../cross-repo-call-analysis-2026-06-05.md | 17 ++--- ...26-06-05-openclaw-thin-adapter-refactor.md | 13 ++-- ...rnal_code_agent_acp_desktop_transport.dart | 20 +++-- .../runtime_models_runtime_payloads.dart | 17 +---- lib/widgets/assistant_artifact_sidebar.dart | 6 ++ .../assistant_execution_target_test.dart | 4 + .../runtime/gateway_acp_client_auth_test.dart | 76 ++++++++++++++++--- 11 files changed, 143 insertions(+), 79 deletions(-) diff --git a/docs/ai-context/chain-map.md b/docs/ai-context/chain-map.md index 8e92274a..8f89089c 100644 --- a/docs/ai-context/chain-map.md +++ b/docs/ai-context/chain-map.md @@ -97,7 +97,8 @@ lib/runtime/external_code_agent_acp_desktop_transport.dart lib/app/app_controller_openclaw_task_queue.dart OpenClawTaskQueue → 本地队列管理 (max 5 active, 20 queued) - → 持久化 & 恢复 (pollOpenClawTaskAssociationInternal) + → 持久化 typed association + → 恢复查询只发送 appThreadKey/openclawSessionKey/runId 到 native task-registry ``` Protocol boundary: @@ -124,7 +125,9 @@ Protocol boundary: ### 断点风险: - 网关 WebSocket 断连 → 任务丢失 - 分布式转发 hop=3 限制 → 深层拓扑不可达 -- 任务轮询恢复依赖 `xworkmate.tasks.get` → 非实时 +- 任务轮询恢复依赖 `xworkmate.tasks.get` → Bridge 只转发 typed lookup, + terminal state 以 OpenClaw native task-registry 为准;缺少 native task + record 时返回 `no_native_task_record`,不从 artifactScope/runId 重建任务。 --- diff --git a/docs/architecture/chain-map-artifact-lifecycle.md b/docs/architecture/chain-map-artifact-lifecycle.md index 81fb4123..cf508fd6 100644 --- a/docs/architecture/chain-map-artifact-lifecycle.md +++ b/docs/architecture/chain-map-artifact-lifecycle.md @@ -198,26 +198,32 @@ Fragile: - signing secret rotation invalidates all existing refs ``` -## State 5: Snapshot (Bridge) +## State 5: Native Task Lookup And Artifact Snapshot ``` -Caller: completeOpenClawTask() in openclaw_async_tasks.go - triggered by probeOpenClawTask() detecting completion +Caller: App polling/recovery via xworkmate.tasks.get + Bridge forwards typed appThreadKey/openclawSessionKey/runId to Plugin Process: - 1. Call gateway.request('xworkmate.artifacts.collect-and-snapshot') + 1. Plugin resolves task state from OpenClaw native task-registry + → api.runtime.tasks.runs.bindSession({sessionKey: openclawSessionKey}) + → resolve(runId) or findLatest() + + 2. Call gateway.request('xworkmate.artifacts.collect-and-snapshot') → Copy OpenClaw media/tmp outputs into the task scope - 2. Call gateway.request('xworkmate.artifacts.export') + 3. Call gateway.request('xworkmate.artifacts.export') → Get manifest from plugin - 3. openClawArtifactExport() + 4. openClawArtifactExport() → Transform manifest files into stable result shape → decorateOpenClawArtifactDownloadURLs() → Replace each file.ref with signed download URL: /artifacts/openclaw/download?ref=&t= - 4. Build terminal snapshot: + 5. Return task-registry-backed snapshot: + → Terminal success/failure is not decided by Bridge state + → Missing native task record returns no_native_task_record { success: true, status: "completed", @@ -232,10 +238,16 @@ Process: } } - 5. Store snapshot for xworkmate.tasks.get queries + 5. Do not store terminal task truth for xworkmate.tasks.get queries. + The query path forwards to OpenClaw native task-registry through the plugin. 6. Send SSE session.update to app +Removed compatibility paths: + - Bridge no longer falls back when xworkmate.session.prepare is unsupported. + - Bridge no longer reassociates OpenClaw tasks from artifactScope/runId. + - Bridge no longer treats artifact export as terminal task-state evidence. + Fragile: - If export returns empty manifest, snapshot has no artifacts - Artifact download URLs expire after 24h @@ -339,7 +351,7 @@ stateDiagram-v2 | Plugin workspace | resolveWorkspaceDir() | `~/.openclaw/workspace` | | Plugin scope | tasks/// | `/tasks///` | | Plugin export | exportXWorkmateArtifacts() | Scans only scope dir | -| Bridge snapshot | completeOpenClawTask() | In-memory, 24h signed URLs | +| Bridge snapshot | xworkmate.tasks.get proxy | Native task-registry + plugin artifact manifest | | Bridge download | /artifacts/openclaw/download | Proxied from plugin read | | App sync | syncArtifactsFromBridge() | `~/.xworkmate/threads//` | | OpenClaw media | saveMediaBuffer(subdir) | `~/.openclaw/media//` | diff --git a/docs/architecture/chain-map-session-recovery.md b/docs/architecture/chain-map-session-recovery.md index e7edc2a8..c0d236c2 100644 --- a/docs/architecture/chain-map-session-recovery.md +++ b/docs/architecture/chain-map-session-recovery.md @@ -165,9 +165,9 @@ No persistence: | Method | Params | Returns | |--------|--------|---------| -| `xworkmate.tasks.get` | sessionId, threadId, turnId (optional) | Terminal snapshot or running status | -| `xworkmate.tasks.cancel` | sessionId, threadId, turnId | Cancel confirmation | -| `reassociateOpenClawTask` | taskHandle (from stored params) | Reconnected session | +| `xworkmate.tasks.get` | appThreadKey, openclawSessionKey, runId/taskId | Native task-registry snapshot or structured lookup error | +| `xworkmate.tasks.cancel` | appThreadKey, openclawSessionKey, runId/taskId | Cancel confirmation | +| Removed: Bridge task reassociation | artifactScope/runId-derived taskHandle | No longer supported; route through native task registry | ## App Recovery Flow (Detailed) diff --git a/docs/architecture/chain-map-task-execution.md b/docs/architecture/chain-map-task-execution.md index e59c82f3..68866e34 100644 --- a/docs/architecture/chain-map-task-execution.md +++ b/docs/architecture/chain-map-task-execution.md @@ -70,7 +70,7 @@ xworkmate-bridge │ ├─ queueTimeout: 10 min │ └─ Returns: admission slot or OPENCLAW_GATEWAY_BUSY │ - └─ startOpenClawGatewayTask() + └─ startOpenClawGatewayTask() ├─ ensureProductionGatewayConnected() ├─ openClawArtifactPrepare() │ └─ gateway.request('xworkmate.session.prepare') @@ -85,20 +85,18 @@ xworkmate-bridge │ sessionKey is the OpenClaw native field and equals openclawSessionKey │ (no expectedArtifactDirs root field) │ - ├─ Create OpenClawTaskRecord + ├─ Keep OpenClawTaskRecord only as live transport context │ ├─ SessionID, ThreadID, TurnID, RunID │ ├─ SessionKey (from gateway response) │ ├─ TaskLoadClass (short_task/long_task/complex_chain_task) │ ├─ RuntimeBudgetMinutes (10/30/60) │ └─ PreparedArtifact scope ref │ - └─ startOpenClawTaskMonitor() - └─ Every 1s: probeOpenClawTask() - └─ gateway.request('agent.wait', timeout=1s) - ├─ completed → completeOpenClawTask() - ├─ failed → failOpenClawTask() - ├─ SLA expired → TASK_SLA_EXPIRED - └─ silent failure >10min → cleanup + └─ Native task status lookup + └─ xworkmate.tasks.get forwards typed + {appThreadKey, openclawSessionKey, runId/taskId} + to OpenClaw native task-registry via the plugin. + Bridge no longer rebuilds task state from artifactScope/runId. ─────────────────────────────────────────────────────────── Protocol: Custom JSON-RPC v4 over WebSocket @@ -177,11 +175,11 @@ now copied into tasks///artifacts/ before export. ─────────────────────────────────────────────────────────── Back to xworkmate-bridge: - completeOpenClawTask() + terminal transport handling ├─ Call xworkmate.artifacts.collect-and-snapshot via gateway ├─ Call xworkmate.artifacts.export via gateway ├─ Collect artifact manifest - ├─ Build terminal snapshot with: + ├─ Build App transport payload with: │ ├─ status: completed/failed/cancelled │ ├─ artifacts: { items: [...], scope: "..." } │ └─ text: final output @@ -192,6 +190,12 @@ now copied into tasks///artifacts/ before export. │ └─ Send SSE session.update to app + xworkmate.tasks.get: + Bridge forwards typed lookup to the plugin/native task-registry. + Terminal state comes from native task records only. Missing native records + return structured errors such as no_native_task_record instead of inferring + success from artifacts or reconstructing a Bridge task dictionary. + Back to xworkmate-app: ExternalCodeAgentAcpDesktopTransport ├─ Receive terminal snapshot via SSE or xworkmate.tasks.get diff --git a/docs/architecture/cross-repo-call-analysis-2026-06-05.md b/docs/architecture/cross-repo-call-analysis-2026-06-05.md index 75963ed3..8b9e6aa8 100644 --- a/docs/architecture/cross-repo-call-analysis-2026-06-05.md +++ b/docs/architecture/cross-repo-call-analysis-2026-06-05.md @@ -61,7 +61,7 @@ Scope: xworkmate-app, xworkmate-bridge, openclaw-multi-session-plugins, openclaw → startOpenClawGatewayTask() → openClawArtifactPrepare() // calls xworkmate.artifacts.prepare → gatewayruntime.send('chat.send') - → startOpenClawTaskMonitor() // background probe every 1s + → returns running handle; Bridge does not own terminal state 3. openclaw.svc.plus Gateway receives 'chat.send' @@ -75,9 +75,9 @@ Scope: xworkmate-app, xworkmate-bridge, openclaw-multi-session-plugins, openclaw → returns manifest + base64 files to bridge 5. xworkmate-bridge - openClawArtifactExport() → collects artifacts from plugin - → completeOpenClawTask() → builds terminal snapshot - → sends session.update (SSE) to app + forwards native transcript/task events to app + → xworkmate.tasks.get is forwarded to Plugin/OpenClaw native task-registry + → artifact collect/export remains an explicit proxy operation 6. xworkmate-app ExternalCodeAgentAcpDesktopTransport receives SSE @@ -171,15 +171,10 @@ type OpenClawTaskRecord struct { SessionKey string // 传递给 plugin 做 scope GatewayProviderID string // "openclaw" TaskLoadClass string // short_task/long_task/complex_chain_task - ArtifactSinceUnixMs int64 // artifact 时间窗口起始 RuntimeBudgetMinutes int // 10/30/60 - StartedAt, DeadlineAt, LastProbeAt time.Time + StartedAt, DeadlineAt time.Time ProgressStage, ProgressMessage string - ProgressTerminal bool - FirstSilentFailureAt time.Time // 静默失败计时 PreparedArtifact *openClawPreparedArtifactScope - ArtifactContract openClawArtifactContract - AdmissionRelease func() // 释放并发槽位 } ``` @@ -307,7 +302,7 @@ xworkmate-bridge 的 session 存储在 `Server.sessions map[string]*session`, **6.7 TaskSnapshot 字段不完整** -Bridge 返回的 terminal snapshot 依赖 `completeOpenClawTask()` 正确组装 artifact 列表。如果 plugin 返回的 artifact manifest 中有文件但内容为空、或相对路径超出 scope,bridge 的 snapshot 会缺少 artifact 条目。 +Bridge 不再组装 terminal snapshot。Terminal success/failure 必须来自 OpenClaw native task-registry;artifact manifest 只由 Plugin 的 collect/export/read 路径提供。如果 plugin 返回的 artifact manifest 中有文件但内容为空、或相对路径超出 scope,App 侧 artifact 栏可能缺少条目,但任务终态不应因此由 Bridge 推断。 **6.8 SSE 流中断后的轮询策略** diff --git a/docs/plans/2026-06-05-openclaw-thin-adapter-refactor.md b/docs/plans/2026-06-05-openclaw-thin-adapter-refactor.md index 05ce200e..6d0a6ebe 100644 --- a/docs/plans/2026-06-05-openclaw-thin-adapter-refactor.md +++ b/docs/plans/2026-06-05-openclaw-thin-adapter-refactor.md @@ -14,8 +14,8 @@ - Do not modify `/Users/shenlan/workspaces/cloud-neutral-toolkit/openclaw.svc.plus`. - Do not rely on prompt text parsing for metadata. -- Do not use `agent:main:${appThreadKey}` or `replace("agent:main:", "")` as the new-path session mapping. -- Legacy string derivation is allowed only as one-time migration that writes a durable mapping. +- Do not reverse-map session keys from string conventions. The deterministic `agent:main:` form is allowed only for initial native session creation before prepare persists the mapping. +- Legacy string derivation and migration-only mapping fields are removed from the current flow. - Do not restore plugin-owned task DB, session DB, or event bus. - Validation must reference `docs/cases/`, especially `docs/cases/openclaw-gateway-e2e-regression/README.md`. @@ -48,8 +48,7 @@ type XWorkmateSessionMappingV1 = { expectedArtifactDirs: string[]; createdAt: string; updatedAt: string; - source: "session_start" | "bridge_prepare" | "legacy_migration"; - legacyDerived?: boolean; + source: "session_start" | "bridge_prepare"; }; ``` @@ -69,7 +68,7 @@ Add tests for: - Mapping is written to `SessionEntry.pluginExtensions`. - Idempotent same mapping updates `updatedAt`. - Conflicting existing mapping fails closed. -- Legacy derivation, if needed, writes `legacyDerived: true` and persists the mapping. +- Missing typed mapping returns `mapping_not_found` / `invalid_lookup`; current flow does not derive replacement keys. **Step 2: Run failing test** @@ -133,7 +132,7 @@ In `index.ts`, register `xworkmate.session.prepare`. It should: - call `prepareXWorkmateArtifacts`; - return `{ ok: true, mapping, artifactScope, artifactDirectory, expectedArtifactDirs }`. -Keep `xworkmate.artifacts.prepare/export/collect-and-snapshot` as thin artifact operations. Remove in-memory `createXWorkmateTaskStore()` as a required source of truth. +Keep `xworkmate.artifacts.prepare/export/collect-and-snapshot` as thin artifact operations. The in-memory plugin task store is removed and must not be reintroduced as a task source of truth. **Step 4: Run plugin suite** @@ -322,7 +321,7 @@ Retain only transport/reconciliation caches that have clear owner, scope, and in Run: ```bash -rg -n "agent:main:\\$|replace\\(|ThreadSessionMapper|createXWorkmateTaskStore|sessionMappingsBy|records = new Map|bridgeAgents" \ +rg -n "ThreadSessionMapper|sessionMappingsBy|records = new Map|bridgeAgents" \ /Users/shenlan/workspaces/cloud-neutral-toolkit/openclaw-multi-session-plugins \ /Users/shenlan/workspaces/cloud-neutral-toolkit/xworkmate-bridge \ /Users/shenlan/workspaces/cloud-neutral-toolkit/xworkmate-app diff --git a/lib/runtime/external_code_agent_acp_desktop_transport.dart b/lib/runtime/external_code_agent_acp_desktop_transport.dart index 029ef255..83b83bcc 100644 --- a/lib/runtime/external_code_agent_acp_desktop_transport.dart +++ b/lib/runtime/external_code_agent_acp_desktop_transport.dart @@ -62,9 +62,7 @@ class ExternalCodeAgentAcpDesktopTransport implements GoTaskServiceClient { if (update.isDone && update.message.trim().isNotEmpty) { completedMessage = update.message.trim(); } - if (update.payload['status']?.toString().trim().toLowerCase() == - 'running' && - (update.payload['runId']?.toString().trim().isNotEmpty == true)) { + if (OpenClawTaskAssociation.fromJsonOrNull(update.payload) != null) { runningTaskSnapshot = {...update.payload}; } onUpdate(update); @@ -130,6 +128,9 @@ class ExternalCodeAgentAcpDesktopTransport implements GoTaskServiceClient { final association = OpenClawTaskAssociation.fromJsonOrNull( runningTaskSnapshot, ); + if (association == null) { + return null; + } final attempts = _recoveryAttemptsForRequest(request); for (var attempt = 0; attempt < attempts; attempt += 1) { if (attempt > 0) { @@ -139,12 +140,7 @@ class ExternalCodeAgentAcpDesktopTransport implements GoTaskServiceClient { try { response = await _client.request( method: 'xworkmate.tasks.get', - params: - association?.toTaskGetParams() ?? - { - 'sessionId': request.sessionId, - 'threadId': request.threadId, - }, + params: association.toTaskGetParams(), endpointOverride: endpoint, ); } on GatewayAcpException { @@ -177,9 +173,11 @@ class ExternalCodeAgentAcpDesktopTransport implements GoTaskServiceClient { final result = _recoveredResultFromTaskSnapshot(snapshot); final resultArtifacts = _castMap(result['artifacts']); final artifactItems = resultArtifacts['items'] ?? resultArtifacts; - final hasArtifacts = result.isNotEmpty && + final hasArtifacts = + result.isNotEmpty && (artifactItems is List && artifactItems.isNotEmpty || - result['artifacts'] is List && (result['artifacts'] as List).isNotEmpty); + result['artifacts'] is List && + (result['artifacts'] as List).isNotEmpty); if (!hasArtifacts && status == 'completed' && attempt < attempts - 1) { continue; } diff --git a/lib/runtime/runtime_models_runtime_payloads.dart b/lib/runtime/runtime_models_runtime_payloads.dart index f370e295..68ee77ed 100644 --- a/lib/runtime/runtime_models_runtime_payloads.dart +++ b/lib/runtime/runtime_models_runtime_payloads.dart @@ -993,19 +993,10 @@ class OpenClawTaskAssociation { Map toTaskGetParams() { return { - 'sessionId': sessionId, - 'threadId': threadId, - 'turnId': turnId, 'runId': runId, - 'artifactScope': artifactScope, - 'artifactDirectory': artifactDirectory, - 'gatewayProviderId': gatewayProviderId, - 'taskLoadClass': taskLoadClass, 'appThreadKey': appThreadKey, 'openclawSessionKey': openclawSessionKey, 'includeArtifacts': true, - 'requiredArtifactExtensions': requiredArtifactExtensions, - 'expectedArtifactExtensions': expectedArtifactExtensions, }; } @@ -1015,14 +1006,10 @@ class OpenClawTaskAssociation { } final json = value.cast(); final runId = json['runId']?.toString().trim() ?? ''; - final artifactScope = json['artifactScope']?.toString().trim() ?? ''; final appThreadKey = json['appThreadKey']?.toString().trim() ?? ''; final openclawSessionKey = json['openclawSessionKey']?.toString().trim() ?? ''; - if (runId.isEmpty || - artifactScope.isEmpty || - appThreadKey.isEmpty || - openclawSessionKey.isEmpty) { + if (runId.isEmpty || appThreadKey.isEmpty || openclawSessionKey.isEmpty) { return null; } double asDouble(Object? raw) { @@ -1037,7 +1024,7 @@ class OpenClawTaskAssociation { threadId: json['threadId']?.toString().trim() ?? '', turnId: json['turnId']?.toString().trim() ?? '', runId: runId, - artifactScope: artifactScope, + artifactScope: json['artifactScope']?.toString().trim() ?? '', artifactDirectory: json['artifactDirectory']?.toString().trim() ?? '', gatewayProviderId: json['gatewayProviderId']?.toString().trim().isNotEmpty == true diff --git a/lib/widgets/assistant_artifact_sidebar.dart b/lib/widgets/assistant_artifact_sidebar.dart index 039d99b2..bd853975 100644 --- a/lib/widgets/assistant_artifact_sidebar.dart +++ b/lib/widgets/assistant_artifact_sidebar.dart @@ -399,6 +399,12 @@ class _AssistantArtifactSidebarState extends State { } String _filesEmptyMessage(AssistantArtifactSnapshot snapshot) { + if (widget.artifactSyncStatus.trim().toLowerCase() == 'failed') { + return appText( + '本轮没有检测到实际生成的文件。请重新执行,并要求 OpenClaw 在当前 workspace 中创建文件。', + 'No files were generated for this run. Try again and ask OpenClaw to create files in the current workspace.', + ); + } final filesMessage = snapshot.filesMessage.trim(); if (filesMessage.isNotEmpty) { return filesMessage; diff --git a/test/runtime/assistant_execution_target_test.dart b/test/runtime/assistant_execution_target_test.dart index a39f87c2..184b8ddd 100644 --- a/test/runtime/assistant_execution_target_test.dart +++ b/test/runtime/assistant_execution_target_test.dart @@ -87,7 +87,11 @@ void main() { params['openclawSessionKey'], 'agent:main:draft:1780658097668838-1', ); + expect(params['runId'], 'run-1'); expect(params, isNot(contains('sessionKey'))); + expect(params, isNot(contains('sessionId'))); + expect(params, isNot(contains('threadId'))); + expect(params, isNot(contains('artifactScope'))); }); test('recognizes openclaw as the canonical gateway provider', () { diff --git a/test/runtime/gateway_acp_client_auth_test.dart b/test/runtime/gateway_acp_client_auth_test.dart index 95375752..e0c76f91 100644 --- a/test/runtime/gateway_acp_client_auth_test.dart +++ b/test/runtime/gateway_acp_client_auth_test.dart @@ -658,8 +658,19 @@ void main() { if (method == 'session.start') { final event = jsonEncode({ 'jsonrpc': '2.0', - 'method': 'xworkmate.bridge.accepted', - 'params': {'sessionId': 'unit-fixture-task-a'}, + 'method': 'session.update', + 'params': { + 'sessionId': 'unit-fixture-task-a', + 'threadId': 'unit-fixture-task-a', + 'turnId': 'turn-recovered', + 'type': 'status', + 'event': 'running', + 'status': 'running', + 'runId': 'turn-recovered', + 'appThreadKey': 'unit-fixture-task-a', + 'openclawSessionKey': 'agent:main:unit-fixture-task-a', + 'gatewayProviderId': 'openclaw', + }, }); final eventBytes = utf8.encode('data: $event\n\n'); request.response.headers.set( @@ -774,6 +785,10 @@ void main() { 'turnId': 'turn-final', 'type': 'status', 'event': 'completed', + 'status': 'completed', + 'runId': 'turn-final', + 'appThreadKey': 'unit-fixture-task-final', + 'openclawSessionKey': 'agent:main:unit-fixture-task-final', 'message': 'early completed output without artifacts', 'success': true, }, @@ -882,8 +897,19 @@ void main() { if (method == 'session.message') { final event = jsonEncode({ 'jsonrpc': '2.0', - 'method': 'xworkmate.bridge.accepted', - 'params': {'sessionId': 'unit-fixture-task-sse'}, + 'method': 'session.update', + 'params': { + 'sessionId': 'unit-fixture-task-sse', + 'threadId': 'unit-fixture-task-sse', + 'turnId': 'turn-recovered-sse', + 'type': 'status', + 'event': 'running', + 'status': 'running', + 'runId': 'turn-recovered-sse', + 'appThreadKey': 'unit-fixture-task-sse', + 'openclawSessionKey': 'agent:main:unit-fixture-task-sse', + 'gatewayProviderId': 'openclaw', + }, }); request.response.headers.set( HttpHeaders.contentTypeHeader, @@ -971,8 +997,19 @@ void main() { if (method == 'session.start') { final event = jsonEncode({ 'jsonrpc': '2.0', - 'method': 'xworkmate.bridge.accepted', - 'params': {'sessionId': 'unit-fixture-task-b'}, + 'method': 'session.update', + 'params': { + 'sessionId': 'unit-fixture-task-b', + 'threadId': 'unit-fixture-task-b', + 'turnId': 'turn-recovered-running', + 'type': 'status', + 'event': 'running', + 'status': 'running', + 'runId': 'turn-recovered-running', + 'appThreadKey': 'unit-fixture-task-b', + 'openclawSessionKey': 'agent:main:unit-fixture-task-b', + 'gatewayProviderId': 'openclaw', + }, }); final eventBytes = utf8.encode('data: $event\n\n'); request.response.headers.set( @@ -1092,6 +1129,8 @@ void main() { 'artifactDirectory': '/home/ubuntu/.openclaw/workspace/tasks/unit-fixture-task-handle/run-running', 'gatewayProviderId': 'openclaw', + 'appThreadKey': 'unit-fixture-task-handle', + 'openclawSessionKey': 'agent:main:unit-fixture-task-handle', }, }); final eventBytes = utf8.encode('data: $event\n\n'); @@ -1178,9 +1217,15 @@ void main() { expect(snapshotPolls, 1); expect(taskGetParams.single['runId'], 'run-running'); expect( - taskGetParams.single['artifactScope'], - 'tasks/unit-fixture-task-handle/run-running', + taskGetParams.single['appThreadKey'], + 'unit-fixture-task-handle', ); + expect( + taskGetParams.single['openclawSessionKey'], + 'agent:main:unit-fixture-task-handle', + ); + expect(taskGetParams.single, isNot(contains('sessionKey'))); + expect(taskGetParams.single, isNot(contains('artifactScope'))); expect(result.success, isTrue); expect(result.message, 'completed after task handle'); expect(result.artifacts.single.relativePath, 'reports/final.md'); @@ -1200,8 +1245,19 @@ void main() { if (method == 'session.start') { final event = jsonEncode({ 'jsonrpc': '2.0', - 'method': 'xworkmate.bridge.accepted', - 'params': {'sessionId': 'unit-fixture-task-c'}, + 'method': 'session.update', + 'params': { + 'sessionId': 'unit-fixture-task-c', + 'threadId': 'unit-fixture-task-c', + 'turnId': 'turn-failed', + 'type': 'status', + 'event': 'running', + 'status': 'running', + 'runId': 'turn-failed', + 'appThreadKey': 'unit-fixture-task-c', + 'openclawSessionKey': 'agent:main:unit-fixture-task-c', + 'gatewayProviderId': 'openclaw', + }, }); final eventBytes = utf8.encode('data: $event\n\n'); request.response.headers.set(