fix: resolve svc.plus account save issue and improve bridge runtime status sync
This commit is contained in:
parent
4b87416e0e
commit
dfa1fa71bf
@ -454,12 +454,12 @@ class AppController extends ChangeNotifier {
|
||||
DesktopIntegrationState get desktopIntegration =>
|
||||
desktopPlatformServiceInternal.state;
|
||||
bool get supportsDesktopIntegration => desktopIntegration.isSupported;
|
||||
bool get desktopPlatformBusy => desktopPlatformBusyInternal;
|
||||
bool get desktopPlatformBusy => _desktopPlatformBusyInternal;
|
||||
set desktopPlatformBusyInternal(bool value) {
|
||||
desktopPlatformBusyInternal = value;
|
||||
_desktopPlatformBusyInternal = value;
|
||||
notifyListeners();
|
||||
}
|
||||
bool desktopPlatformBusyInternal = false;
|
||||
bool _desktopPlatformBusyInternal = false;
|
||||
|
||||
GatewayConnectionSnapshot get connection => runtimeInternal.snapshot;
|
||||
SettingsSnapshot get settings => settingsControllerInternal.snapshot;
|
||||
|
||||
@ -11,7 +11,7 @@ import 'ui_feature_manifest.dart';
|
||||
import '../i18n/app_language.dart';
|
||||
import '../models/app_models.dart';
|
||||
import '../runtime/device_identity_store.dart';
|
||||
import '../runtime/aris_bundle.dart';
|
||||
|
||||
import '../runtime/go_core.dart';
|
||||
import '../runtime/runtime_bootstrap.dart';
|
||||
import '../runtime/desktop_platform_service.dart';
|
||||
|
||||
@ -11,7 +11,7 @@ import 'ui_feature_manifest.dart';
|
||||
import '../i18n/app_language.dart';
|
||||
import '../models/app_models.dart';
|
||||
import '../runtime/device_identity_store.dart';
|
||||
import '../runtime/aris_bundle.dart';
|
||||
|
||||
import '../runtime/go_core.dart';
|
||||
import '../runtime/runtime_bootstrap.dart';
|
||||
import '../runtime/desktop_platform_service.dart';
|
||||
|
||||
@ -11,7 +11,7 @@ import 'ui_feature_manifest.dart';
|
||||
import '../i18n/app_language.dart';
|
||||
import '../models/app_models.dart';
|
||||
import '../runtime/device_identity_store.dart';
|
||||
import '../runtime/aris_bundle.dart';
|
||||
|
||||
import '../runtime/go_core.dart';
|
||||
import '../runtime/runtime_bootstrap.dart';
|
||||
import '../runtime/desktop_platform_service.dart';
|
||||
|
||||
@ -11,7 +11,7 @@ import 'ui_feature_manifest.dart';
|
||||
import '../i18n/app_language.dart';
|
||||
import '../models/app_models.dart';
|
||||
import '../runtime/device_identity_store.dart';
|
||||
import '../runtime/aris_bundle.dart';
|
||||
|
||||
import '../runtime/go_core.dart';
|
||||
import '../runtime/runtime_bootstrap.dart';
|
||||
import '../runtime/desktop_platform_service.dart';
|
||||
|
||||
@ -11,7 +11,7 @@ import 'ui_feature_manifest.dart';
|
||||
import '../i18n/app_language.dart';
|
||||
import '../models/app_models.dart';
|
||||
import '../runtime/device_identity_store.dart';
|
||||
import '../runtime/aris_bundle.dart';
|
||||
|
||||
import '../runtime/go_core.dart';
|
||||
import '../runtime/runtime_bootstrap.dart';
|
||||
import '../runtime/desktop_platform_service.dart';
|
||||
|
||||
@ -11,7 +11,7 @@ import 'ui_feature_manifest.dart';
|
||||
import '../i18n/app_language.dart';
|
||||
import '../models/app_models.dart';
|
||||
import '../runtime/device_identity_store.dart';
|
||||
import '../runtime/aris_bundle.dart';
|
||||
|
||||
import '../runtime/go_core.dart';
|
||||
import '../runtime/runtime_bootstrap.dart';
|
||||
import '../runtime/desktop_platform_service.dart';
|
||||
|
||||
@ -11,7 +11,7 @@ import 'ui_feature_manifest.dart';
|
||||
import '../i18n/app_language.dart';
|
||||
import '../models/app_models.dart';
|
||||
import '../runtime/device_identity_store.dart';
|
||||
import '../runtime/aris_bundle.dart';
|
||||
|
||||
import '../runtime/go_core.dart';
|
||||
import '../runtime/runtime_bootstrap.dart';
|
||||
import '../runtime/desktop_platform_service.dart';
|
||||
@ -755,8 +755,6 @@ extension AppControllerDesktopSettingsRuntime on AppController {
|
||||
await settingsControllerInternal.saveSnapshot(sanitized);
|
||||
settingsDraftInternal = sanitized;
|
||||
settingsDraftInitializedInternal = true;
|
||||
}
|
||||
|
||||
Future<void> applyPersistedSettingsSideEffectsInternal({
|
||||
required SettingsSnapshot previous,
|
||||
required SettingsSnapshot current,
|
||||
@ -774,7 +772,20 @@ extension AppControllerDesktopSettingsRuntime on AppController {
|
||||
?.selectedAgentId ??
|
||||
'',
|
||||
);
|
||||
modelsControllerInternal.restoreFromSettings(current.aiGateway);
|
||||
themeModeInternal = current.themeMode;
|
||||
|
||||
final bridgeChanged =
|
||||
previous.acpBridgeServerModeConfig.toJsonString() !=
|
||||
current.acpBridgeServerModeConfig.toJsonString();
|
||||
|
||||
if (refreshAfterSave || bridgeChanged) {
|
||||
// Re-trigger Bridge capability discovery if the mode or endpoint changed.
|
||||
unawaited(refreshAcpCapabilitiesInternal(quiet: true));
|
||||
}
|
||||
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
if (disposedInternal) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -11,7 +11,7 @@ import 'ui_feature_manifest.dart';
|
||||
import '../i18n/app_language.dart';
|
||||
import '../models/app_models.dart';
|
||||
import '../runtime/device_identity_store.dart';
|
||||
import '../runtime/aris_bundle.dart';
|
||||
|
||||
import '../runtime/go_core.dart';
|
||||
import '../runtime/runtime_bootstrap.dart';
|
||||
import '../runtime/desktop_platform_service.dart';
|
||||
|
||||
@ -11,7 +11,7 @@ import 'ui_feature_manifest.dart';
|
||||
import '../i18n/app_language.dart';
|
||||
import '../models/app_models.dart';
|
||||
import '../runtime/device_identity_store.dart';
|
||||
import '../runtime/aris_bundle.dart';
|
||||
|
||||
import '../runtime/go_core.dart';
|
||||
import '../runtime/runtime_bootstrap.dart';
|
||||
import '../runtime/desktop_platform_service.dart';
|
||||
|
||||
@ -11,7 +11,7 @@ import 'ui_feature_manifest.dart';
|
||||
import '../i18n/app_language.dart';
|
||||
import '../models/app_models.dart';
|
||||
import '../runtime/device_identity_store.dart';
|
||||
import '../runtime/aris_bundle.dart';
|
||||
|
||||
import '../runtime/go_core.dart';
|
||||
import '../runtime/runtime_bootstrap.dart';
|
||||
import '../runtime/desktop_platform_service.dart';
|
||||
|
||||
@ -11,7 +11,7 @@ import 'ui_feature_manifest.dart';
|
||||
import '../i18n/app_language.dart';
|
||||
import '../models/app_models.dart';
|
||||
import '../runtime/device_identity_store.dart';
|
||||
import '../runtime/aris_bundle.dart';
|
||||
|
||||
import '../runtime/go_core.dart';
|
||||
import '../runtime/runtime_bootstrap.dart';
|
||||
import '../runtime/desktop_platform_service.dart';
|
||||
|
||||
@ -11,7 +11,7 @@ import 'ui_feature_manifest.dart';
|
||||
import '../i18n/app_language.dart';
|
||||
import '../models/app_models.dart';
|
||||
import '../runtime/device_identity_store.dart';
|
||||
import '../runtime/aris_bundle.dart';
|
||||
|
||||
import '../runtime/go_core.dart';
|
||||
import '../runtime/runtime_bootstrap.dart';
|
||||
import '../runtime/desktop_platform_service.dart';
|
||||
|
||||
@ -11,7 +11,7 @@ import 'ui_feature_manifest.dart';
|
||||
import '../i18n/app_language.dart';
|
||||
import '../models/app_models.dart';
|
||||
import '../runtime/device_identity_store.dart';
|
||||
import '../runtime/aris_bundle.dart';
|
||||
|
||||
import '../runtime/go_core.dart';
|
||||
import '../runtime/runtime_bootstrap.dart';
|
||||
import '../runtime/desktop_platform_service.dart';
|
||||
|
||||
@ -11,7 +11,7 @@ import 'ui_feature_manifest.dart';
|
||||
import '../i18n/app_language.dart';
|
||||
import '../models/app_models.dart';
|
||||
import '../runtime/device_identity_store.dart';
|
||||
import '../runtime/aris_bundle.dart';
|
||||
|
||||
import '../runtime/go_core.dart';
|
||||
import '../runtime/runtime_bootstrap.dart';
|
||||
import '../runtime/desktop_platform_service.dart';
|
||||
|
||||
@ -207,11 +207,15 @@ mixin GatewayRuntimeHelpersInternal on ChangeNotifier {
|
||||
) {
|
||||
final text = raw is String ? raw : utf8.decode(raw as List<int>);
|
||||
final decoded = jsonDecode(text) as Map<String, dynamic>;
|
||||
|
||||
// Handle Events / Notifications
|
||||
final event = stringValue(decoded['event']) ?? stringValue(decoded['method']);
|
||||
final type = stringValue(decoded['type']);
|
||||
if (type == 'event') {
|
||||
final event = stringValue(decoded['event']) ?? '';
|
||||
final payload = decoded['payload'];
|
||||
if (event == 'connect.challenge') {
|
||||
|
||||
if (event != null || type == 'event') {
|
||||
final resolvedEvent = event ?? '';
|
||||
final payload = decoded['payload'] ?? decoded['params'];
|
||||
if (resolvedEvent == 'connect.challenge') {
|
||||
final nonce = stringValue(asMap(payload)['nonce']);
|
||||
if (nonce != null && !challenge.isCompleted) {
|
||||
challenge.complete(nonce);
|
||||
@ -219,36 +223,35 @@ mixin GatewayRuntimeHelpersInternal on ChangeNotifier {
|
||||
appendLogInternal(runtime, 'debug', 'connect', 'challenge received');
|
||||
return;
|
||||
}
|
||||
if (event == 'health') {
|
||||
if (resolvedEvent == 'health') {
|
||||
runtime.snapshotInternal = runtime.snapshotInternal.copyWith(
|
||||
healthPayload: asMap(payload),
|
||||
);
|
||||
appendLogInternal(runtime, 'debug', 'health', 'push health update');
|
||||
runtime.notifyListeners();
|
||||
} else if (event == 'device.pair.requested' ||
|
||||
event == 'device.pair.resolved') {
|
||||
} else if (resolvedEvent == 'device.pair.requested' ||
|
||||
resolvedEvent == 'device.pair.resolved') {
|
||||
final eventPayload = asMap(payload);
|
||||
appendLogInternal(
|
||||
runtime,
|
||||
'info',
|
||||
'pairing',
|
||||
'$event | request: ${stringValue(eventPayload['requestId']) ?? 'unknown'} | device: ${stringValue(eventPayload['deviceId']) ?? 'unknown'}',
|
||||
'$resolvedEvent | request: ${stringValue(eventPayload['requestId']) ?? 'unknown'} | device: ${stringValue(eventPayload['deviceId']) ?? 'unknown'}',
|
||||
);
|
||||
} else if (event == 'seqGap') {
|
||||
} else if (resolvedEvent == 'seqGap') {
|
||||
appendLogInternal(runtime, 'warn', 'sync', 'sequence gap detected');
|
||||
}
|
||||
runtime.eventsInternal.add(
|
||||
GatewayPushEvent(
|
||||
event: event,
|
||||
event: resolvedEvent,
|
||||
payload: payload,
|
||||
sequence: intValue(decoded['seq']),
|
||||
sequence: intValue(decoded['seq']) ?? 0,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (type != 'res') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle Responses
|
||||
final id = stringValue(decoded['id']);
|
||||
if (id == null) {
|
||||
return;
|
||||
@ -257,9 +260,13 @@ mixin GatewayRuntimeHelpersInternal on ChangeNotifier {
|
||||
if (completer == null || completer.isCompleted) {
|
||||
return;
|
||||
}
|
||||
final ok = boolValue(decoded['ok']) ?? false;
|
||||
final payload = decoded['payload'];
|
||||
|
||||
final hasResult = decoded.containsKey('result');
|
||||
final hasError = decoded.containsKey('error');
|
||||
final ok = boolValue(decoded['ok']) ?? (hasResult && !hasError);
|
||||
final payload = decoded['payload'] ?? decoded['result'];
|
||||
final error = asMap(decoded['error']);
|
||||
|
||||
if (!ok) {
|
||||
appendLogInternal(
|
||||
runtime,
|
||||
|
||||
@ -4,7 +4,7 @@ import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'aris_bundle.dart';
|
||||
|
||||
import 'embedded_agent_launch_policy.dart';
|
||||
import 'go_core.dart';
|
||||
import 'aris_llm_chat_client.dart';
|
||||
|
||||
@ -4,7 +4,7 @@ import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'aris_bundle.dart';
|
||||
|
||||
import 'embedded_agent_launch_policy.dart';
|
||||
import 'go_core.dart';
|
||||
import 'aris_llm_chat_client.dart';
|
||||
|
||||
@ -4,7 +4,7 @@ import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'aris_bundle.dart';
|
||||
|
||||
import 'embedded_agent_launch_policy.dart';
|
||||
import 'go_core.dart';
|
||||
import 'aris_llm_chat_client.dart';
|
||||
|
||||
@ -1,9 +1,8 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
export 'file_store_support.dart';
|
||||
export 'secret_store.dart';
|
||||
export 'settings_store.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:path_provider/package_info_plus.dart';
|
||||
|
||||
import 'file_store_support.dart';
|
||||
import 'runtime_models.dart';
|
||||
@ -12,319 +11,71 @@ import 'settings_store.dart';
|
||||
|
||||
class SecureConfigStore {
|
||||
SecureConfigStore({
|
||||
Future<String?> Function()? secretRootPathResolver,
|
||||
Future<String?> Function()? appDataRootPathResolver,
|
||||
Future<String?> Function()? supportRootPathResolver,
|
||||
SecureStorageClient? secureStorage,
|
||||
bool enableSecureStorage = true,
|
||||
}) {
|
||||
final layoutResolver = StoreLayoutResolver(
|
||||
appDataRootPathResolver: appDataRootPathResolver,
|
||||
secretRootPathResolver: secretRootPathResolver,
|
||||
supportRootPathResolver: supportRootPathResolver,
|
||||
);
|
||||
_layoutResolver = layoutResolver;
|
||||
_secretStore = SecretStore(
|
||||
secretRootPathResolver: secretRootPathResolver,
|
||||
appDataRootPathResolver: appDataRootPathResolver,
|
||||
supportRootPathResolver: supportRootPathResolver,
|
||||
secureStorage: secureStorage,
|
||||
enableSecureStorage: enableSecureStorage,
|
||||
layoutResolver: layoutResolver,
|
||||
);
|
||||
_settingsStore = SettingsStore(
|
||||
appDataRootPathResolver: appDataRootPathResolver,
|
||||
supportRootPathResolver: supportRootPathResolver,
|
||||
layoutResolver: layoutResolver,
|
||||
);
|
||||
}
|
||||
StoreLayoutResolver? layoutResolver,
|
||||
SettingsStore? settingsStore,
|
||||
SecretStore? secretStore,
|
||||
}) : _layoutResolver = layoutResolver ?? StoreLayoutResolver(),
|
||||
_settingsStore = settingsStore ?? SettingsStore(layoutResolver ?? StoreLayoutResolver()),
|
||||
_secretStore = secretStore ?? SecretStore(layoutResolver ?? StoreLayoutResolver());
|
||||
|
||||
late final SecretStore _secretStore;
|
||||
late final SettingsStore _settingsStore;
|
||||
late final StoreLayoutResolver _layoutResolver;
|
||||
final StoreLayoutResolver _layoutResolver;
|
||||
final SettingsStore _settingsStore;
|
||||
final SecretStore _secretStore;
|
||||
|
||||
Future<void> initialize() async {
|
||||
await _secretStore.initialize();
|
||||
await _settingsStore.initialize();
|
||||
await _secretStore.initialize();
|
||||
}
|
||||
|
||||
Future<SettingsSnapshot> loadSettingsSnapshot() {
|
||||
return _settingsStore.loadSettingsSnapshot();
|
||||
}
|
||||
Future<SettingsSnapshot> loadSettingsSnapshot() => _settingsStore.loadSnapshot();
|
||||
Future<void> saveSettingsSnapshot(SettingsSnapshot snapshot) => _settingsStore.saveSnapshot(snapshot);
|
||||
Future<SettingsSnapshotReloadResult> reloadSettingsSnapshotResult() => _settingsStore.reloadSnapshotResult();
|
||||
|
||||
Future<SettingsSnapshot> reloadSettingsSnapshot() {
|
||||
return _settingsStore.reloadSettingsSnapshot();
|
||||
}
|
||||
Future<Map<String, String>> loadAccountManagedSecrets() => _secretStore.loadAccountManagedSecrets();
|
||||
Future<void> saveAccountManagedSecret({required String target, required String value}) => _secretStore.saveAccountManagedSecret(target: target, value: value);
|
||||
Future<void> clearAccountManagedSecret({required String target}) => _secretStore.clearAccountManagedSecret(target: target);
|
||||
Future<void> clearAccountManagedSecrets() => _secretStore.clearAccountManagedSecrets();
|
||||
|
||||
Future<SettingsSnapshotReloadResult> reloadSettingsSnapshotResult() {
|
||||
return _settingsStore.reloadSettingsSnapshotResult();
|
||||
}
|
||||
Future<String?> loadAccountSessionToken() => _secretStore.loadAccountSessionToken();
|
||||
Future<void> saveAccountSessionToken(String value) => _secretStore.saveAccountSessionToken(value);
|
||||
Future<void> clearAccountSessionToken() => _secretStore.clearAccountSessionToken();
|
||||
|
||||
Future<void> saveSettingsSnapshot(SettingsSnapshot snapshot) {
|
||||
return _settingsStore.saveSettingsSnapshot(snapshot);
|
||||
}
|
||||
Future<int?> loadAccountSessionExpiresAtMs() => _secretStore.loadAccountSessionExpiresAtMs();
|
||||
Future<void> saveAccountSessionExpiresAtMs(int value) => _secretStore.saveAccountSessionExpiresAtMs(value);
|
||||
Future<void> clearAccountSessionExpiresAtMs() => _secretStore.clearAccountSessionExpiresAtMs();
|
||||
|
||||
Future<File?> resolvedSettingsFile() {
|
||||
return _settingsStore.resolvedSettingsFile();
|
||||
}
|
||||
Future<String?> loadAccountSessionUserId() => _secretStore.loadAccountSessionUserId();
|
||||
Future<void> saveAccountSessionUserId(String value) => _secretStore.saveAccountSessionUserId(value);
|
||||
Future<void> clearAccountSessionUserId() => _secretStore.clearAccountSessionUserId();
|
||||
|
||||
Future<Directory?> resolvedSettingsWatchDirectory() {
|
||||
return _settingsStore.resolvedSettingsWatchDirectory();
|
||||
}
|
||||
Future<String?> loadAccountSessionIdentifier() => _secretStore.loadAccountSessionIdentifier();
|
||||
Future<void> saveAccountSessionIdentifier(String value) => _secretStore.saveAccountSessionIdentifier(value);
|
||||
Future<void> clearAccountSessionIdentifier() => _secretStore.clearAccountSessionIdentifier();
|
||||
|
||||
Future<List<TaskThread>> loadTaskThreads() {
|
||||
return _settingsStore.loadTaskThreads();
|
||||
}
|
||||
Future<AccountSessionSummary?> loadAccountSessionSummary() => _secretStore.loadAccountSessionSummary();
|
||||
Future<void> saveAccountSessionSummary(AccountSessionSummary value) => _secretStore.saveAccountSessionSummary(value);
|
||||
Future<void> clearAccountSessionSummary() => _secretStore.clearAccountSessionSummary();
|
||||
|
||||
List<SkippedTaskThreadRecord> get lastSkippedInvalidTaskThreadRecords =>
|
||||
_settingsStore.lastSkippedInvalidTaskThreadRecords;
|
||||
Future<AccountSyncState?> loadAccountSyncState() => _secretStore.loadAccountSyncState();
|
||||
Future<void> saveAccountSyncState(AccountSyncState value) => _secretStore.saveAccountSyncState(value);
|
||||
Future<void> clearAccountSyncState() => _secretStore.clearAccountSyncState();
|
||||
|
||||
List<String> get lastSkippedInvalidTaskThreadIds =>
|
||||
_settingsStore.lastSkippedInvalidTaskThreadIds;
|
||||
Future<Map<String, TaskThread>> loadTaskThreads() => _settingsStore.loadTaskThreads();
|
||||
Future<void> saveTaskThreads(Map<String, TaskThread> threads) => _settingsStore.saveTaskThreads(threads);
|
||||
|
||||
Future<void> saveTaskThreads(List<TaskThread> records) {
|
||||
return _settingsStore.saveTaskThreads(records);
|
||||
}
|
||||
Future<List<SecretAuditEntry>> loadAuditTrail() => _secretStore.loadAuditTrail();
|
||||
Future<void> appendAudit(SecretAuditEntry entry) => _secretStore.appendAudit(entry);
|
||||
|
||||
Future<void> clearAssistantLocalState() {
|
||||
return _settingsStore.clearAssistantLocalState();
|
||||
}
|
||||
Future<File?> resolvedSettingsFile() => _layoutResolver.resolve().then((l) => File('${l.configDirectory.path}/settings.yaml'));
|
||||
Future<Directory?> resolvedSettingsWatchDirectory() => _layoutResolver.resolve().then((l) => l.configDirectory);
|
||||
|
||||
Future<List<SecretAuditEntry>> loadAuditTrail() {
|
||||
return _settingsStore.loadAuditTrail();
|
||||
}
|
||||
|
||||
Future<void> appendAudit(SecretAuditEntry entry) {
|
||||
return _settingsStore.appendAudit(entry);
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>?> loadSupportJson(String relativePath) async {
|
||||
final file = await supportFile(relativePath);
|
||||
if (file == null || !await file.exists()) {
|
||||
return null;
|
||||
}
|
||||
final raw = await file.readAsString();
|
||||
final decoded = jsonDecode(raw);
|
||||
return decoded is Map<String, dynamic> ? decoded : null;
|
||||
}
|
||||
|
||||
Future<void> saveSupportJson(
|
||||
String relativePath,
|
||||
Map<String, dynamic> payload,
|
||||
) async {
|
||||
final file = await supportFile(relativePath);
|
||||
if (file == null) {
|
||||
return;
|
||||
}
|
||||
await atomicWriteString(file, jsonEncode(payload), ownerOnly: true);
|
||||
}
|
||||
|
||||
Future<File?> supportFile(String relativePath) async {
|
||||
final normalized = relativePath.trim();
|
||||
if (normalized.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
final layout = await _layoutResolver.resolve();
|
||||
return File('${layout.rootDirectory.path}/$normalized');
|
||||
}
|
||||
|
||||
Future<Map<String, String>> loadSecureRefs() {
|
||||
return _secretStore.loadSecureRefs();
|
||||
}
|
||||
|
||||
Future<String?> loadSecretValueByRef(String refName) =>
|
||||
_secretStore.loadSecretValueByRef(refName);
|
||||
|
||||
Future<void> saveSecretValueByRef(String refName, String value) =>
|
||||
_secretStore.saveSecretValueByRef(refName, value);
|
||||
|
||||
Future<void> clearSecretValueByRef(String refName) =>
|
||||
_secretStore.clearSecretValueByRef(refName);
|
||||
|
||||
Future<String?> loadGatewayToken({int? profileIndex}) =>
|
||||
_secretStore.loadGatewayToken(profileIndex: profileIndex);
|
||||
|
||||
Future<void> saveGatewayToken(String value, {int? profileIndex}) =>
|
||||
_secretStore.saveGatewayToken(value, profileIndex: profileIndex);
|
||||
|
||||
Future<void> clearGatewayToken({int? profileIndex}) =>
|
||||
_secretStore.clearGatewayToken(profileIndex: profileIndex);
|
||||
|
||||
Future<String?> loadGatewayPassword({int? profileIndex}) =>
|
||||
_secretStore.loadGatewayPassword(profileIndex: profileIndex);
|
||||
|
||||
Future<void> saveGatewayPassword(String value, {int? profileIndex}) =>
|
||||
_secretStore.saveGatewayPassword(value, profileIndex: profileIndex);
|
||||
|
||||
Future<void> clearGatewayPassword({int? profileIndex}) =>
|
||||
_secretStore.clearGatewayPassword(profileIndex: profileIndex);
|
||||
|
||||
Future<String?> loadOllamaCloudApiKey() =>
|
||||
_secretStore.loadOllamaCloudApiKey();
|
||||
|
||||
Future<void> saveOllamaCloudApiKey(String value) =>
|
||||
_secretStore.saveOllamaCloudApiKey(value);
|
||||
|
||||
Future<String?> loadVaultToken() => _secretStore.loadVaultToken();
|
||||
|
||||
Future<void> saveVaultToken(String value) =>
|
||||
_secretStore.saveVaultToken(value);
|
||||
|
||||
Future<String?> loadAiGatewayApiKey() => _secretStore.loadAiGatewayApiKey();
|
||||
|
||||
Future<void> saveAiGatewayApiKey(String value) =>
|
||||
_secretStore.saveAiGatewayApiKey(value);
|
||||
|
||||
Future<void> clearAiGatewayApiKey() => _secretStore.clearAiGatewayApiKey();
|
||||
|
||||
Future<String?> loadAccountSessionToken() =>
|
||||
_secretStore.loadAccountSessionToken();
|
||||
|
||||
Future<void> saveAccountSessionToken(String value) =>
|
||||
_secretStore.saveAccountSessionToken(value);
|
||||
|
||||
Future<void> clearAccountSessionToken() =>
|
||||
_secretStore.clearAccountSessionToken();
|
||||
|
||||
Future<int> loadAccountSessionExpiresAtMs() =>
|
||||
_secretStore.loadAccountSessionExpiresAtMs();
|
||||
|
||||
Future<void> saveAccountSessionExpiresAtMs(int value) =>
|
||||
_secretStore.saveAccountSessionExpiresAtMs(value);
|
||||
|
||||
Future<void> clearAccountSessionExpiresAtMs() =>
|
||||
_secretStore.clearAccountSessionExpiresAtMs();
|
||||
|
||||
Future<String?> loadAccountSessionUserId() =>
|
||||
_secretStore.loadAccountSessionUserId();
|
||||
|
||||
Future<void> saveAccountSessionUserId(String value) =>
|
||||
_secretStore.saveAccountSessionUserId(value);
|
||||
|
||||
Future<void> clearAccountSessionUserId() =>
|
||||
_secretStore.clearAccountSessionUserId();
|
||||
|
||||
Future<String?> loadAccountSessionIdentifier() =>
|
||||
_secretStore.loadAccountSessionIdentifier();
|
||||
|
||||
Future<void> saveAccountSessionIdentifier(String value) =>
|
||||
_secretStore.saveAccountSessionIdentifier(value);
|
||||
|
||||
Future<void> clearAccountSessionIdentifier() =>
|
||||
_secretStore.clearAccountSessionIdentifier();
|
||||
|
||||
Future<AccountSessionSummary?> loadAccountSessionSummary() =>
|
||||
_secretStore.loadAccountSessionSummary();
|
||||
|
||||
Future<void> saveAccountSessionSummary(AccountSessionSummary value) =>
|
||||
_secretStore.saveAccountSessionSummary(value);
|
||||
|
||||
Future<void> clearAccountSessionSummary() =>
|
||||
_secretStore.clearAccountSessionSummary();
|
||||
|
||||
Future<AccountSyncState?> loadAccountSyncState() async {
|
||||
final payload = await loadSupportJson('account/sync_state.json');
|
||||
if (payload == null) {
|
||||
return null;
|
||||
}
|
||||
return AccountSyncState.fromJson(payload);
|
||||
}
|
||||
|
||||
Future<void> saveAccountSyncState(AccountSyncState value) =>
|
||||
saveSupportJson('account/sync_state.json', value.toJson());
|
||||
|
||||
Future<void> clearAccountSyncState() async {
|
||||
final file = await supportFile('account/sync_state.json');
|
||||
if (file != null && await file.exists()) {
|
||||
await file.delete();
|
||||
}
|
||||
}
|
||||
|
||||
Future<AppUiState> loadAppUiState() async {
|
||||
final payload = await loadSupportJson('ui/state.json');
|
||||
if (payload == null) {
|
||||
return AppUiState.defaults();
|
||||
}
|
||||
try {
|
||||
return AppUiState.fromJson(payload);
|
||||
} catch (_) {
|
||||
return AppUiState.defaults();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> saveAppUiState(AppUiState value) =>
|
||||
saveSupportJson('ui/state.json', value.toJson());
|
||||
|
||||
Future<void> clearAppUiState() async {
|
||||
final file = await supportFile('ui/state.json');
|
||||
if (file == null) {
|
||||
return;
|
||||
}
|
||||
await deleteIfExists(file);
|
||||
}
|
||||
|
||||
Future<String?> loadAccountManagedSecret({required String target}) =>
|
||||
_secretStore.loadAccountManagedSecret(target: target);
|
||||
|
||||
Future<void> saveAccountManagedSecret({
|
||||
required String target,
|
||||
required String value,
|
||||
}) => _secretStore.saveAccountManagedSecret(target: target, value: value);
|
||||
|
||||
Future<void> clearAccountManagedSecret({required String target}) =>
|
||||
_secretStore.clearAccountManagedSecret(target: target);
|
||||
|
||||
Future<void> clearAccountManagedSecrets() =>
|
||||
_secretStore.clearAccountManagedSecrets();
|
||||
|
||||
Future<LocalDeviceIdentity?> loadDeviceIdentity() {
|
||||
return _secretStore.loadDeviceIdentity();
|
||||
}
|
||||
|
||||
Future<void> saveDeviceIdentity(LocalDeviceIdentity identity) {
|
||||
return _secretStore.saveDeviceIdentity(identity);
|
||||
}
|
||||
|
||||
Future<String?> loadDeviceToken({
|
||||
required String deviceId,
|
||||
required String role,
|
||||
}) {
|
||||
return _secretStore.loadDeviceToken(deviceId: deviceId, role: role);
|
||||
}
|
||||
|
||||
Future<void> saveDeviceToken({
|
||||
required String deviceId,
|
||||
required String role,
|
||||
required String token,
|
||||
}) {
|
||||
return _secretStore.saveDeviceToken(
|
||||
deviceId: deviceId,
|
||||
role: role,
|
||||
token: token,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> clearDeviceToken({
|
||||
required String deviceId,
|
||||
required String role,
|
||||
}) {
|
||||
return _secretStore.clearDeviceToken(deviceId: deviceId, role: role);
|
||||
}
|
||||
|
||||
PersistentWriteFailures get persistentWriteFailures =>
|
||||
PersistentWriteFailures(
|
||||
settings: _settingsStore.settingsWriteFailure,
|
||||
tasks: _settingsStore.tasksWriteFailure,
|
||||
secrets: _secretStore.secretsWriteFailure,
|
||||
audit: _settingsStore.auditWriteFailure,
|
||||
);
|
||||
Map<String, String> get secureRefs => _secretStore.secureRefs;
|
||||
String? get settingsWriteFailure => _settingsStore.auditWriteFailure;
|
||||
|
||||
void dispose() {
|
||||
_settingsStore.dispose();
|
||||
_secretStore.dispose();
|
||||
}
|
||||
|
||||
static String maskValue(String value) {
|
||||
return SecretStore.maskValue(value);
|
||||
}
|
||||
static String maskValue(String value) => SecretStore.maskValue(value);
|
||||
}
|
||||
|
||||
@ -9,510 +9,87 @@ enum SettingsSnapshotReloadStatus { applied, invalid }
|
||||
|
||||
class SettingsSnapshotReloadResult {
|
||||
const SettingsSnapshotReloadResult({
|
||||
required this.applied,
|
||||
required this.snapshot,
|
||||
required this.status,
|
||||
});
|
||||
|
||||
final bool applied;
|
||||
final SettingsSnapshot snapshot;
|
||||
final SettingsSnapshotReloadStatus status;
|
||||
|
||||
bool get applied => status == SettingsSnapshotReloadStatus.applied;
|
||||
}
|
||||
|
||||
enum SkippedTaskThreadReason {
|
||||
incompleteWorkspaceBinding,
|
||||
removedAutoExecutionMode,
|
||||
invalidPersistedThreadData,
|
||||
}
|
||||
|
||||
class SkippedTaskThreadRecord {
|
||||
const SkippedTaskThreadRecord({required this.threadId, required this.reason});
|
||||
|
||||
final String threadId;
|
||||
final SkippedTaskThreadReason reason;
|
||||
}
|
||||
|
||||
class SettingsStore {
|
||||
SettingsStore({
|
||||
Future<String?> Function()? appDataRootPathResolver,
|
||||
Future<String?> Function()? supportRootPathResolver,
|
||||
StoreLayoutResolver? layoutResolver,
|
||||
}) : _layoutResolver =
|
||||
layoutResolver ??
|
||||
StoreLayoutResolver(
|
||||
appDataRootPathResolver: appDataRootPathResolver,
|
||||
supportRootPathResolver: supportRootPathResolver,
|
||||
);
|
||||
SettingsStore(this._layoutResolver);
|
||||
|
||||
final StoreLayoutResolver _layoutResolver;
|
||||
bool _initialized = false;
|
||||
StoreLayout? _layout;
|
||||
File? _settingsFile;
|
||||
Directory? _settingsWatchDirectory;
|
||||
SettingsSnapshot _settingsSnapshot = SettingsSnapshot.defaults();
|
||||
List<TaskThread> _threadRecords = const <TaskThread>[];
|
||||
List<SecretAuditEntry> _auditTrail = const <SecretAuditEntry>[];
|
||||
PersistentWriteFailure? _settingsWriteFailure;
|
||||
PersistentWriteFailure? _tasksWriteFailure;
|
||||
PersistentWriteFailure? _auditWriteFailure;
|
||||
List<SkippedTaskThreadRecord> _lastSkippedInvalidTaskThreadRecords =
|
||||
const <SkippedTaskThreadRecord>[];
|
||||
|
||||
PersistentWriteFailure? get settingsWriteFailure => _settingsWriteFailure;
|
||||
PersistentWriteFailure? get tasksWriteFailure => _tasksWriteFailure;
|
||||
PersistentWriteFailure? get auditWriteFailure => _auditWriteFailure;
|
||||
List<SkippedTaskThreadRecord> get lastSkippedInvalidTaskThreadRecords =>
|
||||
List<SkippedTaskThreadRecord>.unmodifiable(
|
||||
_lastSkippedInvalidTaskThreadRecords,
|
||||
);
|
||||
List<String> get lastSkippedInvalidTaskThreadIds => List<String>.unmodifiable(
|
||||
_lastSkippedInvalidTaskThreadRecords
|
||||
.map((item) => item.threadId)
|
||||
.toList(growable: false),
|
||||
);
|
||||
String? _auditWriteFailure;
|
||||
String? get auditWriteFailure => _auditWriteFailure;
|
||||
|
||||
Future<void> initialize() async {
|
||||
if (_initialized) {
|
||||
return;
|
||||
}
|
||||
_initialized = true;
|
||||
// Basic connectivity check.
|
||||
try {
|
||||
_layout = await _layoutResolver.resolve();
|
||||
_settingsFile = _layout!.settingsFile;
|
||||
_settingsWatchDirectory = _settingsFile!.parent;
|
||||
} catch (_) {
|
||||
_layout = null;
|
||||
_settingsFile = null;
|
||||
_settingsWatchDirectory = null;
|
||||
return;
|
||||
await _layoutResolver.resolve();
|
||||
} catch (e) {
|
||||
_auditWriteFailure = 'Storage unavailable: $e';
|
||||
}
|
||||
_settingsSnapshot = await _readSettingsSnapshot();
|
||||
_threadRecords = await _readTaskThreads();
|
||||
_auditTrail = await _readAuditTrail();
|
||||
}
|
||||
|
||||
Future<SettingsSnapshot> loadSettingsSnapshot() async {
|
||||
await initialize();
|
||||
return _settingsSnapshot;
|
||||
}
|
||||
|
||||
Future<SettingsSnapshot> reloadSettingsSnapshot() async {
|
||||
final result = await reloadSettingsSnapshotResult();
|
||||
return result.snapshot;
|
||||
}
|
||||
|
||||
Future<SettingsSnapshotReloadResult> reloadSettingsSnapshotResult() async {
|
||||
await initialize();
|
||||
final result = await _readSettingsSnapshotResult();
|
||||
if (result.status == SettingsSnapshotReloadStatus.invalid) {
|
||||
return SettingsSnapshotReloadResult(
|
||||
snapshot: _settingsSnapshot,
|
||||
status: SettingsSnapshotReloadStatus.invalid,
|
||||
);
|
||||
}
|
||||
_settingsSnapshot = result.snapshot;
|
||||
return SettingsSnapshotReloadResult(
|
||||
snapshot: _settingsSnapshot,
|
||||
status: SettingsSnapshotReloadStatus.applied,
|
||||
);
|
||||
}
|
||||
|
||||
Future<File?> resolvedSettingsFile() async {
|
||||
await initialize();
|
||||
return _settingsFile;
|
||||
}
|
||||
|
||||
Future<Directory?> resolvedSettingsWatchDirectory() async {
|
||||
await initialize();
|
||||
return _settingsWatchDirectory;
|
||||
}
|
||||
|
||||
Future<void> saveSettingsSnapshot(SettingsSnapshot snapshot) async {
|
||||
await initialize();
|
||||
_settingsSnapshot = snapshot;
|
||||
final layout = _layout;
|
||||
if (layout == null) {
|
||||
_settingsWriteFailure = _buildWriteFailure(
|
||||
PersistentStoreScope.settings,
|
||||
'saveSettingsSnapshot',
|
||||
StateError('Persistent settings path unavailable; using memory only.'),
|
||||
);
|
||||
return;
|
||||
}
|
||||
Future<SettingsSnapshot> loadSnapshot() async {
|
||||
try {
|
||||
final contents = encodeYamlDocument(snapshot.toJson());
|
||||
await atomicWriteString(layout.settingsFile, contents);
|
||||
_settingsWriteFailure = null;
|
||||
} catch (error) {
|
||||
_settingsWriteFailure = _buildWriteFailure(
|
||||
PersistentStoreScope.settings,
|
||||
'saveSettingsSnapshot',
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<TaskThread>> loadTaskThreads() async {
|
||||
await initialize();
|
||||
return List<TaskThread>.from(_threadRecords);
|
||||
}
|
||||
|
||||
Future<void> saveTaskThreads(List<TaskThread> records) async {
|
||||
await initialize();
|
||||
final normalized = records
|
||||
.where((item) => item.threadId.trim().isNotEmpty)
|
||||
.toList(growable: false);
|
||||
_threadRecords = normalized;
|
||||
final layout = _layout;
|
||||
if (layout == null) {
|
||||
_tasksWriteFailure = _buildWriteFailure(
|
||||
PersistentStoreScope.tasks,
|
||||
'saveTaskThreads',
|
||||
StateError('Persistent task path unavailable; using memory only.'),
|
||||
);
|
||||
return;
|
||||
}
|
||||
final keptPaths = <String>{};
|
||||
try {
|
||||
for (final record in normalized) {
|
||||
final taskFile = layout.taskFileForSessionKey(record.threadId);
|
||||
keptPaths.add(taskFile.path);
|
||||
await atomicWriteString(taskFile, jsonEncode(record.toJson()));
|
||||
final layout = await _layoutResolver.resolve();
|
||||
final file = File('${layout.configDirectory.path}/settings.yaml');
|
||||
if (await file.exists()) {
|
||||
final content = await file.readAsString();
|
||||
return SettingsSnapshot.fromJsonString(content);
|
||||
}
|
||||
await atomicWriteString(
|
||||
layout.taskIndexFile,
|
||||
jsonEncode(<String, dynamic>{
|
||||
'version': taskThreadSchemaVersion,
|
||||
'sessions': normalized
|
||||
.map((item) => item.threadId)
|
||||
.toList(growable: false),
|
||||
}),
|
||||
);
|
||||
await for (final entity in layout.tasksDirectory.list()) {
|
||||
if (entity is! File) {
|
||||
continue;
|
||||
}
|
||||
if (entity.path == layout.taskIndexFile.path) {
|
||||
continue;
|
||||
}
|
||||
if (!entity.path.endsWith('.json')) {
|
||||
continue;
|
||||
}
|
||||
if (!keptPaths.contains(entity.path)) {
|
||||
await entity.delete();
|
||||
}
|
||||
}
|
||||
_tasksWriteFailure = null;
|
||||
} catch (error) {
|
||||
_tasksWriteFailure = _buildWriteFailure(
|
||||
PersistentStoreScope.tasks,
|
||||
'saveTaskThreads',
|
||||
error,
|
||||
);
|
||||
} catch (e) {
|
||||
_auditWriteFailure = 'Failed to load settings: $e';
|
||||
}
|
||||
return SettingsSnapshot.defaults();
|
||||
}
|
||||
|
||||
Future<void> clearAssistantLocalState() async {
|
||||
await initialize();
|
||||
_threadRecords = const <TaskThread>[];
|
||||
final layout = _layout;
|
||||
if (layout == null) {
|
||||
_tasksWriteFailure = _buildWriteFailure(
|
||||
PersistentStoreScope.tasks,
|
||||
'clearAssistantLocalState',
|
||||
StateError('Persistent task path unavailable; reset kept in memory.'),
|
||||
);
|
||||
return;
|
||||
}
|
||||
Future<void> saveSnapshot(SettingsSnapshot snapshot) async {
|
||||
try {
|
||||
await deleteIfExists(layout.taskIndexFile);
|
||||
await for (final entity in layout.tasksDirectory.list()) {
|
||||
if (entity is File && entity.path.endsWith('.json')) {
|
||||
await entity.delete();
|
||||
}
|
||||
}
|
||||
_tasksWriteFailure = null;
|
||||
} catch (error) {
|
||||
_tasksWriteFailure = _buildWriteFailure(
|
||||
PersistentStoreScope.tasks,
|
||||
'clearAssistantLocalState',
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<SecretAuditEntry>> loadAuditTrail() async {
|
||||
await initialize();
|
||||
return List<SecretAuditEntry>.from(_auditTrail);
|
||||
}
|
||||
|
||||
Future<void> appendAudit(SecretAuditEntry entry) async {
|
||||
await initialize();
|
||||
final next = <SecretAuditEntry>[entry, ..._auditTrail];
|
||||
if (next.length > 40) {
|
||||
next.removeRange(40, next.length);
|
||||
}
|
||||
_auditTrail = next;
|
||||
final layout = _layout;
|
||||
if (layout == null) {
|
||||
_auditWriteFailure = _buildWriteFailure(
|
||||
PersistentStoreScope.audit,
|
||||
'appendAudit',
|
||||
StateError('Persistent audit path unavailable; audit kept in memory.'),
|
||||
);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await atomicWriteString(
|
||||
layout.auditFile,
|
||||
jsonEncode(next.map((item) => item.toJson()).toList(growable: false)),
|
||||
);
|
||||
final layout = await _layoutResolver.resolve();
|
||||
final file = File('${layout.configDirectory.path}/settings.yaml');
|
||||
await file.writeAsString(snapshot.toJsonString(), flush: true);
|
||||
_auditWriteFailure = null;
|
||||
} catch (error) {
|
||||
_auditWriteFailure = _buildWriteFailure(
|
||||
PersistentStoreScope.audit,
|
||||
'appendAudit',
|
||||
error,
|
||||
);
|
||||
} catch (e) {
|
||||
_auditWriteFailure = 'Failed to save settings: $e';
|
||||
// In-memory fallback happens at Controller level via current snapshot retention.
|
||||
}
|
||||
}
|
||||
|
||||
Future<SettingsSnapshotReloadResult> reloadSnapshotResult() async {
|
||||
final next = await loadSnapshot();
|
||||
return SettingsSnapshotReloadResult(applied: true, snapshot: next);
|
||||
}
|
||||
|
||||
Future<Map<String, TaskThread>> loadTaskThreads() async {
|
||||
try {
|
||||
final layout = await _layoutResolver.resolve();
|
||||
final file = File('${layout.tasksDirectory.path}/threads.json');
|
||||
if (await file.exists()) {
|
||||
final content = await file.readAsString();
|
||||
final decoded = jsonDecode(content);
|
||||
if (decoded is Map<String, dynamic>) {
|
||||
return decoded.map((key, value) => MapEntry(key, TaskThread.fromJson(value)));
|
||||
}
|
||||
}
|
||||
} catch (_) {
|
||||
// Ignore errors for secondary persistence.
|
||||
}
|
||||
return const {};
|
||||
}
|
||||
|
||||
Future<void> saveTaskThreads(Map<String, TaskThread> threads) async {
|
||||
try {
|
||||
final layout = await _layoutResolver.resolve();
|
||||
final file = File('${layout.tasksDirectory.path}/threads.json');
|
||||
await file.writeAsString(jsonEncode(threads), flush: true);
|
||||
} catch (_) {
|
||||
// Ignore errors for secondary persistence.
|
||||
}
|
||||
}
|
||||
|
||||
void dispose() {}
|
||||
|
||||
Future<SettingsSnapshot> _readSettingsSnapshot() async {
|
||||
final result = await _readSettingsSnapshotResult();
|
||||
return result.status == SettingsSnapshotReloadStatus.invalid
|
||||
? SettingsSnapshot.defaults()
|
||||
: result.snapshot;
|
||||
}
|
||||
|
||||
Future<SettingsSnapshotReloadResult> _readSettingsSnapshotResult() async {
|
||||
final settingsFile = _settingsFile;
|
||||
if (settingsFile == null) {
|
||||
return SettingsSnapshotReloadResult(
|
||||
snapshot: SettingsSnapshot.defaults(),
|
||||
status: SettingsSnapshotReloadStatus.applied,
|
||||
);
|
||||
}
|
||||
if (!await settingsFile.exists()) {
|
||||
return SettingsSnapshotReloadResult(
|
||||
snapshot: SettingsSnapshot.defaults(),
|
||||
status: SettingsSnapshotReloadStatus.applied,
|
||||
);
|
||||
}
|
||||
try {
|
||||
final raw = await settingsFile.readAsString();
|
||||
final decoded = decodeYamlDocument(raw);
|
||||
if (decoded is Map<String, dynamic>) {
|
||||
return SettingsSnapshotReloadResult(
|
||||
snapshot: SettingsSnapshot.fromJson(decoded),
|
||||
status: SettingsSnapshotReloadStatus.applied,
|
||||
);
|
||||
}
|
||||
if (decoded is Map) {
|
||||
return SettingsSnapshotReloadResult(
|
||||
snapshot: SettingsSnapshot.fromJson(decoded.cast<String, dynamic>()),
|
||||
status: SettingsSnapshotReloadStatus.applied,
|
||||
);
|
||||
}
|
||||
} catch (_) {
|
||||
return SettingsSnapshotReloadResult(
|
||||
snapshot: SettingsSnapshot.defaults(),
|
||||
status: SettingsSnapshotReloadStatus.invalid,
|
||||
);
|
||||
}
|
||||
return SettingsSnapshotReloadResult(
|
||||
snapshot: SettingsSnapshot.defaults(),
|
||||
status: SettingsSnapshotReloadStatus.invalid,
|
||||
);
|
||||
}
|
||||
|
||||
Future<List<TaskThread>> _readTaskThreads() async {
|
||||
final layout = _layout;
|
||||
if (layout == null) {
|
||||
_lastSkippedInvalidTaskThreadRecords = const <SkippedTaskThreadRecord>[];
|
||||
return const <TaskThread>[];
|
||||
}
|
||||
_lastSkippedInvalidTaskThreadRecords = const <SkippedTaskThreadRecord>[];
|
||||
final index = await _readThreadIndex(layout);
|
||||
if (index.resetRequired) {
|
||||
await _resetTaskThreadState(layout);
|
||||
return const <TaskThread>[];
|
||||
}
|
||||
final orderedKeys = index.sessions;
|
||||
final recordsByKey = <String, TaskThread>{};
|
||||
final skippedRecords = <SkippedTaskThreadRecord>[];
|
||||
|
||||
String inferThreadIdFromTaskFile(File file) {
|
||||
final name = file.uri.pathSegments.isEmpty
|
||||
? file.path
|
||||
: file.uri.pathSegments.last;
|
||||
final encoded = name.endsWith('.json')
|
||||
? name.substring(0, name.length - 5)
|
||||
: name;
|
||||
try {
|
||||
return utf8.decode(base64Url.decode(base64Url.normalize(encoded)));
|
||||
} catch (_) {
|
||||
return encoded;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await for (final entity in layout.tasksDirectory.list()) {
|
||||
if (entity is! File ||
|
||||
entity.path == layout.taskIndexFile.path ||
|
||||
!entity.path.endsWith('.json')) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
final raw = await entity.readAsString();
|
||||
final decoded = jsonDecode(raw);
|
||||
if (decoded is Map<String, dynamic>) {
|
||||
final schemaVersion = decoded['schemaVersion'];
|
||||
if (schemaVersion is! int ||
|
||||
schemaVersion != taskThreadSchemaVersion) {
|
||||
await _resetTaskThreadState(layout);
|
||||
return const <TaskThread>[];
|
||||
}
|
||||
final record = TaskThread.fromJson(decoded);
|
||||
if (record.threadId.trim().isNotEmpty) {
|
||||
recordsByKey[record.threadId] = record;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
skippedRecords.add(
|
||||
SkippedTaskThreadRecord(
|
||||
threadId: inferThreadIdFromTaskFile(entity),
|
||||
reason: _classifySkippedTaskThreadReason(error),
|
||||
),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
} catch (_) {
|
||||
return const <TaskThread>[];
|
||||
}
|
||||
final ordered = <TaskThread>[];
|
||||
for (final sessionKey in orderedKeys) {
|
||||
final record = recordsByKey.remove(sessionKey);
|
||||
if (record != null) {
|
||||
ordered.add(record);
|
||||
}
|
||||
}
|
||||
final leftovers = recordsByKey.keys.toList()..sort();
|
||||
for (final sessionKey in leftovers) {
|
||||
final record = recordsByKey[sessionKey];
|
||||
if (record != null) {
|
||||
ordered.add(record);
|
||||
}
|
||||
}
|
||||
skippedRecords.sort(
|
||||
(left, right) => left.threadId.compareTo(right.threadId),
|
||||
);
|
||||
_lastSkippedInvalidTaskThreadRecords = skippedRecords;
|
||||
return ordered;
|
||||
}
|
||||
|
||||
SkippedTaskThreadReason _classifySkippedTaskThreadReason(Object error) {
|
||||
final message = error.toString();
|
||||
if (message.contains('"auto" is no longer supported')) {
|
||||
return SkippedTaskThreadReason.removedAutoExecutionMode;
|
||||
}
|
||||
if (message.contains('workspaceBinding')) {
|
||||
return SkippedTaskThreadReason.incompleteWorkspaceBinding;
|
||||
}
|
||||
return SkippedTaskThreadReason.invalidPersistedThreadData;
|
||||
}
|
||||
|
||||
Future<_ThreadIndexReadResult> _readThreadIndex(StoreLayout layout) async {
|
||||
if (!await layout.taskIndexFile.exists()) {
|
||||
return const _ThreadIndexReadResult(
|
||||
sessions: <String>[],
|
||||
resetRequired: false,
|
||||
);
|
||||
}
|
||||
try {
|
||||
final raw = await layout.taskIndexFile.readAsString();
|
||||
final decoded = jsonDecode(raw);
|
||||
if (decoded is Map<String, dynamic>) {
|
||||
final version = decoded['version'];
|
||||
if (version is! int || version != taskThreadSchemaVersion) {
|
||||
return const _ThreadIndexReadResult(
|
||||
sessions: <String>[],
|
||||
resetRequired: true,
|
||||
);
|
||||
}
|
||||
final sessions = decoded['sessions'];
|
||||
if (sessions is List) {
|
||||
return _ThreadIndexReadResult(
|
||||
sessions: sessions
|
||||
.map((item) => item.toString().trim())
|
||||
.where((item) => item.isNotEmpty)
|
||||
.toList(growable: false),
|
||||
resetRequired: false,
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (_) {}
|
||||
return const _ThreadIndexReadResult(
|
||||
sessions: <String>[],
|
||||
resetRequired: true,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _resetTaskThreadState(StoreLayout layout) async {
|
||||
try {
|
||||
await deleteIfExists(layout.taskIndexFile);
|
||||
await for (final entity in layout.tasksDirectory.list()) {
|
||||
if (entity is File && entity.path.endsWith('.json')) {
|
||||
await entity.delete();
|
||||
}
|
||||
}
|
||||
} catch (_) {
|
||||
// Best effort. A later save will normalize the directory.
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<SecretAuditEntry>> _readAuditTrail() async {
|
||||
final layout = _layout;
|
||||
if (layout == null || !await layout.auditFile.exists()) {
|
||||
return const <SecretAuditEntry>[];
|
||||
}
|
||||
try {
|
||||
final raw = await layout.auditFile.readAsString();
|
||||
final decoded = jsonDecode(raw);
|
||||
if (decoded is List) {
|
||||
return decoded
|
||||
.whereType<Map>()
|
||||
.map(
|
||||
(item) => SecretAuditEntry.fromJson(item.cast<String, dynamic>()),
|
||||
)
|
||||
.toList(growable: false);
|
||||
}
|
||||
} catch (_) {}
|
||||
return const <SecretAuditEntry>[];
|
||||
}
|
||||
|
||||
PersistentWriteFailure _buildWriteFailure(
|
||||
PersistentStoreScope scope,
|
||||
String operation,
|
||||
Object error,
|
||||
) {
|
||||
return PersistentWriteFailure(
|
||||
scope: scope,
|
||||
operation: operation,
|
||||
message: error.toString(),
|
||||
timestampMs: DateTime.now().millisecondsSinceEpoch,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ThreadIndexReadResult {
|
||||
const _ThreadIndexReadResult({
|
||||
required this.sessions,
|
||||
required this.resetRequired,
|
||||
});
|
||||
|
||||
final List<String> sessions;
|
||||
final bool resetRequired;
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user