fix(logging): wrap sidecar IChatClient calls in SessionDiagnosticsContext (#920)#926
Merged
Aaronontheweb merged 3 commits intoMay 8, 2026
Conversation
…text Closes the gap tracked in netclaw-dev#920: session-owned sidecar paths bypassed the SessionDiagnosticsContext scope, so MEL provider diagnostics emitted during their LLM calls landed in the daemon log without a session-id tag and never reached session.log. Wrapped every direct IChatClient.{Get,GetStreaming}ResponseAsync call in session-owned code with a using-scope of SessionDiagnosticsContext.Push(sessionId.Value): - SessionCompactionPipeline.GenerateObservationsAsync — sessionId already on the signature; one-line wrap. - SessionMemoryObserverActor.RunDistillationAsync — sessionId already on the signature; one-line wrap. - SessionTitleGenerator.GenerateAsync — added SessionId parameter; caller in LlmSessionActor passes _sessionId. - LlmSessionActor.InvokeMemoryExtractionCoreAsync — added SessionId parameter; caller threads _sessionId. - SubAgentActor.InvokeLlmAsync — added string? sessionId parameter; passes _toolExecutionContext.SessionId. Sub-agents share the parent's session.log (SessionDiagnosticsContext.NormalizeSessionId strips the /subagent/ suffix back to the parent id). - MemoryCurationActor — added SessionId field + constructor param; CreateProps signature changed; LlmSessionActor caller updated; storage-integration tests updated to pass a placeholder sessionId. Test: SidecarDiagnosticsContextTests verifies the contract for SessionTitleGenerator and SessionCompactionPipeline (the two pure-static helpers, easiest to drive). A capturing fake IChatClient records SessionDiagnosticsContext.SessionId at the moment its method is invoked — that AsyncLocal value is what any real provider plugin emitting MEL log lines inside the call would see. Sidecar paths that did not need changes: - SessionLlmInvoker.InvokeAsync already pushes the scope (the original PR netclaw-dev#916 wired that path). Net diagnostic coverage now matches what the Roslyn analyzer planned in netclaw-dev#915 will require.
- MemoryCurationActor: replace Protocol.SessionId qualifier with a using alias matching the SessionLlmInvoker.cs:9 pattern. A full Netclaw.Actors.Protocol namespace import would have introduced a ChatRole ambiguity (Protocol.ChatRole vs Microsoft.Extensions.AI.ChatRole) that the bare alias avoids cleanly. - SidecarDiagnosticsContextTests: drop two defensive SessionDiagnosticsContext.SessionId = null pre-test resets. xUnit creates a fresh class instance per test method and AsyncLocal is rooted in the per-task ExecutionContext; there's no shared state to reset. The Assert.Null at the end of each test still verifies the using-scope restored properly. - SubAgentActor: tighten the diagnostics-scope comment from 4 lines to 3, dropping the redundant "shares parent's session.log" framing that's now covered by the SessionDiagnosticsContext doc.
Three follow-up fixes from the simplify-pass review on the sidecar
diagnostics-context PR. Each addresses something I had previously left
on the table:
1. Stringly-typed sessionId on SubAgentActor.InvokeLlmAsync replaced
with `SessionId? sessionId`. The call site converts the upstream
`_toolExecutionContext.SessionId` (which is still typed as string?
in Netclaw.Tools.Abstractions; out of scope to retype here)
into the value object once at the boundary.
2. Positional ordering on InvokeLlmAsync normalized: ct now last,
sessionId after options. The previous "ct between options and self"
placement was inherited from the original signature and was awkward
for the new sessionId parameter; the conventional order makes the
private API harder to mis-call from a future test.
3. Test coverage gap closed. All seven sidecar IChatClient call sites
now have direct unit tests in SidecarDiagnosticsContextTests:
- SessionTitleGenerator.GenerateAsync
- SessionCompactionPipeline.GenerateObservationsAsync
- LlmSessionActor.InvokeMemoryExtractionCoreAsync (now internal static)
- SubAgentActor.InvokeLlmAsync (now internal static; null-sessionId
case also covered)
- SessionMemoryObserverActor.RunDistillationAsync (now internal
static; was private instance — extracted to match the existing
SessionTitleGenerator/SessionCompactionPipeline pattern, _log
passed as ILoggingAdapter parameter)
- MemoryCurationActor.TryLlmEvaluationAsync (now internal static;
same shape — _llmClient, _sessionId, _log all passed as params)
The static extractions for RunDistillationAsync and TryLlmEvaluationAsync
are behavior-preserving refactors that follow the established pattern
of the other sidecar helpers in the same namespace. They make these
methods unit-testable in isolation without spinning up the parent
actor or driving the message protocol.
This closes the coverage gap I previously deferred to the planned
Roslyn analyzer (netclaw-dev#915). Now we don't depend on netclaw-dev#915 landing — the
contract is verified at the unit-test level for every call site.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes #920. Wraps every session-owned sidecar
IChatClient.{Get,GetStreaming}ResponseAsynccall inSessionDiagnosticsContext.Push(sessionId.Value)so MEL provider diagnostics emitted during the call route into the per-sessionsession.log.Before this PR, only the main turn path (
SessionLlmInvoker.InvokeAsync) populated the diagnostics scope. Sidecar paths — compaction, title generation, sub-agent orchestration, memory extraction, memory observer distillation, and memory curation — emitted to the daemon log without a session tag and never reachedsession.log. That gap was the limitation called out in #920 and the rationale for the planned Roslyn analyzer in #915.Sites covered
SessionCompactionPipeline.GenerateObservationsAsyncSessionTitleGenerator.GenerateAsync+SessionIdparameterSubAgentActor.InvokeLlmAsync+SessionId? sessionId; bumped tointernal static; null-sessionId case for sub-agents that run outside any sessionLlmSessionActor.InvokeMemoryExtractionCoreAsync+SessionIdparameter; bumped tointernal staticSessionMemoryObserverActor.RunDistillationAsyncprivateinstance tointernal static(passes_logas param) — matches the pattern of the other sidecar helpersMemoryCurationActor.TryLlmEvaluationAsyncprivateinstance tointernal static; constructor+SessionIdparameterThe two static extractions (
RunDistillationAsync,TryLlmEvaluationAsync) are behavior-preserving refactors that bring those helpers into the same shape as the existingSessionTitleGenerator/SessionCompactionPipeline. They make the methods unit-testable in isolation without spinning up the parent actor or driving the full message protocol.Tests
SidecarDiagnosticsContextTestscovers every path listed above. ASessionContextCapturingChatClientrecordsSessionDiagnosticsContext.SessionIdat the moment the chat client method is invoked — that AsyncLocal value is exactly what any real provider plugin would see when emitting MEL log lines inside the call.7 tests, all green:
TitleGenerator_populates_session_diagnostics_scopeCompactionPipeline_populates_session_diagnostics_scopeMemoryExtraction_populates_session_diagnostics_scopeSubAgent_InvokeLlm_populates_session_diagnostics_scopeSubAgent_InvokeLlm_with_null_sessionId_pushes_null_scopeMemoryObserver_RunDistillation_populates_session_diagnostics_scopeMemoryCuration_TryLlmEvaluation_populates_session_diagnostics_scopeThis PR's coverage is independent of #915 (the analyzer): the contract is now verified at the unit-test level for every call site whether or not the analyzer ever lands.
Quality gates
References