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
19 changes: 18 additions & 1 deletion src/MeshWeaver.Blazor.Portal/Chat/ThreadChatView.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -823,6 +823,14 @@ private static bool NeedsAgentDefault(string? agentName)
/// so the read never hits a missing node. <see cref="MeshWeaver.AI.ThreadComposerNodeType.ComposerOf"/>
/// discriminates by NodeType: a ThreadComposer node's own content, or the thread's embedded
/// <c>Thread.Composer</c>.
/// <para>An idle-collected composer grain whose reactivation outruns the request budget answers
/// the subscribe with a TRANSIENT owner failure ("No response received in hub … target hub was
/// not found"). The stream-cache contract forwards that to the subscriber for the CALLER to
/// retry (<c>MeshNodeStreamCache.IsTransientOwnerFailure</c>) — the bounded backoff here is that
/// retry, the same <see cref="AreaStreamRetry"/> every layout area uses. Only a NON-transient
/// failure is surfaced; a transient one that survives the whole budget is logged and stays
/// silent (the raw framework banner is hostile and unactionable), and the next chat rebind
/// restarts the projection naturally.</para>
/// </summary>
private void OpenComposerProjection(string path)
{
Expand All @@ -832,6 +840,7 @@ private void OpenComposerProjection(string path)
composerSubscription = Hub.GetMeshNodeStream(path)
.Select(n => MeshWeaver.AI.ThreadComposerNodeType.ComposerOf(n, Hub.JsonSerializerOptions, Logger))
.Where(c => c is not null)
.RetryAreaWithBackoff(AreaErrorClassifier.ShouldRetryArea)
.Subscribe(
c => InvokeAsync(() =>
{
Expand All @@ -842,7 +851,15 @@ private void OpenComposerProjection(string path)
boundEffort = c.Effort;
StateHasChanged();
}),
ex => SurfaceError(ex, "Loading the composer"));
ex =>
{
if (AreaErrorClassifier.IsTransientHubFailure(ex))
Logger.LogWarning(ex,
"[ThreadChat:{InstanceId}] composer projection for {Path} still unavailable after bounded retries — staying silent; the next chat rebind resubscribes",
_instanceId, path);
else
SurfaceError(ex, "Loading the composer");
});
StateHasChanged();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -158,10 +158,16 @@ private void OpenComposerObserver(string path)
{
if (_observedComposerPath != path) return;
_composerObserver?.Dispose();
// Bounded retry for the transient owner miss (idle-collected composer grain whose
// reactivation outruns the request budget → "No response received in hub … target hub was
// not found"): the stream cache forwards it for the CALLER to retry. Without this, one
// transient miss silently killed the observer for the circuit's whole lifetime — Send from
// the composer then never navigated to the created thread.
_composerObserver = Hub.GetMeshNodeStream(path)
.Select(n => ThreadComposerNodeType.ComposerOf(n, Hub.JsonSerializerOptions, _logger)?.OpenThreadPath)
.Where(p => !string.IsNullOrEmpty(p))
.DistinctUntilChanged()
.RetryAreaWithBackoff(AreaErrorClassifier.ShouldRetryArea)
.Subscribe(
threadPath => InvokeAsync(() =>
{
Expand Down Expand Up @@ -194,7 +200,8 @@ private void OpenComposerObserver(string path)
selectedThreadPath = threadPath;
StateHasChanged();
}),
ex => _logger?.LogDebug(ex, "[ThreadSidePanel] composer observer errored for {Path}", path));
ex => _logger?.LogWarning(ex,
"[ThreadSidePanel] composer observer for {Path} gave up after bounded retries — Send-click navigation is inert until the panel rebinds", path));
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
---
Name: The chat input no longer greets you with a technical error
Category: Fix
Description: Opening the portal could pop a raw "No response received…" message while the chat input was waking up. The chat now waits and retries quietly, and only reports genuine failures.
Icon: Sparkle
Order: -20260810
---

# The chat input no longer greets you with a technical error

Navigating to the home page sometimes greeted you with an internal-looking error — "Loading the
composer: No response received in hub … the target hub was not found" — before you had done
anything at all. Dismissing it and carrying on usually worked, which made the message all the more
confusing: nothing was actually broken.

Your chat input keeps its selections (agent, model, effort) on a small per-user record. Like
everything else in the mesh, that record's owner is put to sleep when it has been idle for a while
and woken on the next visit. Waking up occasionally takes longer than a single request is allowed
to wait, and the chat treated that late wake-up as a hard failure: it gave up on the first try and
showed you the framework's raw timeout text — a message that describes plumbing, not anything you
could act on.

The rest of the portal already knew better. Every page view classifies this exact situation as
transient and retries a bounded number of times while the owner finishes waking; the chat input was
the one reader that never did. It now uses the same bounded retry: a slow wake-up is retried
quietly until the record answers, and the selections appear on their own. If something genuinely
fails — a real error, not a slow start — it is still reported. The same fix covers the side
panel's listener that opens your newly created thread after you press Send, which a single slow
wake-up could previously switch off unnoticed until the next page visit.
Loading