xworkmate-app/lib/app/app_controller_desktop_thread_sessions.dart
2026-06-07 07:38:04 +08:00

807 lines
28 KiB
Dart

// ignore_for_file: unused_import, unnecessary_import
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:flutter/material.dart';
import 'app_metadata.dart';
import 'app_capabilities.dart';
import 'app_store_policy.dart';
import 'ui_feature_manifest.dart';
import '../i18n/app_language.dart';
import '../models/app_models.dart';
import '../runtime/device_identity_store.dart';
import '../runtime/runtime_bootstrap.dart';
import '../runtime/desktop_platform_service.dart';
import '../runtime/gateway_runtime.dart';
import '../runtime/runtime_controllers.dart';
import '../runtime/runtime_models.dart';
import '../runtime/secure_config_store.dart';
import '../runtime/embedded_agent_launch_policy.dart';
import '../runtime/runtime_coordinator.dart';
import '../runtime/gateway_acp_client.dart';
import '../runtime/codex_runtime.dart';
import '../runtime/codex_config_bridge.dart';
import '../runtime/code_agent_node_orchestrator.dart';
import '../runtime/assistant_artifacts.dart';
import '../runtime/desktop_thread_artifact_service.dart';
import '../runtime/go_task_service_client.dart';
import '../runtime/mode_switcher.dart';
import '../runtime/agent_registry.dart';
import '../runtime/platform_environment.dart';
import 'app_controller_desktop_core.dart';
import 'app_controller_desktop_navigation.dart';
import 'app_controller_desktop_gateway.dart';
import 'app_controller_desktop_settings.dart';
import 'app_controller_desktop_thread_binding.dart';
import 'app_controller_desktop_thread_actions.dart';
import 'app_controller_desktop_workspace_execution.dart';
import 'app_controller_desktop_settings_runtime.dart';
import 'app_controller_desktop_thread_storage.dart';
import 'app_controller_desktop_skill_permissions.dart';
import 'app_controller_desktop_runtime_helpers.dart';
import 'app_controller_desktop_thread_sessions_collaboration_impl.dart';
// ignore_for_file: invalid_use_of_visible_for_testing_member, invalid_use_of_protected_member
final RegExp _runtimeSessionKeyPatternInternal = RegExp(
r'^session-\d+$',
caseSensitive: false,
);
AssistantThreadConnectionState resolveGatewayThreadConnectionStateInternal({
required AssistantExecutionTarget target,
required bool bridgeReady,
required String bridgeLabel,
required AccountSyncState? accountSyncState,
required bool accountSignedIn,
required bool bridgeConfigured,
bool bridgeDiscoveryAttempted = false,
String bridgeDiscoveryError = '',
bool providerCatalogEmpty = false,
}) {
if (bridgeReady) {
return AssistantThreadConnectionState(
executionTarget: target,
status: RuntimeConnectionStatus.connected,
primaryLabel: RuntimeConnectionStatus.connected.label,
detailLabel: bridgeLabel,
ready: true,
gatewayTokenMissing: false,
lastError: null,
);
}
if (!accountSignedIn) {
return AssistantThreadConnectionState(
executionTarget: target,
status: RuntimeConnectionStatus.offline,
primaryLabel: appText('已退出登录', 'Signed out'),
detailLabel: appText('请先登录 svc.plus', 'Please sign in to svc.plus first'),
ready: false,
gatewayTokenMissing: false,
lastError: null,
);
}
final syncState = accountSyncState?.syncState.trim().toLowerCase() ?? '';
final syncMessage = accountSyncState?.syncMessage.trim() ?? '';
final tokenMissing = syncMessage == 'Bridge authorization is unavailable';
final endpointMissing = syncMessage == 'Bridge endpoint is unavailable';
final blocked = syncState == 'blocked';
final failed = blocked && !tokenMissing && !endpointMissing;
// SyncBlocked logic
if (tokenMissing || failed || blocked) {
final status = RuntimeConnectionStatus.error;
final primaryLabel = tokenMissing
? appText('缺少令牌', 'Missing Token')
: failed
? appText('连接失败', 'Connection Failed')
: status.label;
final detailLabel = tokenMissing
? appText(
'xworkmate-bridge 授权不可用',
'xworkmate-bridge authorization unavailable',
)
: failed
? appText('xworkmate-bridge 连接失败', 'xworkmate-bridge connection failed')
: appText('xworkmate-bridge 未连接', 'xworkmate-bridge is not connected');
return AssistantThreadConnectionState(
executionTarget: target,
status: status,
primaryLabel: primaryLabel,
detailLabel: detailLabel,
ready: false,
gatewayTokenMissing: tokenMissing,
lastError: failed ? syncMessage : null,
);
}
final discoveryError = bridgeDiscoveryError.trim();
if (bridgeConfigured && bridgeDiscoveryAttempted) {
final status = RuntimeConnectionStatus.error;
final detailLabel = discoveryError.isNotEmpty
? discoveryError
: providerCatalogEmpty
? appText(
'Gateway ACP 未报告可用的 provider',
'Gateway ACP did not report a usable provider',
)
: appText(
'xworkmate-bridge 连接失败',
'xworkmate-bridge connection failed',
);
return AssistantThreadConnectionState(
executionTarget: target,
status: status,
primaryLabel: appText('连接失败', 'Connection Failed'),
detailLabel: detailLabel,
ready: false,
gatewayTokenMissing: false,
lastError: detailLabel,
);
}
// BridgeDiscovering logic (Signed in, not blocked, but not ready yet)
if (bridgeConfigured) {
return AssistantThreadConnectionState(
executionTarget: target,
status: RuntimeConnectionStatus.offline,
primaryLabel: appText('正在发现', 'Discovering'),
detailLabel: appText(
'正在加载 Bridge 能力...',
'Loading Bridge capabilities...',
),
ready: false,
gatewayTokenMissing: false,
lastError: null,
);
}
// Default Offline/Unconnected
return AssistantThreadConnectionState(
executionTarget: target,
status: RuntimeConnectionStatus.offline,
primaryLabel: RuntimeConnectionStatus.offline.label,
detailLabel: appText(
'xworkmate-bridge 未连接',
'xworkmate-bridge is not connected',
),
ready: false,
gatewayTokenMissing: false,
lastError: null,
);
}
bool bridgeCapabilityReadyForExecutionTargetInternal({
required AssistantExecutionTarget target,
required bool bridgeConfigured,
required List<SingleAgentProvider> providers,
required List<AssistantExecutionTarget> availableTargets,
}) {
if (!bridgeConfigured || providers.isEmpty) {
return false;
}
return availableTargets.isEmpty || availableTargets.contains(target);
}
extension AppControllerDesktopThreadSessions on AppController {
bool isRuntimeOwnedAssistantSessionKeyInternal(String sessionKey) {
final normalizedSessionKey = normalizedAssistantSessionKeyInternal(
sessionKey,
).toLowerCase();
if (normalizedSessionKey.isEmpty) {
return true;
}
if (normalizedSessionKey == 'main') {
return true;
}
return _runtimeSessionKeyPatternInternal.hasMatch(normalizedSessionKey);
}
bool isAppOwnedAssistantSessionKeyInternal(String sessionKey) {
return !isRuntimeOwnedAssistantSessionKeyInternal(sessionKey);
}
AssistantExecutionTarget resolveAssistantExecutionTargetFromRecordsInternal(
TaskThread? record,
) {
return resolveAssistantExecutionTargetFromRecordForTest(
record,
defaultExecutionTarget: pickDraftThreadExecutionTargetInternal(
currentTarget: sanitizePersistedExecutionTargetInternal(
settings.assistantExecutionTarget,
),
visibleTargets: visibleAssistantExecutionTargets(
AssistantExecutionTarget.values,
),
),
);
}
TaskThread? taskThreadForSessionInternal(String sessionKey) {
final normalizedSessionKey = normalizedAssistantSessionKeyInternal(
sessionKey,
);
if (!isAppOwnedAssistantSessionKeyInternal(normalizedSessionKey)) {
return null;
}
return taskThreadRepositoryInternal.taskThreadForSession(
normalizedSessionKey,
);
}
TaskThread requireTaskThreadForSessionInternal(String sessionKey) {
final normalizedSessionKey = normalizedAssistantSessionKeyInternal(
sessionKey,
);
return taskThreadRepositoryInternal.requireTaskThreadForSession(
normalizedSessionKey,
);
}
bool hasAssistantTaskStateInternal(String sessionKey) {
final normalizedSessionKey = normalizedAssistantSessionKeyInternal(
sessionKey,
);
if (!isAppOwnedAssistantSessionKeyInternal(normalizedSessionKey)) {
return false;
}
return taskThreadRepositoryInternal.containsKey(normalizedSessionKey) ||
assistantThreadMessagesInternal.containsKey(normalizedSessionKey) ||
localSessionMessagesInternal.containsKey(normalizedSessionKey);
}
String createAssistantDraftSessionKeyInternal() {
final selectedAgentId = agentsControllerInternal.selectedAgentId.trim();
for (var attempt = 0; attempt < 32; attempt += 1) {
assistantDraftSessionCounterInternal += 1;
final stamp = DateTime.now().microsecondsSinceEpoch;
final suffix = '$stamp-$assistantDraftSessionCounterInternal';
final candidate = selectedAgentId.isEmpty
? 'draft:$suffix'
: 'draft:$selectedAgentId:$suffix';
if (!hasAssistantTaskStateInternal(candidate)) {
return candidate;
}
}
throw StateError('Unable to allocate a unique draft task session key.');
}
List<String> assistantSelectedSkillKeysForSession(String sessionKey) {
final normalizedSessionKey = normalizedAssistantSessionKeyInternal(
sessionKey,
);
final selected =
assistantThreadRecordsInternal[normalizedSessionKey]
?.selectedSkillKeys ??
const <String>[];
final availableKeys = skills.map((item) => item.skillKey).toSet();
return selected.where(availableKeys.contains).toList(growable: false);
}
String assistantModelForSession(String sessionKey) {
final normalizedSessionKey = normalizedAssistantSessionKeyInternal(
sessionKey,
);
final recordModel =
assistantThreadRecordsInternal[normalizedSessionKey]?.assistantModelId
.trim() ??
'';
final availableChoices = assistantModelChoicesForSessionInternal(
normalizedSessionKey,
);
if (recordModel.isNotEmpty &&
(availableChoices.isEmpty || availableChoices.contains(recordModel))) {
return recordModel;
}
return resolvedAssistantModelForTargetInternal(
AssistantExecutionTarget.gateway,
);
}
String assistantDisplayModelForSession(String sessionKey) {
final normalizedSessionKey = normalizedAssistantSessionKeyInternal(
sessionKey,
);
final availableChoices = assistantModelChoicesForSessionInternal(
normalizedSessionKey,
);
if (availableChoices.isEmpty) {
return '';
}
final thread = taskThreadForSessionInternal(normalizedSessionKey);
final latestResolvedModel = thread?.latestResolvedRuntimeModel.trim() ?? '';
if (availableChoices.contains(latestResolvedModel)) {
return latestResolvedModel;
}
final selectedModel = thread?.assistantModelId.trim() ?? '';
if (availableChoices.contains(selectedModel)) {
return selectedModel;
}
final target = assistantExecutionTargetForSession(normalizedSessionKey);
final defaultModel = resolvedAssistantModelForTargetInternal(target).trim();
if (availableChoices.contains(defaultModel)) {
return defaultModel;
}
return availableChoices.length == 1 ? availableChoices.first : '';
}
String assistantWorkspacePathForSession(String sessionKey) {
final normalizedSessionKey = normalizedAssistantSessionKeyInternal(
sessionKey,
);
return taskThreadForSessionInternal(
normalizedSessionKey,
)?.workspaceBinding.workspacePath.trim() ??
'';
}
WorkspaceRefKind assistantWorkspaceKindForSession(String sessionKey) {
final record = taskThreadForSessionInternal(
normalizedAssistantSessionKeyInternal(sessionKey),
);
if (record == null) {
return WorkspaceRefKind.remotePath;
}
return record.workspaceBinding.workspaceKind == WorkspaceKind.localFs
? WorkspaceRefKind.localPath
: WorkspaceRefKind.remotePath;
}
String assistantWorkspaceDisplayPathForSession(String sessionKey) {
final normalizedSessionKey = normalizedAssistantSessionKeyInternal(
sessionKey,
);
return taskThreadForSessionInternal(
normalizedSessionKey,
)?.workspaceBinding.displayPath.trim() ??
'';
}
double? assistantArtifactSyncAtMsForSession(String sessionKey) {
return taskThreadForSessionInternal(
normalizedAssistantSessionKeyInternal(sessionKey),
)?.lastArtifactSyncAtMs;
}
String assistantArtifactSyncStatusForSession(String sessionKey) {
final normalizedSessionKey = normalizedAssistantSessionKeyInternal(
sessionKey,
);
final thread = taskThreadForSessionInternal(normalizedSessionKey);
return thread?.lastArtifactSyncStatus?.trim() ?? '';
}
Future<AssistantArtifactSnapshot> loadAssistantArtifactSnapshot({
String? sessionKey,
}) async {
final resolvedSessionKey = normalizedAssistantSessionKeyInternal(
sessionKey ?? currentSessionKey,
);
final thread = taskThreadForSessionInternal(resolvedSessionKey);
final snapshot = await threadArtifactServiceInternal.loadSnapshot(
workspacePath: assistantWorkspacePathForSession(resolvedSessionKey),
workspaceKind: assistantWorkspaceKindForSession(resolvedSessionKey),
artifactRelativePaths:
thread?.lastTaskArtifactRelativePaths ?? const <String>[],
);
if (thread == null) {
return snapshot;
}
final shouldRefreshRemote = _shouldRefreshRemoteArtifactSnapshotInternal(
thread,
);
if (snapshot.fileEntries.isNotEmpty && !shouldRefreshRemote) {
return snapshot;
}
final synced = await syncRemoteTaskArtifactsForSessionInternal(
resolvedSessionKey,
);
if (!synced) {
return snapshot;
}
final refreshedThread = taskThreadForSessionInternal(resolvedSessionKey);
return threadArtifactServiceInternal.loadSnapshot(
workspacePath: assistantWorkspacePathForSession(resolvedSessionKey),
workspaceKind: assistantWorkspaceKindForSession(resolvedSessionKey),
artifactRelativePaths:
refreshedThread?.lastTaskArtifactRelativePaths ?? const <String>[],
);
}
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 {
final normalizedSessionKey = normalizedAssistantSessionKeyInternal(
sessionKey,
);
final thread = taskThreadForSessionInternal(normalizedSessionKey);
if (thread == null ||
thread.workspaceBinding.workspaceKind != WorkspaceKind.localFs) {
return false;
}
final association =
thread.openClawTaskAssociation ??
_inferOpenClawTaskAssociationFromThreadInternal(thread);
if (association == null) {
return false;
}
try {
final result = await goTaskServiceClientInternal.getTask(
route: GoTaskServiceRoute.externalAcpSingle,
target: assistantExecutionTargetFromExecutionMode(
thread.executionBinding.executionMode,
),
association: association,
);
if (result.artifacts.isEmpty) {
return false;
}
final status = result.status.trim();
final nextAssociation =
result.openClawTaskAssociation ??
association.copyWith(status: status.isEmpty ? 'completed' : status);
upsertTaskThreadInternal(
normalizedSessionKey,
openClawTaskAssociation: nextAssociation,
lastRemoteWorkingDirectory: result.remoteWorkingDirectory.trim().isEmpty
? thread.lastRemoteWorkingDirectory
: result.remoteWorkingDirectory.trim(),
lastRemoteWorkspaceRefKind:
result.remoteWorkspaceRefKind ?? thread.lastRemoteWorkspaceRefKind,
updatedAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(),
);
await persistGoTaskArtifactsForSessionInternal(
normalizedSessionKey,
result,
);
final refreshed = taskThreadForSessionInternal(normalizedSessionKey);
return refreshed?.lastTaskArtifactRelativePaths.isNotEmpty == true;
} catch (error) {
debugPrint(
'Remote artifact sync failed for $normalizedSessionKey: $error',
);
return false;
}
}
Future<AssistantArtifactPreview> loadAssistantArtifactPreview(
AssistantArtifactEntry entry, {
String? sessionKey,
}) {
final resolvedSessionKey = normalizedAssistantSessionKeyInternal(
sessionKey ?? currentSessionKey,
);
final thread = taskThreadForSessionInternal(resolvedSessionKey);
return threadArtifactServiceInternal.loadPreview(
entry: entry,
workspacePath: assistantWorkspacePathForSession(resolvedSessionKey),
workspaceKind: assistantWorkspaceKindForSession(resolvedSessionKey),
artifactRelativePaths:
thread?.lastTaskArtifactRelativePaths ?? const <String>[],
);
}
String get assistantConversationOwnerLabel {
return activeAgentName;
}
String get resolvedAssistantModel =>
resolvedAssistantModelForTargetInternal(currentAssistantExecutionTarget);
AssistantThreadConnectionState get currentAssistantConnectionState =>
assistantConnectionStateForSession(currentSessionKey);
AssistantThreadConnectionState assistantConnectionStateForSession(
String sessionKey,
) {
final normalizedSessionKey = normalizedAssistantSessionKeyInternal(
sessionKey,
);
final target = assistantExecutionTargetForSession(normalizedSessionKey);
final providers = providerCatalogForExecutionTarget(target);
final availableTargets = bridgeAvailableExecutionTargets;
final bridgeConfigured = isBridgeAcpRuntimeConfiguredInternal();
final bridgeReady = bridgeCapabilityReadyForExecutionTargetInternal(
target: target,
bridgeConfigured: bridgeConfigured,
providers: providers,
availableTargets: availableTargets,
);
final bridgeEndpoint = resolveBridgeAcpEndpointInternal();
final bridgeLabel = bridgeEndpoint?.host.trim().isNotEmpty == true
? bridgeEndpoint!.host.trim()
: 'xworkmate-bridge';
return resolveGatewayThreadConnectionStateInternal(
target: target,
bridgeReady: bridgeReady,
bridgeLabel: bridgeLabel,
accountSyncState: settingsControllerInternal.accountSyncState,
accountSignedIn: settingsControllerInternal.accountSignedIn,
bridgeConfigured: bridgeConfigured,
bridgeDiscoveryAttempted: bridgeCapabilitiesRefreshAttemptedInternal,
bridgeDiscoveryError: bridgeCapabilitiesRefreshErrorInternal,
providerCatalogEmpty: providers.isEmpty,
);
}
bool bridgeCapabilityReadyForAssistantTargetInternal(
AssistantExecutionTarget target,
) {
return bridgeCapabilityReadyForExecutionTargetInternal(
target: target,
bridgeConfigured: isBridgeAcpRuntimeConfiguredInternal(),
providers: providerCatalogForExecutionTarget(target),
availableTargets: bridgeAvailableExecutionTargets,
);
}
bool bridgeCapabilityRefreshNeededForAssistantTargetInternal(
AssistantExecutionTarget target,
) {
if (!isBridgeAcpRuntimeConfiguredInternal()) {
return false;
}
return !bridgeCapabilityReadyForAssistantTargetInternal(target);
}
String get assistantConnectionStatusLabel =>
currentAssistantConnectionState.primaryLabel;
String get assistantConnectionTargetLabel =>
currentAssistantConnectionState.detailLabel;
Future<String> loadAiGatewayApiKey() =>
loadAiGatewayApiKeyThreadSessionInternal(this);
Future<void> openOnlineWorkspace() =>
openOnlineWorkspaceThreadSessionInternal(this);
List<String> get aiGatewayModelChoices =>
aiGatewayModelChoicesThreadSessionInternal(this);
List<String> get connectedGatewayModelChoices =>
connectedGatewayModelChoicesThreadSessionInternal(this);
List<String> get assistantModelChoices =>
assistantModelChoicesThreadSessionInternal(this);
List<String> assistantModelChoicesForSessionInternal(String sessionKey) =>
assistantModelChoicesForSessionThreadSessionInternal(this, sessionKey);
String get resolvedDefaultModel =>
resolvedDefaultModelThreadSessionInternal(this);
bool get canQuickConnectGateway =>
canQuickConnectGatewayThreadSessionInternal(this);
String joinConnectionPartsInternal(List<String> parts) =>
joinConnectionPartsThreadSessionInternal(parts);
String gatewayAddressLabelInternal(GatewayConnectionProfile profile) =>
gatewayAddressLabelThreadSessionInternal(profile);
List<AssistantFocusEntry> get assistantNavigationDestinations =>
normalizeAssistantNavigationDestinations(
appUiState.assistantNavigationDestinations,
).where(supportsAssistantFocusEntry).toList(growable: false);
bool supportsAssistantFocusEntry(AssistantFocusEntry entry) {
final destination = entry.destination;
if (destination != null) {
return capabilities.supportsDestination(destination);
}
return capabilities.supportsDestination(WorkspaceDestination.settings);
}
List<GatewayChatMessage> get chatMessages {
final sessionKey = normalizedAssistantSessionKeyInternal(
sessionsControllerInternal.currentSessionKey,
);
final chatSessionKey = normalizedAssistantSessionKeyInternal(
chatControllerInternal.sessionKey,
);
final items = matchesSessionKey(chatSessionKey, sessionKey)
? List<GatewayChatMessage>.from(chatControllerInternal.messages)
: <GatewayChatMessage>[];
final threadItems = assistantThreadMessagesInternal[sessionKey];
if (threadItems != null && threadItems.isNotEmpty) {
items.addAll(threadItems);
}
final localItems = localSessionMessagesInternal[sessionKey];
if (localItems != null && localItems.isNotEmpty) {
items.addAll(localItems);
}
final streaming = matchesSessionKey(chatSessionKey, sessionKey)
? chatControllerInternal.streamingAssistantText?.trim() ?? ''
: '';
if (streaming.isNotEmpty) {
items.add(
GatewayChatMessage(
id: 'streaming',
role: 'assistant',
text: streaming,
timestampMs: DateTime.now().millisecondsSinceEpoch.toDouble(),
toolCallId: null,
toolName: null,
stopReason: null,
pending: true,
error: false,
),
);
}
return dedupeGatewayChatMessagesByIdThreadSessionInternal(items);
}
List<GatewayChatMessage> dedupeGatewayChatMessagesByIdThreadSessionInternal(
List<GatewayChatMessage> messages,
) {
final seenIds = <String>{};
final deduped = <GatewayChatMessage>[];
for (final message in messages) {
final id = message.id.trim();
if (id.isNotEmpty && !seenIds.add(id)) {
continue;
}
deduped.add(message);
}
return deduped;
}
String normalizedAssistantSessionKeyInternal(String sessionKey) {
final trimmed = sessionKey.trim();
return trimmed.isEmpty ? 'main' : trimmed;
}
AssistantExecutionTarget assistantExecutionTargetForSession(
String sessionKey,
) {
final normalizedSessionKey = normalizedAssistantSessionKeyInternal(
sessionKey,
);
final record = taskThreadForSessionInternal(normalizedSessionKey);
return resolveAssistantExecutionTargetFromRecordsInternal(record);
}
AssistantMessageViewMode assistantMessageViewModeForSession(
String sessionKey,
) {
final normalizedSessionKey = normalizedAssistantSessionKeyInternal(
sessionKey,
);
return assistantThreadRecordsInternal[normalizedSessionKey]
?.messageViewMode ??
AssistantMessageViewMode.rendered;
}
WorkspaceRefKind defaultWorkspaceRefKindForTargetInternal(
AssistantExecutionTarget target,
) => WorkspaceRefKind.remotePath;
List<GatewaySessionSummary> assistantSessionsInternal() {
final byKey = <String, GatewaySessionSummary>{};
for (final record in assistantThreadRecordsInternal.values) {
final normalizedSessionKey = normalizedAssistantSessionKeyInternal(
record.sessionKey,
);
if (normalizedSessionKey.isEmpty ||
!isAppOwnedAssistantSessionKeyInternal(normalizedSessionKey) ||
isAssistantTaskArchived(normalizedSessionKey) ||
record.archived) {
continue;
}
byKey.putIfAbsent(
normalizedSessionKey,
() => assistantSessionSummaryForInternal(
normalizedSessionKey,
record: record,
),
);
}
final currentKey = normalizedAssistantSessionKeyInternal(currentSessionKey);
if (isAppOwnedAssistantSessionKeyInternal(currentKey) &&
!isAssistantTaskArchived(currentKey) &&
!byKey.containsKey(currentKey)) {
byKey[currentKey] = assistantSessionSummaryForInternal(currentKey);
}
return byKey.values.toList(growable: false);
}
List<GatewaySessionSummary> archivedAssistantSessionsInternal() {
final items = <GatewaySessionSummary>[];
for (final record in assistantThreadRecordsInternal.values) {
final sessionKey = normalizedAssistantSessionKeyInternal(
record.sessionKey,
);
if (!isAppOwnedAssistantSessionKeyInternal(sessionKey) ||
!record.archived) {
continue;
}
items.add(assistantSessionSummaryForInternal(sessionKey, record: record));
}
items.sort((left, right) {
return (right.updatedAtMs ?? 0).compareTo(left.updatedAtMs ?? 0);
});
return items;
}
}
OpenClawTaskAssociation? _inferOpenClawTaskAssociationFromThreadInternal(
TaskThread thread,
) {
final remoteWorkspace = thread.lastRemoteWorkingDirectory?.trim() ?? '';
if (remoteWorkspace.isEmpty) {
return null;
}
final normalized = remoteWorkspace.replaceAll('\\', '/');
final segments = normalized
.split('/')
.where((segment) => segment.trim().isNotEmpty)
.toList(growable: false);
final tasksIndex = segments.lastIndexOf('tasks');
if (tasksIndex < 0 || tasksIndex + 2 >= segments.length) {
return null;
}
final taskDir = segments[tasksIndex + 1].trim();
final runId = segments[tasksIndex + 2].trim();
final openclawSessionKey = _openClawSessionKeyFromTaskDirInternal(taskDir);
final appThreadKey = _appThreadKeyFromSessionKeyInternal(thread.threadId);
if (runId.isEmpty || openclawSessionKey.isEmpty || appThreadKey.isEmpty) {
return null;
}
return OpenClawTaskAssociation(
sessionId: thread.threadId,
threadId: thread.threadId,
turnId: runId,
runId: runId,
artifactScope: remoteWorkspace,
artifactDirectory: remoteWorkspace,
gatewayProviderId: 'openclaw',
startedAtMs: thread.lifecycleState.lastRunAtMs ?? 0,
status: 'completed',
appThreadKey: appThreadKey,
openclawSessionKey: openclawSessionKey,
);
}
String _appThreadKeyFromSessionKeyInternal(String sessionKey) {
final normalized = sessionKey.trim();
if (normalized.startsWith('draft:')) {
return normalized;
}
if (normalized.startsWith('draft-')) {
return 'draft:${normalized.substring('draft-'.length)}';
}
return normalized;
}
String _openClawSessionKeyFromTaskDirInternal(String taskDir) {
final normalized = taskDir.trim();
if (normalized.startsWith('agent_main_draft_')) {
return 'agent:main:draft:${normalized.substring('agent_main_draft_'.length)}';
}
if (normalized.startsWith('draft_')) {
return 'draft:${normalized.substring('draft_'.length)}';
}
return '';
}
AssistantExecutionTarget resolveAssistantExecutionTargetFromRecordForTest(
TaskThread? record, {
required AssistantExecutionTarget defaultExecutionTarget,
}) {
return record == null
? defaultExecutionTarget
: assistantExecutionTargetFromExecutionMode(
record.executionBinding.executionMode,
);
}