merge: poll openclaw task handle to terminal snapshot

This commit is contained in:
Cowork 3P 2026-06-05 06:16:14 +08:00
commit c4191fa5c9
5 changed files with 137 additions and 60 deletions

View File

@ -1324,15 +1324,11 @@ extension AppControllerDesktopThreadActions on AppController {
return;
}
if (!result.success) {
if (hasCurrentRunArtifacts) {
await persistGoTaskArtifactsForSessionInternal(sessionKey, result);
} else {
clearGatewayTaskArtifactStateInternal(
sessionKey,
completedAtMs: completedAtMs,
syncStatus: 'failed',
);
}
clearGatewayTaskArtifactStateInternal(
sessionKey,
completedAtMs: completedAtMs,
syncStatus: 'failed',
);
appendLocalSessionMessageInternal(
sessionKey,
assistantErrorMessageInternal(

View File

@ -218,18 +218,6 @@ class ExternalCodeAgentAcpDesktopTransport
final association = OpenClawTaskAssociation.fromJsonOrNull(
runningTaskSnapshot,
);
if (association != null) {
return goTaskServiceResultFromAcpResponse(
<String, dynamic>{
'jsonrpc': '2.0',
'id': 'recovered-from-running-task-handle',
'result': runningTaskSnapshot,
},
route: request.route,
streamedText: streamedText,
completedMessage: completedMessage,
);
}
final attempts = _recoveryAttemptsForRequest(request);
for (var attempt = 0; attempt < attempts; attempt += 1) {
if (attempt > 0) {
@ -239,10 +227,11 @@ class ExternalCodeAgentAcpDesktopTransport
try {
response = await _client.request(
method: 'xworkmate.tasks.get',
params: <String, dynamic>{
'sessionId': request.sessionId,
'threadId': request.threadId,
},
params: association?.toTaskGetParams() ??
<String, dynamic>{
'sessionId': request.sessionId,
'threadId': request.threadId,
},
endpointOverride: endpoint,
);
} on GatewayAcpException {

View File

@ -621,19 +621,15 @@ Object? _firstGoTaskArtifactList(Map<String, dynamic> result) {
final artifacts = <Object?>[];
for (final candidate in <Object?>[
result['artifacts'],
result['finalArtifacts'],
result['files'],
result['attachments'],
_castMap(result['payload'])['artifacts'],
_castMap(result['payload'])['finalArtifacts'],
_castMap(result['payload'])['files'],
_castMap(result['payload'])['attachments'],
_castMap(result['result'])['artifacts'],
_castMap(result['result'])['finalArtifacts'],
_castMap(result['result'])['files'],
_castMap(result['result'])['attachments'],
_castMap(result['data'])['artifacts'],
_castMap(result['data'])['finalArtifacts'],
_castMap(result['data'])['files'],
_castMap(result['data'])['attachments'],
]) {

View File

@ -1954,7 +1954,7 @@ void main() {
);
test(
'sendChatMessage keeps partial OpenClaw artifacts on terminal artifact failure',
'sendChatMessage rejects partial OpenClaw artifacts on terminal artifact failure',
() async {
final fakeGoTaskService = _RecordingGoTaskServiceClient()
..outcomes.add(
@ -1995,10 +1995,8 @@ void main() {
failedThread?.lifecycleState.lastResultCode,
'OPENCLAW_REQUIRED_ARTIFACT_MISSING',
);
expect(failedThread?.lastArtifactSyncStatus, 'synced');
expect(failedThread?.lastTaskArtifactRelativePaths, <String>[
'stages/chapter.md',
]);
expect(failedThread?.lastArtifactSyncStatus, 'failed');
expect(failedThread?.lastTaskArtifactRelativePaths, isEmpty);
},
);

View File

@ -252,32 +252,6 @@ void main() {
);
});
test('uses OpenClaw finalArtifacts as required deliverables', () {
final result = goTaskServiceResultFromAcpResponse(<String, dynamic>{
'jsonrpc': '2.0',
'id': 'request-id',
'result': <String, dynamic>{
'success': true,
'message': 'created final deliverable',
'finalArtifacts': <Map<String, dynamic>>[
<String, dynamic>{
'relativePath': 'exports/final.pdf',
'downloadUrl':
'https://xworkmate-bridge.svc.plus/artifacts/final.pdf',
'contentType': 'application/pdf',
},
],
},
}, route: GoTaskServiceRoute.externalAcpSingle);
expect(result.success, isTrue);
expect(result.artifacts, hasLength(1));
expect(result.artifacts.single.relativePath, 'exports/final.pdf');
expect(
result.artifacts.single.downloadUrl,
'https://xworkmate-bridge.svc.plus/artifacts/final.pdf',
);
});
});
group('GatewayAcpClient authorization', () {
@ -1075,6 +1049,130 @@ void main() {
},
);
test(
'polls terminal OpenClaw snapshot after receiving a running task handle',
() async {
final server = await HttpServer.bind(InternetAddress.loopbackIPv4, 0);
addTearDown(() => server.close(force: true));
var snapshotPolls = 0;
final taskGetParams = <Map<String, dynamic>>[];
server.listen((request) async {
final body = await utf8.decoder.bind(request).join();
final decoded = jsonDecode(body) as Map<String, dynamic>;
final method = decoded['method']?.toString() ?? '';
final id = decoded['id']?.toString() ?? 'request-id';
if (method == 'session.start') {
final event = jsonEncode(<String, dynamic>{
'jsonrpc': '2.0',
'method': 'session.update',
'params': <String, dynamic>{
'sessionId': 'unit-fixture-task-handle',
'threadId': 'unit-fixture-task-handle',
'turnId': 'turn-running',
'type': 'status',
'event': 'running',
'status': 'running',
'runId': 'run-running',
'artifactScope':
'tasks/unit-fixture-task-handle/run-running',
'artifactDirectory':
'/home/ubuntu/.openclaw/workspace/tasks/unit-fixture-task-handle/run-running',
'gatewayProviderId': 'openclaw',
},
});
final eventBytes = utf8.encode('data: $event\n\n');
request.response.headers.set(
HttpHeaders.contentTypeHeader,
'text/event-stream',
);
request.response.contentLength = eventBytes.length + 128;
final socket = await request.response.detachSocket();
socket.add(eventBytes);
await socket.flush();
socket.destroy();
return;
}
if (method == 'xworkmate.tasks.get') {
snapshotPolls += 1;
taskGetParams.add(
(decoded['params'] as Map).cast<String, dynamic>(),
);
request.response.headers.contentType = ContentType.json;
request.response.write(
jsonEncode(<String, dynamic>{
'jsonrpc': '2.0',
'id': id,
'result': <String, dynamic>{
'status': 'completed',
'sessionId': 'unit-fixture-task-handle',
'threadId': 'unit-fixture-task-handle',
'turnId': 'turn-running',
'result': <String, dynamic>{
'success': true,
'output': 'completed after task handle',
'turnId': 'turn-running',
},
'artifacts': <String, dynamic>{
'items': <Map<String, dynamic>>[
<String, dynamic>{
'relativePath': 'reports/final.md',
'downloadUrl':
'https://xworkmate-bridge.svc.plus/artifacts/openclaw/download'
'?sessionKey=unit-fixture-task-handle&runId=run-running&relativePath=reports%2Ffinal.md',
'contentType': 'text/markdown',
},
],
},
},
}),
);
await request.response.close();
return;
}
request.response.statusCode = HttpStatus.badRequest;
await request.response.close();
});
final endpoint = Uri.parse('http://127.0.0.1:${server.port}');
final transport = ExternalCodeAgentAcpDesktopTransport(
client: GatewayAcpClient(endpointResolver: () => endpoint),
endpointResolver: (_) => endpoint,
taskEndpointResolver: (_) => endpoint,
recoveryPollDelay: Duration.zero,
recoveryMaxAttempts: 2,
);
addTearDown(transport.dispose);
final result = await transport.executeTask(
const GoTaskServiceRequest(
sessionId: 'unit-fixture-task-handle',
threadId: 'unit-fixture-task-handle',
target: AssistantExecutionTarget.gateway,
provider: SingleAgentProvider.openclaw,
prompt: 'create final markdown',
workingDirectory: '/tmp/workspace',
model: '',
thinking: 'off',
selectedSkills: <String>[],
inlineAttachments: <GatewayChatAttachmentPayload>[],
localAttachments: <CollaborationAttachment>[],
agentId: '',
metadata: <String, dynamic>{},
),
onUpdate: (_) {},
);
expect(snapshotPolls, 1);
expect(taskGetParams.single['runId'], 'run-running');
expect(
taskGetParams.single['artifactScope'],
'tasks/unit-fixture-task-handle/run-running',
);
expect(result.success, isTrue);
expect(result.message, 'completed after task handle');
expect(result.artifacts.single.relativePath, 'reports/final.md');
},
);
test(
'recovers terminal failed OpenClaw snapshot without displayable result',
() async {