Preserve artifacts after interrupted bridge responses

This commit is contained in:
Haitao Pan 2026-06-02 16:21:17 +08:00
parent 55a1ce2af4
commit c4ca5a2c34
3 changed files with 64 additions and 7 deletions

View File

@ -366,6 +366,38 @@ extension AppControllerDesktopRuntimeHelpers on AppController {
return false;
}
Future<List<String>> recoverGatewayFailureArtifactPathsInternal(
String sessionKey,
Object error,
) async {
if (interruptedAcpHttpTransportCodeInternal(error) !=
'ACP_HTTP_CONNECTION_CLOSED') {
return const <String>[];
}
final normalizedSessionKey = normalizedAssistantSessionKeyInternal(
sessionKey,
);
final thread = taskThreadForSessionInternal(normalizedSessionKey);
if (thread == null ||
thread.workspaceBinding.workspaceKind != WorkspaceKind.localFs) {
return const <String>[];
}
final root = Directory(thread.workspaceBinding.workspacePath);
try {
final policy = await _loadArtifactSyncPolicyInternal(
root,
thread.selectedSkillKeys,
);
return _workspaceArtifactPathsModifiedSinceInternal(
root,
thread.lifecycleState.lastRunAtMs,
policy,
);
} catch (_) {
return const <String>[];
}
}
String jsonLikeTextForDiagnosticsInternal(Object? value) {
try {
return jsonEncode(value);

View File

@ -626,7 +626,7 @@ extension AppControllerDesktopThreadActions on AppController {
clearAiGatewayStreamingTextInternal(sessionKey);
return;
}
applyGatewayChatFailureInternal(
await applyGatewayChatFailureInternal(
sessionKey: sessionKey,
target: target,
error: error,
@ -1087,7 +1087,7 @@ extension AppControllerDesktopThreadActions on AppController {
);
} catch (error) {
if (!disposedInternal) {
applyGatewayChatFailureInternal(
await applyGatewayChatFailureInternal(
sessionKey: turn.sessionKey,
target: turn.target,
error: error,
@ -1252,13 +1252,15 @@ extension AppControllerDesktopThreadActions on AppController {
await persistGoTaskArtifactsForSessionInternal(sessionKey, result);
}
void applyGatewayChatFailureInternal({
Future<void> applyGatewayChatFailureInternal({
required String sessionKey,
required AssistantExecutionTarget target,
required Object error,
}) {
}) async {
clearAiGatewayStreamingTextInternal(sessionKey);
final completedAtMs = DateTime.now().millisecondsSinceEpoch.toDouble();
final recoveredArtifactPaths =
await recoverGatewayFailureArtifactPathsInternal(sessionKey, error);
upsertTaskThreadInternal(
sessionKey,
lifecycleStatus: 'ready',
@ -1266,8 +1268,10 @@ extension AppControllerDesktopThreadActions on AppController {
lastResultCode: gatewayFailureResultCodeInternal(error),
lastRemoteWorkingDirectory: '',
lastArtifactSyncAtMs: completedAtMs,
lastArtifactSyncStatus: 'failed',
lastTaskArtifactRelativePaths: const <String>[],
lastArtifactSyncStatus: recoveredArtifactPaths.isEmpty
? 'failed'
: 'interrupted',
lastTaskArtifactRelativePaths: recoveredArtifactPaths,
clearOpenClawTaskAssociation: true,
updatedAtMs: completedAtMs,
);

View File

@ -1370,6 +1370,14 @@ void main() {
}
});
final fakeGoTaskService = _RecordingGoTaskServiceClient()
..onExecuteTask = ((request) async {
await Directory(
'${request.workingDirectory}/assets/images',
).create(recursive: true);
await File(
'${request.workingDirectory}/assets/images/final.v2.png',
).writeAsBytes(<int>[1, 2, 3, 4]);
})
..updatesBeforeNextOutcome.add(
const GoTaskServiceUpdate(
sessionId: 'unit-fixture-task-a',
@ -1432,7 +1440,13 @@ void main() {
controller
.taskThreadForSessionInternal('unit-fixture-task-a')
?.lastArtifactSyncStatus,
'failed',
'interrupted',
);
expect(
controller
.taskThreadForSessionInternal('unit-fixture-task-a')
?.lastTaskArtifactRelativePaths,
<String>['assets/images/final.v2.png'],
);
await controller.sendChatMessage('follow up');
@ -1869,6 +1883,11 @@ void main() {
expect(fakeGoTaskService.requests.last.resumeSession, isFalse);
await _waitForLastChatMessageText(controller, '全部 6 个文件已生成 ✅');
expect(controller.chatMessages.last.text, '全部 6 个文件已生成 ✅');
await _waitForThreadLastResultCode(
controller,
'unit-fixture-task-a',
'SUCCESS',
);
final thread = controller.taskThreadForSessionInternal(
'unit-fixture-task-a',
);
@ -4185,6 +4204,7 @@ class _RecordingGoTaskServiceClient implements GoTaskServiceClient {
final List<GoTaskServiceUpdate> updatesBeforeNextOutcome =
<GoTaskServiceUpdate>[];
final List<Object> outcomes = <Object>[];
Future<void> Function(GoTaskServiceRequest request)? onExecuteTask;
@override
Future<ExternalCodeAgentAcpCapabilities> loadExternalAcpCapabilities({
@ -4207,6 +4227,7 @@ class _RecordingGoTaskServiceClient implements GoTaskServiceClient {
}) async {
executeCount += 1;
requests.add(request);
await onExecuteTask?.call(request);
for (final update in List<GoTaskServiceUpdate>.from(
updatesBeforeNextOutcome,
)) {