fix: keep syncing partial OpenClaw artifacts
This commit is contained in:
parent
7b0502323b
commit
f81c4e8c76
@ -799,7 +799,22 @@ extension AppControllerDesktopRuntimeHelpers on AppController {
|
||||
var wroteArtifact = false;
|
||||
var failedArtifact = false;
|
||||
var skippedArtifact = false;
|
||||
final previousSyncStatus =
|
||||
existingThread.lastArtifactSyncStatus?.trim().toLowerCase() ?? '';
|
||||
final preserveExistingArtifactPaths =
|
||||
previousSyncStatus == 'partial' ||
|
||||
previousSyncStatus == 'syncing' ||
|
||||
previousSyncStatus == 'running' ||
|
||||
previousSyncStatus == 'queued';
|
||||
final currentTaskArtifactPaths = <String>{};
|
||||
if (preserveExistingArtifactPaths) {
|
||||
for (final relativePath in existingThread.lastTaskArtifactRelativePaths) {
|
||||
final sanitized = _sanitizeArtifactRelativePathInternal(relativePath);
|
||||
if (sanitized.isNotEmpty && !artifactSyncPolicy.ignores(sanitized)) {
|
||||
currentTaskArtifactPaths.add(sanitized);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (final artifact in artifacts) {
|
||||
final relativePath = _sanitizeArtifactRelativePathInternal(
|
||||
artifact.relativePath,
|
||||
|
||||
@ -388,7 +388,13 @@ extension AppControllerDesktopThreadSessions on AppController {
|
||||
artifactRelativePaths:
|
||||
thread?.lastTaskArtifactRelativePaths ?? const <String>[],
|
||||
);
|
||||
if (snapshot.fileEntries.isNotEmpty || thread == null) {
|
||||
if (thread == null) {
|
||||
return snapshot;
|
||||
}
|
||||
final shouldRefreshRemote = _shouldRefreshRemoteArtifactSnapshotInternal(
|
||||
thread,
|
||||
);
|
||||
if (snapshot.fileEntries.isNotEmpty && !shouldRefreshRemote) {
|
||||
return snapshot;
|
||||
}
|
||||
final synced = await syncRemoteTaskArtifactsForSessionInternal(
|
||||
@ -406,6 +412,18 @@ extension AppControllerDesktopThreadSessions on AppController {
|
||||
);
|
||||
}
|
||||
|
||||
bool _shouldRefreshRemoteArtifactSnapshotInternal(TaskThread thread) {
|
||||
final syncStatus = thread.lastArtifactSyncStatus?.trim().toLowerCase();
|
||||
if (syncStatus == 'partial' ||
|
||||
syncStatus == 'syncing' ||
|
||||
syncStatus == 'running' ||
|
||||
syncStatus == 'queued') {
|
||||
return true;
|
||||
}
|
||||
final association = thread.openClawTaskAssociation;
|
||||
return association != null && !association.isTerminal;
|
||||
}
|
||||
|
||||
Future<bool> syncRemoteTaskArtifactsForSessionInternal(
|
||||
String sessionKey,
|
||||
) async {
|
||||
|
||||
@ -76,11 +76,13 @@ class _AssistantArtifactSidebarState extends State<AssistantArtifactSidebar> {
|
||||
bool _loadingSnapshot = false;
|
||||
bool _loadingPreview = false;
|
||||
bool _taskContextExpanded = false;
|
||||
Timer? _refreshTimer;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
unawaited(_refreshSnapshot());
|
||||
_syncRefreshTimer();
|
||||
}
|
||||
|
||||
@override
|
||||
@ -96,6 +98,13 @@ class _AssistantArtifactSidebarState extends State<AssistantArtifactSidebar> {
|
||||
_preview = const AssistantArtifactPreview.empty();
|
||||
unawaited(_refreshSnapshot());
|
||||
}
|
||||
_syncRefreshTimer();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_refreshTimer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
@ -449,6 +458,9 @@ class _AssistantArtifactSidebarState extends State<AssistantArtifactSidebar> {
|
||||
}
|
||||
|
||||
Future<void> _refreshSnapshot() async {
|
||||
if (_loadingSnapshot) {
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
_loadingSnapshot = true;
|
||||
_loadError = null;
|
||||
@ -482,6 +494,36 @@ class _AssistantArtifactSidebarState extends State<AssistantArtifactSidebar> {
|
||||
}
|
||||
}
|
||||
|
||||
void _syncRefreshTimer() {
|
||||
final shouldPoll = _shouldPollArtifactSnapshot(widget.artifactSyncStatus);
|
||||
if (!shouldPoll) {
|
||||
_refreshTimer?.cancel();
|
||||
_refreshTimer = null;
|
||||
return;
|
||||
}
|
||||
if (_refreshTimer?.isActive == true) {
|
||||
return;
|
||||
}
|
||||
_refreshTimer = Timer.periodic(const Duration(seconds: 3), (_) {
|
||||
if (!mounted || _loadingSnapshot) {
|
||||
return;
|
||||
}
|
||||
unawaited(_refreshSnapshot());
|
||||
});
|
||||
}
|
||||
|
||||
bool _shouldPollArtifactSnapshot(String status) {
|
||||
switch (status.trim().toLowerCase()) {
|
||||
case 'partial':
|
||||
case 'syncing':
|
||||
case 'running':
|
||||
case 'queued':
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
AssistantArtifactEntry? _reconcileSelection(
|
||||
AssistantArtifactSnapshot snapshot, {
|
||||
AssistantArtifactEntry? previous,
|
||||
|
||||
@ -46,6 +46,46 @@ void main() {
|
||||
expect(find.text('artifact-2.txt'), findsAtLeastNWidgets(1));
|
||||
});
|
||||
|
||||
testWidgets('keeps polling partial artifact snapshots', (tester) async {
|
||||
var loadCount = 0;
|
||||
|
||||
await tester.pumpWidget(
|
||||
_buildTestApp(
|
||||
artifactSyncAtMs: 1,
|
||||
artifactSyncStatus: 'partial',
|
||||
loadSnapshot: () async {
|
||||
loadCount += 1;
|
||||
return AssistantArtifactSnapshot(
|
||||
workspacePath: '/tmp/thread',
|
||||
workspaceKind: WorkspaceRefKind.localPath,
|
||||
fileEntries: <AssistantArtifactEntry>[
|
||||
AssistantArtifactEntry(
|
||||
id: 'entry-$loadCount',
|
||||
label: 'artifact-$loadCount.txt',
|
||||
relativePath: 'artifact-$loadCount.txt',
|
||||
kind: AssistantArtifactEntryKind.file,
|
||||
mimeType: 'text/plain',
|
||||
previewable: true,
|
||||
workspacePath: '/tmp/thread',
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
await tester.pump();
|
||||
|
||||
expect(loadCount, 1);
|
||||
|
||||
await tester.pump(const Duration(milliseconds: 3100));
|
||||
await tester.pump();
|
||||
|
||||
expect(loadCount, greaterThanOrEqualTo(2));
|
||||
expect(find.text('artifact-2.txt'), findsAtLeastNWidgets(1));
|
||||
|
||||
await tester.pumpWidget(const SizedBox.shrink());
|
||||
});
|
||||
|
||||
testWidgets('explains OpenClaw runs with no exported artifacts', (
|
||||
tester,
|
||||
) async {
|
||||
|
||||
@ -1021,6 +1021,93 @@ void main() {
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'refreshing a partial artifact snapshot keeps backfilling OpenClaw task artifacts',
|
||||
() async {
|
||||
var getTaskCount = 0;
|
||||
late OpenClawTaskAssociation observedAssociation;
|
||||
final goTaskClient = _ArtifactBackfillGoTaskServiceClient(
|
||||
onGetTask: (association) {
|
||||
getTaskCount += 1;
|
||||
observedAssociation = association;
|
||||
},
|
||||
);
|
||||
final controller = AppController(
|
||||
environmentOverride: const <String, String>{},
|
||||
goTaskServiceClient: goTaskClient,
|
||||
);
|
||||
addTearDown(controller.dispose);
|
||||
|
||||
final taskWorkspace = await Directory.systemTemp.createTemp(
|
||||
'xworkmate-partial-association-backfill-',
|
||||
);
|
||||
addTearDown(() async {
|
||||
if (await taskWorkspace.exists()) {
|
||||
await taskWorkspace.delete(recursive: true);
|
||||
}
|
||||
});
|
||||
await Directory(
|
||||
'${taskWorkspace.path}/assets/images',
|
||||
).create(recursive: true);
|
||||
await File(
|
||||
'${taskWorkspace.path}/assets/images/09-AI-Agent.v32.png',
|
||||
).writeAsBytes(<int>[1, 2, 3]);
|
||||
|
||||
const sessionKey = 'draft-partial-sync';
|
||||
const runId = 'turn-partial';
|
||||
const openClawSessionKey = 'agent:main:draft:partial-sync';
|
||||
controller.upsertTaskThreadInternal(
|
||||
sessionKey,
|
||||
workspaceBinding: WorkspaceBinding(
|
||||
workspaceId: sessionKey,
|
||||
workspaceKind: WorkspaceKind.localFs,
|
||||
workspacePath: taskWorkspace.path,
|
||||
displayPath: taskWorkspace.path,
|
||||
writable: true,
|
||||
),
|
||||
openClawTaskAssociation: const OpenClawTaskAssociation(
|
||||
sessionId: sessionKey,
|
||||
threadId: sessionKey,
|
||||
turnId: runId,
|
||||
runId: runId,
|
||||
artifactScope: 'tasks/$openClawSessionKey/$runId',
|
||||
artifactDirectory:
|
||||
'/home/ubuntu/.openclaw/workspace/tasks/$openClawSessionKey/$runId',
|
||||
gatewayProviderId: 'openclaw',
|
||||
startedAtMs: 1,
|
||||
status: 'completed',
|
||||
appThreadKey: 'draft:partial-sync',
|
||||
openclawSessionKey: openClawSessionKey,
|
||||
),
|
||||
lastArtifactSyncStatus: 'partial',
|
||||
lastTaskArtifactRelativePaths: const <String>[
|
||||
'assets/images/09-AI-Agent.v32.png',
|
||||
],
|
||||
);
|
||||
|
||||
final snapshot = await controller.loadAssistantArtifactSnapshot(
|
||||
sessionKey: sessionKey,
|
||||
);
|
||||
|
||||
expect(getTaskCount, 1);
|
||||
expect(observedAssociation.runId, runId);
|
||||
expect(
|
||||
snapshot.fileEntries.map((entry) => entry.relativePath),
|
||||
containsAll(<String>[
|
||||
'assets/images/09-AI-Agent.v32.png',
|
||||
'ai-news-report.md',
|
||||
]),
|
||||
);
|
||||
expect(
|
||||
await File('${taskWorkspace.path}/ai-news-report.md').readAsString(),
|
||||
'# AI news\n',
|
||||
);
|
||||
final thread = controller.requireTaskThreadForSessionInternal(sessionKey);
|
||||
expect(thread.lastArtifactSyncStatus, 'synced');
|
||||
expect(thread.openClawTaskAssociation?.status, 'completed');
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'resumes bridge artifact downloads after a weak network disconnect',
|
||||
() async {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user