Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions src/OpenClaw.SetupEngine.UI/Pages/AiSetupPage.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,15 @@
<ScrollViewer x:Name="ChoicesScroller" Grid.Row="1" Margin="0,24,0,16" VerticalScrollBarVisibility="Auto">
<StackPanel Spacing="12">
<InfoBar x:Name="ErrorBar" IsOpen="False" IsClosable="False" Severity="Error" />
<StackPanel x:Name="LocalAiFailurePanel" Spacing="4" Visibility="Collapsed"
AutomationProperties.AutomationId="OnboardingLocalAiFailure">
<TextBlock x:Name="LocalAiDiagnosticsText" TextWrapping="Wrap" IsTextSelectionEnabled="True"
Visibility="Collapsed" FontSize="12" FontFamily="Cascadia Code,Consolas,monospace"
Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
<HyperlinkButton x:Name="OpenLocalAiLogsButton" Padding="0" Visibility="Collapsed"
Click="OpenLocalAiLogs_Click"
AutomationProperties.AutomationId="OnboardingOpenLocalAiLogs" />
</StackPanel>
<ItemsControl x:Name="SetupActivitySteps" Visibility="Collapsed"
AutomationProperties.AutomationId="OnboardingAiProgress">
<ItemsControl.ItemTemplate>
Expand Down
67 changes: 58 additions & 9 deletions src/OpenClaw.SetupEngine.UI/Pages/AiSetupPage.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using OpenClaw.SetupEngine.UI.Controls;
using OpenClaw.Shared;
using OpenClaw.Connection;
using OpenClaw.Connection.LocalAi;
using OpenClawTray.Helpers;

namespace OpenClaw.SetupEngine.UI.Pages;
Expand Down Expand Up @@ -72,6 +73,8 @@ private sealed record ProviderBackdrop(Control? FocusTarget, double VerticalOffs
private LocalAiOnboardingObservation? _localObservation;
private bool _localActionBusy;
private LocalAiOnboardingUse? _localUse;
private LocalAiFailureDetail? _localAiFailure;
private int _localAiFailureVersion;
private string? _localExpectedModel => _localUse?.Expected?.ModelRef;
private string? ExpectedGatewayId => _localUse?.Expected?.GatewayId ?? _args?.ExpectedGatewayId;
private readonly ProviderArtworkSession _artworkSession = new();
Expand All @@ -81,6 +84,7 @@ public AiSetupPage()
{
InitializeComponent();
TitleText.Text = S("Title.Text");
OpenLocalAiLogsButton.Content = S("OpenLocalAiLogs");
CandidatesHeading.Text = SetupLocalization.GetString("Onboarding_V4_DetectedAi.Text");
RecommendedHeading.Text = S("Recommended");
RecommendedDetail.Text = S("RecommendedDetail");
Expand Down Expand Up @@ -345,11 +349,8 @@ await GatewayAiSetupTransport.BorrowNativeAsync(_args.DataDir, manager,
await _localObservation!.RefreshAsync(CreateLocalProgress(ct));
TitleText.Text = S("Title.Text");
await DetectAsync(ct);
if (ex is LocalAiStartFailedException)
{
ErrorBar.Message = S("LocalStartFailed") + " " + ex.Message;
ErrorBar.IsOpen = true;
}
if (ex is LocalAiStartFailedException startFailed)
ShowLocalAiStartFailure(startFailed);
else
ShowError("LocalChanged");
return;
Expand Down Expand Up @@ -409,6 +410,7 @@ void ReportIssue(GatewayLogTailIssue issue) => DispatcherQueue.TryEnqueue(() =>
_nativeConsoleIssue = issue;
var message = SetupLocalization.GetString(issue == GatewayLogTailIssue.Skipped
? "Onboarding_Wizard_GatewayConsoleGap" : "Onboarding_Wizard_GatewayConsoleUnavailable");
ClearLocalAiFailure();
if (BackdropLocked) _providerError = message;
else
{
Expand Down Expand Up @@ -644,10 +646,7 @@ await RunAsync(async ct =>
if (_closed) return;
await _localObservation.RefreshAsync();
if (!_closed)
{
ErrorBar.Message = S("LocalStartFailed") + " " + ex.Message;
ErrorBar.IsOpen = true;
}
ShowLocalAiStartFailure(ex);
return;
}
finally { ++_progressScope; }
Expand Down Expand Up @@ -885,6 +884,7 @@ private async Task RunCoreAsync(Func<CancellationToken, Task> action, bool submi
_activityStatus = SetupInstallationStatus.Running;
// Uncertain is also used for recovery. Retain a prompt only for its own answer request.
_submittingAnswer = submittingAnswer;
ClearLocalAiFailure();
if (BackdropLocked) _providerError = null;
else ErrorBar.IsOpen = false;
Render();
Expand Down Expand Up @@ -1027,6 +1027,7 @@ private void Render()
LocalAiCard.IsClickEnabled = false;
LocalAiCard.IsEnabled = false;
}
LocalAiFailurePanel.Visibility = Visible(!freezeBackdrop && ErrorBar.IsOpen && _localAiFailure is not null);
var backgroundEnabled = !_busy && !_localActionBusy && !BackdropLocked;
CandidateChoices.IsEnabled = PrepareChoices.IsEnabled = FeaturedSignInChoices.IsEnabled =
MoreSignInChoices.IsEnabled = ApiProviderPicker.IsEnabled = ApiKeyInput.IsEnabled =
Expand Down Expand Up @@ -1217,6 +1218,7 @@ private bool CanConnectManual() =>
private static Visibility Visible(bool value) => value ? Visibility.Visible : Visibility.Collapsed;
private void ShowError(string key, string? detail = null)
{
ClearLocalAiFailure();
var message = S(key);
detail ??= key is "VerificationFailed" or "Rejected"
? Client?.VerificationFailure ?? Client?.Wizard?.Error : null;
Expand All @@ -1232,8 +1234,55 @@ private void ShowError(string key, string? detail = null)
ErrorBar.Message = message;
ErrorBar.IsOpen = true;
}
if (key is "VerificationFailed" or "Rejected" && _localUse?.Expected is not null)
CaptureLocalAiFailure();
Render();
}

private void ClearLocalAiFailure()
{
_localAiFailure = null;
++_localAiFailureVersion;
}

/// <summary>
/// llama-server reports model-load causes only in its own logs; the Gateway's verification
/// error does not carry them. Reads the bounded log tail off the request path and shows it
/// with a link to the folder, mirroring CompletePage.ShowServerDiagnostics.
/// </summary>
private void CaptureLocalAiFailure()
{
if (_args is null) return;
var version = ++_localAiFailureVersion;
var paths = new LocalAiPaths(_args.LocalDataDir);
AsyncEventHandlerGuard.Run(async () =>
{
var diagnostics = await LocalAiLogTail.ReadDiagnosticLinesAsync(paths, CancellationToken.None);
if (_closed || version != _localAiFailureVersion) return;
var directory = LogFileLauncher.ResolveRealPath(paths.LogsDirectory);
var hasDirectory = Directory.Exists(directory);
if (diagnostics.Count == 0 && !hasDirectory) return;
_localAiFailure = new LocalAiFailureDetail(diagnostics, hasDirectory ? directory : "");
LocalAiDiagnosticsText.Text = string.Join(Environment.NewLine,
diagnostics.Select(line => $"llama-server: {line}"));
LocalAiDiagnosticsText.Visibility = Visible(diagnostics.Count > 0);
OpenLocalAiLogsButton.Visibility = Visible(hasDirectory);
ToolTipService.SetToolTip(OpenLocalAiLogsButton, hasDirectory ? directory : null);
Render();
}, onError: ex => Trace.TraceWarning("Local AI diagnostics could not be read ({0}).", ex.GetType().Name));
}

private void ShowLocalAiStartFailure(LocalAiStartFailedException error)
{
ClearLocalAiFailure();
ErrorBar.Message = S("LocalStartFailed") + " " + error.Message;
ErrorBar.IsOpen = true;
CaptureLocalAiFailure();
}

private void OpenLocalAiLogs_Click(object sender, RoutedEventArgs e) =>
LogFileLauncher.RevealInExplorer(_localAiFailure?.LogDirectory);

private void ReportFailure(Exception error)
{
Trace.TraceWarning("AI setup request failed ({0}).", error.GetType().Name);
Expand Down
41 changes: 30 additions & 11 deletions src/OpenClaw.SetupEngine/LocalAiLogTail.cs
Original file line number Diff line number Diff line change
Expand Up @@ -72,34 +72,43 @@ internal static async Task<IReadOnlyList<string>> ReadDiagnosticLinesAsync(
}

/// <summary>
/// Keeps the last <paramref name="maximumLines"/> distinct lines that look like a failure, in
/// file order. llama.cpp reports the proximate cause (a CUDA kernel fault, a nonzero instance
/// exit status) close to the end of the log, so the tail is the interesting part.
/// Keeps at most <paramref name="maximumLines"/> distinct failure lines, in log order. Cause
/// lines (a CUDA fault, an allocation failure, a model-load error) take precedence over
/// consequence lines (a "failed to load" echo, a nonzero instance exit status), which fill
/// the remaining slots newest first. The input is the stdout tail followed by the stderr tail,
/// so the router's stderr consequences always sort last and would otherwise push a model
/// instance's cause out of a plain last-N window. Lines that differ only by the router's
/// "[pid]" instance prefix are one line, kept at its newest occurrence, so repeated
/// attempts of the same failing load do not fill every slot.
/// </summary>
internal static IReadOnlyList<string> ExtractDiagnosticLines(string log, int maximumLines = 4)
{
if (string.IsNullOrWhiteSpace(log))
return [];

var matches = new List<string>();
var seen = new HashSet<string>(StringComparer.Ordinal);
var newest = new Dictionary<string, (int Order, string Line, bool IsCause)>(StringComparer.Ordinal);
int order = 0;
foreach (string rawLine in log.Split('\n'))
{
string line = rawLine.Trim();
if (line.Length == 0 ||
RequestOrResponsePattern().IsMatch(line) ||
!DiagnosticPattern().IsMatch(line))
continue;
bool isCause = CausePattern().IsMatch(line);
line = NormalizeSingleLine(TokenSanitizer.SanitizeLogMessage(line));
if (line.Length > MaximumDiagnosticLineLength)
line = line[..MaximumDiagnosticLineLength];
if (seen.Add(line))
matches.Add(line);
newest[InstancePrefixPattern().Replace(line, "")] = (order++, line, isCause);
}

return matches.Count <= maximumLines
? matches
: matches[^maximumLines..];
var byRecency = newest.Values.OrderByDescending(match => match.Order).ToList();
var causes = byRecency.Where(match => match.IsCause).Take(maximumLines).ToList();
return causes
.Concat(byRecency.Where(match => !match.IsCause).Take(maximumLines - causes.Count))
.OrderBy(match => match.Order)
.Select(match => match.Line)
.ToList();
}

private static string NormalizeSingleLine(string value)
Expand All @@ -123,11 +132,21 @@ private static string NormalizeSingleLine(string value)
return builder.ToString();
}

/// <summary>Markers that name a failure's cause rather than a consequence of it.</summary>
private const string CauseMarkers =
"CUDA error|cudaError|failed to allocate|out of memory|error loading model|unable to load model";

[GeneratedRegex(
@"CUDA error|cudaError|exited with status|failed to load|failed to allocate|out of memory|error loading model|unable to load model",
CauseMarkers + "|exited with status|failed to load",
RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)]
private static partial Regex DiagnosticPattern();

[GeneratedRegex(CauseMarkers, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)]
private static partial Regex CausePattern();

[GeneratedRegex(@"^\[\d+\]\s*", RegexOptions.CultureInvariant)]
private static partial Regex InstancePrefixPattern();

[GeneratedRegex(
@"(?:\blog_server_[a-z_]*|\brequest|\bresponse)\s*:",
RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)]
Expand Down
1 change: 1 addition & 0 deletions src/OpenClaw.Tray.WinUI/Strings/en-us/Resources.resw
Original file line number Diff line number Diff line change
Expand Up @@ -7630,6 +7630,7 @@ Make sure the gateway is running.</value>
<data name="Onboarding_AiSetup_LocalInstalling" xml:space="preserve"><value>Setting up Local AI</value></data>
<data name="Onboarding_AiSetup_LocalChanged" xml:space="preserve"><value>The Gateway or Local AI readiness changed. Go back to Connect your AI and check again before installing.</value></data>
<data name="Onboarding_AiSetup_LocalStartFailed" xml:space="preserve"><value>Local AI could not start. Cleanup is complete. Repair Local AI or choose another provider.</value></data>
<data name="Onboarding_AiSetup_OpenLocalAiLogs" xml:space="preserve"><value>Open Local AI logs</value></data>
<data name="Onboarding_AiSetup_LocalState_Checking" xml:space="preserve"><value>Checking this PC. You can still choose an AI provider.</value></data>
<data name="Onboarding_AiSetup_LocalState_SetUp" xml:space="preserve"><value>Compatible PC. Review setup before installing.</value></data>
<data name="Onboarding_AiSetup_LocalState_StartAndUse" xml:space="preserve"><value>Installed, not running</value></data>
Expand Down
1 change: 1 addition & 0 deletions src/OpenClaw.Tray.WinUI/Strings/fr-fr/Resources.resw
Original file line number Diff line number Diff line change
Expand Up @@ -7565,6 +7565,7 @@ Le binaire wxc-exec est introuvable. {1} S'il s'agit d'une build développeur, c
<data name="Onboarding_AiSetup_LocalInstalling" xml:space="preserve"><value>Configuration de l’IA locale</value></data>
<data name="Onboarding_AiSetup_LocalChanged" xml:space="preserve"><value>Le Gateway ou la disponibilité de l’IA locale a changé. Revenez à la connexion de votre IA et vérifiez à nouveau avant l’installation.</value></data>
<data name="Onboarding_AiSetup_LocalStartFailed" xml:space="preserve"><value>L’IA locale n’a pas pu démarrer. Le nettoyage est terminé. Réparez l’IA locale ou choisissez un autre fournisseur.</value></data>
<data name="Onboarding_AiSetup_OpenLocalAiLogs" xml:space="preserve"><value>Ouvrir les journaux de l’IA locale</value></data>
<data name="Onboarding_AiSetup_LocalState_Checking" xml:space="preserve"><value>Vérification de ce PC. Vous pouvez toujours choisir un fournisseur d’IA.</value></data>
<data name="Onboarding_AiSetup_LocalState_SetUp" xml:space="preserve"><value>PC compatible. Vérifiez la configuration avant l’installation.</value></data>
<data name="Onboarding_AiSetup_LocalState_StartAndUse" xml:space="preserve"><value>Installé, à l’arrêt</value></data>
Expand Down
1 change: 1 addition & 0 deletions src/OpenClaw.Tray.WinUI/Strings/nl-nl/Resources.resw
Original file line number Diff line number Diff line change
Expand Up @@ -7566,6 +7566,7 @@ Het binaire bestand wxc-exec is niet gevonden. {1} Als dit een ontwikkelaarsbuil
<data name="Onboarding_AiSetup_LocalInstalling" xml:space="preserve"><value>Lokale AI instellen</value></data>
<data name="Onboarding_AiSetup_LocalChanged" xml:space="preserve"><value>De Gateway of de status van lokale AI is gewijzigd. Ga terug naar AI verbinden en controleer opnieuw voordat je installeert.</value></data>
<data name="Onboarding_AiSetup_LocalStartFailed" xml:space="preserve"><value>Lokale AI kon niet starten. Het opruimen is voltooid. Herstel lokale AI of kies een andere provider.</value></data>
<data name="Onboarding_AiSetup_OpenLocalAiLogs" xml:space="preserve"><value>Logboeken van lokale AI openen</value></data>
<data name="Onboarding_AiSetup_LocalState_Checking" xml:space="preserve"><value>Deze pc wordt gecontroleerd. Je kunt nog steeds een AI-provider kiezen.</value></data>
<data name="Onboarding_AiSetup_LocalState_SetUp" xml:space="preserve"><value>Compatibele pc. Controleer de instellingen vóór installatie.</value></data>
<data name="Onboarding_AiSetup_LocalState_StartAndUse" xml:space="preserve"><value>Geïnstalleerd, niet actief</value></data>
Expand Down
1 change: 1 addition & 0 deletions src/OpenClaw.Tray.WinUI/Strings/pt-br/Resources.resw
Original file line number Diff line number Diff line change
Expand Up @@ -7619,6 +7619,7 @@ O binário wxc-exec não pôde ser localizado. {1} Se esta for uma build de dese
<data name="Onboarding_AiSetup_LocalInstalling" xml:space="preserve"><value>Configurando a IA local</value></data>
<data name="Onboarding_AiSetup_LocalChanged" xml:space="preserve"><value>O Gateway ou a disponibilidade da IA local mudou. Volte à conexão da IA e verifique novamente antes de instalar.</value></data>
<data name="Onboarding_AiSetup_LocalStartFailed" xml:space="preserve"><value>A IA local não pôde iniciar. A limpeza foi concluída. Repare a IA local ou escolha outro provedor.</value></data>
<data name="Onboarding_AiSetup_OpenLocalAiLogs" xml:space="preserve"><value>Abrir logs da IA local</value></data>
<data name="Onboarding_AiSetup_LocalState_Checking" xml:space="preserve"><value>Verificando este PC. Você ainda pode escolher um provedor de IA.</value></data>
<data name="Onboarding_AiSetup_LocalState_SetUp" xml:space="preserve"><value>PC compatível. Revise a configuração antes de instalar.</value></data>
<data name="Onboarding_AiSetup_LocalState_StartAndUse" xml:space="preserve"><value>Instalado, não está em execução</value></data>
Expand Down
1 change: 1 addition & 0 deletions src/OpenClaw.Tray.WinUI/Strings/zh-cn/Resources.resw
Original file line number Diff line number Diff line change
Expand Up @@ -7565,6 +7565,7 @@
<data name="Onboarding_AiSetup_LocalInstalling" xml:space="preserve"><value>正在设置本地 AI</value></data>
<data name="Onboarding_AiSetup_LocalChanged" xml:space="preserve"><value>Gateway 或本地 AI 的就绪状态已更改。请返回连接 AI 页面并重新检查,然后再安装。</value></data>
<data name="Onboarding_AiSetup_LocalStartFailed" xml:space="preserve"><value>本地 AI 无法启动。清理已完成。请修复本地 AI 或选择其他提供商。</value></data>
<data name="Onboarding_AiSetup_OpenLocalAiLogs" xml:space="preserve"><value>打开本地 AI 日志</value></data>
<data name="Onboarding_AiSetup_LocalState_Checking" xml:space="preserve"><value>正在检查此电脑。你仍可选择 AI 提供商。</value></data>
<data name="Onboarding_AiSetup_LocalState_SetUp" xml:space="preserve"><value>电脑兼容。安装前请查看设置。</value></data>
<data name="Onboarding_AiSetup_LocalState_StartAndUse" xml:space="preserve"><value>已安装,未运行</value></data>
Expand Down
1 change: 1 addition & 0 deletions src/OpenClaw.Tray.WinUI/Strings/zh-tw/Resources.resw
Original file line number Diff line number Diff line change
Expand Up @@ -7565,6 +7565,7 @@
<data name="Onboarding_AiSetup_LocalInstalling" xml:space="preserve"><value>正在設定本機 AI</value></data>
<data name="Onboarding_AiSetup_LocalChanged" xml:space="preserve"><value>Gateway 或本機 AI 的就緒狀態已變更。請返回連線 AI 頁面並重新檢查,然後再安裝。</value></data>
<data name="Onboarding_AiSetup_LocalStartFailed" xml:space="preserve"><value>本機 AI 無法啟動。清理已完成。請修復本機 AI 或選擇其他提供者。</value></data>
<data name="Onboarding_AiSetup_OpenLocalAiLogs" xml:space="preserve"><value>開啟本機 AI 記錄</value></data>
<data name="Onboarding_AiSetup_LocalState_Checking" xml:space="preserve"><value>正在檢查此電腦。你仍可選擇 AI 提供者。</value></data>
<data name="Onboarding_AiSetup_LocalState_SetUp" xml:space="preserve"><value>電腦相容。安裝前請檢閱設定。</value></data>
<data name="Onboarding_AiSetup_LocalState_StartAndUse" xml:space="preserve"><value>已安裝,未執行</value></data>
Expand Down
Loading
Loading