fix: isolate same-prompt draft artifacts
This commit is contained in:
parent
e288be2454
commit
3e188cc3df
@ -223,12 +223,14 @@ String? resolveLocalAssistantWorkingDirectoryForSessionRuntimeInternal(
|
||||
}
|
||||
|
||||
CodeAgentNodeState buildCodeAgentNodeStateRuntimeInternal(
|
||||
AppController controller,
|
||||
) {
|
||||
AppController controller, {
|
||||
AssistantExecutionTarget? executionTarget,
|
||||
}) {
|
||||
return CodeAgentNodeState(
|
||||
selectedAgentId: controller.agentsControllerInternal.selectedAgentId,
|
||||
gatewayConnected: controller.runtimeInternal.isConnected,
|
||||
executionTarget: controller.currentAssistantExecutionTarget,
|
||||
executionTarget:
|
||||
executionTarget ?? controller.currentAssistantExecutionTarget,
|
||||
runtimeMode: controller.effectiveCodeAgentRuntimeMode,
|
||||
bridgeEnabled: controller.isCodexBridgeEnabledInternal,
|
||||
bridgeState: controller.codexCooperationStateInternal.name,
|
||||
|
||||
@ -603,8 +603,12 @@ extension AppControllerDesktopRuntimeHelpers on AppController {
|
||||
);
|
||||
}
|
||||
|
||||
CodeAgentNodeState buildCodeAgentNodeStateInternal() =>
|
||||
buildCodeAgentNodeStateRuntimeInternal(this);
|
||||
CodeAgentNodeState buildCodeAgentNodeStateInternal({
|
||||
AssistantExecutionTarget? executionTarget,
|
||||
}) => buildCodeAgentNodeStateRuntimeInternal(
|
||||
this,
|
||||
executionTarget: executionTarget,
|
||||
);
|
||||
|
||||
GatewayMode bridgeGatewayModeInternal() =>
|
||||
bridgeGatewayModeRuntimeInternal(this);
|
||||
|
||||
@ -231,9 +231,11 @@ extension AppControllerDesktopThreadActions on AppController {
|
||||
const <CollaborationAttachment>[],
|
||||
List<String> selectedSkillLabels = const <String>[],
|
||||
}) async {
|
||||
final currentSessionKey = sessionsControllerInternal.currentSessionKey;
|
||||
final currentTarget = assistantExecutionTargetForSession(currentSessionKey);
|
||||
var connectionState = currentAssistantConnectionState;
|
||||
final sessionKey = normalizedAssistantSessionKeyInternal(
|
||||
sessionsControllerInternal.currentSessionKey,
|
||||
);
|
||||
final currentTarget = assistantExecutionTargetForSession(sessionKey);
|
||||
var connectionState = assistantConnectionStateForSession(sessionKey);
|
||||
if (!connectionState.connected &&
|
||||
isBridgeAcpRuntimeConfiguredInternal() &&
|
||||
bridgeCapabilityRefreshNeededForAssistantTargetInternal(
|
||||
@ -241,7 +243,7 @@ extension AppControllerDesktopThreadActions on AppController {
|
||||
)) {
|
||||
try {
|
||||
await refreshAcpCapabilitiesInternal(forceRefresh: true);
|
||||
connectionState = currentAssistantConnectionState;
|
||||
connectionState = assistantConnectionStateForSession(sessionKey);
|
||||
} catch (_) {
|
||||
// Fallback to existing connection state if refresh fails.
|
||||
}
|
||||
@ -249,7 +251,7 @@ extension AppControllerDesktopThreadActions on AppController {
|
||||
if (!connectionState.connected) {
|
||||
final error = StateError(connectionState.detailLabel);
|
||||
appendAssistantThreadMessageInternal(
|
||||
currentSessionKey,
|
||||
sessionKey,
|
||||
assistantErrorMessageInternal(error.message),
|
||||
);
|
||||
await flushAssistantThreadPersistenceInternal();
|
||||
@ -258,17 +260,14 @@ extension AppControllerDesktopThreadActions on AppController {
|
||||
throw error;
|
||||
}
|
||||
await ensureDesktopTaskThreadBindingInternal(
|
||||
currentSessionKey,
|
||||
sessionKey,
|
||||
executionTarget: currentTarget,
|
||||
);
|
||||
final workingDirectory =
|
||||
assistantWorkingDirectoryForSessionInternal(
|
||||
currentSessionKey,
|
||||
)?.trim() ??
|
||||
'';
|
||||
assistantWorkingDirectoryForSessionInternal(sessionKey)?.trim() ?? '';
|
||||
final remoteWorkingDirectoryHint =
|
||||
assistantRemoteWorkingDirectoryHintForSessionInternal(
|
||||
currentSessionKey,
|
||||
sessionKey,
|
||||
)?.trim() ??
|
||||
'';
|
||||
if (workingDirectory.isEmpty) {
|
||||
@ -279,7 +278,7 @@ extension AppControllerDesktopThreadActions on AppController {
|
||||
),
|
||||
);
|
||||
appendAssistantThreadMessageInternal(
|
||||
currentSessionKey,
|
||||
sessionKey,
|
||||
assistantErrorMessageInternal(error.message),
|
||||
);
|
||||
await flushAssistantThreadPersistenceInternal();
|
||||
@ -293,11 +292,8 @@ extension AppControllerDesktopThreadActions on AppController {
|
||||
// Keep the local guard focused on the post-refresh catalog state.
|
||||
}
|
||||
if (providerCatalogForExecutionTarget(currentTarget).isEmpty) {
|
||||
final normalizedSessionKey = normalizedAssistantSessionKeyInternal(
|
||||
currentSessionKey,
|
||||
);
|
||||
upsertTaskThreadInternal(
|
||||
normalizedSessionKey,
|
||||
sessionKey,
|
||||
selectedProvider: SingleAgentProvider.unspecified,
|
||||
selectedProviderSource: ThreadSelectionSource.inherited,
|
||||
latestResolvedProviderId: '',
|
||||
@ -315,7 +311,7 @@ extension AppControllerDesktopThreadActions on AppController {
|
||||
),
|
||||
);
|
||||
appendAssistantThreadMessageInternal(
|
||||
normalizedSessionKey,
|
||||
sessionKey,
|
||||
assistantErrorMessageInternal(error.message),
|
||||
);
|
||||
await flushAssistantThreadPersistenceInternal();
|
||||
@ -324,41 +320,158 @@ extension AppControllerDesktopThreadActions on AppController {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
await enqueueThreadTurnInternal<void>(
|
||||
normalizedAssistantSessionKeyInternal(currentSessionKey),
|
||||
() async {
|
||||
final sessionKey = normalizedAssistantSessionKeyInternal(
|
||||
currentSessionKey,
|
||||
await enqueueThreadTurnInternal<void>(sessionKey, () async {
|
||||
final resumeSession = shouldResumeGatewaySessionForNextSendInternal(
|
||||
sessionKey,
|
||||
);
|
||||
final lifecycleStatus = taskThreadForSessionInternal(
|
||||
sessionKey,
|
||||
)?.lifecycleState.status.trim().toLowerCase();
|
||||
final lastResultCode = taskThreadForSessionInternal(
|
||||
sessionKey,
|
||||
)?.lifecycleState.lastResultCode?.trim().toLowerCase();
|
||||
final continuableTransportResult =
|
||||
lastResultCode == 'acp_http_connection_closed' ||
|
||||
lastResultCode ==
|
||||
gatewayAcpHttpHandshakeInterruptedCode.toLowerCase();
|
||||
final runStatus =
|
||||
resumeSession &&
|
||||
(lifecycleStatus == 'interrupted' || continuableTransportResult)
|
||||
? 'continuing'
|
||||
: resumeSession && lastResultCode == 'error'
|
||||
? 'retrying'
|
||||
: 'running';
|
||||
final userText = message.trim().isEmpty
|
||||
? 'See attached.'
|
||||
: message.trim();
|
||||
appendLocalSessionMessageInternal(
|
||||
sessionKey,
|
||||
GatewayChatMessage(
|
||||
id: nextLocalMessageIdInternal(),
|
||||
role: 'user',
|
||||
text: userText,
|
||||
timestampMs: DateTime.now().millisecondsSinceEpoch.toDouble(),
|
||||
toolCallId: null,
|
||||
toolName: null,
|
||||
stopReason: null,
|
||||
pending: false,
|
||||
error: false,
|
||||
),
|
||||
persistInThreadContext: true,
|
||||
);
|
||||
aiGatewayPendingSessionKeysInternal.add(sessionKey);
|
||||
upsertTaskThreadInternal(
|
||||
sessionKey,
|
||||
lifecycleStatus: runStatus,
|
||||
lastRunAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(),
|
||||
lastResultCode: runStatus,
|
||||
updatedAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(),
|
||||
);
|
||||
recomputeTasksInternal();
|
||||
notifyIfActiveInternal();
|
||||
try {
|
||||
final dispatch = await codeAgentNodeOrchestratorInternal
|
||||
.buildGatewayDispatch(
|
||||
buildCodeAgentNodeStateInternal(executionTarget: currentTarget),
|
||||
);
|
||||
upsertTaskThreadInternal(
|
||||
sessionKey,
|
||||
lifecycleStatus: runStatus,
|
||||
lastRunAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(),
|
||||
lastResultCode: runStatus,
|
||||
updatedAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(),
|
||||
);
|
||||
final resumeSession = shouldResumeGatewaySessionForNextSendInternal(
|
||||
sessionKey,
|
||||
final result = await goTaskServiceClientInternal.executeTask(
|
||||
GoTaskServiceRequest(
|
||||
sessionId: sessionKey,
|
||||
threadId: sessionKey,
|
||||
target: currentTarget,
|
||||
provider: assistantProviderForSession(sessionKey),
|
||||
prompt: message,
|
||||
workingDirectory: workingDirectory,
|
||||
remoteWorkingDirectoryHint: remoteWorkingDirectoryHint,
|
||||
model: assistantModelForSession(sessionKey),
|
||||
thinking: thinking,
|
||||
selectedSkills: selectedSkillLabels,
|
||||
inlineAttachments: attachments,
|
||||
localAttachments: localAttachments,
|
||||
agentId: dispatch.agentId ?? '',
|
||||
metadata: dispatch.metadata,
|
||||
routing: buildExternalAcpRoutingForSessionInternal(sessionKey),
|
||||
routingHint: 'gateway',
|
||||
resumeSession: resumeSession,
|
||||
),
|
||||
onUpdate: (update) {
|
||||
if (update.isDelta) {
|
||||
appendAiGatewayStreamingTextInternal(sessionKey, update.text);
|
||||
notifyIfActiveInternal();
|
||||
}
|
||||
},
|
||||
);
|
||||
final lifecycleStatus = taskThreadForSessionInternal(
|
||||
if (!aiGatewayPendingSessionKeysInternal.contains(sessionKey)) {
|
||||
clearAiGatewayStreamingTextInternal(sessionKey);
|
||||
return;
|
||||
}
|
||||
clearAiGatewayStreamingTextInternal(sessionKey);
|
||||
upsertTaskThreadInternal(
|
||||
sessionKey,
|
||||
)?.lifecycleState.status.trim().toLowerCase();
|
||||
final lastResultCode = taskThreadForSessionInternal(
|
||||
sessionKey,
|
||||
)?.lifecycleState.lastResultCode?.trim().toLowerCase();
|
||||
final continuableTransportResult =
|
||||
lastResultCode == 'acp_http_connection_closed' ||
|
||||
lastResultCode ==
|
||||
gatewayAcpHttpHandshakeInterruptedCode.toLowerCase();
|
||||
final runStatus =
|
||||
resumeSession &&
|
||||
(lifecycleStatus == 'interrupted' || continuableTransportResult)
|
||||
? 'continuing'
|
||||
: resumeSession && lastResultCode == 'error'
|
||||
? 'retrying'
|
||||
: 'running';
|
||||
final userText = message.trim().isEmpty
|
||||
? 'See attached.'
|
||||
: message.trim();
|
||||
gatewayEntryState: goTaskServiceGatewayEntryState(
|
||||
requestedTarget: currentTarget,
|
||||
result: result,
|
||||
),
|
||||
latestResolvedRuntimeModel: result.resolvedModel.trim(),
|
||||
lastRemoteWorkingDirectory:
|
||||
result.remoteWorkingDirectory.trim().isNotEmpty
|
||||
? result.remoteWorkingDirectory.trim()
|
||||
: null,
|
||||
lastRemoteWorkspaceRefKind: result.remoteWorkspaceRefKind,
|
||||
lifecycleStatus: 'ready',
|
||||
lastRunAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(),
|
||||
lastResultCode: result.success ? 'success' : 'error',
|
||||
updatedAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(),
|
||||
);
|
||||
if (isOpenClawNoExportedArtifactsGuardResultInternal(result)) {
|
||||
await persistGoTaskArtifactsForSessionInternal(sessionKey, result);
|
||||
return;
|
||||
}
|
||||
if (!result.success) {
|
||||
appendLocalSessionMessageInternal(
|
||||
sessionKey,
|
||||
assistantErrorMessageInternal(
|
||||
result.errorMessage.trim().isEmpty
|
||||
? appText(
|
||||
'GoTaskService 执行失败。',
|
||||
'GoTaskService execution failed.',
|
||||
)
|
||||
: gatewayExecutionErrorLabelInternal(
|
||||
result.errorMessage,
|
||||
target: currentTarget,
|
||||
),
|
||||
),
|
||||
persistInThreadContext: true,
|
||||
);
|
||||
return;
|
||||
}
|
||||
final assistantText = result.message.trim();
|
||||
if (assistantText.isEmpty) {
|
||||
appendLocalSessionMessageInternal(
|
||||
sessionKey,
|
||||
assistantErrorMessageInternal(
|
||||
appText(
|
||||
'GoTaskService 没有返回可显示的输出。',
|
||||
'GoTaskService returned no displayable output.',
|
||||
),
|
||||
),
|
||||
persistInThreadContext: true,
|
||||
);
|
||||
return;
|
||||
}
|
||||
appendLocalSessionMessageInternal(
|
||||
sessionKey,
|
||||
GatewayChatMessage(
|
||||
id: nextLocalMessageIdInternal(),
|
||||
role: 'user',
|
||||
text: userText,
|
||||
role: 'assistant',
|
||||
text: assistantText,
|
||||
timestampMs: DateTime.now().millisecondsSinceEpoch.toDouble(),
|
||||
toolCallId: null,
|
||||
toolName: null,
|
||||
@ -368,175 +481,54 @@ extension AppControllerDesktopThreadActions on AppController {
|
||||
),
|
||||
persistInThreadContext: true,
|
||||
);
|
||||
aiGatewayPendingSessionKeysInternal.add(sessionKey);
|
||||
upsertTaskThreadInternal(
|
||||
sessionKey,
|
||||
lifecycleStatus: runStatus,
|
||||
lastRunAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(),
|
||||
lastResultCode: runStatus,
|
||||
updatedAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(),
|
||||
);
|
||||
recomputeTasksInternal();
|
||||
notifyIfActiveInternal();
|
||||
try {
|
||||
final dispatch = await codeAgentNodeOrchestratorInternal
|
||||
.buildGatewayDispatch(buildCodeAgentNodeStateInternal());
|
||||
upsertTaskThreadInternal(
|
||||
sessionKey,
|
||||
lifecycleStatus: runStatus,
|
||||
lastRunAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(),
|
||||
lastResultCode: runStatus,
|
||||
updatedAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(),
|
||||
);
|
||||
final result = await goTaskServiceClientInternal.executeTask(
|
||||
GoTaskServiceRequest(
|
||||
sessionId: sessionKey,
|
||||
threadId: sessionKey,
|
||||
target: currentTarget,
|
||||
provider: assistantProviderForSession(sessionKey),
|
||||
prompt: message,
|
||||
workingDirectory: workingDirectory,
|
||||
remoteWorkingDirectoryHint: remoteWorkingDirectoryHint,
|
||||
model: assistantModelForSession(sessionKey),
|
||||
thinking: thinking,
|
||||
selectedSkills: selectedSkillLabels,
|
||||
inlineAttachments: attachments,
|
||||
localAttachments: localAttachments,
|
||||
agentId: dispatch.agentId ?? '',
|
||||
metadata: dispatch.metadata,
|
||||
routing: buildExternalAcpRoutingForSessionInternal(sessionKey),
|
||||
routingHint: 'gateway',
|
||||
resumeSession: resumeSession,
|
||||
),
|
||||
onUpdate: (update) {
|
||||
if (update.isDelta) {
|
||||
appendAiGatewayStreamingTextInternal(sessionKey, update.text);
|
||||
notifyIfActiveInternal();
|
||||
}
|
||||
},
|
||||
);
|
||||
if (!aiGatewayPendingSessionKeysInternal.contains(sessionKey)) {
|
||||
clearAiGatewayStreamingTextInternal(sessionKey);
|
||||
return;
|
||||
}
|
||||
await persistGoTaskArtifactsForSessionInternal(sessionKey, result);
|
||||
} catch (error) {
|
||||
if (!aiGatewayPendingSessionKeysInternal.contains(sessionKey) &&
|
||||
taskThreadForSessionInternal(
|
||||
sessionKey,
|
||||
)?.lifecycleState.lastResultCode ==
|
||||
'aborted') {
|
||||
clearAiGatewayStreamingTextInternal(sessionKey);
|
||||
upsertTaskThreadInternal(
|
||||
sessionKey,
|
||||
gatewayEntryState: goTaskServiceGatewayEntryState(
|
||||
requestedTarget: currentTarget,
|
||||
result: result,
|
||||
),
|
||||
latestResolvedRuntimeModel: result.resolvedModel.trim(),
|
||||
lastRemoteWorkingDirectory:
|
||||
result.remoteWorkingDirectory.trim().isNotEmpty
|
||||
? result.remoteWorkingDirectory.trim()
|
||||
: null,
|
||||
lastRemoteWorkspaceRefKind: result.remoteWorkspaceRefKind,
|
||||
lifecycleStatus: 'ready',
|
||||
lastRunAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(),
|
||||
lastResultCode: result.success ? 'success' : 'error',
|
||||
updatedAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(),
|
||||
);
|
||||
if (isOpenClawNoExportedArtifactsGuardResultInternal(result)) {
|
||||
await persistGoTaskArtifactsForSessionInternal(sessionKey, result);
|
||||
return;
|
||||
}
|
||||
if (!result.success) {
|
||||
appendLocalSessionMessageInternal(
|
||||
sessionKey,
|
||||
assistantErrorMessageInternal(
|
||||
result.errorMessage.trim().isEmpty
|
||||
? appText(
|
||||
'GoTaskService 执行失败。',
|
||||
'GoTaskService execution failed.',
|
||||
)
|
||||
: gatewayExecutionErrorLabelInternal(
|
||||
result.errorMessage,
|
||||
target: currentTarget,
|
||||
),
|
||||
),
|
||||
persistInThreadContext: true,
|
||||
);
|
||||
return;
|
||||
}
|
||||
final assistantText = result.message.trim();
|
||||
if (assistantText.isEmpty) {
|
||||
appendLocalSessionMessageInternal(
|
||||
sessionKey,
|
||||
assistantErrorMessageInternal(
|
||||
appText(
|
||||
'GoTaskService 没有返回可显示的输出。',
|
||||
'GoTaskService returned no displayable output.',
|
||||
),
|
||||
),
|
||||
persistInThreadContext: true,
|
||||
);
|
||||
return;
|
||||
}
|
||||
appendLocalSessionMessageInternal(
|
||||
sessionKey,
|
||||
GatewayChatMessage(
|
||||
id: nextLocalMessageIdInternal(),
|
||||
role: 'assistant',
|
||||
text: assistantText,
|
||||
timestampMs: DateTime.now().millisecondsSinceEpoch.toDouble(),
|
||||
toolCallId: null,
|
||||
toolName: null,
|
||||
stopReason: null,
|
||||
pending: false,
|
||||
error: false,
|
||||
),
|
||||
persistInThreadContext: true,
|
||||
);
|
||||
recomputeTasksInternal();
|
||||
notifyIfActiveInternal();
|
||||
await persistGoTaskArtifactsForSessionInternal(sessionKey, result);
|
||||
} catch (error) {
|
||||
if (!aiGatewayPendingSessionKeysInternal.contains(sessionKey) &&
|
||||
taskThreadForSessionInternal(
|
||||
sessionKey,
|
||||
)?.lifecycleState.lastResultCode ==
|
||||
'aborted') {
|
||||
clearAiGatewayStreamingTextInternal(sessionKey);
|
||||
return;
|
||||
}
|
||||
clearAiGatewayStreamingTextInternal(sessionKey);
|
||||
final recoverableTransportCode =
|
||||
recoverableAcpHttpTransportCodeInternal(error);
|
||||
final unconfirmedConnectCode = unconfirmedAcpHttpConnectCodeInternal(
|
||||
error,
|
||||
);
|
||||
final recoverableTransportInterrupted =
|
||||
recoverableTransportCode != null;
|
||||
final visibleResultCode =
|
||||
unconfirmedConnectCode ?? recoverableTransportCode;
|
||||
upsertTaskThreadInternal(
|
||||
sessionKey,
|
||||
lifecycleStatus: recoverableTransportInterrupted
|
||||
? 'interrupted'
|
||||
: 'ready',
|
||||
lastRunAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(),
|
||||
lastResultCode: visibleResultCode ?? 'error',
|
||||
lastArtifactSyncStatus: recoverableTransportInterrupted
|
||||
? 'interrupted'
|
||||
: null,
|
||||
updatedAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(),
|
||||
);
|
||||
appendLocalSessionMessageInternal(
|
||||
sessionKey,
|
||||
assistantErrorMessageInternal(
|
||||
gatewayExecutionErrorLabelInternal(error, target: currentTarget),
|
||||
),
|
||||
persistInThreadContext: true,
|
||||
);
|
||||
} finally {
|
||||
aiGatewayPendingSessionKeysInternal.remove(sessionKey);
|
||||
clearAiGatewayStreamingTextInternal(sessionKey);
|
||||
recomputeTasksInternal();
|
||||
notifyIfActiveInternal();
|
||||
return;
|
||||
}
|
||||
},
|
||||
);
|
||||
clearAiGatewayStreamingTextInternal(sessionKey);
|
||||
final recoverableTransportCode =
|
||||
recoverableAcpHttpTransportCodeInternal(error);
|
||||
final unconfirmedConnectCode = unconfirmedAcpHttpConnectCodeInternal(
|
||||
error,
|
||||
);
|
||||
final recoverableTransportInterrupted =
|
||||
recoverableTransportCode != null;
|
||||
final visibleResultCode =
|
||||
unconfirmedConnectCode ?? recoverableTransportCode;
|
||||
upsertTaskThreadInternal(
|
||||
sessionKey,
|
||||
lifecycleStatus: recoverableTransportInterrupted
|
||||
? 'interrupted'
|
||||
: 'ready',
|
||||
lastRunAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(),
|
||||
lastResultCode: visibleResultCode ?? 'error',
|
||||
lastArtifactSyncStatus: recoverableTransportInterrupted
|
||||
? 'interrupted'
|
||||
: null,
|
||||
updatedAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(),
|
||||
);
|
||||
appendLocalSessionMessageInternal(
|
||||
sessionKey,
|
||||
assistantErrorMessageInternal(
|
||||
gatewayExecutionErrorLabelInternal(error, target: currentTarget),
|
||||
),
|
||||
persistInThreadContext: true,
|
||||
);
|
||||
} finally {
|
||||
aiGatewayPendingSessionKeysInternal.remove(sessionKey);
|
||||
clearAiGatewayStreamingTextInternal(sessionKey);
|
||||
recomputeTasksInternal();
|
||||
notifyIfActiveInternal();
|
||||
}
|
||||
});
|
||||
recomputeTasksInternal();
|
||||
}
|
||||
|
||||
|
||||
@ -1153,6 +1153,265 @@ void main() {
|
||||
);
|
||||
});
|
||||
|
||||
test(
|
||||
'sendChatMessage keeps same-prompt draft task artifacts isolated',
|
||||
() async {
|
||||
final localHome = await Directory.systemTemp.createTemp(
|
||||
'xworkmate-same-prompt-home-',
|
||||
);
|
||||
addTearDown(() async {
|
||||
if (await localHome.exists()) {
|
||||
await localHome.delete(recursive: true);
|
||||
}
|
||||
});
|
||||
final fakeGoTaskService = _BlockingGoTaskServiceClient();
|
||||
final controller = _connectedController(fakeGoTaskService);
|
||||
addTearDown(controller.dispose);
|
||||
controller.resolvedUserHomeDirectoryInternal = localHome.path;
|
||||
|
||||
const prompt = '用户要求我生成一个关于现代AI基础设施的技术营销内容';
|
||||
final uniqueSuffix = DateTime.now().microsecondsSinceEpoch.toString();
|
||||
final sessionA = 'draft-task-a-$uniqueSuffix';
|
||||
final sessionB = 'draft-task-b-$uniqueSuffix';
|
||||
addTearDown(() async {
|
||||
for (final sessionKey in <String>[sessionA, sessionB]) {
|
||||
final workspace = controller.assistantWorkspacePathForSession(
|
||||
sessionKey,
|
||||
);
|
||||
if (workspace.trim().isEmpty) {
|
||||
continue;
|
||||
}
|
||||
final directory = Directory(workspace);
|
||||
if (await directory.exists()) {
|
||||
await directory.delete(recursive: true);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await controller.switchSession(sessionA);
|
||||
final taskAFuture = controller.sendChatMessage(prompt);
|
||||
await fakeGoTaskService.waitForRequestCount(1);
|
||||
|
||||
await controller.switchSession(sessionB);
|
||||
final taskBFuture = controller.sendChatMessage(prompt);
|
||||
await fakeGoTaskService.waitForRequestCount(2);
|
||||
|
||||
final taskARequest = fakeGoTaskService.requests[0];
|
||||
final taskBRequest = fakeGoTaskService.requests[1];
|
||||
expect(taskARequest.sessionId, sessionA);
|
||||
expect(taskBRequest.sessionId, sessionB);
|
||||
expect(taskARequest.prompt, taskBRequest.prompt);
|
||||
expect(taskARequest.resumeSession, isFalse);
|
||||
expect(taskBRequest.resumeSession, isFalse);
|
||||
expect(taskARequest.workingDirectory, endsWith('/$sessionA'));
|
||||
expect(taskBRequest.workingDirectory, endsWith('/$sessionB'));
|
||||
expect(
|
||||
taskARequest.workingDirectory,
|
||||
isNot(taskBRequest.workingDirectory),
|
||||
);
|
||||
expect(
|
||||
taskARequest.remoteWorkingDirectoryHint,
|
||||
isNot(taskBRequest.remoteWorkingDirectoryHint),
|
||||
);
|
||||
expect(
|
||||
taskARequest.remoteWorkingDirectoryHint,
|
||||
endsWith('/threads/$sessionA'),
|
||||
);
|
||||
expect(
|
||||
taskBRequest.remoteWorkingDirectoryHint,
|
||||
endsWith('/threads/$sessionB'),
|
||||
);
|
||||
|
||||
fakeGoTaskService.complete(
|
||||
sessionA,
|
||||
GoTaskServiceResult(
|
||||
success: true,
|
||||
message: 'result A',
|
||||
turnId: 'turn-a',
|
||||
raw: <String, dynamic>{
|
||||
'remoteWorkingDirectory':
|
||||
'/home/ubuntu/.openclaw/workspace/tasks/$sessionA/turn-a',
|
||||
'artifacts': <Map<String, dynamic>>[
|
||||
<String, dynamic>{
|
||||
'relativePath': 'same-prompt-a.md',
|
||||
'content': 'artifact A',
|
||||
'contentType': 'text/markdown',
|
||||
},
|
||||
],
|
||||
},
|
||||
errorMessage: '',
|
||||
resolvedModel: '',
|
||||
route: GoTaskServiceRoute.externalAcpSingle,
|
||||
),
|
||||
);
|
||||
await taskAFuture;
|
||||
|
||||
fakeGoTaskService.complete(
|
||||
sessionB,
|
||||
GoTaskServiceResult(
|
||||
success: true,
|
||||
message: 'result B',
|
||||
turnId: 'turn-b',
|
||||
raw: <String, dynamic>{
|
||||
'remoteWorkingDirectory':
|
||||
'/home/ubuntu/.openclaw/workspace/tasks/$sessionB/turn-b',
|
||||
'artifacts': <Map<String, dynamic>>[
|
||||
<String, dynamic>{
|
||||
'relativePath': 'same-prompt-b.md',
|
||||
'content': 'artifact B',
|
||||
'contentType': 'text/markdown',
|
||||
},
|
||||
],
|
||||
},
|
||||
errorMessage: '',
|
||||
resolvedModel: '',
|
||||
route: GoTaskServiceRoute.externalAcpSingle,
|
||||
),
|
||||
);
|
||||
await taskBFuture;
|
||||
|
||||
final taskAWorkspace = controller.assistantWorkspacePathForSession(
|
||||
sessionA,
|
||||
);
|
||||
final taskBWorkspace = controller.assistantWorkspacePathForSession(
|
||||
sessionB,
|
||||
);
|
||||
expect(
|
||||
await File('$taskAWorkspace/same-prompt-a.md').readAsString(),
|
||||
'artifact A',
|
||||
);
|
||||
expect(
|
||||
await File('$taskBWorkspace/same-prompt-b.md').readAsString(),
|
||||
'artifact B',
|
||||
);
|
||||
|
||||
final taskAThread = controller.requireTaskThreadForSessionInternal(
|
||||
sessionA,
|
||||
);
|
||||
final taskBThread = controller.requireTaskThreadForSessionInternal(
|
||||
sessionB,
|
||||
);
|
||||
expect(taskAThread.lastArtifactSyncStatus, 'synced');
|
||||
expect(taskBThread.lastArtifactSyncStatus, 'synced');
|
||||
expect(taskAThread.lastTaskArtifactRelativePaths, <String>[
|
||||
'same-prompt-a.md',
|
||||
]);
|
||||
expect(taskBThread.lastTaskArtifactRelativePaths, <String>[
|
||||
'same-prompt-b.md',
|
||||
]);
|
||||
expect(
|
||||
taskAThread.lastRemoteWorkingDirectory,
|
||||
'/home/ubuntu/.openclaw/workspace/tasks/$sessionA/turn-a',
|
||||
);
|
||||
expect(
|
||||
taskBThread.lastRemoteWorkingDirectory,
|
||||
'/home/ubuntu/.openclaw/workspace/tasks/$sessionB/turn-b',
|
||||
);
|
||||
|
||||
final taskBSnapshot = await controller.loadAssistantArtifactSnapshot(
|
||||
sessionKey: sessionB,
|
||||
);
|
||||
expect(
|
||||
taskBSnapshot.fileEntries.map((entry) => entry.relativePath),
|
||||
<String>['same-prompt-b.md'],
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'sendChatMessage clears same-prompt draft task artifacts when no files return',
|
||||
() async {
|
||||
final localHome = await Directory.systemTemp.createTemp(
|
||||
'xworkmate-same-prompt-empty-home-',
|
||||
);
|
||||
addTearDown(() async {
|
||||
if (await localHome.exists()) {
|
||||
await localHome.delete(recursive: true);
|
||||
}
|
||||
});
|
||||
final fakeGoTaskService = _BlockingGoTaskServiceClient();
|
||||
final controller = _connectedController(fakeGoTaskService);
|
||||
addTearDown(controller.dispose);
|
||||
controller.resolvedUserHomeDirectoryInternal = localHome.path;
|
||||
|
||||
const prompt = '用户要求我生成一个关于现代AI基础设施的技术营销内容';
|
||||
final uniqueSuffix = DateTime.now().microsecondsSinceEpoch.toString();
|
||||
final sessionA = 'draft-task-a-empty-$uniqueSuffix';
|
||||
final sessionB = 'draft-task-b-empty-$uniqueSuffix';
|
||||
addTearDown(() async {
|
||||
for (final sessionKey in <String>[sessionA, sessionB]) {
|
||||
final workspace = controller.assistantWorkspacePathForSession(
|
||||
sessionKey,
|
||||
);
|
||||
if (workspace.trim().isEmpty) {
|
||||
continue;
|
||||
}
|
||||
final directory = Directory(workspace);
|
||||
if (await directory.exists()) {
|
||||
await directory.delete(recursive: true);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await controller.switchSession(sessionA);
|
||||
final taskAFuture = controller.sendChatMessage(prompt);
|
||||
await fakeGoTaskService.waitForRequestCount(1);
|
||||
fakeGoTaskService.complete(
|
||||
sessionA,
|
||||
const GoTaskServiceResult(
|
||||
success: true,
|
||||
message: 'result A',
|
||||
turnId: 'turn-a',
|
||||
raw: <String, dynamic>{
|
||||
'artifacts': <Map<String, dynamic>>[
|
||||
<String, dynamic>{
|
||||
'relativePath': 'same-prompt-a.md',
|
||||
'content': 'artifact A',
|
||||
'contentType': 'text/markdown',
|
||||
},
|
||||
],
|
||||
},
|
||||
errorMessage: '',
|
||||
resolvedModel: '',
|
||||
route: GoTaskServiceRoute.externalAcpSingle,
|
||||
),
|
||||
);
|
||||
await taskAFuture;
|
||||
|
||||
await controller.switchSession(sessionB);
|
||||
final taskBFuture = controller.sendChatMessage(prompt);
|
||||
await fakeGoTaskService.waitForRequestCount(2);
|
||||
fakeGoTaskService.complete(
|
||||
sessionB,
|
||||
const GoTaskServiceResult(
|
||||
success: true,
|
||||
message: 'result B',
|
||||
turnId: 'turn-b',
|
||||
raw: <String, dynamic>{},
|
||||
errorMessage: '',
|
||||
resolvedModel: '',
|
||||
route: GoTaskServiceRoute.externalAcpSingle,
|
||||
),
|
||||
);
|
||||
await taskBFuture;
|
||||
|
||||
final taskBThread = controller.requireTaskThreadForSessionInternal(
|
||||
sessionB,
|
||||
);
|
||||
expect(taskBThread.lastArtifactSyncStatus, 'no-artifacts');
|
||||
expect(taskBThread.lastTaskArtifactRelativePaths, isEmpty);
|
||||
|
||||
final taskBSnapshot = await controller.loadAssistantArtifactSnapshot(
|
||||
sessionKey: sessionB,
|
||||
);
|
||||
expect(taskBSnapshot.fileEntries, isEmpty);
|
||||
expect(
|
||||
taskBSnapshot.filesMessage,
|
||||
'No files found in the recorded working directory.',
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test('abortRun cancels only the current pending session', () async {
|
||||
final fakeGoTaskService = _BlockingGoTaskServiceClient();
|
||||
final controller = _connectedController(fakeGoTaskService);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user