Quick reference
π¨ The portal hosts live in
MeshWeaver.Pluginssince 2026-08-26 (MeshWeaver#2293 β the GUI extraction).Memex.Portal.Monolith,Memex.AppHostandMemex.Portal.Distributedare undersrc/in that repository, and so are the login pages (Memex.Portal.Gui). A core checkout on its own has no portal to run: following an older copy of this page from core alone gives a process with no login UI and no control views (#2367). Every command below assumes the standard sibling layout βMeshWeaver/andMeshWeaver.Plugins/side by side β which is what plugins'src/Directory.Build.propsdefaultsMeshWeaverRootto (../MeshWeaver); set-p:MeshWeaverRoot=<path>if yours differs.
When you change code in Memex.Portal.Distributed (or any project it references β MeshWeaver.AI, MeshWeaver.Graph, MeshWeaver.Hosting.Orleans, β¦), you have three ways to apply the change without touching the whole Aspire stack:
All three options restart only Memex.Portal.Distributed; containers, dashboard, and OTLP collector stay up.
| Approach | Typical cost | When to use |
|---|---|---|
dotnet watch --project ../MeshWeaver.Plugins/src/Memex.AppHost |
Seconds | Default. File save triggers a per-resource restart automatically. |
| Dashboard UI: Resources β β― β Restart | ~10 s | Watch isn't running, or it missed a change. |
Kill the portal process (pkill -f Memex.Portal.Distributed) |
~5 s | Last resort β dashboard hangs or AppHost is wedged. |
Do NOT kill the whole
aspire/Memex.AppHostprocess unless you changed AppHost wiring itself. A full restart costs 30β60 s, rebuilds every resource, re-launches the Postgres and blob-storage containers, and invalidates the dashboard's browser-token URL.
Starting Aspire
The image-based AppHost (
deploy/aspire/Memex.Deploy.AppHost, and any customer AppHost on the publishedMeshWeaver.Aspire.Hosting.Memexpackage) declares its instance as a Deployment record βbuilder.AddMemex("memex").WithImage(β¦).WithPluginRepo(β¦)β¦β the same record the Helm chart renders from and the portal binds at boot (Deployment:Record).--record-out <path>writes the final record for the setup wizard or aProvisionaction. ConfiguringAnInstanceFromAspire.
Three modes β pick by whether you want to hold a terminal and whether you need a build:
# A. Interactive (foreground) β builds, holds the terminal, prints live status.
# Registers with `aspire mcp` so MCP tools can drive it. Ctrl+C to stop.
aspire run --project ../MeshWeaver.Plugins/src/Memex.AppHost
# B. Background daemon β detaches and returns immediately; survives across shells.
# `--no-build` reuses the existing binaries (fast β no rebuild). Drop --no-build
# to build first. Also registers with `aspire mcp`.
aspire start --no-build --project ../MeshWeaver.Plugins/src/Memex.AppHost
aspire ps # list running AppHosts (Path Β· PID Β· dashboard URL)
aspire stop # stop the background AppHost
aspire logs [<resource>] # tail logs without the dashboard
# C. Visual Studio's "F5" path.
dotnet run --project ../MeshWeaver.Plugins/src/Memex.AppHost
π¨
aspire start --no-buildreuses the LAST build. It's the fast way to bring the stack back up, but it does not pick up source changes β if you edited code, either drop--no-build, build the changed project first, or use one of the per-resource reloads below. Use--no-buildwhen the binaries are already current (e.g. you just stopped Aspire to run a build and want it back up).
aspire run / aspire start print a one-shot dashboard token URL:
Dashboard: https://localhost:17200/login?t=<TOKEN>
Open that URL in your browser. The token is per-process β restart Aspire and you get a new one. To skip token authentication in development, add the following to ../MeshWeaver.Plugins/src/Memex.AppHost/Properties/launchSettings.json:
"environmentVariables": {
"DOTNET_DASHBOARD_UNSECURED_ALLOW_ANONYMOUS": "true"
}
Visual Studio's F5 path does this implicitly via its launch profile, which is why it never prompts.
Full stop β rebuild β restart (clean cold-start after a code change)
Hot reload (Option 1) and the per-resource restarts (Options 2β3) are the default. But when you want a clean cold-start with new code β a non-trivial change spanning several referenced projects, or recovery from a wedged stack β use the explicit stop β rebuild β start cycle. The two steps people miss (and that cause the "I rebuilt but it still runs old code" / "my build won't compile, file in use" loops) are in bold:
On macOS/Linux (zsh/bash):
# 1. Stop Aspire β AND force-kill, because `aspire stop` can exit while leaving the
# AppHost + portal processes alive:
aspire stop
pkill -f Memex.AppHost; pkill -f Memex.Portal.Distributed
# 2. π¨ RELEASE THE BUILD LOCKS. Lingering MSBuild / VB-C# compiler-server nodes keep
# obj/*.dll open AFTER the AppHost is gone β that is the cause of the rebuild failing with
# "CS2012: cannot open 'β¦/obj/β¦/X.dll' for writing" or "MSB3021: being used by another
# process". Killing the portal is NOT enough; you must shut the build server too:
dotnet build-server shutdown
# 3. Rebuild the portal project (it lives in the sibling MeshWeaver.Plugins checkout; MeshWeaverRoot
# resolves back to this repo). Building Memex.Portal.Distributed also rebuilds every project it
# references here (MeshWeaver.AI, .Graph, .Mesh.Contract, β¦) and in plugins (.Blazor.Portal), so ALL your edits
# compile β this doubles as your compile gate before bringing the stack up:
dotnet build ../MeshWeaver.Plugins/src/Memex.Portal.Distributed/Memex.Portal.Distributed.csproj --no-restore
# 4. Start fast, reusing the build from step 3 (no second compile):
aspire start --no-build --project ../MeshWeaver.Plugins/src/Memex.AppHost
On Windows (PowerShell), step 1's force-kill is instead:
Get-Process Memex.AppHost,Memex.Portal.Distributed,aspire -ErrorAction SilentlyContinue | Stop-Process -Force
Why
--no-buildis safe here: step 3 already produced current binaries for the portal and everything it references, so--no-buildjust launches them. The Postgres / blob containers are persistent and survive the stop, so the database is intact across the cycle.
Option 1 β Hot reload with dotnet watch
dotnet watch --project ../MeshWeaver.Plugins/src/Memex.AppHost
Save a file and Aspire detects the change, rebuilds the affected project, and restarts only that resource. The Postgres container, blob-storage container, and dashboard all stay up. Most edits apply within seconds.
What hot reload handles and what it doesn't:
| Change type | Behaviour |
|---|---|
| Razor component edits | Applied in-place via Blazor hot reload β no restart. |
| Method body / addition changes | Applied in-place by the runtime hot-reload engine. |
| Type / interface / DI-registration changes | Triggers a resource restart (~5β10 s for the portal). |
| AppHost wiring changes | Requires a full Aspire restart β dotnet watch cannot reload its own host. |
Option 2 β Dashboard restart
Navigate to https://localhost:17200/ β Resources tab β find the resource (e.g. memex-portal-distributed) β click the β― menu β Restart.
This is equivalent to running dotnet build and relaunching the resource process. The hub state is wiped (action blocks drained, hosted hubs disposed), but the dashboard, OTLP collector, and every other resource stay up.
Use this when:
- Watch isn't running.
- A hub-configuration change (
WithHandler<T>,WithRoutes,ConfigureDefaultNodeHub) needs to take effect cleanly. - Hot reload reports an unsupported edit.
Option 3 β Process kill (last resort)
pkill -f Memex.Portal.Distributed # macOS / Linux
Get-Process Memex.Portal.Distributed -ErrorAction SilentlyContinue | Stop-Process -Force # Windows
Aspire's resource watcher notices the exit and restarts the resource, typically within a few seconds. Use this when the dashboard UI is unresponsive (for example, a layout area is hung in JS and is blocking dashboard rendering), or when you want to confirm a clean cold-start.
What NOT to restart
| Process | Only restart if⦠| Cost |
|---|---|---|
Memex.AppHost |
You changed AppHost wiring | 30β60 s + container relaunch |
aspire (CLI) |
You're switching projects | New dashboard token URL required |
dcp Γ N |
They crashed (pgrep -l dcp, or Get-Process dcp on Windows) |
AppHost may not recover; full restart needed |
| Postgres / blob containers | You need a clean database | Full Aspire restart + migration replay |
When aspire mcp doesn't see your AppHost
If mcp__aspire__list_apphosts returns [] even though Aspire is running, the AppHost was started via dotnet run. Only the aspire CLI writes the discovery file the MCP server reads β both aspire run and aspire start do; dotnet run does not.
Fix β stop the current AppHost and restart with either CLI form:
aspire run --project ../MeshWeaver.Plugins/src/Memex.AppHost # foreground
aspire start --project ../MeshWeaver.Plugins/src/Memex.AppHost # background daemon
After this, MCP tools (list_resources, list_traces, list_structured_logs, execute_resource_command) all work against the running AppHost.
Logging triage from the dashboard
Navigate to https://localhost:17200/structuredlogs?level=info and filter by:
| Filter | What you find |
|---|---|
category contains MessageHub |
Hub action-block events (HUB_HANDLE_START/END, FinishDelivery) |
category contains MessageService |
Routing failures ("No handler found", "Could not deserialize") |
category contains OrleansRoutingService |
Cross-grain dispatch warnings |
category contains Hosting.Orleans.MessageHubGrain |
Grain activation, GrainDeliver IN/OUT |
category contains GrainKeepAlive |
Heartbeat traffic β high volume suggests a stream leak |
Message contains SLOW_DISPATCH |
Per-message latency > 500 ms (instrumented in MessageHub.HandleMessageAsync and OrleansRoutingService.DispatchObservable) |
Message contains "Allocating agent" |
Chat starting β should be followed by [ThreadExec] lines |
Message contains "Could not deserialize" |
Type-registry mismatch β see DebuggingMessageFlow.md |
For distributed traces across resources, open https://localhost:17200/traces and click any trace to see span timing across the AppHost, Portal, and Postgres boundary.
Common gotchas
"No AppHost is currently running" from aspire mcp.
The AppHost was started via dotnet run, not aspire run. Restart with the latter (see above).
Builds fail with MSB3021: cannot copy β¦ being used by another process or CS2012: cannot open 'β¦/obj/β¦/X.dll' for writing.
A previous AppHost/portal and/or a lingering MSBuild / VB-C# compiler-server node is holding the binaries open. Killing the portal alone is often not enough β the build-server nodes survive it. Force-kill the processes and run dotnet build-server shutdown, then retry. See Full stop β rebuild β restart for the full cycle.
Portal's chat hangs at "Allocating agentβ¦". Almost always one of two causes:
- The portal hub's TypeRegistry doesn't have
AppendUserMessageResponseregistered β the response arrives asRawJsonand the originalObservenever resolves. Fix:WithPortalConfiguration(c => { c.TypeRegistry.AddAITypes(); return c.AddData().WithGraphTypes(); })inMemexConfiguration(already applied inMemexConfiguration.ConfigureMemexPortal). - Multiple portal hubs created per page navigation (the per-DI-scope shape) β a chat response routes to an already-disposed transient portal. Fix: the per-user portal address in
PortalApplication.
Both issues are post-mortemed in commit messages on the routing branch β see git log --grep "portal hub".