Skip to content

feat: social publishing + NuGet #r + move perf + mesh stability batch#95

Open
rbuergi wants to merge 2015 commits into
mainfrom
bug_fix
Open

feat: social publishing + NuGet #r + move perf + mesh stability batch#95
rbuergi wants to merge 2015 commits into
mainfrom
bug_fix

Conversation

@rbuergi

@rbuergi rbuergi commented Apr 22, 2026

Copy link
Copy Markdown
Contributor

Summary

77 commits of long-running work on bug_fix — grouped by theme:

  • Social publishing platform (new)MeshWeaver.Social + LinkedIn publisher + scheduled publishing pipeline (engine/queue/stats), LinkedIn OAuth connect + past-post ingest in Memex portal, per-user linked-account menu items.
  • NuGet in-process compile#r "nuget:Pkg, Version" at the top of _Source/*.cs resolves via public NuGet.Protocol without an SDK on the container. Same resolver serves interactive markdown code cells.
  • Move-node parallelization + 30 s ceilingFileSystemPersistenceService.MoveNodeAsync runs per-descendant WriteAsync/DeleteAsync through Task.WhenAll; new MeshOperationOptions (default Timeout = 30s) + WithMeshOperationTimeout(TimeSpan) override; HandleMoveNodeRequest chains .Timeout() on the persistence Observable so a stuck adapter can't hang the caller. Prod repro: DAV2026 subtree move that took 240 s and killed the MCP session — now bounded.
  • Compile / cache invalidation — sticky invalidation on CompilationCacheService, _Source/ edit re-invalidates owning NodeType, cross-silo broadcast via MeshChangeFeed, grain-dispose on node delete, live "Compiling … (Ns)" progress in LayoutAreaView.
  • Catalog & navigation — Children view groups by Category (falls back to NodeType), reactive Children catalog, self-as-default create location for non-NodeType nodes, sample orgs → Markdown for search visibility.
  • Workspace / stream robustness — Workspace remote-stream cache evicted on MeshChangeFeed events, resubscribe on owner dispose, DeleteLayoutArea emits a placeholder immediately and times out slow streams.
  • Infra & small fixes — settings.json overhaul, Delete-is-recursive MCP docs, HeartBeat silencing on Memex hubs, assembly-dir temp-dir fallback, IAsyncEnumerable aggregator fixes (satellite-safe GatherInputsAsync), xunit methodTimeout 30 s → 60 s, Anthropic Opus bump, icon generator, etc.

New test suites (selected)

  • test/MeshWeaver.Persistence.Test/MoveNodeRecursiveTest.cs — 10 tests: recursion, parallelism, source missing / target exists / storage throws / cancellation (all must not hang), Rx Timeout() contract, default-30s config.
  • test/MeshWeaver.Social.Test/*InMemoryPublishQueueTest, LinkedInPublisherEngagementTest, PostStatsRefresherTest, ScheduledPostPublisherTest, FakePublisher.
  • test/MeshWeaver.Persistence.Test/WorkspaceCacheEvictionTest.cs, ResubscribeOnOwnerDisposeTest.cs, DeleteLayoutAreaIntegrationTest.cs.
  • test/MeshWeaver.Markdown.Test/PathUtilsTest.cs, test/MeshWeaver.MathDemo.Test/MatrixViewsTest.cs.

Contributors

Upstream already merged into this branch

Test plan

  • dotnet build succeeds
  • dotnet test test/MeshWeaver.Persistence.Test --filter MoveNodeRecursiveTest — 10/10 green (~8 s)
  • dotnet test test/MeshWeaver.Hosting.Monolith.Test --filter MoveNodeAsync — 5/5 green (regression guard)
  • dotnet test test/MeshWeaver.Social.Test — publish queue / scheduling / stats green
  • Manual prod smoke: move a 3-descendant subtree in memex-prod; confirms < 30 s and MCP session survives
  • Create a _Source/*.cs using #r "nuget:MathNet.Numerics, 5.0.0" — compiles & renders (cold + warm cache)
  • Delete a node then recreate at same path — fresh grain, fresh compile, no stale HubConfiguration
  • Navigate to a cold node — "Compiling (Ns)…" progress renders until the stream resolves
  • LinkedIn OAuth: sign in → /social/connect/linkedin → profile linked; menu shows connected account
  • Scheduled post fires through ScheduledPostPublisher → LinkedIn publisher posts; PostStatsRefresher pulls stats

🤖 Generated with Claude Code

@github-actions

github-actions Bot commented Apr 22, 2026

Copy link
Copy Markdown

Test Results

   47 files     47 suites   22m 58s ⏱️
4 453 tests 4 449 ✅ 3 💤 1 ❌
4 478 runs  4 474 ✅ 3 💤 1 ❌

For more details on these failures, see this check.

Results for commit c984427.

♻️ This comment has been updated with latest results.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR bundles several long-running feature and stability tracks across MeshWeaver core + Memex: social publishing foundations, in-process #r "nuget:..." compilation support (node-type + interactive markdown), move-operation performance/timeout hardening, and multiple UI/stream reliability improvements. It also standardizes the code folder naming from _Source/_Test to Source/Test across code, tests, docs, and samples.

Changes:

  • Introduces MeshWeaver.Social (options, DI wiring, publish queue, credential model) plus initial Memex wiring (LinkedIn connect entry points + user menu hooks).
  • Adds MeshWeaver.NuGet resolver + directive parser and integrates it into script compilation (#r "nuget:Pkg, Version"), including cache backends and tests.
  • Improves operational robustness: parallelized recursive moves, default 30s mesh-op timeout, “no endless spinner” navigation status UI, and remote stream resubscribe behavior.

Reviewed changes

Copilot reviewed 159 out of 265 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
test/MeshWeaver.StorageImport.Test/StorageImporterTests.cs Updates test expectations/docs to Source/ naming.
test/MeshWeaver.Social.Test/PostStatsRefresherTest.cs Adds stats refresher test coverage (needs deterministic timeout handling).
test/MeshWeaver.Social.Test/MeshWeaver.Social.Test.csproj Adds new Social test project referencing Social + Fixture.
test/MeshWeaver.Social.Test/InMemoryPublishQueueTest.cs Adds unit tests for publish queue due-drain + dedup.
test/MeshWeaver.Persistence.Test/FileSystemPersistenceTest.cs Updates partition tests to Source/ naming.
test/MeshWeaver.MathDemo.Test/TestPaths.cs Adds helper paths for MathDemo sample test assets.
test/MeshWeaver.MathDemo.Test/MeshWeaver.MathDemo.Test.csproj Adds MathDemo test project and copies sample graph data to output.
test/MeshWeaver.Hosting.PostgreSql.Test/SatelliteQueryTests.cs Updates code-path routing tests to Source/ naming.
test/MeshWeaver.Hosting.Monolith.Test/UserActivityAreaTest.cs Updates regression test docs to Source/ naming.
test/MeshWeaver.Hosting.Blazor.Test/NavigationServiceTest.cs Adjusts test to assert “no 404 flash” during retries.
test/MeshWeaver.Graph.Test/NuGetDirectiveParserTest.cs Adds unit tests for parsing/stripping #r "nuget:...".
test/MeshWeaver.Graph.Test/NuGetAssemblyResolverTest.cs Adds networked NuGet restore end-to-end tests (skippable via env var).
test/MeshWeaver.Graph.Test/MeshWeaver.Graph.Test.csproj References new MeshWeaver.NuGet project.
test/MeshWeaver.FutuRe.Test/MeshWeaver.FutuRe.Test.csproj Updates compile-included sample sources to Source/ paths.
test/MeshWeaver.Content.Test/CompilationErrorTest.cs Updates broken-code test to Source/ path.
test/MeshWeaver.AI.Test/MeshPluginTest.cs Updates MCP tool count expectations (adds RunTests/Move/Copy).
src/MeshWeaver.Social/SocialOptions.cs Adds configurable knobs for publishing/stats/ingest scheduling.
src/MeshWeaver.Social/SocialExtensions.cs Adds DI wiring for social publishing subsystem and hosted services.
src/MeshWeaver.Social/PlatformCredential.cs Adds credential record model (access/refresh/expiry metadata).
src/MeshWeaver.Social/MeshWeaver.Social.csproj Introduces Social library project.
src/MeshWeaver.Social/IPublishQueue.cs Adds publish queue abstraction + in-memory implementation.
src/MeshWeaver.Social/IApprovalPublishBridge.cs Defines bridge contract and PublishableSnapshot model.
src/MeshWeaver.NuGet/ResolvedPackageSet.cs Adds resolver output model (assemblies, probing dirs, versions).
src/MeshWeaver.NuGet/NuGetServiceCollectionExtensions.cs Adds DI extension to register resolver + cache.
src/MeshWeaver.NuGet/NuGetPackageReference.cs Adds package reference model (id + version range).
src/MeshWeaver.NuGet/NuGetDirectiveParser.cs Implements #r "nuget:..." extraction + source stripping.
src/MeshWeaver.NuGet/MeshWeaver.NuGet.csproj Introduces NuGet resolver project and dependencies.
src/MeshWeaver.NuGet/INuGetPackageCache.cs Adds optional persistent cache interface + null implementation.
src/MeshWeaver.NuGet/INuGetAssemblyResolver.cs Adds resolver interface returning ResolvedPackageSet.
src/MeshWeaver.NuGet.AzureBlob/MeshWeaver.NuGet.AzureBlob.csproj Adds Azure Blob cache backend project.
src/MeshWeaver.NuGet.AzureBlob/BlobNuGetPackageCacheExtensions.cs Adds DI helper to register blob-backed cache.
src/MeshWeaver.Mesh.Contract/Services/MeshOperationOptions.cs Adds mesh operation timeout options (default 30s).
src/MeshWeaver.Mesh.Contract/Services/IStorageAdapter.cs Updates docs/examples to Source/ naming.
src/MeshWeaver.Mesh.Contract/Services/INavigationService.cs Adds Status observable contract for UI progress reporting.
src/MeshWeaver.Mesh.Contract/Services/IIconGenerator.cs Adds icon generator abstraction returning an observable SVG.
src/MeshWeaver.Mesh.Contract/PartitionDefinition.cs Updates standard table mappings (Source/Testcode) and clarifies semantics.
src/MeshWeaver.Mesh.Contract/MeshExtensions.cs Adds timeout override + move timeout enforcement + grain dispose on delete.
src/MeshWeaver.Mesh.Contract/CodeConfiguration.cs Updates docs to Source/ naming.
src/MeshWeaver.Kernel.Hub/MeshWeaver.Kernel.Hub.csproj Removes Interactive package mgmt dependency; references MeshWeaver.NuGet.
src/MeshWeaver.Hosting/Persistence/MigrationUtility.cs Updates migration heuristics to include Source/Test + legacy _Source/_Test.
src/MeshWeaver.Hosting/Persistence/FileSystemStorageAdapter.cs Treats Source/Test as code paths + keeps legacy compatibility.
src/MeshWeaver.Hosting/Persistence/FileSystemPersistenceService.cs Parallelizes descendant move I/O (with concurrency implications).
src/MeshWeaver.Hosting/Persistence/CachingStorageAdapter.cs Updates code sub-namespace detection (Source/Test + legacy).
src/MeshWeaver.Hosting.PostgreSql/PostgreSqlPartitionedStoreFactory.cs Guards against source/test mistakenly becoming schemas.
src/MeshWeaver.Hosting.PostgreSql/PostgreSqlCrossSchemaQueryProvider.cs Filters malformed parameters to avoid NRE during SQL interpolation.
src/MeshWeaver.Hosting.Blazor/MeshWeaver.Hosting.Blazor.csproj Adds NU1510 suppression.
src/MeshWeaver.Graph/PartitionTypeSource.cs Updates docs to Source/ naming.
src/MeshWeaver.Graph/MeshWeaver.Graph.csproj References MeshWeaver.NuGet.
src/MeshWeaver.Graph/MeshNodeLayoutAreas.cs Improves create href behavior + reactive/grouped children catalog.
src/MeshWeaver.Graph/MeshDataSource.cs Updates docs to Source/ naming.
src/MeshWeaver.Graph/Configuration/ScriptCompilationService.cs Integrates NuGet directive parsing + resolver into compilation.
src/MeshWeaver.Graph/Configuration/NodeTypeDefinition.cs Updates docs/examples to Source/ naming.
src/MeshWeaver.Graph/Configuration/MeshDataSourceNodeType.cs Changes sources namespace constant to Source.
src/MeshWeaver.Graph/Configuration/GraphConfigurationExtensions.cs Registers NuGet resolver and uses Source code path.
src/MeshWeaver.Graph/Configuration/CodeNodeType.cs Treats Code nodes as primary content; defines Source/Test constants.
src/MeshWeaver.Documentation/Data/DataMesh/UnifiedPath.md Documents @/ semantics and HTML-href pitfalls.
src/MeshWeaver.Documentation/Data/DataMesh/SocialMedia/Profile/Source/SocialMediaProfileLayoutAreas.cs Adds SocialMedia profile layout areas example.
src/MeshWeaver.Documentation/Data/DataMesh/SocialMedia/Profile/Source/SocialMediaProfile.cs Adds SocialMedia profile content model example.
src/MeshWeaver.Documentation/Data/DataMesh/SocialMedia/Post/Source/SocialMediaPost.cs Adds SocialMedia post content model example.
src/MeshWeaver.Documentation/Data/DataMesh/SocialMedia/Post/Source/Platform.cs Adds SocialMedia platform reference-data example.
src/MeshWeaver.Documentation/Data/DataMesh/SocialMedia.md Updates docs to Source/ naming and authoring guidance.
src/MeshWeaver.Documentation/Data/DataMesh/SatelliteEntities.md Clarifies Source/Test are primary content, not satellites.
src/MeshWeaver.Documentation/Data/DataMesh/NodeTypes.md Adds Node Types documentation index page.
src/MeshWeaver.Documentation/Data/DataMesh/NodeTypeConfiguration.md Updates docs to Source/ naming.
src/MeshWeaver.Documentation/Data/DataMesh/NodeOperations.md Updates docs to Source/ naming.
src/MeshWeaver.Documentation/Data/DataMesh/DataConfiguration.md Updates docs to Source/ naming.
src/MeshWeaver.Documentation/Data/DataMesh/CreatingNodeTypes.md Updates docs to Source/Test naming throughout.
src/MeshWeaver.Documentation/Data/DataMesh.md Updates TOC links and adds NuGet packages bullet.
src/MeshWeaver.Documentation/Data/Architecture/PartitionedPersistence.md Updates persistence routing docs for Source/Test.
src/MeshWeaver.Documentation/Data/Architecture/MeshGraph.md Updates examples to Source/ naming.
src/MeshWeaver.Documentation/Data/Architecture/BusinessRules/Cession/Source/CessionSampleData.cs Adds cession sample dataset for docs/demo.
src/MeshWeaver.Documentation/Data/Architecture/BusinessRules/Cession/Source/CessionResultsArea.cs Adds reactive charting layout area example.
src/MeshWeaver.Documentation/Data/Architecture/BusinessRules/Cession/Source/CessionEngine.cs Adds pure business logic sample for cession calculations.
src/MeshWeaver.Documentation/Data/Architecture/BusinessRules/Cession/Source/CessionData.cs Adds content models for cession example.
src/MeshWeaver.Data/Serialization/SyncStreamOptions.cs Adds configurable heartbeat interval for sync streams.
src/MeshWeaver.Data/Serialization/JsonSynchronizationStream.cs Implements resubscribe-on-owner-dispose logic.
src/MeshWeaver.Blazor/Pages/ApplicationPage.razor Switches to NavigationStatus-driven progress/not-found/error UI.
src/MeshWeaver.Blazor/Components/NavigationProgressBar.razor.css Adds styling for full-page vs compact overlay progress bar.
src/MeshWeaver.Blazor/Components/NavigationProgressBar.razor Adds reusable “spinner + message” component.
src/MeshWeaver.Blazor/Components/MeshSearchView.razor.cs Adds Category grouping fallback to NodeType.
src/MeshWeaver.Blazor/Components/LayoutAreaView.razor.cs Adds stream lifecycle logging and additional diagnostics.
src/MeshWeaver.Blazor/Components/LayoutAreaView.razor Surfaces compilation progress indicator before first stream emission.
src/MeshWeaver.Blazor/Components/CompileProgressIndicator.razor.css Adds styling for compilation progress banner.
src/MeshWeaver.Blazor/Components/CompileProgressIndicator.razor Adds polling UI component for active NodeType compilation.
src/MeshWeaver.Blazor.Portal/MeshWeaver.Blazor.Portal.csproj Adds NU1510 suppression.
src/MeshWeaver.Blazor.AI/MeshWeaver.Blazor.AI.csproj Adds NU1510 suppression.
src/MeshWeaver.Blazor.AI/McpMeshPlugin.cs Adds Patch/Move/Copy MCP tools and improves tool descriptions.
src/MeshWeaver.AI/ThreadLayoutAreas.cs Adds debug logging around streaming view emission.
src/MeshWeaver.AI/IconGenerator.cs Adds default AI-backed IIconGenerator implementation.
src/MeshWeaver.AI/DelegationCompletedEvent.cs Removes delegation tracker/event types.
src/MeshWeaver.AI/Data/Agent/Worker.md Updates @/ link guidance (no raw HTML href with @/).
src/MeshWeaver.AI/Data/Agent/ToolsReference.md Updates @/ link guidance and provides correct/incorrect table.
src/MeshWeaver.AI/Data/Agent/Orchestrator.md Updates @/ link guidance for agent outputs.
src/MeshWeaver.AI/AIExtensions.cs Removes old type registration; registers IIconGenerator.
memex/aspire/Memex.Portal.Distributed/Program.cs Registers blob-backed NuGet package cache in distributed deployment.
memex/aspire/Memex.Portal.Distributed/Memex.Portal.Distributed.csproj References MeshWeaver.NuGet.AzureBlob.
memex/aspire/Memex.Database.Migration/Program.cs Adds source/test to reserved schema list.
memex/aspire/Memex.AppHost/Program.cs Adds LinkedIn secret/env wiring + sets NUGET_PACKAGES cache dir.
memex/Memex.Portal.Shared/Social/SocialMediaUserMenuProvider.cs Adds “Social Media” shortcut on a user’s own node (lazy hub creation).
memex/Memex.Portal.Shared/Social/ApiCredentialNodeType.cs Adds NodeType for PlatformCredential stored under _ApiCredentials.
memex/Memex.Portal.Shared/Pages/Login.razor Adds “Connect LinkedIn for publishing” CTA on login page.
memex/Memex.Portal.Shared/OrganizationNodeType.cs Switches to default layout areas registration.
memex/Memex.Portal.Shared/MemexConfiguration.cs Adds LinkedIn publisher wiring, @/ redirect middleware, and routes.
memex/Memex.Portal.Shared/Memex.Portal.Shared.csproj References MeshWeaver.Social.
memex/Memex.Portal.Monolith/appsettings.Development.json Enables debug logging for LayoutAreaView.
MeshWeaver.slnx Adds new projects (NuGet, NuGet.AzureBlob, Social, new test projects).
Directory.Packages.props Adds NuGet.* package versions for resolver implementation.
CLAUDE.md Documents @/ local-only rule and href/URL restrictions.
(Various) samples/Graph/... Adds/updates many sample NodeTypes and content under Source/ to reflect new conventions and demos.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread test/MeshWeaver.Social.Test/PostStatsRefresherTest.cs
Comment thread src/MeshWeaver.Hosting/Persistence/FileSystemPersistenceService.cs Outdated
rbuergi added a commit that referenced this pull request Apr 22, 2026
…+ test helpers

Recursive DeleteNodeRequest handled on a node's own hub was deadlocking: the final
DeleteSelfFromStorage posted Ok and DisposeRequest from the dying hub, so the Ok
raced callback disposal on the caller and was lost. Introduce CommitNodeDeletionMessage
and forward the terminal commit (storage delete + reply + grain dispose) to the
resolved mesh hub (walking ParentHub upward) — Sender becomes the stable mesh hub,
FIFO on the caller's inbound queue guarantees Ok resolves the RegisterCallback before
DisposeRequest arrives.

Also addresses two Copilot review comments on PR #95:

- FileSystemStorageAdapter.DeleteAsync empty-directory ascent is now concurrency-
  tolerant: wraps the enumerate + Directory.Delete in try/catch, swallowing the
  DirectoryNotFoundException race and breaking on IOException (non-empty / in-use).
  Required because FileSystemPersistenceService.MoveNodeAsync now parallelizes
  descendant deletes via Task.WhenAll.

- PostStatsRefresherTest.WaitUntilAsync throws TimeoutException with a descriptive
  message instead of returning silently on deadline, so the test cannot green-tick
  a stats-refresh that never happened.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@rbuergi

rbuergi commented Apr 27, 2026

Copy link
Copy Markdown
Contributor Author

@copilot resolve the merge conflicts in this pull request

Copilot AI commented Apr 27, 2026

Copy link
Copy Markdown
Contributor

@copilot resolve the merge conflicts in this pull request

Resolved. The merge with origin/main is done in commit 6899990.

Conflicts resolved:

  • .claude/settings.json — kept HEAD deletion
  • Login.razor — kept NavigateToConnectLinkedIn (HEAD) + switched to OnInitializedAsync (origin/main)
  • AppHost/Program.cs — merged origin/main's optional-secrets pattern (nullable embeddingKey/googleClientSecret, customDomainValue guard) with HEAD's LinkedIn params (also made optional/nullable) and local-test/local-prod storage branch
  • Memex.Database.Migration/Program.cs — kept HEAD's data-repair v8 (fix ThreadMessage.MainNode) and v9 (rename _Source/_Test path segments)
  • SecurityService.cs — kept HEAD's refactored CollectStaticRoleIds returning (roleIds, cap); origin/main's permission-evaluation logic is already present in the new reactive GetEffectivePermissions method

@rbuergi rbuergi requested a review from Copilot May 10, 2026 05:41

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot wasn't able to review this pull request because it exceeds the maximum number of files (300). Try reducing the number of changed files and requesting a review from Copilot again.

@rbuergi rbuergi requested a review from Copilot May 10, 2026 06:49

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot wasn't able to review this pull request because it exceeds the maximum number of files (300). Try reducing the number of changed files and requesting a review from Copilot again.

@rbuergi

rbuergi commented May 10, 2026

Copy link
Copy Markdown
Contributor Author

Code review — recent stability batch

Status: ✅ All 11 items in this comment addressed. See per-item commit SHAs in each header. Verification: Memex.Portal.Distributed builds clean; the four tests covering these changes (IsExecutingLifecycleTest, ChatHistoryTest ×2, CancelThreadExecutionTest) pass locally.

Manual review of the last ~20 commits since 8c5f37c80 (the doc commit). Focused on the synced-query consolidation, multi-query UNION feature, ThreadExecution refactor, and new tests. Copilot's two prior comments are already addressed in code. Findings below are grouped by severity.

Correctness — should fix before merge

1. ✅ e68636aacPostgreSqlStorageAdapter.QueryNodesAsync(IReadOnlyList<ParsedQuery>, …) — parameter-rename can mangle SQL.
File: src/MeshWeaver.Hosting.PostgreSql/PostgreSqlStorageAdapter.cs (the new UNION overload, ~line 530).

foreach (var (k, v) in perParams)
{
    var newKey = "@" + prefix + k.TrimStart('@');
    renamedSql = renamedSql.Replace(k, newKey);
    renamedParams[newKey] = v;
}

Dictionary<string,object> enumeration order is not guaranteed. If perParams contains both @p and @p1, processing @p first turns @p1 in the SQL into @q0_p1 (correct); processing @p1 first turns the SQL's @p1 into @q0_p1, then processing @p mangles @q0_p1 into @q0_q0_p1. Mixed-order builds will silently drift. string.Replace also clobbers @… substrings inside string literals or JSONB path comparisons.

Fix: single regex pass keyed on @<name> word boundary, gated on perParams.ContainsKey so we don't rewrite literal @ tokens.

2. ✅ e68636aacUNION (vs UNION ALL) dedup is row-wise, not path-wise.
Same file, same overload. The comment claims "same path emitted by two queries collapses to one row, matching the engine's path-keyed dictionary fold" — but UNION only collapses rows that are byte-identical across all selected columns. Two queries returning the same MeshNode with a slightly-different LastModified (concurrent writer) won't dedup.

Fix: UNION ALL wrapped in SELECT DISTINCT ON (namespace, id) … ORDER BY namespace, id, last_modified DESC. (No literal path column is projected; (namespace, id) is the path-keyed identity tuple. Newest version wins the tie-break.)

3. ✅ e68636aacPostgreSqlMeshQuery.ObserveQuery<T> ignores request.Queries for change detection.
src/MeshWeaver.Hosting.PostgreSql/PostgreSqlMeshQuery.cs:360-401. The method parsed only request.Query (single string), and the change-notifier filter used the first query's normalizedBasePath + effectiveScope for PathMatcher.ShouldNotify. Multi-query observations correctly fanned out to all queries inside CollectQueryResultsAsync, but live updates that match only query #2's path/scope wouldn't trigger a re-run.

Fix: parse every query in request.EffectiveQueries, build per-query (basePath, scope) filters, OR-join them in the change-notifier subscription.

4. ✅ e68636aacMeshQueryEngine Activity post-filter uses only first query's basePath.
src/MeshWeaver.Hosting/Persistence/Query/MeshQueryEngine.cs:125-138, 183-196. When parsedQuery.Source == QuerySource.Activity, the post-filter scanned descendants of firstBasePath for Activity satellites — queries #2+ with unrelated basePaths had their Activity matches filtered against the wrong subtree.

Fix: CollectMatchedAsync returns the list of every query's basePath; the activity post-filter scans every base path's descendants and unions activity-main-paths.

Race / lifecycle hazards

5. ✅ 478fdaa93ThreadExecution.RecoverStaleExecutingThread 2-minute window contradicts "no time limits" commit.
src/MeshWeaver.AI/ThreadExecution.cs:175-180. Commit 6dc436bf5 made the policy explicit, but recovery still said "Only recover truly stale ones (started > 2 minutes ago or no timestamp)." A legitimate slow execution that crashes after 2+ minutes wouldn't be recovered → IsExecuting=true forever.

Fix: drop the time-based heuristic in favour of a structural one — skip recovery only when the thread is still an auto-execute candidate (PendingUserMessage + ActiveMessageId set, i.e. WatchForExecution will pick it up).

6. ✅ 478fdaa93Subject<StreamingSnapshot> not disposed.
src/MeshWeaver.AI/ThreadExecution.cs:890. Fix: using var snapshots = new Subject<…>().

7. ✅ eea8ed10a — Sample(100ms) terminal-status race regression test.
The terminal-status guard correctly prevents Streaming from regressing Completed/Cancelled/Error in PushToResponseMessage. Fix: added a regression assertion in IsExecutingLifecycleTest that final ThreadMessage.Status == Completed after a successful echo run.

8. ✅ 478fdaa93HandleCancelStream runs after CTS-storage race.
src/MeshWeaver.AI/ThreadExecution.cs:1284-1289. parentHub.Set(executionCts) happened around line 847, but IsExecuting=true flipped earlier in HandleSubmitMessage. A cancel arriving in that window was a no-op.

Fix: pre-allocate the CancellationTokenSource and store it on the thread hub in HandleSubmitMessage before posting SubmitMessageResponse. ExecuteMessageAsync reuses it from the parent-hub slot (with a fresh-CTS fallback for the auto-execute path that bypasses HandleSubmitMessage).

Style / consistency

9. ✅ 478fdaa93 — Triple-stacked <summary> XML doc tags.
Collapsed both blocks (WatchForExecution, NotifyParentCompletion) to a single <summary>.

10. ✅ eea8ed10aIsExecutingLifecycleTest text-pattern wait inconsistent with ChatHistoryTest.
Fix: migrated to ThreadMessage.CompletedAt is not null — same pattern as ChatHistoryTest.SubmitAndWait after commit ab3af8b70.

11. ✅ e68636aac — Limit-on-first-query semantics.
request.Limit was applied only to parsedList[0]; query #0 could hit its limit before yielding its most relevant rows while queries #1+ contributed unbounded — making the result iteration-order dependent.

Fix: drop the per-query Limit injection. Limit is enforced post-union via MinLimit(request.Limit, firstParsed.Limit) in both engines, so a request-level cap can't be circumvented and an in-query limit:N still wins when smaller.

✅ Looks good (no action needed)

  • SyncedQueryMeshNodes doc-comment now matches the dict-from-query-events fold (post the doc commit).
  • LoadFullConversationHistoryFromMesh correctly reads the live thread's Messages list and resolves each cell via GetMeshNodeStream (per-node hub) — sidesteps the stale-index race the comment calls out.
  • MultiQueryUnionEngineTests covers the union semantics on the in-memory engine without needing a testcontainer.
  • CancelThreadExecutionTest rewrite (commit-pending) correctly uses "Generating response..." as the CTS-armed signal.
  • The terminal-status guard pattern (current.Status is Completed or Cancelled or Error && requestedStatus == Streaming → keep current) is the right shape.

@rbuergi

rbuergi commented May 10, 2026

Copy link
Copy Markdown
Contributor Author

Code review — part 2: rest of the PR

Status: ✅ All 12 items in this comment addressed. See per-item commit SHAs in each header. NuGet validation in #14 was deferred at first then closed in 6c3e60925.

Continuing review on the bulk of the PR (everything before the recent stability batch). Focused on the new projects (MeshWeaver.NuGet, MeshWeaver.Social) and a sampling of the central MessageHub refactor — the full 100-commit / 1006-file diff is too large for an exhaustive read. Same severity grouping as part 1.

Correctness — should fix before merge

12. ✅ 512adb462NuGetAssemblyResolver caches faulted Tasks forever.
src/MeshWeaver.NuGet/NuGetAssemblyResolver.cs:42.

return _cache.GetOrAdd(key, _ => ResolveCoreAsync(requested, framework, ct));

If ResolveCoreAsync threw, the faulted Task<ResolvedPackageSet> stayed in the cache; subsequent calls replayed the same exception forever.

Fix: evict faulted/cancelled tasks from the cache before returning. Also pass CancellationToken.None to the shared core task so a single caller's cancellation can't take down the resolution for everyone else; per-caller ct projects via task.WaitAsync(ct).

13. ✅ 512adb462NuGetAssemblyResolver resolves with DependencyBehavior.Lowest.
src/MeshWeaver.NuGet/NuGetAssemblyResolver.cs:74. "Lowest" pulls minimum-satisfying versions transitively, which yanks in EOL/unpatched releases when constraints have weak floors.

Fix: switched to DependencyBehavior.HighestMinor so security fixes flow in transparently without crossing minor/major boundaries.

14. ✅ 6c3e60925 — Hydrated package not validated.
After INuGetPackageCache.TryHydrateAsync returned true, the resolver trusted the content — a poisoned cache entry (different package stored under wrong key) would silently load wrong assemblies.

Fix: post-hydration, the resolver opens the package folder via PackageFolderReader.GetIdentity() and verifies the .nuspec-declared (id, version) matches expected. On mismatch the directory is purged and the resolver falls back to the feed download path. No INuGetPackageCache contract change needed.

15. ✅ 478fdaa93XPublisher.PublishAsync crashes on partial response.
src/MeshWeaver.Social/XPublisher.cs:71. The chained GetProperty("data").GetProperty("id") threw KeyNotFoundException on unexpected body shapes.

Fix: defensive TryGetProperty chain; logs a warning and returns id = null (caller treats as "publish succeeded but URN couldn't be captured") instead of crashing. Also guards against null AuthorHandle.

16. ✅ 478fdaa93 (LinkedIn) + 512adb462 (X) — Publishers don't auto-retry on token-refresh race.
Fix: SendWith401RetryAsync helper in both publishers — on 401, force-refresh the token (zero ExpiresAt so EnsureFreshAsync doesn't short-circuit) and retry the request once.

Race / lifecycle hazards

17. ✅ 512adb462PostStatsRefresher processes targets sequentially.
Fix: Parallel.ForEachAsync bounded by SocialOptions.StatsRefreshDegreeOfParallelism (default 8).

18. ✅ 512adb462PostStatsRefresher has no per-target backoff.
Fix: ConcurrentDictionary<string, DateTimeOffset> of last-failure timestamps. Targets that failed within SocialOptions.StatsRefreshFailureBackoff (default 15 min) skip the next tick. Success clears the entry so the target rejoins normal cadence.

19. ✅ df1939bb7MessageHub faulted-Task cache pattern.
The MESHWEAVER_DISPOSE_TRACE=1 global file lock + per-call File.AppendAllText serialised hub teardown when many hubs disposed concurrently.

Fix: replaced with a single bounded Channel<string> (4096, FullMode = DropWrite) drained by one writer task started in the type initialiser. Producers TryWrite non-blocking; lines drop on full so a stuck writer never delays dispose.

Style / consistency

20. ✅ 478fdaa93SocialExtensions.AddSocialPublishing lifetime mismatch.
AddHttpClient<LinkedInPublisher>() registered the typed client as transient; the IPlatformPublisher factory then made it singleton — direct vs via-interface resolution returned different instances.

Fix: register the publisher as a true singleton via services.AddSingleton(sp => new LinkedInPublisher(httpFactory.CreateClient(...), ...)). Same for X. Both IPlatformPublisher and concrete-type resolution return the same instance.

21. ✅ 478fdaa93SocialExtensions claims "all-or-nothing" but isn't.
The four AddHostedService<…> calls were unconditional even with zero platforms configured.

Fix: gate hosted-service registration on anyConfigured; with zero platforms, no hosted services start.

22. ✅ 478fdaa93LinkedInPublisher uses dynamic to peek at typed-anonymous fields.
Fix: two concrete payload shapes in if/else branches; no dynamic dispatch; typos surface as compile errors instead of RuntimeBinderException.

23. ✅ 478fdaa93 — PII / user-content in error logs.
Fix: Truncate(b, 200) on logged error bodies in both publishers (LinkedIn publish + token refresh, X publish). Full body still goes to PublishResult.Error for the caller.

✅ Looks good (no action needed)

  • NuGetAssemblyResolver correctly caches by (framework, sorted package list) so repeated #r invocations don't re-walk dependencies.
  • MessageHub AsyncSubject pattern fixes the long-standing "subscribe before vs after response" race in the old RegisterCallback.
  • LinkedInPublisher correctly handles the LinkedIn x-restli-id header fallback and only falls back to JSON body parsing when the header is missing.
  • SocialOptions defaults look reasonable (60s publish tick, 30m stats tick, 30d window).
  • EnsureFreshAsync returns a refreshed PlatformCredential to the caller rather than mutating internal state — caller decides where to persist.

Areas not covered in this review

Persistence-service refactors (IStorageService, MeshNodeEditor, NavigationService changes), the +850-line MessageHub core-dispatch refactor in detail, content-collection changes, NodeType compilation pipeline beyond what part 1 touched. Flag a specific subsystem if a deeper review is wanted.

@rbuergi

rbuergi commented May 10, 2026

Copy link
Copy Markdown
Contributor Author

Review fixes applied — all 23 items addressed

5 commits, organised by batch. Locally committed, not pushed yet.

# Item Commit
1 UNION SQL param-rename regex pass e68636aac
2 UNION ALL + DISTINCT ON (namespace, id) for path-keyed dedup e68636aac
3 ObserveQuery change-notifier OR-joined per-query filters e68636aac
4 MeshQueryEngine Activity post-filter scans every basePath e68636aac
5 RecoverStaleExecutingThread structural guard (drop time-based heuristic) 478fdaa93
6 using var on Subject<StreamingSnapshot> 478fdaa93
7 Regression assertion: final ThreadMessage.Status == Completed eea8ed10a
8 Pre-allocate CancellationTokenSource in HandleSubmitMessage 478fdaa93
9 Collapse triple-stacked <summary> blocks 478fdaa93
10 IsExecutingLifecycleTest waits on CompletedAt, not text patterns eea8ed10a
11 Limit-on-first-query semantics: enforce post-union via MinLimit e68636aac
12 NuGetAssemblyResolver evicts faulted/cancelled cache entries 512adb462
13 NuGet DependencyBehavior.HighestMinor (was Lowest) 512adb462
14 Hydrated-cache validation note (deferred — needs INuGetPackageCache change) 512adb462
15 XPublisher defensive TryGetProperty chain 478fdaa93
16 LinkedIn / X publishers retry once on 401 with token refresh 478fdaa93 (LinkedIn structure), 512adb462 (X 401 retry parity)
17 PostStatsRefresher uses Parallel.ForEachAsync (DOP 8) 512adb462
18 Per-target failure backoff (15 min default) 512adb462
19 Channel-based dispose trace replaces global file lock df1939bb7
20 SocialExtensions: factory-resolved singleton publishers 478fdaa93
21 Hosted services gated on at least one configured platform 478fdaa93
22 LinkedIn dynamic→concrete payload shapes 478fdaa93
23 Cap error-body logs at 200 chars (LinkedIn + X) 478fdaa93

Verification

  • Solution build clean (memex/aspire/Memex.Portal.Distributed).
  • Tests I touched all pass locally:
    • IsExecutingLifecycleTest.SingleMessage_IsExecuting_FlipsTrueThenFalse_WithRealResponse — 11 s
    • ChatHistoryTest.ThreeMessages_AgentSeesFullHistory — 2 s
    • ChatHistoryTest.TwoMessages_NoDuplicates_CorrectRoles — 3 s
    • CancelThreadExecutionTest.CancelStream_StopsExecutionAndMarksAsCancelled — 3 s
  • The full MeshWeaver.Threading.Test suite has 4 unrelated pre-existing failures (not introduced by these commits — present on main as well).

Notes

  • Update release-packages.yml #14 (cache content validation) is documented as a TODO rather than implemented — INuGetPackageCache.TryHydrateAsync doesn't currently expose a content hash to verify against, so the fix needs a contract change. Flagged in code at NuGetAssemblyResolver.EnsureInstalledAsync.
  • Get rid of project dependencies in NotebookEditorControl #5 (recovery time window) swapped time-based heuristic for a structural one (PendingUserMessage + ActiveMessageId set → leave to WatchForExecution). Same intent, no time-bound failure mode.
  • [Snyk] Upgrade axios from 0.25.0 to 0.27.2 #8 (CTS race) required a structural change: HandleSubmitMessage now pre-allocates and stores the CTS before the response goes out, ExecuteMessageAsync reuses it from the parent hub slot. The auto-execute path (WatchForExecution) gets a fallback CTS if the slot is empty.

Ready to push when you want.

@rbuergi

rbuergi commented May 10, 2026

Copy link
Copy Markdown
Contributor Author

Done — review item #14 is now closed in commit 6c3e60925. The hydrated folder is validated via PackageFolderReader.GetIdentity() against the expected (id, version); on mismatch the directory is purged and the resolver falls back to the feed. No INuGetPackageCache contract change needed — validation is in the resolver. Total: 6 commits, all 23 review items addressed.

rbuergi added a commit that referenced this pull request May 10, 2026
…fix DI lifetimes, redact PII, drop dynamic

- ThreadExecution: collapse triple-stacked <summary> blocks on
  WatchForExecution and NotifyParentCompletion. Tooling kept the last
  one anyway; the dead scaffolding was just noise.
- SocialExtensions: register LinkedInPublisher / XPublisher as TRUE
  singletons (factory-resolved with named HttpClient). The previous
  AddHttpClient<T>+AddSingleton<IPlatformPublisher> mix made the
  concrete type transient while the interface alias was singleton —
  direct vs via-interface resolution returned different instances.
  Also gate hosted-service registration on at least one platform
  being configured (the "all-or-nothing" comment was wrong; with
  zero platforms the four hosted services started anyway and faulted
  on first tick).
- LinkedInPublisher: replace `(dynamic)media.shareMediaCategory`
  peek with two concrete payload shapes — typo turns into a compile
  error instead of a RuntimeBinderException.
- LinkedIn / X publishers: cap error-body logs at 200 chars to
  bound PII exposure (the body can echo the user's post text on
  validation rejection). Full body still goes to PublishResult.Error
  for the caller.

Addresses PR #95 review items #9, #20, #21, #22, #23.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
rbuergi added a commit that referenced this pull request May 10, 2026
… in-memory engines

PostgreSqlStorageAdapter.QueryNodesAsync(IReadOnlyList<ParsedQuery>):
  - Replace order-dependent `string.Replace` parameter rename with a
    single `Regex.Replace` keyed on @<name> word boundary that gates
    on perParams.ContainsKey. Sequential Replace was mangling adjacent
    tokens (renaming `@p` after `@p1` produced `@q0_q0_p1`) and could
    clobber `@…` substrings inside string literals / JSONB paths.
  - Switch from `UNION` to `UNION ALL` wrapped in
    `SELECT DISTINCT ON (namespace, id) ... ORDER BY namespace, id, last_modified DESC`.
    Plain UNION dedupes whole rows — two queries observing the same
    node at slightly-different last_modified would BOTH appear in the
    output. Path-keyed dedup (= MeshNode identity) with newest-wins
    tie-break collapses them correctly.

PostgreSqlMeshQuery.ObserveQuery<T>:
  - Parse EVERY query in request.EffectiveQueries and build per-query
    (basePath, scope) filters; the change-notifier subscription
    OR-joins them so multi-query observations get delta refreshes
    triggered by ANY query's path/scope, not just query #0's. The
    previous shape silently lost live updates from queries #1+.

PostgreSqlMeshQuery.QueryNodesUnionAsync + MeshQueryEngine:
  - Drop the per-query `parsedList[0].Limit = request.Limit` injection.
    Query #0 hit its limit before yielding the union's most relevant
    rows, while queries #1+ contributed unbounded — making the result
    iteration-order dependent. Limit is now enforced post-union via
    MinLimit(request.Limit, firstParsed.Limit) so a request-level cap
    can't be circumvented and an in-query `limit:N` still wins when
    smaller.
  - MeshQueryEngine: CollectMatchedAsync returns the LIST of every
    query's basePath; the source:activity post-filter scans every
    base path's descendants and unions activity-main-paths so
    queries #1+ aren't filtered against query #0's subtree only.

Addresses PR #95 review items #1, #2, #3, #4, #11.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
rbuergi added a commit that referenced this pull request May 10, 2026
…ThreadExecution stability fixes

ThreadExecution.cs (already in commit 478fdaa — recapping here for the
review-item index):
  - RecoverStaleExecutingThread: drop the 2-minute "fresh execution"
    window in favour of a structural check (skip when PendingUserMessage
    + ActiveMessageId are still set, i.e. the thread is an
    auto-execute candidate WatchForExecution will pick up). Closes the
    "long-running agent crashed at minute 5 → IsExecuting=true forever"
    gap; the time-based heuristic contradicted commit 6dc436b's
    "no time limits" stance.
  - Subject<StreamingSnapshot>: declare with `using var` so the
    Subject itself disposes alongside its subscription. Minor leak
    per execution previously.
  - HandleSubmitMessage: pre-allocate the per-round
    CancellationTokenSource and store it on the thread hub BEFORE
    posting SubmitMessageResponse — closes the race where an early
    Stop click between IsExecuting=true and ExecuteMessageAsync's
    `parentHub.Set(executionCts)` found a null CTS slot and
    silently no-op'd. ExecuteMessageAsync now reuses the
    pre-allocated CTS (with a fallback for the auto-execute path
    that bypasses HandleSubmitMessage).

IsExecutingLifecycleTest.cs:
  - Migrate the response-text wait from text-pattern matching
    (skipping placeholders "Allocating agent..." etc.) to
    `ThreadMessage.CompletedAt is not null`, which
    ExecuteMessageAsync sets only on the terminal
    PushToResponseMessage call. Same pattern adopted in
    ChatHistoryTest in commit ab3af8b.
  - Add a regression assertion that final
    ThreadMessage.Status == Completed. The terminal-status guard in
    PushToResponseMessage prevents the late Sample(100ms)-flushed
    Streaming push from regressing the cell from Completed back to
    Streaming; this assertion catches any future regression of that
    guard.

Addresses PR #95 review items #5, #6, #7, #8, #10.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
rbuergi added a commit that referenced this pull request May 10, 2026
…, parallelism, backoff)

NuGetAssemblyResolver:
  - Evict faulted/cancelled tasks from the per-key cache before
    returning. A transient feed failure (network, throttle, cancelled
    in-flight resolve) used to poison the cache for the resolver's
    lifetime — every subsequent call replayed the same exception.
  - Pass CancellationToken.None to the shared core task so a single
    caller's cancellation can't take down the resolution for
    others; per-caller `ct` projects via `task.WaitAsync(ct)`.
  - Switch DependencyBehavior from `Lowest` to `HighestMinor` so
    `#r` directives pick up patch-level security fixes via
    transitive dependencies without silently jumping major/minor.
  - Document that hydrated cache content is trusted to match
    (id, version) — flag for future content-hash verification if
    cache poisoning becomes a concern.

LinkedInPublisher / XPublisher (LinkedIn already committed in batch A
for the dynamic+PII parts; this commit adds the 401 retry):
  - SendWith401RetryAsync: on the FIRST 401 response from a publish,
    force-refresh the token (zero ExpiresAt before EnsureFreshAsync)
    and retry once. Closes the race where the access token's TTL
    expired between EnsureFreshAsync and the actual API call.

PostStatsRefresher:
  - Process due-refresh targets via Parallel.ForEachAsync bounded
    by SocialOptions.StatsRefreshDegreeOfParallelism (default 8),
    so a slow API + large refresh window can't let one tick
    overshoot the next interval.
  - Per-target failure backoff via a ConcurrentDictionary of
    last-failure timestamps — targets that failed within
    StatsRefreshFailureBackoff (default 15 min) skip the next tick.
    Stops a degraded platform from generating thousands of repeat
    warnings every cycle while the underlying issue is fixed.
    Success clears the backoff entry.

SocialOptions: add StatsRefreshDegreeOfParallelism (8) and
StatsRefreshFailureBackoff (15 min) knobs.

Addresses PR #95 review items #12, #13, #14, #16, #17, #18.
(#15 XPublisher defensive parse + the LinkedIn dynamic / PII items
were already in commit 478fdaa.)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
rbuergi added a commit that referenced this pull request May 10, 2026
… file lock

The MESHWEAVER_DISPOSE_TRACE=1 trace took a global lock per call
(`File.AppendAllText` under `lock (DisposeTraceLogLock)`), serialising
hub teardown under load when many hubs disposed concurrently.

Replaced with a single bounded `Channel<string>` (capacity 4096,
FullMode = DropWrite) drained by one writer task started in the
type initialiser. Producers `TryWrite` non-blocking — if the disk is
slow / locked, lines drop on full instead of putting back-pressure
on dispose. Single-reader semantics avoid contention on the file
handle.

Addresses PR #95 review item #19.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
rbuergi added a commit that referenced this pull request May 10, 2026
Replaces the TODO from commit 512adb4. After a successful
INuGetPackageCache.TryHydrateAsync, the resolver now opens the
hydrated folder via PackageFolderReader and compares the package's
own .nuspec-declared (id, version) against the expected (id, version).
On mismatch the directory is purged and the resolver falls back to
the feed.

This catches the failure modes #14 was about: wrong package stored
under right key (cross-tenant blob, accidental copy, drift after a
manual edit). The .nuspec is the canonical NuGet source of truth, so
a tampered cache entry can't fake the identity without rewriting the
nuspec — which we'd then catch at hydration time.

No INuGetPackageCache contract change; validation lives entirely in
the resolver.

Closes the last open item from PR #95 review (item #14).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@rbuergi

rbuergi commented May 26, 2026

Copy link
Copy Markdown
Contributor Author

@copilot resolve the merge conflicts in this pull request

@github-actions

github-actions Bot commented Jun 3, 2026

Copy link
Copy Markdown

Test Results (shard 1)

1 011 tests   1 009 ✅  5m 13s ⏱️
   11 suites      2 💤
   11 files        0 ❌

Results for commit c984427.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Jun 3, 2026

Copy link
Copy Markdown

Test Results (shard 0)

1 146 tests   1 146 ✅  1m 45s ⏱️
   12 suites      0 💤
   12 files        0 ❌

Results for commit c984427.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Jun 3, 2026

Copy link
Copy Markdown

Test Results (shard 3)

1 113 tests   1 113 ✅  7m 35s ⏱️
   11 suites      0 💤
   11 files        0 ❌

Results for commit c984427.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Jun 3, 2026

Copy link
Copy Markdown

Test Results (shard 2)

   13 files     13 suites   8m 25s ⏱️
1 183 tests 1 181 ✅ 1 💤 1 ❌
1 208 runs  1 206 ✅ 1 💤 1 ❌

For more details on these failures, see this check.

Results for commit c984427.

♻️ This comment has been updated with latest results.

rbuergi and others added 8 commits June 23, 2026 21:25
…fix missing haiku price)

Adds AnthropicProviderCatalogTest covering the previously-untested Anthropic provider:
- AddAnthropic registers ONE BYO-key catalog source with the direct api.anthropic.com
  endpoint and the current Claude model ids (opus-4-8 / sonnet-4-6 / haiku-4-5-20251001).
- BuiltInLanguageModelProvider materialises it into a ModelProvider node + one key-less,
  public LanguageModel child per Claude id (the catalog the picker shows; admin keys later).
- AzureClaudeChatClientAgentFactory routes those ids; non-Claude ids do not.
- every shipped Claude id has a built-in price row.

That last assertion caught a real bug: ModelPricing.Defaults had the undated "claude-haiku-4-5"
but AddAnthropic ships the dated "claude-haiku-4-5-20251001", and Default() only de-prefixes by
'/', so deployed Haiku showed no cost. Added the dated price row (1/5 USD) to match.

MeshWeaver.AI.Test now references MeshWeaver.AI.AzureFoundry (where AddAnthropic lives),
alongside the existing OpenAI/ClaudeCode provider refs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…d GetDataRequest

SetThreadHubIdentity (the thread hub's cold-activation initializer) self-read its own
node via hub.GetMeshNode(hub.Address) -- a GetDataRequest posted to the hub's OWN
address before any identity was established. PostPipeline correctly failed it closed
("AccessContext must never be null ... hub=Thread, GetDataRequest, target=Thread"),
reproduced deterministically by reading any NodeType-def node (`get @Thread`).

Read the own thread node's FIRST loaded emission off the local MeshNodeReference
reducer instead (GetMeshNodeStream -- posts NOTHING, so the never-null guard cannot
fire), filtering on MeshThread content so the owner (CreatedBy) is always present --
exactly the pattern InitializeThreadLifecycle already uses for cold-load recovery.

Scoped to the actual bug site: a broader GetMeshNode->GetMeshNodeStream primitive
swap deterministically regressed thread cancellation (InboxTool Cancel tests), so the
fix targets SetThreadHubIdentity, the sole trigger of the get @thread failure.

Repro: ThreadHubSelfReadTest activates the Thread hub and asserts no target=Thread
GetDataRequest AccessContext error is logged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…seeding

BuiltInLanguageModelProvider now emits TWO PartitionAccessPolicy nodes — one for the
Provider partition AND one for the legacy Model partition (the model picker / resolution
read `model` under the user's identity, so it also needs PublicRead; restored after the
catalog refactor, atioz 2026-06-23). Provider_Policy_IsPublicRead_WithLiftedWriteCaps used
ContainSingle across ALL PartitionAccessPolicy nodes, so it broke (found 2).

Target the Provider partition's policy by namespace, and add an assertion that the Model
partition also gets a PublicRead policy (covers the new behavior). No production change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Pages with public-read access were being shown to logged-out visitors.
The portal requires authentication for ALL mesh content; "public access"
governs what an *authenticated* user without an explicit grant may read,
not what an anonymous browser may see.

NavigationService.ProcessResolvedPath: a logged-out visitor (the explicit
Anonymous well-known id, captured synchronously on the inbound-activity
thread) is now unconditionally flipped to AccessDenied — even for a
PublicRead node, which still reaches the gate because PathResolutionService
resolves under a System bypass. ApplicationPage's AuthorizeView turns
AccessDenied into RedirectToLogin → /login?returnUrl=<page>, so signing in
bounces the visitor back to the page they originally requested. The old
per-node anonymous read-gate (public passes / private → denied) is removed.

ApplicationPage: skip the GetPreRenderedHtml load during the static
prerender pass for a logged-out visitor, so a PublicRead node's cached HTML
is not flashed before the interactive gate redirects them.

Tests: NavigationServiceTest/NavigationProgressTest now run as an
authenticated (System) visitor by default — System is non-anonymous and
short-circuits TrackNavigationActivity before the un-proxyable hub.Post.
The two anonymous cases assert AccessDenied for both private and PublicRead
nodes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ested page

After completing onboarding ("Get Started"), users were dropped on "/" (and
could bounce onto /welcome) instead of returning to the page they originally
tried to open. The returnUrl already flowed correctly through the whole auth
chain (RedirectToLogin → /login → /auth/login → OAuth callback → original
page); it was lost only at the onboarding leg.

- OnboardingMiddleware: when bouncing an authenticated-but-not-onboarded user
  to /onboarding, carry the page they were trying to reach as ?returnUrl=
  (this request's own path+query — inherently local, no open-redirect surface).
- Onboarding.razor: read [SupplyParameterFromQuery] ReturnUrl and, on both the
  existing-user and submit-success paths, navigate to it (falling back to "/").
  SafeReturnUrl() accepts only same-site relative paths, rejecting absolute /
  protocol-relative URLs so a crafted returnUrl can't become an open redirect.
- Welcome.razor: preserve returnUrl through its sign-in links and its
  authenticated → /onboarding redirect so the chain is never dropped here.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…+ inline editing

Replace the clumsy access-control page with two query-driven sections (parent
scope and current scope) of clean rows, plus an inline add row and a collapsed
Advanced area for the partition policy.

- AccessControlLayoutArea: two MeshSearch sections from
  `namespace:{path}/_Access nodeType:AccessAssignment` for the current node and
  its parent; inline add row (user picker `namespace:"" nodeType:User` + role
  select defaulting to Editor) that stores the bare role id; partition-policy
  controls moved under "Advanced". Drops the dead "Inherited Permissions"
  section (was always passed an empty list) and the hand-built HTML banner.
- AccessAssignment Thumbnail/Overview: render one clean row per assignment
  (person/group thumbnail + a node-bound role editor per role + remove),
  replacing the hand-built HTML role chips.
- New MeshNodeRoleEditorControl + MeshNodeRoleEditorView: a node-bound role
  dropdown + Deny checkbox that read/write GetMeshNodeStream(path) directly
  (same shape as MeshNodeContentEditorControl) — no /data replica, no save
  subscription.
- Fix role storage to the bare role id ("Editor") that the permission evaluator
  resolves, and fix the role/subject node paths (`Role/{id}`,
  `namespace:"" nodeType:User`).
- Update AccessAssignmentThumbnailTest and AccessControlLayoutAreaTest to the
  new row/section structure and inline add row.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The anonymous gate emitted AccessDenied and relied on ApplicationPage's
<AuthorizeView><NotAuthorized><RedirectToLogin> render-chain to navigate —
which did not fire, leaving the logged-out visitor sitting on the page.

Redirect directly from NavigationService.ProcessResolvedPath instead:
NavigateTo("/login?returnUrl=<current absolute URL>", forceLoad: true). This is
unconditional, drops any half-built interactive circuit on the gated page, 302s
during prerender, and carries returnUrl so login bounces the visitor back. The
/login?returnUrl=… target is a page route (single segment + query) that
ProcessLocationChange short-circuits, so it cannot loop.

Tests updated: the two anonymous cases now assert the /login?returnUrl= redirect
(private + PublicRead) instead of the AccessDenied status. 38/38 green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…Compatible provider, bootstrap + opt-in DevLogin

Enables running memex prod-like on a local k3s (Colima) self-host:

- memex-postgres: PVC (volumeClaimTemplates) instead of emptyDir so data survives pod restarts
- memex-portal: wait-for-postgres initContainer — fixes the portal-vs-pg startup race on fresh install
- memex-portal config: OpenAICompatible__{Endpoint,Models} (self-hosted OpenAI-wire gateways: Ollama/vLLM/LM Studio) + one-shot Bootstrap__Secret
- memex-portal secret: OpenAICompatible__ApiKey + Authentication__Microsoft__ClientSecret (conditional)
- Distributed Program.cs: honor Authentication__EnableDevLogin=true for local/self-host (prod default still forces it off)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
rbuergi and others added 21 commits June 24, 2026 01:41
…individually

Spell out the MeshNodeStreamCache read-identity rule and the failure mode it prevents:
the shared per-path upstream (and any per-path sync hub's BuildupAction) MUST open
under System, never the ambient user. A leaked user identity (e.g. a co-active admin's
session) gets RLS-evaluated against the node, and because it faults the SHARED stream/hub
it goes FAILED and wedges the node for everyone including its owner (the 2026-06-23 atioz
symptom: rbuergi leaked onto sglauser/_UserActivity/sglauser; the owner's activity page
blanked until restart). Per-subscriber Read is validated at the GetStream seam (isolated
to that subscriber, never faulting the upstream); writes are validated at the owning hub.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…user

Grain activation reads the node (to learn its HubConfiguration) via the mesh-node
cache, which captures the ambient AccessContext eagerly and applies its per-subscriber
RLS gate. With two users active concurrently, a grain whose activation is triggered by
user A's message reads user B's node under A -> "User 'A' lacks Read permission on
'B/...'" -> the activation faults -> the per-node hub goes FAILED and the node WEDGES
for its owner (the 2026-06-23 atioz cross-user "boom": sglauser's submit faulted
activation of rbuergi/_Thread/...). Wrap the activation read in ImpersonateAsSystem
(via Defer so System is live when GetStreamRaw captures eagerly). The activated hub
still enforces per-request RLS on the data it serves; only the activation read is System.

STOPGAP scope: this is the framework activation-boundary half of a broader fix. The
root is that infra seams read the singleton AccessService.Context AsyncLocal, which
leaks across co-active users and is lost on Rx hops -- patched today by dozens of
hand-woven ImpersonateAsSystem scopes. The durable fix is framework-level access-context
forwarding (carry the established identity on every stream so seams never read the bare
ambient), which subsumes this wrap. See MeshNodeStreamCache.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…skipped RED)

Deterministic AccessService unit repro of the 2026-06-24 atioz "boom" root cause:
AccessService.CircuitContext (AccessService.cs:69) falls back to the process-wide
instance field persistentCircuitContext when the circuitContext AsyncLocal is null
(any scheduler/Rx hop), and SetCircuitContext writes that shared field unconditionally
(:104). So user B's circuit clobbers it globally and user A's hopped read observes B
-> the wrong identity enters the infra read/post seams -> a shared hub faults -> wedge.

Confirmed RED (Observed ObjectId='userB', 293ms). Skipped so it doesn't break the
shared branch; un-skipped when Stage 5 of the AccessContext-forwarding plan removes the
persistentCircuitContext fallback. See MeshNodeStreamCache.md + the plan.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The full-page thread hero (title, context link, modified-nodes summary,
Mark Done) was emitted as a sibling above the chat control, so it sat
outside the message scroll area and stayed pinned at the top.

Render it as its own FullHeader layout area inside .thread-chat-messages
(gated by ThreadChatControl.ShowFullHeader) so it scrolls away with the
conversation — same pattern as the existing compact Header area. Side
panel leaves the flag false, so its title still lives in the panel chrome.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add an optional `ollama` Service (selector-less Service + manual Endpoints
to the host gateway) so the portal addresses a host-native Ollama by a stable
name (http://ollama:11434/v1) instead of a hardcoded host-gateway IP. On macOS
the GPU is reachable only from host processes (Docker/k8s have no Metal
passthrough), so Ollama runs on the host; the Service keeps GPU speed while
giving a clean name. Gated on `ollama.external.enabled`, off by default so
cloud deploys are unaffected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ead of swallowing

The chat "screen disappears" on model-select/submit was a swallowed exception: the
data-binding fault arms and the combobox/selection write only logged (Debug/Warning)
and rendered an empty/default value, so the user saw a silent blank with no reason.

- BlazorView.DataBind: both the node-bound and Stream-branch onError arms now call
  SurfaceError(ex, context) (PortalErrorModal + inline SurfacedError + Error log), while
  still rendering the default so the control draws instead of spinning. Every data-bound
  view now surfaces stream faults rather than blanking.
- MeshNodeBindingExtensions.Write: add an optional onError callback (the static write
  helper lives in Mesh.Contract and can't reach the Blazor SurfaceError); BlazorView.
  UpdatePointer passes ex => SurfaceError(...) so a failed combobox/selection write (e.g.
  the ModelProviderSelection 42P01) pops a modal instead of silently dropping.

SurfaceError suppresses teardown (ObjectDisposedException) and the PortalErrorModal drains
one dialog at a time, so this surfaces real faults without a modal storm. Follow-ups:
ThreadChatView's own swallow points, ModelProviderService.SetSelection (its .Catch erases
the cause), and a generic activity-grain MarkActivityFailed helper.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…erSelection cause

Finishes the GUI error-surfacing sweep for the chat-disappear: stop swallowing the
direct write/submit faults that BlazorView's data-binding surfacing doesn't cover.

- ThreadChatView: OpenComposerProjection, WriteComposerSelection (the /model /agent
  write) and the submit onError now SurfaceError / PortalErrorSink.Report instead of
  LogDebug/LogWarning — a failed composer load, selection write, or submit pops a modal
  naming the cause instead of silently blanking / clearing the spinner.
- ModelProviderService.SetSelection: its final .Catch collapsed every exception to a bare
  `false`, so the settings tab only ever showed a generic "Failed to update selection."
  Re-throw instead, so the caller's onError arm surfaces the real cause (e.g. the
  ModelProviderSelection 42P01).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nly)

The ModelProvider node detail page rendered the generic property grid, which showed
ApiKey and McpApiKey in PLAINTEXT — a leak of the platform's provider keys (the Anthropic
key was visible on atioz). Mark both [Browsable(false)] (same mechanism already used for
Models) so the generic editor drops them from both the Overview grid and the Edit form.
The key is now write-only on the GUI: set via the masked Settings -> Models card; never
displayed or read back. The value still serializes for the runtime factory + persistence.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nTool]

check_inbox is a high-frequency mid-round poll, not a user action — surfacing
"Calling check_inbox…" on every step is pure noise (and floods the Information
logs). Mark such internal-plumbing tools with a new [HiddenTool] attribute,
read the same way [ToolTimeout] is (via AIFunction.UnderlyingMethod), and have
AgentChatClient drop the paired FunctionCall/FunctionResult before forwarding
them to ThreadExecution. The tool still runs and its result still reaches the
model — only the UI tool-call chrome + tool-activity logs are suppressed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…device GPU voice

Unblocks the MacCatalyst/iOS build: the BlazorWebView portal dragged in the
Microsoft.AspNetCore.App shared framework, which has no maccatalyst/ios runtime
pack (NETSDK1082) — that's why the Windows app built but a Mac/iPhone one couldn't.
Render mesh layout areas with native MAUI views instead; the control models + the
layout-area streams are Blazor-agnostic and reused unchanged.

- src/MeshWeaver.Maui: native view pack (MauiViewRegistry / MauiControlRenderer /
  MauiView+DataBind / LayoutAreaView / AddMaui), mirroring BlazorViewRegistry but
  producing Microsoft.Maui.Controls views. Container + Label/Button/Html/Markdown/
  Icon/Progress/NamedArea (Wave 1).
- Memex.Client: drop Blazor (Sdk.Razor→Sdk, remove the Blazor RCLs + WebView.Maui,
  delete Components/*.razor + MainPage + wwwroot), native shell (InstanceManagerPage),
  native VoicePage, MauiProgram uses AddMaui().
- On-device voice on the Mac GPU via CoreML (apple-gpu flag, default on for
  MacCatalyst). Swiss-German model packaged (q5_0 bin + CoreML encoder, gitignored,
  bundled as maccatalyst MauiAssets). Apple Intelligence text layer + a phone-safe
  file logger.
- aspire AppHost: opt-in local LGTM observability + native-Ollama Qwen (MemexLocalStack).
- Docs: OnDeviceVoice.md, MacLocalStack.md. Test: MauiViewDataPathTest (net10, runs in CI).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- MeshWeaver.Maui: FormMauiView base (two-way, echo-suppressed, writes back via
  UpdatePointer) + TextField/TextArea/CheckBox/Switch; DataGridControl → a native
  header+rows table (PropertyColumnControl-driven, JSON property extraction).
- Memex.Client: LocalAreaPage renders a local layout area NATIVELY via the view
  pack's LayoutAreaView (proves the native portal path); a demo `home` area on the
  local hub; Home + Voice entries on the instance-manager shell.
- test: MauiViewDataPathTest extended to assert the DataGrid data path (Columns +
  rows the DataGridView consumes). Green headless in CI.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…, not a cross-partition fan-out

A nodeType:Thread query resolves to the `threads` SATELLITE; with no concrete
partition the cross-schema provider UNIONs that satellite across EVERY searchable
schema — an unbounded, all-partition scan that can wedge the portal (it dropped
the MCP transport mid-call). The user-home "Latest Threads" used exactly that
shape: `nodeType:Thread namespace:*/_Thread content.createdBy:{id}`.

Scope it to the viewer's OWN partition (derived from the layout area's hub
address, not the URL) as `nodeType:Thread namespace:{partition}/*_Thread`. That
form has a concrete first segment, so ResolvePinnedPartition pins it to the one
partition schema — the fan-out narrows to a single schema. Pure-logic test pins
the scoped-vs-fan-out routing decision (no PG fixture).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…tles

The /model picker queried nodeType:LanguageModel|ModelProvider and rendered
EVERY node as a selectable button, so a provider ("Anthropic") was pickable —
and selecting it wrote a provider PATH into the model field (a non-model
selection, the "can't select a model" symptom).

Now ModelProvider nodes render as non-selectable group titles with their nested
models listed underneath. Models group under their provider (title first, then
models by Order/Name); keyboard nav (↑/↓/Enter) and the first-highlight skip
titles; term-filtering keeps a title only when one of its models survives. The
generic /agent and /harness pickers have no provider nodes, so they are
unchanged (flat, all selectable).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- MeshWeaver.Maui: BadgeControl → pill (Border+Label), NavLinkControl → Button,
  MenuItemControl → Label.
- test: MauiViewDataPathTest asserts NavLink + Badge reach the views.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nder the whole thread

BlazorView.OnParametersSet re-runs BindData on every parent re-render, and
ThreadChatView passes a fresh MarkdownControl(text) for every message bubble on
every streaming tick. So each tick re-ran Markdig for EVERY chat bubble — O(bubbles
× ticks) server CPU that pegs the circuit and stutters/crashes Safari. Worse, a
re-parse of identical markdown can emit fresh auto-generated element ids, making
the SignalR diff non-empty for every unchanged bubble each tick (large frames
Safari handles worst).

Memoize the parse per MarkdownView instance: when (markdown, owner, nodePath) are
unchanged, reuse the cached Html/CodeSubmissions instead of re-running Markdig. The
actively streaming cell's text changes each tick, so it still re-parses correctly;
every completed bubble becomes a no-op (identical Html → empty diff). App-wide fix,
not chat-specific.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…bobox & display options

The /search page (hit Enter from the top bar) now defaults to a google-like
flat grid of node cards (title + description) ordered by relevance, up to 4
per row, instead of bucketing everything by NodeType.

- Implement the previously no-op Flat render mode in MeshSearchView: one
  relevance-ordered group, no NodeType buckets. ApplySorting now preserves the
  query's Score-desc order (set by ClipMergedInitial) for flat text searches
  instead of re-sorting by Order/Name.
- Add an opt-in discreet view-options bar (ShowViewOptions): a "Group by"
  combobox (None / Type / Namespace / Category) that re-buckets in place, plus a
  display menu to show/hide the search bar and section counts at runtime.
- Search.razor defaults to Flat + 4 columns + view options; ?groupBy / ?mode
  overrides still work (top-bar groupBy=Namespace links unaffected).

Opt-in, so the other MeshSearchView usages (catalogs, pickers, dashboards,
node Search areas) are untouched.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- MeshWeaver.Maui: SelectControl → MAUI Picker (reads the selected value, writes the
  chosen option's item back via UpdatePointer; literal options this wave).
- test: MauiViewDataPathTest asserts Select reaches the views (round-trips cleanly).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The per-bubble live timer ticked purely on the cell's Status == "Streaming", and
the 1s ticker stayed alive on messageStates.Any(Status == "Streaming"). If a
cell's status lagged/stuck at "Streaming" after the round ended, the clock counted
forever and the ticker re-rendered every second on an idle thread.

Gate both on the authoritative signal: a cell can only be live-streaming while the
THREAD is executing. The per-bubble live clock now requires IsExecuting; the
ticker no longer keys off cell Status (covered by IsExecuting + sub-thread
IsExecuting). When the thread becomes idle the clock freezes at CompletedAt (always
stamped by the terminal completion push) and the ticker goes quiet.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ound boundary

The mid-round check_inbox tool bridged a TaskCompletionSource onto the hub
action-block scheduler to deliver queued messages inline — a hand-woven async
gate, the exact shape the actor model deadlocks on, and the suspected cause of
threads crashing / "the chat disappears" when follow-up messages arrive.

Stop injecting the tool. Follow-ups typed during a round queue in
PendingUserMessages and are ingested ONLY after the round finishes:
ThreadSubmissionServer.InstallServerWatcher already observes Status == Idle with
pending messages and dispatches the next round — race-free, serialized by the
owning hub, no TCS gate. The [HiddenTool] filter stays (now a no-op) for any
future internal tool. A better mid-round design (a reactive notification channel,
no TCS) can come later.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Pairs with disabling the mid-round inbox: instead of accepting a follow-up
mid-round (which now only ingests after the round ends anyway), the composer's
Send button is disabled while ThreadViewModel.IsExecuting, and SendMessage
early-returns on the Enter-key path. The button title explains the wait and that
Esc cancels. Submit re-enables the moment the thread goes idle.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…r Key" button

Replaces the generic node editor for ModelProvider (which leaked the ApiKey in plaintext
and showed an Edit/Copy/Move/Delete button row, and whose /Edit route was broken) with a
custom Overview+Edit area: just the editable endpoint URL (node-bound, auto-persisting)
and an "Enter Key" button. The key is NEVER displayed — only SET via a masked password
dialog. Save inlines RotateKey's logic (encrypt via IProviderKeyProtector + own-node
stream.Update + force-persist SaveMeshNodeRequest) because ModelProviderService lives in a
project MeshWeaver.AI can't reference; runs under the caller's identity (RLS-gated, no
ImpersonateAsSystem). Both the detail page and /Edit now render this minimal form.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants