fix: require OpenClaw artifact export before completion
This commit is contained in:
parent
b2f4fc7868
commit
b7a842fce3
@ -334,8 +334,6 @@ extension AppControllerDesktopRuntimeHelpers on AppController {
|
||||
: null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
Future<List<String>> recoverGatewayFailureArtifactPathsInternal(
|
||||
String sessionKey,
|
||||
Object error,
|
||||
@ -750,12 +748,34 @@ extension AppControllerDesktopRuntimeHelpers on AppController {
|
||||
);
|
||||
final artifacts = result.artifacts;
|
||||
if (artifacts.isEmpty) {
|
||||
final association = existingThread.openClawTaskAssociation;
|
||||
final waitingForOpenClawArtifacts =
|
||||
association != null &&
|
||||
(association.requiresArtifactExport ||
|
||||
association.requiredArtifactExtensions.isNotEmpty) &&
|
||||
(association.artifactScope.trim().isNotEmpty ||
|
||||
association.artifactDirectory.trim().isNotEmpty) &&
|
||||
result.success;
|
||||
if (waitingForOpenClawArtifacts) {
|
||||
upsertTaskThreadInternal(
|
||||
normalizedSessionKey,
|
||||
lifecycleStatus: 'running',
|
||||
lastResultCode: 'running',
|
||||
lastArtifactSyncAtMs: syncedAtMs,
|
||||
lastArtifactSyncStatus: 'syncing',
|
||||
openClawTaskAssociation: association.copyWith(
|
||||
status: 'syncing-artifacts',
|
||||
),
|
||||
updatedAtMs: syncedAtMs,
|
||||
);
|
||||
return;
|
||||
}
|
||||
final currentTaskArtifactRelativePaths =
|
||||
await _workspaceArtifactPathsModifiedSinceInternal(
|
||||
root,
|
||||
existingThread.lifecycleState.lastRunAtMs,
|
||||
artifactSyncPolicy,
|
||||
);
|
||||
root,
|
||||
existingThread.lifecycleState.lastRunAtMs,
|
||||
artifactSyncPolicy,
|
||||
);
|
||||
if (currentTaskArtifactRelativePaths.isNotEmpty) {
|
||||
upsertTaskThreadInternal(
|
||||
normalizedSessionKey,
|
||||
|
||||
@ -772,16 +772,27 @@ extension AppControllerDesktopThreadActions on AppController {
|
||||
continue;
|
||||
}
|
||||
if (aiGatewayPendingSessionKeysInternal.contains(sessionKey)) {
|
||||
final hasTaskScopedOpenClawArtifacts =
|
||||
current.artifactScope.trim().isNotEmpty ||
|
||||
current.artifactDirectory.trim().isNotEmpty;
|
||||
final hasRequiredExts = current.requiredArtifactExtensions.isNotEmpty;
|
||||
final requiresArtifactExport =
|
||||
current.requiresArtifactExport || hasRequiredExts;
|
||||
final hasEnoughArtifacts =
|
||||
!hasRequiredExts ||
|
||||
current.requiredArtifactExtensions.every((ext) {
|
||||
return result.artifacts.any(
|
||||
(a) =>
|
||||
a.relativePath.toLowerCase().endsWith(ext.toLowerCase()),
|
||||
);
|
||||
});
|
||||
if (!hasEnoughArtifacts) {
|
||||
result.artifacts.isNotEmpty &&
|
||||
(!hasRequiredExts ||
|
||||
current.requiredArtifactExtensions.every((ext) {
|
||||
return result.artifacts.any(
|
||||
(a) => a.relativePath.toLowerCase().endsWith(
|
||||
ext.toLowerCase(),
|
||||
),
|
||||
);
|
||||
}));
|
||||
final shouldKeepPollingForArtifacts =
|
||||
hasTaskScopedOpenClawArtifacts &&
|
||||
requiresArtifactExport &&
|
||||
!hasEnoughArtifacts;
|
||||
if (shouldKeepPollingForArtifacts) {
|
||||
final nowMs = DateTime.now().millisecondsSinceEpoch.toDouble();
|
||||
current = current.copyWith(status: 'syncing-artifacts');
|
||||
upsertTaskThreadInternal(
|
||||
@ -1257,6 +1268,22 @@ extension AppControllerDesktopThreadActions on AppController {
|
||||
final completedAtMs = DateTime.now().millisecondsSinceEpoch.toDouble();
|
||||
final assistantText = result.message.trim();
|
||||
final hasCurrentRunArtifacts = result.artifacts.isNotEmpty;
|
||||
final openClawAssociation = result.openClawTaskAssociation;
|
||||
final waitingForOpenClawArtifacts =
|
||||
openClawAssociation != null &&
|
||||
!hasCurrentRunArtifacts &&
|
||||
result.success &&
|
||||
(openClawAssociation.requiresArtifactExport ||
|
||||
openClawAssociation.requiredArtifactExtensions.isNotEmpty) &&
|
||||
(openClawAssociation.artifactScope.trim().isNotEmpty ||
|
||||
openClawAssociation.artifactDirectory.trim().isNotEmpty);
|
||||
if (waitingForOpenClawArtifacts) {
|
||||
persistOpenClawTaskAssociationInternal(
|
||||
sessionKey: sessionKey,
|
||||
association: openClawAssociation.copyWith(status: 'syncing-artifacts'),
|
||||
);
|
||||
return;
|
||||
}
|
||||
final noDisplayableOutput =
|
||||
result.success && assistantText.isEmpty && !hasCurrentRunArtifacts;
|
||||
final terminalResultCode = noDisplayableOutput
|
||||
|
||||
@ -928,6 +928,7 @@ class OpenClawTaskAssociation {
|
||||
this.taskLoadClass = '',
|
||||
this.requiredArtifactExtensions = const <String>[],
|
||||
this.expectedArtifactExtensions = const <String>[],
|
||||
this.requiresArtifactExport = false,
|
||||
});
|
||||
|
||||
final String sessionId;
|
||||
@ -944,6 +945,7 @@ class OpenClawTaskAssociation {
|
||||
final String taskLoadClass;
|
||||
final List<String> requiredArtifactExtensions;
|
||||
final List<String> expectedArtifactExtensions;
|
||||
final bool requiresArtifactExport;
|
||||
|
||||
bool get isTerminal {
|
||||
final normalized = status.trim().toLowerCase();
|
||||
@ -969,6 +971,7 @@ class OpenClawTaskAssociation {
|
||||
taskLoadClass: taskLoadClass,
|
||||
requiredArtifactExtensions: requiredArtifactExtensions,
|
||||
expectedArtifactExtensions: expectedArtifactExtensions,
|
||||
requiresArtifactExport: requiresArtifactExport,
|
||||
);
|
||||
}
|
||||
|
||||
@ -988,6 +991,7 @@ class OpenClawTaskAssociation {
|
||||
'taskLoadClass': taskLoadClass,
|
||||
'requiredArtifactExtensions': requiredArtifactExtensions,
|
||||
'expectedArtifactExtensions': expectedArtifactExtensions,
|
||||
'requiresArtifactExport': requiresArtifactExport,
|
||||
};
|
||||
}
|
||||
|
||||
@ -1046,6 +1050,10 @@ class OpenClawTaskAssociation {
|
||||
expectedArtifactExtensions: _stringListFromJson(
|
||||
json['expectedArtifactExtensions'],
|
||||
),
|
||||
requiresArtifactExport:
|
||||
_boolFromJson(json['requiresArtifactExport']) ??
|
||||
_boolFromJson(json['requiresExportBeforeFinalResponse']) ??
|
||||
false,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1071,6 +1079,20 @@ List<String> _stringListFromJson(Object? value) {
|
||||
return items;
|
||||
}
|
||||
|
||||
bool? _boolFromJson(Object? value) {
|
||||
if (value is bool) {
|
||||
return value;
|
||||
}
|
||||
final normalized = value?.toString().trim().toLowerCase() ?? '';
|
||||
if (normalized == 'true' || normalized == '1' || normalized == 'yes') {
|
||||
return true;
|
||||
}
|
||||
if (normalized == 'false' || normalized == '0' || normalized == 'no') {
|
||||
return false;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
class ThreadLifecycleState {
|
||||
const ThreadLifecycleState({
|
||||
required this.archived,
|
||||
|
||||
@ -1744,6 +1744,75 @@ void main() {
|
||||
expect(snapshot.resultMessage, 'No task artifacts recorded for this run.');
|
||||
});
|
||||
|
||||
test('keeps OpenClaw task-scope empty artifact results syncing', () async {
|
||||
final controller = AppController(
|
||||
environmentOverride: const <String, String>{},
|
||||
);
|
||||
addTearDown(controller.dispose);
|
||||
|
||||
final localWorkspace = await Directory.systemTemp.createTemp(
|
||||
'xworkmate-openclaw-empty-artifacts-workspace-',
|
||||
);
|
||||
addTearDown(() async {
|
||||
if (await localWorkspace.exists()) {
|
||||
await localWorkspace.delete(recursive: true);
|
||||
}
|
||||
});
|
||||
|
||||
controller.upsertTaskThreadInternal(
|
||||
'unit-fixture-openclaw-empty',
|
||||
workspaceBinding: WorkspaceBinding(
|
||||
workspaceId: 'unit-fixture-openclaw-empty',
|
||||
workspaceKind: WorkspaceKind.localFs,
|
||||
workspacePath: localWorkspace.path,
|
||||
displayPath: localWorkspace.path,
|
||||
writable: true,
|
||||
),
|
||||
lifecycleStatus: 'running',
|
||||
lastResultCode: 'running',
|
||||
openClawTaskAssociation: const OpenClawTaskAssociation(
|
||||
sessionId: 'unit-fixture-openclaw-empty',
|
||||
threadId: 'unit-fixture-openclaw-empty',
|
||||
turnId: 'turn-empty',
|
||||
runId: 'turn-empty',
|
||||
artifactScope:
|
||||
'tasks/agent:main:unit-fixture-openclaw-empty/turn-empty',
|
||||
artifactDirectory:
|
||||
'/home/ubuntu/.openclaw/workspace/tasks/agent:main:unit-fixture-openclaw-empty/turn-empty',
|
||||
gatewayProviderId: 'openclaw',
|
||||
startedAtMs: 1,
|
||||
status: 'running',
|
||||
appThreadKey: 'unit-fixture-openclaw-empty',
|
||||
openclawSessionKey: 'agent:main:unit-fixture-openclaw-empty',
|
||||
requiresArtifactExport: true,
|
||||
),
|
||||
);
|
||||
|
||||
const result = GoTaskServiceResult(
|
||||
success: true,
|
||||
message: 'completed but export is still empty',
|
||||
turnId: 'turn-empty',
|
||||
raw: <String, dynamic>{},
|
||||
errorMessage: '',
|
||||
resolvedModel: '',
|
||||
route: GoTaskServiceRoute.externalAcpSingle,
|
||||
);
|
||||
|
||||
await controller.persistGoTaskArtifactsForSessionInternal(
|
||||
'unit-fixture-openclaw-empty',
|
||||
result,
|
||||
);
|
||||
|
||||
final thread = controller.requireTaskThreadForSessionInternal(
|
||||
'unit-fixture-openclaw-empty',
|
||||
);
|
||||
expect(thread.lifecycleState.status, 'running');
|
||||
expect(thread.lifecycleState.lastResultCode, 'running');
|
||||
expect(thread.lastArtifactSyncStatus, 'syncing');
|
||||
expect(thread.lastTaskArtifactRelativePaths, isEmpty);
|
||||
expect(thread.openClawTaskAssociation?.status, 'syncing-artifacts');
|
||||
});
|
||||
|
||||
test(
|
||||
'records workspace files produced during an empty-artifact task run',
|
||||
() async {
|
||||
|
||||
@ -4055,7 +4055,7 @@ void main() {
|
||||
});
|
||||
|
||||
test(
|
||||
'OpenClaw terminal snapshot without required artifacts keeps polling',
|
||||
'OpenClaw terminal task-scope snapshot without artifacts keeps polling',
|
||||
() async {
|
||||
final fakeGoTaskService = _RecordingGoTaskServiceClient()
|
||||
..outcomes.add(
|
||||
@ -4078,7 +4078,7 @@ void main() {
|
||||
'/tmp/tasks/agent:main:openclaw-missing-screenshot/run-openclaw-missing-screenshot',
|
||||
'gatewayProviderId': 'openclaw',
|
||||
'runtimeBudgetMinutes': 1,
|
||||
'requiredArtifactExtensions': <String>['.png'],
|
||||
'requiresArtifactExport': true,
|
||||
},
|
||||
errorMessage: '',
|
||||
resolvedModel: '',
|
||||
|
||||
Loading…
Reference in New Issue
Block a user