Skip to content

refactor(sessions): decompose LlmSessionActor into transient-state handlers (steps 1–4)#1496

Merged
Aaronontheweb merged 6 commits into
netclaw-dev:devfrom
Aaronontheweb:refactor/llmsession-step1-typed-ids
Jun 25, 2026
Merged

refactor(sessions): decompose LlmSessionActor into transient-state handlers (steps 1–4)#1496
Aaronontheweb merged 6 commits into
netclaw-dev:devfrom
Aaronontheweb:refactor/llmsession-step1-typed-ids

Conversation

@Aaronontheweb

Copy link
Copy Markdown
Collaborator

What

Incremental, behavior-preserving decomposition of the 4,768-line LlmSessionActor, moving loose transient state into small owned handler objects (the established Sessions/Handlers/* idiom) and closing a stringly-typed seam. Net effect on the actor: −43 lines and several loose fields/methods replaced by cohesive handlers.

Steps (one commit each)

  1. Type reminder/job IDs as value objects + extract InFlightTurnDedup — reminder/background-job IDs become the existing ReminderId/BackgroundJobId value objects end-to-end (MessageSource, ChannelInput, SessionState dedup ledgers, TurnRecorded, TurnCompleted). They map to the same bare-string proto/DTO fields, so old journals/snapshots still deserialize (manifest strings + proto field numbers unchanged) — proven by new SerializationRoundTripTests. In-flight dedup state moves into a new InFlightTurnDedup handler.
  2. Extract ModelInputMediaBuffer — the transient model-input media buffer moves into its own handler; AddModelInputMediaNudge (which mutates _state) stays in the actor.
  3. Extract SessionPhaseMachine — phase + legal-transition validation move into a handler; the Become() switch stays co-located in the actor so phase and behavior can't drift (illegal transitions still throw the same exception).
  4. Fold the available-tools list into DiscoveredToolCache — the cache now owns the exposed tool list it already mutated, removing the awkward external-state-passing (EvictAll/PrepareForNewTurn no longer take the list).

Plus a code-review fixups commit (stale-comment repointing, a redundant-copy fix in DrainSnapshot, generic-ifying InFlightTurnDedup, and typing ReminderDeliveryResult.ReminderDeliveryKey as ReminderId).

Invariants held

  • No behavior change — all moved state is transient; the existing suite is the safety net.
  • Wire-compat — value objects serialize to the existing string fields; the dedup ledgers are intentionally not in the snapshot (unchanged).
  • Handler convention — new handlers capture no actor primitives (Timers/Context/Self/_state/CTS) and never call Persist/Become.

Out of scope (deferred to a later test-first effort): the approval subsystem and the passivation/job-reap/restart-drain handshake.

Testing

  • Netclaw.Actors.Tests: 2484 passed
  • Netclaw.Daemon.Tests: 742 passed (covers the SignalR reminder-delivery path)
  • dotnet slopwatch analyze: clean; copyright headers verified
  • Reviewed at max effort via the multi-agent code-review workflow — no correctness bugs; all findings were cleanup items, now addressed.

…InFlightTurnDedup

Convert reminder/background-job identities from string to the existing
ReminderId/BackgroundJobId value objects along the dedup + journaling path
(MessageSource, ChannelInput, SessionState dedup ledgers, TurnRecorded event,
TurnCompleted output, channel reminder-delivery observer dictionaries).

Value objects map to the existing bare-string proto/DTO fields at the
serialization boundary, so persisted journals/snapshots and the SignalR wire
form are byte-identical (proven by new SerializationRoundTripTests).

Extract the in-flight reminder/job dedup state out of LlmSessionActor into a
new Sessions/Handlers/InFlightTurnDedup handler, mirroring ActiveExecutionTracker.
Move the transient model-input media buffer out of LlmSessionActor into a new
Sessions/Handlers/ModelInputMediaBuffer. Accumulation, drain-on-batch-complete,
and reset are behavior-preserving; AddModelInputMediaNudge (which mutates _state)
stays in the actor. DrainSnapshot returns the buffered references and clears in
one call, collapsing the prior nudge-then-clear pairs.
Move the explicit SessionPhase field and legal-transition validation out of
LlmSessionActor into a new Sessions/Handlers/SessionPhaseMachine. TransitionTo
now delegates validation to _phase.TryTransition, but the Become() switch,
processing-state emission, and observer notification stay in the actor so phase
tracking and behavior remain co-located (no drift). Drops the now-redundant
IsLegalTransition wrapper.
DiscoveredToolCache now owns the exposed tool list and base-tool count, removing
the external-state-passing where LlmSessionActor handed its list to
EvictAll/PrepareForNewTurn on every call. The actor seeds base tools once
(SeedBaseTools), reads the list via AvailableTools, and adds discovered tools via
AddIfMissing (returns whether it added, so the actor keeps its log line).

Tool-access-policy orchestration (TryActivateDiscoveredTool,
ResolveExposedToolsForCurrentTurn) stays in the actor: dragging the registry,
access policy, and trust context into the cache would be plumbing without a win.
Cleanup follow-up to the LlmSessionActor decomposition (no behavior change):

1. Repoint three stale comments that named the deleted
   _pendingModelInputMediaReferences field at the now-correct
   ModelInputMediaBuffer / DrainSnapshot mechanism (SessionState.BuildNudgeMessage
   + two regression-test comments).
2. ModelInputMediaBuffer.DrainSnapshot now hands off its backing list and adopts
   a fresh one (swap) instead of ToArray-copying; BuildNudgeMessage's existing
   defensive copy remains the single copy, restoring the pre-refactor 1-copy cost.
3. InFlightTurnDedup collapses its duplicated reminder/job trios into one generic
   InFlightSet<T>, so the reserve-ignores-empty / complete-doesn't guard asymmetry
   lives in one place and can't drift.
4. ReminderDeliveryResult.ReminderDeliveryKey is now ReminderId (was string),
   closing the last stringly-typed seam on the reminder-delivery path: the four
   binding actors stop unwrapping with .Value, ReminderExecutionActor compares
   typed keys, and tests drop the .Value.Value double-unwrap. The message is
   INoSerializationVerificationNeeded (transient, never journaled), so this has
   zero persistence/wire impact.
@Aaronontheweb Aaronontheweb added the cleanup Code quality improvements and tech debt reduction label Jun 25, 2026

@Aaronontheweb Aaronontheweb left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Goal here is to move a bunch of plumbing out of the LlmSessionActor and introduce value objects for reminder / background job ids so we can avoid primitive confusion.

@Aaronontheweb
Aaronontheweb enabled auto-merge (squash) June 25, 2026 18:34
@Aaronontheweb
Aaronontheweb merged commit 972e52e into netclaw-dev:dev Jun 25, 2026
15 checks passed
@Aaronontheweb
Aaronontheweb deleted the refactor/llmsession-step1-typed-ids branch June 25, 2026 18:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cleanup Code quality improvements and tech debt reduction

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant