Stabilize OpenClaw artifact sync
This commit is contained in:
parent
7f665862e6
commit
f034e6f28e
@ -871,9 +871,9 @@ extension AppControllerDesktopRuntimeHelpers on AppController {
|
||||
}
|
||||
|
||||
final thread = taskThreadForSessionInternal(normalizedSessionKey);
|
||||
final association = thread?.openClawTaskAssociation;
|
||||
final requiredExts =
|
||||
thread?.openClawTaskAssociation?.requiredArtifactExtensions ??
|
||||
const <String>[];
|
||||
association?.requiredArtifactExtensions ?? const <String>[];
|
||||
final missingRequired = requiredExts
|
||||
.where((ext) {
|
||||
return !currentTaskArtifactPaths.any(
|
||||
@ -882,6 +882,31 @@ extension AppControllerDesktopRuntimeHelpers on AppController {
|
||||
})
|
||||
.toList(growable: false);
|
||||
|
||||
final shouldKeepPollingAfterDownloadFailure =
|
||||
!wroteArtifact &&
|
||||
failedArtifact &&
|
||||
result.success &&
|
||||
association != null &&
|
||||
(association.requiresArtifactExport ||
|
||||
association.requiredArtifactExtensions.isNotEmpty) &&
|
||||
(association.artifactScope.trim().isNotEmpty ||
|
||||
association.artifactDirectory.trim().isNotEmpty);
|
||||
if (shouldKeepPollingAfterDownloadFailure) {
|
||||
upsertTaskThreadInternal(
|
||||
normalizedSessionKey,
|
||||
lifecycleStatus: 'running',
|
||||
lastResultCode: 'running',
|
||||
lastArtifactSyncAtMs: syncedAtMs,
|
||||
lastArtifactSyncStatus: 'syncing',
|
||||
lastTaskArtifactRelativePaths: const <String>[],
|
||||
openClawTaskAssociation: association.copyWith(
|
||||
status: 'syncing-artifacts',
|
||||
),
|
||||
updatedAtMs: syncedAtMs,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final syncStatus = wroteArtifact
|
||||
? (failedArtifact || skippedArtifact || missingRequired.isNotEmpty
|
||||
? 'partial'
|
||||
@ -1394,7 +1419,9 @@ Future<_ArtifactSyncPolicy> _loadArtifactSyncPolicyInternal(
|
||||
policyFiles.add(resolvedRelativePath);
|
||||
}
|
||||
}
|
||||
final policies = <_ArtifactSyncPolicy>[];
|
||||
final policies = <_ArtifactSyncPolicy>[
|
||||
..._defaultArtifactSyncPoliciesForSkillsInternal(selectedSkillKeys),
|
||||
];
|
||||
try {
|
||||
for (final file in files) {
|
||||
if (!await file.exists()) {
|
||||
@ -1408,6 +1435,33 @@ Future<_ArtifactSyncPolicy> _loadArtifactSyncPolicyInternal(
|
||||
return _ArtifactSyncPolicy.merge(policies, policyFiles: policyFiles);
|
||||
}
|
||||
|
||||
List<_ArtifactSyncPolicy> _defaultArtifactSyncPoliciesForSkillsInternal(
|
||||
List<String> selectedSkillKeys,
|
||||
) {
|
||||
final hasVideoSkill = selectedSkillKeys.any((skillKey) {
|
||||
final normalized = _sanitizeArtifactRelativePathInternal(
|
||||
skillKey,
|
||||
).toLowerCase();
|
||||
final segments = normalized.split('/');
|
||||
final leaf = segments.isEmpty ? normalized : segments.last;
|
||||
return leaf == 'it-infra-evolution-video-v2';
|
||||
});
|
||||
if (!hasVideoSkill) {
|
||||
return const <_ArtifactSyncPolicy>[];
|
||||
}
|
||||
return <_ArtifactSyncPolicy>[
|
||||
_ArtifactSyncPolicy.parse(
|
||||
'```artifact-ignore\n'
|
||||
'assets/audio/\n'
|
||||
'assets/images/\n'
|
||||
'build_segments/\n'
|
||||
'snapshots/\n'
|
||||
'tmp/\n'
|
||||
'```\n',
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
String _normalizeAuthorizationHeaderInternal(String raw) {
|
||||
final trimmed = raw.trim();
|
||||
if (trimmed.isEmpty) {
|
||||
|
||||
@ -1001,6 +1001,16 @@ class OpenClawTaskAssociation {
|
||||
'appThreadKey': appThreadKey,
|
||||
'openclawSessionKey': openclawSessionKey,
|
||||
'includeArtifacts': true,
|
||||
if (artifactScope.trim().isNotEmpty) 'artifactScope': artifactScope,
|
||||
if (artifactDirectory.trim().isNotEmpty)
|
||||
'artifactDirectory': artifactDirectory,
|
||||
if (gatewayProviderId.trim().isNotEmpty)
|
||||
'gatewayProviderId': gatewayProviderId,
|
||||
if (requiresArtifactExport) 'requiresArtifactExport': true,
|
||||
if (expectedArtifactExtensions.isNotEmpty)
|
||||
'expectedArtifactExtensions': expectedArtifactExtensions,
|
||||
if (requiredArtifactExtensions.isNotEmpty)
|
||||
'requiredArtifactExtensions': requiredArtifactExtensions,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -1667,6 +1667,84 @@ void main() {
|
||||
expect(thread.lastTaskArtifactRelativePaths, isEmpty);
|
||||
});
|
||||
|
||||
test(
|
||||
'keeps polling OpenClaw export after required artifact download fails',
|
||||
() async {
|
||||
final controller = AppController(
|
||||
environmentOverride: const <String, String>{},
|
||||
);
|
||||
addTearDown(controller.dispose);
|
||||
|
||||
final localWorkspace = await Directory.systemTemp.createTemp(
|
||||
'xworkmate-openclaw-required-download-failed-',
|
||||
);
|
||||
addTearDown(() async {
|
||||
if (await localWorkspace.exists()) {
|
||||
await localWorkspace.delete(recursive: true);
|
||||
}
|
||||
});
|
||||
controller.upsertTaskThreadInternal(
|
||||
'unit-fixture-task-a',
|
||||
workspaceBinding: WorkspaceBinding(
|
||||
workspaceId: 'unit-fixture-task-a',
|
||||
workspaceKind: WorkspaceKind.localFs,
|
||||
workspacePath: localWorkspace.path,
|
||||
displayPath: localWorkspace.path,
|
||||
writable: true,
|
||||
),
|
||||
openClawTaskAssociation: const OpenClawTaskAssociation(
|
||||
sessionId: 'unit-fixture-task-a',
|
||||
threadId: 'unit-fixture-task-a',
|
||||
turnId: 'turn-1',
|
||||
runId: 'turn-1',
|
||||
artifactScope: 'tasks/agent_main_unit_fixture/turn-1',
|
||||
artifactDirectory: '/remote/tasks/agent_main_unit_fixture/turn-1',
|
||||
gatewayProviderId: 'openclaw',
|
||||
startedAtMs: 1,
|
||||
status: 'completed',
|
||||
appThreadKey: 'unit-fixture-task-a',
|
||||
openclawSessionKey: 'agent:main:unit-fixture',
|
||||
requiresArtifactExport: true,
|
||||
),
|
||||
);
|
||||
|
||||
final bytes = utf8.encode('# Final\n');
|
||||
final result = GoTaskServiceResult(
|
||||
success: true,
|
||||
message: 'done',
|
||||
turnId: 'turn-1',
|
||||
raw: <String, dynamic>{
|
||||
'artifacts': <Map<String, dynamic>>[
|
||||
<String, dynamic>{
|
||||
'relativePath': 'exports/final.md',
|
||||
'contentType': 'text/markdown',
|
||||
'encoding': 'base64',
|
||||
'content': base64Encode(bytes),
|
||||
'sizeBytes': bytes.length,
|
||||
'sha256': '0' * 64,
|
||||
},
|
||||
],
|
||||
},
|
||||
errorMessage: '',
|
||||
resolvedModel: '',
|
||||
route: GoTaskServiceRoute.externalAcpSingle,
|
||||
);
|
||||
|
||||
await controller.persistGoTaskArtifactsForSessionInternal(
|
||||
'unit-fixture-task-a',
|
||||
result,
|
||||
);
|
||||
|
||||
final thread = controller.requireTaskThreadForSessionInternal(
|
||||
'unit-fixture-task-a',
|
||||
);
|
||||
expect(thread.lastArtifactSyncStatus, 'syncing');
|
||||
expect(thread.lifecycleState.lastResultCode, 'running');
|
||||
expect(thread.openClawTaskAssociation?.status, 'syncing-artifacts');
|
||||
expect(thread.lastTaskArtifactRelativePaths, isEmpty);
|
||||
},
|
||||
);
|
||||
|
||||
test('loads global and selected skill artifact-ignore policies', () async {
|
||||
final controller = AppController(
|
||||
environmentOverride: const <String, String>{},
|
||||
@ -1775,6 +1853,78 @@ void main() {
|
||||
]);
|
||||
});
|
||||
|
||||
test('uses default artifact-ignore policy for video skill outputs', () async {
|
||||
final controller = AppController(
|
||||
environmentOverride: const <String, String>{},
|
||||
);
|
||||
addTearDown(controller.dispose);
|
||||
|
||||
final localWorkspace = await Directory.systemTemp.createTemp(
|
||||
'xworkmate-video-default-artifact-policy-',
|
||||
);
|
||||
addTearDown(() async {
|
||||
if (await localWorkspace.exists()) {
|
||||
await localWorkspace.delete(recursive: true);
|
||||
}
|
||||
});
|
||||
final startedAtMs = DateTime.now().millisecondsSinceEpoch.toDouble();
|
||||
await Future<void>.delayed(const Duration(milliseconds: 20));
|
||||
await Directory(
|
||||
'${localWorkspace.path}/assets/images',
|
||||
).create(recursive: true);
|
||||
await Directory('${localWorkspace.path}/build_segments').create();
|
||||
await Directory('${localWorkspace.path}/snapshots').create();
|
||||
await Directory('${localWorkspace.path}/renders').create();
|
||||
await File(
|
||||
'${localWorkspace.path}/assets/images/01.v8.png',
|
||||
).writeAsBytes(<int>[1]);
|
||||
await File(
|
||||
'${localWorkspace.path}/build_segments/segment-01.mp4',
|
||||
).writeAsBytes(<int>[2]);
|
||||
await File(
|
||||
'${localWorkspace.path}/snapshots/frame-01.png',
|
||||
).writeAsBytes(<int>[3]);
|
||||
await File(
|
||||
'${localWorkspace.path}/renders/final.mp4',
|
||||
).writeAsBytes(<int>[4]);
|
||||
|
||||
controller.upsertTaskThreadInternal(
|
||||
'unit-fixture-task-a',
|
||||
workspaceBinding: WorkspaceBinding(
|
||||
workspaceId: 'unit-fixture-task-a',
|
||||
workspaceKind: WorkspaceKind.localFs,
|
||||
workspacePath: localWorkspace.path,
|
||||
displayPath: localWorkspace.path,
|
||||
writable: true,
|
||||
),
|
||||
selectedSkillKeys: const <String>['it-infra-evolution-video-v2'],
|
||||
lifecycleStatus: 'running',
|
||||
lastRunAtMs: startedAtMs,
|
||||
lastResultCode: 'running',
|
||||
);
|
||||
|
||||
const result = GoTaskServiceResult(
|
||||
success: true,
|
||||
message: 'done',
|
||||
turnId: 'turn-1',
|
||||
raw: <String, dynamic>{},
|
||||
errorMessage: '',
|
||||
resolvedModel: '',
|
||||
route: GoTaskServiceRoute.externalAcpSingle,
|
||||
);
|
||||
|
||||
await controller.persistGoTaskArtifactsForSessionInternal(
|
||||
'unit-fixture-task-a',
|
||||
result,
|
||||
);
|
||||
|
||||
final thread = controller.requireTaskThreadForSessionInternal(
|
||||
'unit-fixture-task-a',
|
||||
);
|
||||
expect(thread.lastArtifactSyncStatus, 'synced');
|
||||
expect(thread.lastTaskArtifactRelativePaths, <String>['renders/final.mp4']);
|
||||
});
|
||||
|
||||
test('records ordinary empty artifact results as no artifacts', () async {
|
||||
final controller = AppController(
|
||||
environmentOverride: const <String, String>{},
|
||||
|
||||
@ -91,7 +91,15 @@ void main() {
|
||||
expect(params, isNot(contains('sessionKey')));
|
||||
expect(params, isNot(contains('sessionId')));
|
||||
expect(params, isNot(contains('threadId')));
|
||||
expect(params, isNot(contains('artifactScope')));
|
||||
expect(
|
||||
params['artifactScope'],
|
||||
'tasks/agent:main:draft:1780658097668838-1/run-1',
|
||||
);
|
||||
expect(
|
||||
params['artifactDirectory'],
|
||||
'/tmp/tasks/agent:main:draft:1780658097668838-1/run-1',
|
||||
);
|
||||
expect(params['gatewayProviderId'], 'openclaw');
|
||||
});
|
||||
|
||||
test(
|
||||
|
||||
Loading…
Reference in New Issue
Block a user