mirror of
https://github.com/okieselbach/AutopilotMonitor.git
synced 2026-10-24 12:42:38 +00:00
fbb338efcca5cd7255661d62d0c4ef84cf04326f
219 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
fdc0e3e8ce |
fix(v2): normalize timeline event Source values + stop periodic collectors before diag ZIP
Source attribution review on session df7abbc2 surfaced four inconsistencies in the Source field on the Events table — assembly leakage, V1-parity carry-overs, and a CamelCase / snake_case mix on the same emitter. Normalize so every Source is a CamelCase class-name or the stable lifecycle label "Agent": - Program.cs `agent_started` — "AutopilotMonitor.Agent.V2" -> "Agent" (V1 parity, drops assembly leak) - ImeLogTrackerAdapter `script_completed` — "IME" -> "ImeLogTracker" (consistent with the seven sibling events emitted from the same adapter) - StartupEnvironmentProbes (5 sites: device_location, agent_trace, timezone_auto_set, two ntp_time_check shapes) — "Network" -> "StartupEnvironmentProbes" (class-name consistency; eventType already encodes the sub-probe) - EventTimelineEmitter default — "decision_engine" -> "DecisionEngine" (CamelCase, matches the override path used by phase_transition; eliminates the per-event mix on the wire) Tests adjusted: ImeLogTrackerAdapterTests, StartupEnvironmentProbesTests, EventTimelineEmitterTests, InformationalEventPostTests. EnrollmentEvent.Source XML doc updated to reflect the new convention. Same session also showed `performance_snapshot` (seq 220) firing AFTER `diagnostics_collecting` (seq 219), because periodic collectors keep ticking through the termination handler. Fix: - New EnrollmentOrchestrator.StopCollectorHosts() — idempotent; the existing Stop() now delegates to it so we never double-stop. - EnrollmentTerminationHandler gains an optional `stopPeripheralCollectors` callback and invokes it best-effort right before RunUploadDiagnosticsWithEvents (after DelayLateEventGrace). The WhiteGlove Part 1 path returns earlier and is unaffected — the agent stays alive for Part 2. - Program.cs wires `() => orchestrator.StopCollectorHosts()` so all collector hosts (Performance, AgentSelfMetrics, DeliveryOptimization, ESP/Hello, IME log, …) are stopped before the diag ZIP is built. Late `performance_snapshot` / `agent_metrics_snapshot` no longer slip in after the `diagnostics_collecting` event. Tests: V2.Core 732/732, DecisionCore 232/232, Backend 736/736. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
4de2677a5a |
fix(v2): 882fef64 followup — restore V1 audit trail on enrollment_complete
V2's reducer-emitted enrollment_complete event landed in the backend with
empty Data{} — V1 had carried a signal audit trail (signalsSeen,
signalTimestamps, completionSource, helloOutcome) that downstream consumers
relied on to reconstruct "which signals drove completion" without re-reading
the agent's local final-status.json.
BuildEnrollmentCompleteEffect now takes (state, completionTrigger) and
attaches a structured TypedPayload. EventTimelineEmitter passes that dict
verbatim into EnrollmentEvent.Data, so the wire shape mirrors V1's audit
trail (plus enrollmentMode/JoinMode from the V2 ScenarioProfile).
Both kernel-driven completion paths now carry the trail:
- DecisionEngine.Classic: DeadlineFired:FinalizingGrace
- DecisionEngine.SelfDeploying: DeviceSetupProvisioningComplete
AdminPreemptionDetected keeps its existing adminAction/source params —
different completion semantics, leave alone.
Tests: DecisionCore 232 (+1), V2.Core 732, V1.Core 507, all green.
Solution build 0 warnings, 0 errors.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
ae330f54d9 |
fix(v2): 882fef64 PR4 — Hello policy → DecisionEngine + 10s grace + mismatch detector
Surface the WHfB / Hello-for-Business policy fact to the decision engine and use it to drive the post-ESP-exit Hello wait cadence. Policy=enabled keeps the 30s default; policy=disabled shortens to 10s as a sanity check against a buggy detector. A `hello_policy_detection_mismatch` Warning event fires when a Hello terminal arrives despite policy=disabled — that is the detector-bug indicator. The completion-gate is intentionally left untouched: completion is orthogonal to Hello status (the Hello+Desktop AND-gate already accepts any terminal Hello outcome). A feedback memory is saved so a future iteration doesn't re-add a completion guard on this fact. Foundation: - DecisionSignalKind.HelloPolicyDetected (v1) - SignalPayloadKeys.HelloEnabled / HelloPolicySource - DecisionState.HelloPolicyEnabled : SignalFact<bool>? (null = unknown) - DecisionStateBuilder.WithHelloPolicyEnabled fluent helper - HandleHelloPolicyDetectedV1 reducer (set-once, no-op on same value, update on different value, DeadEnd on missing/unparseable payload) Behavior: - HelloTracker.HelloWaitTimeoutDisabledSeconds = 10 - StartHelloWaitTimer picks 10s when policy explicitly disabled, 30s otherwise - HelloTracker.HelloPolicyDetected event re-raised through EspAndHelloTracker - EspAndHelloTrackerAdapter posts HelloPolicyDetected signal once per session - EmitHelloPolicyDetectionMismatch on Hello-terminal arrival under policy=disabled (wired into both ProcessHelloEvent and ProcessHelloForBusinessEvent paths) Tests: DecisionCore 231 (+11), V2.Core 727 (+11), Functions 736, vitest 179, solution build 0/0. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
511ed70361 |
v2 codex-post-#51: partial-skipUser guard regression + enum serialization parity
Addresses Codex second-pass on commit
|
||
|
|
51d57153ad |
v2 codex-05: typed ScenarioProfile + Observations + ClassifierOutcomes aggregates
Codex review follow-up #5 (Option 2b, clean-cut). Replaces the legacy 9-field hypothesis/fact mosaic on DecisionState with three structured aggregates that separate concerns cleanly: - EnrollmentScenarioProfile — derived classification (Mode, JoinMode, EspConfig, PreProvisioningSide, Confidence, EvidenceOrdinal, Reason) replacing the Hypothesis EnrollmentType + SkipUser/DeviceEsp facts. - EnrollmentScenarioObservations — raw per-signal evidence (ShellCoreWhiteGloveSuccessSeen, WhiteGloveSealingPatternSeen, AadUserJoinWithUserObserved, SkipUserEsp, SkipDeviceEsp) with set-once semantics, feeding the WhiteGlove sealing classifier snapshot. - ClassifierOutcomes — classifier verdict + anti-loop storage (WhiteGloveSealing, WhiteGlovePart2Completion, DeviceOnlyDeployment) replacing the three Hypothesis fields they used to occupy. EnrollmentScenarioProfileUpdater holds the signal → profile mapping rules with monotonic-confidence invariants. EspConfigDetected keeps its partial- first-signal / second-signal-completes semantic via the observations half- facts; the profile's EspConfig is only derived once both halves are known. Critical Codex correction: AadUserJoinWithUserObserved is NOT a JoinMode signal — it is a late-AADJ user-presence hard-excluder for the WhiteGlove classifier. JoinMode comes strictly from SessionStarted.payload.isHybridJoin. Classifier algorithm, weights, and thresholds unchanged; only snapshot input sources, anti-loop verdict storage, and the AwaitingHello guard migrate. The WhiteGloveSealingSnapshot fields keep their names and types, so ComputeInputHash stays byte-stable and the anti-loop contract is preserved across the refactor. AssemblyVersion stays 2.0.0.0. Suites: DecisionCore 203/203 (+39 new aggregate/updater tests), Agent.V2.Core 650/650, Functions 735/735; 0 warnings. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
bd3ec4e195 |
v2 codex-post-#50: recovery + abort durability follow-ups (#A #B #C #D)
Addresses Codex review against PR #50: #A (High) EnrollmentOrchestrator branch (c) now distinguishes empty-log (fresh start) from log-has-entries (full-log replay via ReducerReplay) so a crash-before-first-snapshot cannot regress state to StepIndex=0 while the SignalLog preserves older ordinals. #B (High) EffectInfrastructureFailure is now crash-durable. ApplyStep returns EffectRunResult; on SessionMustAbort the phantom snapshot is skipped, and SignalIngress synthesises + durably appends + reduces the terminal failure signal INLINE within the same worker iteration. The old in-memory PostEffectInfrastructureFailureSignal path in EffectRunner is removed. #C (Medium) ReducerReplay.Replay gained an Action<DecisionTransition>? onTransition overload. The orchestrator's recovery path truncates Journal AHEAD-of-seed phantoms before replay and rematerialises BEHIND entries bit-for-bit via the callback, closing the gap between SignalLog.Append and Journal.Append crashes. #D (Medium) ReducerVerifier.ReplaySemantically was seeding DecisionState with swapped (tenantId, sessionId) — tests mirrored the bug so it slipped through. Args fixed; defensive replay_identity_mismatch issue added so future swaps surface as an Error-severity verifier finding. Suite-Baseline all green, 0 warnings: DecisionCore 164/164, Agent.V2.Core 648/648, Functions 735/735. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
ed1c26f98b |
v2 codex-03: forward-link events to their originating transition + signal
Closes Codex review follow-up #3. Two distinct traceability gaps: 1. DecisionTransition.EmittedEventSequences was always empty. The journal record is appended before the effects run, so at construction time the Event.Sequence values haven't been assigned yet. Populating it later would need a journal-reorder or a sidecar mapping file — both more risk than payoff. 2. SessionTraceOrdinal had two independent meanings in the codebase: the reducer-step correlation counter (SessionTraceOrdinalProvider) shared by a signal and its transition, vs. the transport-cursor on TelemetryItem (== TelemetryItemId when session-scoped). The backend parser already prefers the inner (payload) value, but the shared name was misleading. Resolution: - Forward link instead of backward. EnrollmentEvent gets two new nullable columns, CausedByTransitionStepIndex and CausedBySignalOrdinal, populated in EventTimelineEmitter.Emit from currentState.StepIndex and currentState.LastAppliedSignalOrdinal (the reducer already has both, zero new plumbing). Nullable so legacy events and any future non-reducer emits stay valid. Inspector queries "events emitted by step N" = WHERE CausedByTransitionStepIndex = N — trivial index scan. - DecisionTransition.EmittedEventSequences stays on the type for journal backward-compat but the XML doc now states explicitly that it is always empty and points readers at the new forward link. - SessionTraceOrdinal XML docs clarified on DecisionSignal, DecisionTransition (by extension), and TelemetryItem: reducer- correlation counter is authoritative for payload, transport-cursor for envelope, backend parser resolves by preferring inner value. No code change — the two counters were already independent in practice; the name overload was the whole confusion. - Backend TableStorageService persists both new columns in StoreEventAsync + StoreEventsBatchAsync, reads them in MapToEnrollmentEvent. Entity setters guarded by HasValue so legacy agent uploads don't write null-as-zero. Tests (+2 in EventTimelineEmitterTests): - Emit populates CausedByTransitionStepIndex + CausedBySignalOrdinal from a state with StepIndex=7, LastAppliedSignalOrdinal=12. - CausedBySignalOrdinal is null when LastAppliedSignalOrdinal=-1 (fresh initial state sentinel), avoiding a meaningless -1 on the wire. Suites green: DecisionCore 161/161, Agent.V2.Core 645/645 (+2), Functions 734/734. Build 0 warnings. Plan: .claude/plans/codex-03-traceability.md Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
3bfe7d7fdb |
v2 codex-04: wire AppInstall signals as observation-level facts
Closes Codex review follow-up #4. Before this commit the reducer had no handler for DecisionSignalKind.AppInstallCompleted / AppInstallFailed — the signals were real (ImeLogTrackerAdapter posts one terminal per app) but fell through to HandleUnhandledSignal, so the journal accumulated 'unhandled_signal_kind:AppInstallCompleted:v1' dead-ends and the per-app outcomes disappeared. Subsequent ESP / IME classifiers had no structured evidence of which apps blocked. Scope: observation-only, as the Master-Tracker recommended. Neither handler changes Stage or Outcome — they roll the terminal outcome into a new DecisionState.AppInstallFacts aggregate that: - increments CompletedCount (+ Installed / Skipped / Postponed breakdown from the adapter's newState payload) - increments FailedCount and appends the appId to FailedAppIds, deduplicated and capped at MaxFailedAppIds (50) so pathological sessions cannot bloat DecisionState. Every DecisionSignalKind now has at least one handler; the dead-end path is reachable only through an unknown (kind, schemaVersion) pair — the genuine forward-compat failure mode per plan §2.5. The 'Still unwired' dispatch comment is replaced with a 'Fall-through: unknown (kind, schemaVersion)' label. Tests: - new AppInstallSignalHandlerTests (+10): Installed/Skipped/Postponed breakdowns, unknown newState forward-compat, no stage/outcome change, failed count + appId recording, dedup on repeated appId, MaxFailedAppIds cap keeps first-observed entries, mixed-stream integration covering all buckets in one fold. - HarnessTests.Replay_unknownSignalKind_recordsDeadEnd renamed to Replay_unknownSchemaVersion_recordsDeadEnd, switched from the now-wired AppInstallCompleted to a v99 SessionStarted so the dead-end path stays covered through its real-world trigger. - ReducerFailSafeTests.Reduce_unhandledKind same retarget. Suites green: DecisionCore 161/161 (+10), Agent.V2.Core 643/643, Functions 734/734. Build 0 warnings. Fixtures: - tests/fixtures/signal-kinds/app-install-completed-v1.json - tests/fixtures/signal-kinds/app-install-failed-v1.json Plan: .claude/plans/codex-04-appinstall-signals-wiring.md Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
8da8dc649d |
v2 codex-06: ReducerVerifier semantic replay — reducer-agreement CI gate
Closes Codex review follow-up #6. Until now GetSessionReducerVerificationFunction only ran structural checks (ordinal contiguity, step-index contiguity, ReducerVersion drift, orphaned SignalOrdinalRef). That catches corruption but never verifies the backend's reducer agrees with the agent-produced journal under the same ReducerVersion — exactly the invariant the whole (newState, transition, effects) architecture rests on. Unblocked by Codex #1 phase 1: ReducerReplay.Replay exists in DecisionCore, and the backend already references DecisionCore. Flow: 1. Skip preconditions: drift, ordinal gaps, step-index gaps, empty session, or head PayloadJson deserialization failure → record SemanticReplaySkipReason + optionally emit an Info 'replay_skipped' issue. Never throws. 2. Deserialise each SignalRecord.PayloadJson via SignalSerializer. 3. Fold through a transient DecisionEngine: state = Reduce(state, sig). 4. Compare each produced transition against the stored one on (StepIndex, Trigger, FromStage, ToStage, Taken, DeadEndReason). Discrepancies surface as 'replay_divergence' issues (capped at 20 individual entries; overflow message follows the same pattern as the structural gap reports). 5. Compare replayed final state's Stage to the last stored transition's ToStage — mismatch surfaces as 'replay_final_stage_mismatch' at Error severity. Report additions (backward-compatible): - SemanticReplayPerformed - SemanticReplaySkipReason - SemanticReplayFinalStageMatches - ReplayedFinalStage - TransitionDivergenceCount Side change: GetSessionReducerVerificationFunction XML-doc no longer claims "structural only" — now documents both layers and the skip rules. Tests (+7, ReducerVerifierSemanticReplayTests): - Skip on ReducerVersion drift. - Skip on signal-ordinal gap. - Skip on step-index gap. - Perfect agreement (reducer-seeded journal round-trips). - Divergence: tampered Trigger → one replay_divergence issue. - Divergence: tampered last-ToStage → replay_final_stage_mismatch Error. - Mid-stream deserialization failure tolerated without aborting. Existing structural ReducerVerifierTests unchanged — their stub PayloadJson='{}' deserialization failures now silently set SemanticReplaySkipReason='deserialization_failure' without polluting Issues, preserving the happy-path empty-issues assertion. Suites green: DecisionCore 151/151, Agent.V2.Core 643/643, Functions 734/734 (+7). Build 0 warnings. Plan: .claude/plans/codex-06-reducer-verifier-semantic-replay.md Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
386f3e909f |
v2 codex-02: EffectRunner posts EffectInfrastructureFailure on critical fail
Closes Codex review follow-up #2. Before this commit, a DecisionEffectKind.ScheduleDeadline / CancelDeadline failure raised EffectRunResult.SessionMustAbort but the decision world never heard about it — DecisionStepProcessor only logged and continued. A safety deadline could live in DecisionState without ever being armed on the scheduler, leaving the session to hang until the max-lifetime watchdog tripped hours later. Flow now: 1. EffectRunner.TryRunCriticalDeadlineEffect fails. 2. EffectRunner posts a synthetic DecisionSignalKind.EffectInfrastructureFailure signal onto the ingress before returning. Payload: reason + failingEffect. 3. Ingress worker picks it up next iteration. 4. HandleEffectInfrastructureFailureV1 (new, in DecisionEngine.Shared.cs) transitions Stage=Failed, Outcome=EnrollmentFailed, ClearDeadlines, and emits one EmitEventTimelineEntry effect for enrollment_failed with the reason from the payload. 5. DecisionStepProcessor observes the terminal stage and fires the Terminated event; EnrollmentOrchestrator tears down normally. EnrollmentFailed (not Aborted) distinguishes infrastructure-induced failures from user/operator-initiated aborts in forensics. The ingress post is wrapped in a try/catch so a full/closed channel cannot crash the worker — the max-lifetime watchdog remains the last-resort exit. Changes: - DecisionSignalKind: new EffectInfrastructureFailure enum value. - DecisionEngine: dispatch entry (v1). - DecisionEngine.Shared: HandleEffectInfrastructureFailureV1 handler, mirrors HandleSessionAbortedV1 shape but with EnrollmentFailed outcome and a failure timeline effect. - EffectRunner: PostEffectInfrastructureFailureSignal helper, called from the critical-effect path before returning SessionMustAbort. - EffectRunResult: XML-doc clarifies that SessionMustAbort is now observability-only (reducer terminates on its own). - DecisionStepProcessor: comment update — no code change needed, the synthetic signal reaches state naturally. - tests/fixtures/signal-kinds/effect-infrastructure-failure-v1.json: replay fixture per the enum's self-documented contract. Tests (+8): - DecisionCore EffectInfrastructureFailureHandlerTests: transition shape, deadline clearing, enrollment_failed emission with reason, missing-reason fallback, double-post idempotency. - EffectRunnerTests: schedule-fail posts signal with correct payload, cancel-fail same, ingress-post exception tolerated. - FakeSignalIngressSink: ThrowOnPost toggle for the tolerance test. All suites green: DecisionCore 144/144 (+5), Agent.V2.Core 633/633 (+3), Functions 727/727. Build 0 warnings. Plan: .claude/plans/codex-02-effect-failure-to-decision.md. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
084274629c |
v2 codex-01 phase 1: ReducerReplay.Replay pure fold + determinism tests
Codex review follow-up #1 (recovery correctness) — lays the foundation for crash-safe state reconstruction. ReducerReplay.Replay folds a signal stream through IDecisionEngine.Reduce without side effects: no journal append, no effect runner, no telemetry. Enforces strict ordinal monotonicity against the seed state's LastAppliedSignalOrdinal so a tampered or corrupt log never silently regresses state. Phase 2 wires this into EnrollmentOrchestrator recovery; Phase 3 adds post-replay deadline re-arm. Backend Finding #6 (ReducerVerifier semantic replay) also consumes this. Tests: 7 new cases in ReducerReplayTests (empty stream, single signal, multi-signal determinism, snapshot+tail equivalence, ordinal-at-seed regression guard, mid-stream monotonicity violation, null-arg guards). All suites green: DecisionCore 146/146, Agent.V2.Core 630/630, Functions 727/727. Build 0 warnings with TreatWarningsAsErrors. Plan: .claude/plans/codex-01-recovery-signallog-replay.md Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
8fe75a0f4e |
v2 single-rail 12: first-session-fixes PR 4 — data completeness + operability (Fixes 4+5)
Closes the "final-status.json shows only 3 apps", "every app ~30 s" and "agent log silent during uploads" gaps observed in session 30410cd7. Fix 4c — Per-app install-lifecycle timing. New AppInstallTiming model (StartedAtUtc / CompletedAtUtc / DurationSeconds). ImeLogTrackerAdapter tracks timing in a set-once dictionary (first Downloading/Installing/ InProgress stamps StartedAt; first terminal stamps CompletedAt) and exposes it via a new public AppTimings snapshot. app_install_* DataJson carries startedAt / completedAt / durationSeconds when known; omitted otherwise so the UI can distinguish "not tracked" from "stamp=epoch". Fix 4a — FinalStatusBuilder uses timings. Optional appTimings param on Build(); FinalStatusPackageInfo gets StartedAt / CompletedAt / DurationSeconds with NullValueHandling.Ignore. Existing per-targeted phase bucketing preserved — that was correct. Fix 4b — app_tracking_summary terminal event. EnrollmentTerminationHandler. EmitAppTrackingSummary runs once at termination before diagnostics upload with aggregate counts, per-phase breakdown, and per-app records (appId/name/phase/finalState/optional timing). ImmediateUpload=true, Phase=Unknown (feedback_phase_strategy). Skipped on WhiteGlove Part 1 (apps haven't installed yet). Fix 5 — Upload-cadence logging. BackendTelemetryUploader + TelemetrySpool gained optional AgentLogger params. Uploader logs INFO flushing/ingest OK, WARN ingest TRANSIENT (5xx/timeout/network), ERROR ingest PERMANENT (4xx), with items + durationMs. Spool logs DEBUG per-enqueue with kind + immediate flag + pending count. Empty-batch path stays silent (short-circuit). Plumbing: ImeLogHost.AppTimings → DefaultComponentFactory.ImeAppTimings → Program.cs wires appTimingsAccessor into EnrollmentTerminationHandler; logger flows into uploader (Program.cs) and spool (EnrollmentOrchestrator). All new ctor params are nullable-default → zero breakage for existing call sites and tests. Tests: +18 new (7 adapter timing, 2 builder timing, 5 handler summary, 4 uploader logging). DecisionCore 139, Agent.V2.Core 630, Functions 727, Web vitest 168 — all green. Web typecheck clean. Solution build 0/0. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
017c7ca1cd |
v2 single-rail 11: first-session-fixes PR 3 — phase tagging + Finalizing stage (Fixes 2+6)
Fix 6 — Finalizing pseudo-stage with ~5 s grace. New non-terminal SessionStage.Finalizing between "both prerequisites resolved" and Completed + DeadlineNames.FinalizingGrace. HandleHelloResolvedV1 / HandleDesktopArrivedV1 / HandleHelloSafetyDeadlineFired (desktop-already-arrived branch) now route through a shared TransitionToFinalizing helper instead of jumping straight to Completed — the helper arms the 5 s deadline and emits a phase_transition declaration effect (Phase=FinalizingSetup). HandleFinalizingGraceDeadlineFired transitions Finalizing → Completed + enrollment_complete. IsTerminal() deliberately unchanged so DecisionStepProcessor.OnDecisionTerminalStage (and therefore EnrollmentTerminationHandler) fires only on the real terminal, not on Finalizing — giving the reducer-emitted phase_transition + enrollment_complete headroom to reach the backend before spool drain. Fix 2 — Sub-phase declarations AppsDevice / AppsUser. ImeLogTrackerAdapter emits a phase_transition InformationalEvent on the first app activity in a given ESP phase (DeviceSetup → AppsDevice, AccountSetup → AppsUser). Fire-once per ESP phase, ImmediateUpload=true, respects feedback_phase_strategy (only phase_transition / esp_phase_changed / agent_started may carry Phase != Unknown). FinalizingSetup + unmapped ESP phases short-circuit — the reducer owns the FinalizingSetup declaration (see Fix 6) and adapter mustn't duplicate. Test harness race-safety. PostFixture in the integration fixture had a latent race where ApproximateQueueLength==0 + "stepIndex > stepIndexBefore" could be met while the actually-posted signal was still executing ApplyStep. Replaced with authoritative LastAppliedSignalOrdinal >= signalLog.LastOrdinal watermark. ReplayHarness + ClassifierAwareReplayHarness + PostFixture all auto-fire a synthetic DeadlineFired:finalizing_grace at end-of-stream when the reducer is parked in Finalizing — fixtures stay unchanged. Tests: +12 new (6 DecisionCore FinalizingStageTests, 6 ImeLogTrackerAdapter SubPhaseDeclarationTests). DecisionCore 139, Agent.V2.Core 612, Functions 727, Web vitest 168 — all green. Web typecheck clean. Solution build 0/0. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
7b64f98e63 |
v2 single-rail 9: soft admin-action semantics + shutdown-race fixes
Addresses two tasks/todo.md entries from 2026-04-23:
- Soft Admin-Action Semantik + SignalIngress-Race-Fix
- Backend AdminAction-Trigger fix
Backend — authoritative AdminMarkedAction field:
New Session.AdminMarkedAction property (null | "Succeeded" | "Failed")
replaces the fragile heuristic "Status is terminal AND current event is not
a completion marker", which fired falsely on every post-completion event
the agent sent (agent_shutting_down, diagnostics_uploaded,
enrollment_summary_shown). The backend's AdminAction response to /register
and /ingest now reads the explicit field. ISessionRepository +
TableSessionRepository + TableStorageService.Sessions +
Ingest{Events,Telemetry}Function + MarkSession{Succeeded,Failed}Function +
RegisterSessionFunction + EventIngestProcessor all updated to set / clear /
read the new field coherently.
Agent — Program.cs WireTelemetryServerResponse refactor:
Routes TelemetryUploadOrchestrator.ServerResponseReceived through three
distinct semantic paths instead of one terminate_session synthesis:
1. DeviceKillSignal -> terminate_session with forceSelfDestruct=true
(hard admin kill — overrides SelfDestructOnComplete).
2. AdminAction=Succeeded -> single admin_marked_session timeline entry
(informational; agent continues own path).
3. AdminAction=Failed -> EnrollmentTerminationHandler.Handle(outcome=Failed)
(soft shutdown; same analyzers/diagnostics/summary
as a locally-detected failure).
Plus a shutdown-race guard: if shutdownComplete is already set, short-circuit
instead of crashing deep in the dispatcher with an InvalidOperationException
from a disposed SignalIngress. Backed by new HandleAdminActionTests.
Agent — ServerActionDispatcher ingress-disposed race:
Added InvalidOperationException swallow around InformationalEventPost.Emit
for the narrow window between SignalIngress.Stop and
TelemetryUploadOrchestrator.DrainAllAsync completion. Dropped events are
acceptable during teardown; the alternative was a nested Emit failure that
corrupted the outer termination sequence. Backed by new
ServerActionDispatcherTests case.
Tests all green: DecisionCore 133, Agent.V2.Core 586, Functions 727.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
df2133cbb3 |
v2 single-rail 8: first-session-fixes PR 1 — reducer correctness (Fixes 9/7/10/8)
Addresses the premature AwaitingHello + HelloSafety-baseline pathology observed in
session 30410cd7: on Classic V1 (SkipUser=false) enrollments the Device-ESP exit was
treated as the final ESP exit, arming HelloSafety from the wrong timestamp.
Fix 9 (foundation):
- New DecisionSignalKind.EspConfigDetected + DecisionState.SkipUserEsp / SkipDeviceEsp
facts with set-once semantics (per-fact, not per-signal).
- Shared EspSkipConfigurationProbe reads HKLM\...\FirstSync once per tracker lifetime.
- DeviceInfoCollector posts the signal alongside the esp_config_detected event.
Fix 7 (tracker guard — primary cut):
- EspAndHelloTracker swallows the esp_exiting forward when SkipUser=false AND no
AccountSetup activity yet. Injectable probes as test seams.
Fix 10 (deadline safety net):
- HandleEspPhaseChangedV1(AccountSetup) cancels HelloSafety whenever the stage is
bouncing back from AwaitingHello.
Fix 8 (reducer guards — belt-and-suspenders):
- New ShouldTransitionToAwaitingHello helper: true only when AccountSetup entered OR
SkipUser explicitly true.
- HandleEspPhaseChangedV1(FinalizingSetup) + HandleEspExitingV1 gate the AwaitingHello
promotion and HelloSafety arm behind the helper; observability facts still recorded
on the suppressed path.
Tests (all green):
- DecisionCore xUnit: 131 pass (+10 new — EspConfigDetectedHandlerTests 9,
HelloSafetyBounceCancelTests 2, ClassicAwaitingHelloGuardTests 8).
- Agent.V2.Core xUnit: 582 pass (+6 new — EspAndHelloTrackerFinalizingGuardTests).
- Functions xUnit: 727 pass (regression check).
- Solution build clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
258f18d3dd |
v2 single-rail 7.1: codex pass-2 — preserve JSON null values in TypedPayload round-trip
Medium — SignalSerializer.Deserialize coerced JSON null values inside
TypedPayload to string.Empty, breaking wire/replay parity for the new
single-rail path. The comment promised "cast null tokens to null objects"
but the code did the opposite.
Realistic producers like DeviceInfoCollector place nullable fields (e.g.
displayVersion returned by GetOsDisplayVersion() when the registry key
is absent) directly into EnrollmentEvent.Data. Live Newtonsoft emits
{"displayVersion":null}; under the pre-fix deserializer, replay turned
that into {"displayVersion":""} on the next outbound Emit.
Fix: preserve JSON null as C# null when restoring TypedPayload dict
entries. Dictionary<string, object> accepts null values at runtime and
Newtonsoft re-serializes them as JSON null — wire-parity restored.
Other JToken types (JValue non-null, JArray, JObject) continue to pass
through unchanged.
Regression test: SignalSerializer_roundtrip_null_value_in_TypedPayload_stays_null_not_empty_string
- on-disk JSON contains "displayVersion":null (not "")
- restored dict carries C# null (not "")
- re-serialized restored dict still emits JSON null (wire-parity)
- sibling non-null values untouched
Tests: 567 V2.Core + 112 DecisionCore green (+1 new regression test).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
92d300d4c2 |
v2 single-rail 7: codex-review feedback — 4 findings fixed
Addresses all 4 findings from the Codex review on PR #48. 564 V2.Core + 111 DecisionCore tests green (up from 559 + 109; 7 new regression tests). Finding 1 (P1) — terminate_session drain deadlock TelemetryUploadOrchestrator raised ServerResponseReceived synchronously while holding _drainGuard. The terminate callback then invoked orchestrator.Stop() on the main thread, which tried a terminal DrainAllAsync and blocked on the same guard — timing out before the final agent_shutting_down event could be uploaded. Fix: buffer signal-bearing UploadResults during drain, release the guard, then raise events. Also added a defensive re-entry short-circuit in onTerminateRequested (skips if shutdown.IsSet) for the pathological case of a nested drain producing a different ActionId. Files: TelemetryUploadOrchestrator.cs, Program.cs Test: Handler_may_invoke_reentrant_drain_without_deadlock Finding 2 (P1) — structured Event.Data was degenerating to type-name strings InformationalEventPost.StringifyData fell through to kv.Value.ToString() for nested containers, so e.g. network_adapters.adapters became the literal string "System.Collections.Generic.List\`1[...]" on the wire. Fix (redesigned after review): typed-sidecar through the bus, mirroring the existing DecisionEffect.ClassifierSnapshot pattern. DecisionSignal.TypedPayload (object?) ← new DecisionEffect.TypedPayload (object?) ← new ISignalIngressSink.Post(..., typedPayload) EventTimelineEmitter.Emit(..., typedPayload) uses it as Data verbatim Live path does ZERO serialization for structured event data — the Dictionary<string,object> flows object-identity-preserving from collector to TelemetryEventEmitter, where JsonConvert.SerializeObject runs once at the wire boundary. Persistence writes TypedPayload to JSONL once at the disk boundary (SignalSerializer); replay restores it as JObject/JArray tokens that re-serialize identically. Files: DecisionSignal.cs, DecisionEffect.cs, DecisionEngine.Shared.cs, SignalSerializer.cs, ISignalIngressSink.cs, SignalIngress.cs, EnrollmentOrchestrator.cs (LazyIngressSinkRelay), IEventTimelineEmitter.cs, EventTimelineEmitter.cs, EffectRunner.cs, InformationalEventPost.cs, + FakeSignalIngressSink, FakeEventTimelineEmitter test doubles Tests: TypedPayload_dictionary_becomes_EnrollmentEvent_Data_verbatim_on_wire, TypedPayload_missing_falls_back_to_string_parameter_reconstruction, Emit_EnrollmentEvent_overload_forwards_Data_as_typed_payload_by_reference, Emit_EnrollmentEvent_overload_passes_DateTime_as_live_object_through_typed_payload, SignalSerializer_roundtrip_preserves_TypedPayload_structure, SignalSerializer_roundtrip_null_TypedPayload_stays_null Finding 3 (P2) — ImmediateUpload=false silently flipped to true Helper wrote the ImmediateUpload payload key only when true; the emitter defaulted "missing key → true". Net effect: a caller passing immediateUpload:false (e.g. performance_snapshot, agent_metrics_snapshot) had their intent silently overridden; every snapshot was flushed immediately. Fix: helper now writes the key both-ways ("true" or "false"). Emitter default stays true because reducer-synthesised terminal events (whiteglove_complete, enrollment_complete, enrollment_failed) construct effect parameters with just eventType and rely on that default for immediate flush — documented inline. Files: InformationalEventPost.cs, EventTimelineEmitter.cs (doc-only) Tests: ImmediateUpload_false_writes_explicit_false_key_to_payload, Emit_optional_payload_keys_are_set_only_when_caller_provides_them (updated) Finding 4 (P2) — PeriodicCollectorLifecycleHost saw only its own posts IdleActivityObservingIngressSink wrapped the ingress per-host, so _lastRealEventTimeUtc was only updated by signals routed through this host's own _post. Every other component (Program.cs lifecycle, IME, ESP, DeviceInfo, Analyzer, Gather, StallProbe, Network, AAD, Desktop, etc.) constructs its own InformationalEventPost against the raw shared ingress and bypassed the wrapper — and periodic events posted by the host itself were correctly filtered out. Result: the idle clock never advanced past session start; collectors stopped after idle timeout regardless of live enrollment activity. Fix: moved observation into SignalIngress itself as an event SignalPosted, fired after every successful enqueue (fast path + slow path). PeriodicCollectorLifecycleHost subscribes via the concrete SignalIngress type at Start, unsubscribes at Stop. The observer now sees ALL signal kinds (ESP, IME, Classifier, DeadlineFired, etc.), not just InformationalEvents. Deleted the obsolete wrapper class. Files: SignalIngress.cs, DefaultComponentFactory.cs Tests: SignalPosted_fires_for_every_successful_post_regardless_of_kind, SignalPosted_handler_exception_does_not_break_ingress, SignalPosted_is_raised_even_when_channel_took_slow_path Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
7c29b607dc |
v2 single-rail 6.1: guard DeviceSetupProvisioningComplete terminal on Classic path
HandleDeviceSetupProvisioningCompleteV1 completed unconditionally — in session e259c121-dc13-46d6-8e96-118f1da9845e (2026-04-22, Classic UserDriven-v1) a late DeviceSetupProvisioningComplete with payload deviceSetupResolved="unknown" fired AFTER AccountSetup had started and DesktopArrived had been observed, but BEFORE HelloResolved — terminating the session at Stage=Completed before Hello even ran. Guard: when state.AccountSetupEnteredUtc != null, treat the signal as informational only (step index + ordinal advance, stage unchanged, no completion effect). On the Classic path, terminal completion still requires the Hello+Desktop AND-gate (HandleHelloResolvedV1 / HandleDesktopArrivedV1). The original SelfDeploying / Device-Only terminal semantics are preserved for AccountSetupEnteredUtc == null. The redundant AccountSetupEnteredUtc == null check around the EnrollmentType upgrade is now implicit and removed. Regression coverage: - New fixture tests/fixtures/enrollment-sessions/userdriven-late-provisioning-complete-v1.jsonl reproduces the 6-signal sequence from the reference session. - New test ClassicScenarioTests.LateProvisioningComplete_afterAccountSetup_doesNotCompletePrematurely asserts terminal trigger == HelloResolved, DeviceSetupProvisioningComplete transition is taken but stage-unchanged, and DeviceOnlyDeployment hypothesis stays Unknown. Tests: 109/109 DecisionCore + 559/559 V2.Core green; solution builds clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
b7f74dbd35 |
v2 single-rail 5.9a: ImeLogTrackerAdapter add InformationalEvent posts (B29 part 1)
Plan §5.9 / PR #9 — V1 Parity Issue #3: the IME log tracker callbacks now dual-emit a specific DecisionSignalKind + an InformationalEvent so the Events table regains the V1-compatible wire shape (esp_phase_changed, ime_user_session_completed, app_install_started, app_install_completed, app_install_failed, app_download_started, download_progress) while remaining on the single-rail signal pipeline. Changes: - ImeLogTrackerAdapter: construct an InformationalEventPost from the existing ingress/clock; after every EspPhaseChanged / UserSessionCompleted / AppStateChanged post, also emit a matching InformationalEvent. EspPhase declarations carry the mapped EnrollmentPhase (DeviceSetup / AccountSetup / FinalizingSetup) per feedback_phase_strategy. - AppState handler expanded from terminal-only to all state transitions with V1-compatible eventType mapping: Downloading transitions → app_download_started + byte updates → download_progress (Debug); Installing/InProgress → app_install_started; Installed/Skipped/Postponed → app_install_completed; Error → app_install_failed. Payload carries appId/appName/state/intent/ targeted/runAs/progressPercent/bytesDownloaded/bytesTotal/isError/isCompleted + appVersion/appType/detectionResult/attempt, plus error* fields on Error. - Shared.Constants.EventTypes: add EspPhaseChanged, ImeAgentVersion, ImeUserSessionCompleted, DoTelemetry, AllAppsCompleted. - Adapter tests: refactor to filter the posted-signal list by kind + eventType so the dual-emission contract is verifiable. 14 new tests cover the full state-transition matrix + payload shape + error-field population; total adapter test count 37 (up from 17). Verification: dotnet build AutopilotMonitor.sln clean; V2.Core test suite green (552 / 552, up from 538). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
491c749688 |
v2 single-rail 5.3b: migrate EnrollmentTerminationHandler + agent_shutting_down event
Plan §5.3 + §6.2 — EnrollmentTerminationHandler now emits the six peripheral lifecycle events (enrollment_summary_shown, diagnostics_collecting/uploaded/upload_failed, reboot_triggered, whiteglove_part1_complete) through InformationalEventPost instead of the legacy Action<EnrollmentEvent> bypass. New acknowledgement: `agent_shutting_down` fires with ImmediateUpload=true just before CleanupService.ExecuteSelfDestruct. This gives the backend a definitive "agent accepted termination" signal so it can stop re-queuing terminate_session even when the cleanup script wins the race against the final telemetry drain. Program.cs: EnrollmentTerminationHandler construction moved into orchestrator.Start's onIngressReady hook (alongside ServerActionDispatcher) so the live lifecyclePost backs it. The Terminated-event subscription uses a null-checking dispatch wrapper so the registration can sit before Start returns without referencing the not-yet-constructed handler; by the time the decision loop processes its first signal, the hook has run synchronously and the handler is live. Constants.EventTypes.AgentShuttingDown = "agent_shutting_down" added. Tests: EnrollmentTerminationHandlerTests refactored — Rig now creates a real InformationalEventPost backed by FakeSignalIngressSink; assertions consult EmittedEventTypes / PayloadOf(eventType) helpers that read the posted signal payloads. Three new tests cover - agent_shutting_down emitted on self-destruct path with correct payload - skipped when SelfDestructOnComplete=false - skipped on WhiteGlove Part-1 exit (no self-destruct) V2.Core: 535 → 538, DecisionCore: 108, all green. Architecture baseline unchanged — EnrollmentTerminationHandler now takes InformationalEventPost, no structural reference to TelemetryEventEmitter. |
||
|
|
d1b8be12de |
v2 single-rail 4.1b: HandleInformationalEventV1 reducer + tests
Wire the InformationalEvent signal through the decision engine:
- New dispatch entry (InformationalEvent, 1) → HandleInformationalEventV1.
- Handler lives in DecisionEngine.Shared.cs next to the other neutral
observation handlers. Copies the signal payload 1:1 into a single
EmitEventTimelineEntry effect; DecisionState is only touched by the
standard StepIndex / LastAppliedSignalOrdinal bookkeeping.
- Missing or empty eventType / source produce a DeadEnd transition with
reason informational_event_missing_{key} so malformed signals are
visible in the transitions table rather than silently lost. Bookkeeping
still advances on a DeadEnd so replay cannot re-apply the ordinal.
- dataJson payload key is reserved for future JSON-blob expansion;
today's reducer forwards it verbatim (ends up as a flat Data string
in the emitted event). Docstring clarifies the reserved status.
- 13 xUnit tests cover taken-transition + one effect, payload copy
identity, state / hypothesis / deadline invariance, bookkeeping,
all deadend branches, and schema-version fallthrough.
Single-rail pipe is now functionally complete end-to-end (signal post →
reducer → effect → EventTimelineEmitter → TelemetryEventEmitter). Next
subtasks wire up the V2.Core signal-post helper (4.3) and the caller-
side architecture test (4.4).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
489f124c6d |
v2 single-rail 4.1a: add InformationalEvent signal kind + payload keys
Introduce the generic pass-through signal that carries an EnrollmentEvent shape through the decision engine. Any peripheral collector / lifecycle source needing the Events timeline will post this instead of calling TelemetryEventEmitter directly, so the engine is the single ordering source for the wire. Promotion to a decision-specific kind is a later, localized change. - DecisionSignalKind.InformationalEvent (new). - SignalPayloadKeys: EventType + Source (mandatory), Message, Severity, ImmediateUpload, DataJson (optional). - tests/fixtures/signal-kinds/informational-event-v1.json documents the wire shape with a realistic NTP-probe payload. No reducer handler yet — that is the next subtask (4.1b); this commit is purely the contract extension. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
23aa51a3f2 | fix warnings and fix v2 agent hash compare | ||
|
|
2ecbefce73 | backend route fix and v2 agent version check fix | ||
|
|
5b7f424d19 |
refactor(agent-v2): route whiteglove_resumed / system_reboot_detected / admin preemption through reducer effects
V2 parity cleanup - the three lifecycle events were emitted from Program.cs
directly while the corresponding reducer handlers (HandleWhiteGlovePart1To2Bridge,
HandleSystemRebootObservedV1) returned Array.Empty<DecisionEffect>(). Admin
preemption synthesised an EnrollmentTerminatedEventArgs and invoked
terminationHandler.Handle directly, bypassing the reducer entirely.
This commit unifies all three flows: the reducer is the single state owner, the
telemetry event appears as an EmitEventTimelineEntry effect of the state
transition, and the orchestrator's Terminated event fires automatically when
stage goes terminal - no more synthetic bypass.
- B1: HandleWhiteGlovePart1To2Bridge emits whiteglove_resumed timeline entry.
- B2: HandleSystemRebootObservedV1 emits system_reboot_detected with payload
keys previousExitType/lastBootUtc.
- B3: new DecisionSignalKind.AdminPreemptionDetected + new SessionOutcome.AdminPreempted.
Reducer HandleAdminPreemptionDetectedV1 transitions to Completed/Failed based on
adminOutcome payload, emits enrollment_complete/_failed timeline entry with
adminAction+source data keys. Program.cs posts the signal; terminationHandler
still subscribes to orchestrator.Terminated (no direct Handle calls on the
startup-preemption path).
Fixtures: tests/fixtures/signal-kinds/{system-reboot-observed,admin-preemption-detected}-v1.json.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
fd7bf0e7ac |
feat(backend): M5.d.4 index-reconcile timer + release-gate — M5 complete
Closes the M5.d dual-write loop with a 2h safety-net timer and the
per-index-table PK/RK consistency release-gate tests required by
Plan §M5-Pflicht.
Components:
- ISignalRepository.QueryByTimestampAtOrAfterAsync +
IDecisionTransitionRepository.QueryByTimestampAtOrAfterAsync: new
cross-partition scans filtered on the Azure Tables system-managed
`Timestamp` property. Bounded to maxResults=50000 with a warn-log
on cap-reached so silent truncation can't masquerade as success.
- IndexReconcileTimer: TimerTrigger on "0 0 */2 * * *" (same cadence
as MaintenanceFunction). Scans the last 4h (window deliberately 2×
the cadence so every primary row gets re-checked at least twice
before falling out of range), builds envelopes via the existing
IndexReconcileEnvelopeFactory, and hands them to the producer —
reusing the M5.d.2 flag-gate + queue-failure-swallow semantics
instead of duplicating them.
- Testable core: RunReconcileAsync(cutoffUtc, CT) split out from the
TimerTrigger entry so unit tests can drive with deterministic
cutoffs without an Azure Functions host.
- AdminConfigurationService.GetConfigurationAsync is now virtual —
enables Moq-based mocking in timer flag-gate tests. Low-risk prod
change (no existing caller relies on final).
Release gate (Plan §M5-Pflicht line 1395): IndexDualWriteConsistencyTests
verifies per-index-table that the captured index row's back-ref
fields (TenantId, SessionId, StepIndex / SessionSignalOrdinal) plug
directly into the primary repos' key builders to reconstruct the
primary (PartitionKey, RowKey). The resubmit-idempotency test simulates
a timer-triggered replay of the same primaries and asserts all
duplicated rows collapse onto identical Index (PK, RK) pairs — which
is exactly what the real TableIndexRepository's UpsertReplace does
at the storage layer.
Left deferred (not blocking M5 close):
- SessionsByStage stale-row pruning: stage-X→Y transitions still leave
an orphan row in {tenantId}_X. Either query-side dedupe (per
SessionId, max-Ticks RK) or a follow-up pruner handles it.
- Actual EnableIndexDualWrite=true flip: stays default-false until
post-deploy verification runs. Producer + timer both no-op in the
meantime, so the change is zero-risk on merge.
- Cross-partition-scan perf: fine for current primary-table sizes;
revisit if prod metrics after pilot show degradation.
Tests (+10 in Functions):
- 4 IndexReconcileTimerTests: flag-off short-circuits queries, flag-on
with primary rows enqueues all envelopes, empty-window skips enqueue,
ReconcileWindow=4h invariant.
- 6 IndexDualWriteConsistencyTests: 5 per-index-table PK/RK back-ref
(SignalsByKind + all 4 transition-derived indices) + 1 resubmit
idempotency ensuring all duplicated rows collapse onto the same
index keys.
M5 status: M5.a + M5.b (+ 3 sub-commits) + M5.c (+ 3 sub-commits) +
M5.d (+ 4 sub-commits) all complete. M5.d loop:
Primary commit (M5.a) → Envelope enqueue (M5.d.2, flag-gated)
→ Queue → Handler (M5.d.3) → Index-Tables (M5.d.1)
+ 2h safety-net Timer (M5.d.4) reconciles missed envelopes.
Full Functions suite: 692 green (prev 682 + 10 new). Full solution
build green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
c3ee0cad55 |
feat(backend): M5.d.2 index-reconcile queue producer + feature flag
Adds the async fan-out producer path from Plan §2.8 / §M5.d so primary Signals + DecisionTransitions rows can be projected onto their index tables without blocking the hot ingest loop. Components: - Constants.QueueNames.TelemetryIndexReconcile (azure-queue-name-safe, all lowercase + hyphens). - IndexReconcileEnvelope (Shared DTO, flat shape with nullable fields per SourceKind="Signal"/"DecisionTransition"). Avoids polymorphic Newtonsoft deserialization — see M5.c.3 finding. - IndexReconcileEnvelopeFactory pure static helper (FromSignal / FromDecisionTransition / BuildBatch) — state-less projection. - IIndexReconcileProducer (Shared interface) contract: no-op when the feature flag is off; never rethrows underlying-queue failures. - AzureQueueIndexReconcileProducer (Functions) — Base64-encoded JSON messages, lazy CreateIfNotExistsAsync guarded by Interlocked flag, Managed-Identity-or-connection-string construction mirroring BlobStorageService / TableStorageService. - IngestTelemetryFunction ctor gains IIndexReconcileProducer; after primary Signals + DecisionTransitions commit it builds envelopes and enqueues them. Event path is untouched (events don't feed the index tables per §2.8). - Program.cs registers the producer as a singleton. Feature gate (AdminConfiguration.EnableIndexDualWrite, default false): - New bool field in AdminConfiguration. - TableConfigRepository Store + Map extension (per feedback_table_storage_serialization). - Producer checks config each call via AdminConfigurationService's 5-min memory cache. Flag off → primary behaviour is bit-exact. Fault tolerance: queue-side exceptions (network, throttling, missing queue before first CreateIfNotExists) are logged per envelope and swallowed. The primary row is the source of truth; the 2h reconcile timer (M5.d.4) is the safety-net recovery path. Tests (+11 in Functions): - 5 IndexReconcileEnvelopeFactoryTests: Signal/DecisionTransition projection, dead-end preservation, batch ordering, null/empty. - 3 IndexReconcileEnvelopeSerializationTests: Newtonsoft round-trip (Signal, full DecisionTransition, minimal-envelope tolerance). - 3 AdminConfigurationEnableIndexDualWriteTests: default-false on new config + CreateDefault, persists when set. Deferred to later sub-milestones: - M5.d.3: queue-triggered consumer that maps envelopes to 0–3 index rows using M5.d.1 TableIndexRepository. - M5.d.4: 2h reconcile timer + release-gate dual-write verification tests, then flip EnableIndexDualWrite=true in prod admin config. Full Functions suite: 672 green (prev 661 + 11 new). Full solution build green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
5eb09176b9 |
feat(backend): M5.d.1 index-table scaffolding for Plan §2.8 query matrix
Registers 5 secondary-index tables (SessionsByTerminal, SessionsByStage, DeadEndsByReason, ClassifierVerdictsByIdLevel, SignalsByKind) in Constants.TableNames.All[] so TableStorageService.InitializeTablesAsync auto-creates them at startup. Adds: - 5 sealed DTO records in Shared/Models/IndexRecords.cs carrying only discriminator columns + back-refs (StepIndex / SessionSignalOrdinal) to the primary row — no PayloadJson chunking needed. - IIndexTableRepository with 5 Store*Async methods (single-interface shape keeps boilerplate low; Cosmos swap remains trivial since each Store* is independent). - IndexRowKeys pure static helper for all PK/RK formats per Plan §2.8: D6 StepIndex suffix, D10 SessionSignalOrdinal suffix, D19 Ticks padding for SessionsByStage (matches lex ordering to numeric time). Defensive Sanitize() for forbidden PK/RK chars (/\#?). - TableIndexRepository with a generic group-by-PK + 100-op-chunk UpsertReplace write path mirroring TableSignalRepository. - DI wiring in DataAccessServiceExtensions. Scope: write-only. Query methods deferred until MCP/Web concretely need them. No queue producer / reconcile handler yet — that lands in M5.d.2+. Tests (+25 in Functions): 15 PK/RK format assertions in IndexRowKeysTests + 10 entity-projection round-trips (incl. null-tolerance) in TableIndexRepositoryMappingTests. Full Functions suite: 661 green (prev 636 + 25 new). Full solution build green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
44f0e44b72 |
feat(backend): M5.c.3 GET /api/sessions/{id}/reducer-verification — structural audit
Third and final V2 Inspector read endpoint. Admin-only (not tenant-exposed) structural health check over a session's persisted SignalLog + DecisionTransitions journal. Closes the M5 read-endpoint trio. - Shared DTO ReducerVerificationReport + VerificationIssue: counts, ordinal / step-index contiguity, ReducerVersion drift, orphaned-reference count, human-readable issue stream for the Inspector's verification panel. - ReducerVerifier.Verify — pure function, zero IO. Five detectors: empty-session (Info), signal ordinal gaps (Warning, capped at 20 issues + overflow summary to keep reports bounded), step-index gaps (Warning, same cap), orphaned transitions whose SignalOrdinalRef falls outside the loaded signal set (Warning, capped), and ReducerVersion drift (Info — known condition per plan §2.10, not an error). - GetSessionReducerVerificationFunction — reads signals + transitions for the session, loads the live DecisionEngine.ReducerVersion, runs the verifier, returns the report. Global-Admin cross-tenant fallback via SessionsIndex when the effective tenant has no data. Truncation flag when either the 5000-signal or 5000-transition cap is hit. - Endpoint gated by EndpointPolicy.GlobalAdminOnly — the plan explicitly scopes this to adminMode/ops access. The fail-closed catalog policy completeness test enforces the registration. Scope boundary explicitly documented on the function: full engine replay (DecisionEngine.Reduce over reconstructed DecisionSignals with per-step diff) is NOT implemented in this commit. It would require polymorphic deserialisation of SignalRecord.PayloadJson back into DecisionSignal with its Evidence abstract type, which is a non-trivial refactor. The structural checks land today catch the realistic release-gate failure modes (corruption, drift, orphan refs) and give ops a usable tool. Full replay is a dedicated follow-up once the Evidence deserialisation contract is pinned down. 9 new tests cover all five detectors plus happy path, shuffled input, and the issue-cap overflow message. 636/636 backend green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
04270f1fb0 |
feat(backend): M5.c.2 GET /api/sessions/{id}/decision-graph — Inspector DAG
Second V2 Inspector read endpoint. Server-side projection from the persisted
DecisionTransitions journal into a renderable DAG so the UI consumes one
structured shape instead of rebuilding the graph on every page open, and so
"what counts as terminal" stays authoritative next to the backend copy of
that list.
- Shared DTO DecisionGraphProjection (Nodes + Edges + ReducerVersion).
Nodes are de-duplicated stages with IsTerminal + a coarse TerminalOutcome
label ("Succeeded" / "Failed" / "PausedForPart2"). Edges are one per
transition, preserving dead-ends (Taken=false) for the UI to render in a
distinct style. VisitCount counts *taken* transitions only — dead-ends
don't advance the heat-map.
- IDecisionTransitionRepository.QueryBySessionAsync added alongside
StoreBatchAsync. Mirror of the M5.c.1 Signals pattern: PartitionKey filter,
explicit StepIndex sort as a redundancy guard (the D10 RowKey padding
already matches numeric order).
- TableDecisionTransitionRepository.FromEntity — reverse mapping (entity →
record) including TableStorageChunking.ReassembleProperty for oversized
PayloadJson. Round-trip tested end-to-end.
- DecisionGraphBuilder.Build — pure projection function, no IO, no service
deps. Unit-testable against canned transition lists; the test suite covers
empty input, node de-duplication, terminal classification for all 4 stages,
dead-end preservation, StepIndex ordering under shuffled input, and
VisitCount semantics.
- GetSessionDecisionGraphFunction — same shape as GetSessionSignalsFunction
(RequestContext resolution, Global-Admin cross-tenant fallback), plus a
truncation flag when the 5000-step cap is hit.
- Endpoint registered in EndpointAccessPolicyCatalog under MemberRead +
TenantScoping.QueryParam (sibling of sessions/{id}/events).
Scope boundary — the plan calls for terminal-node decoration with the richer
TerminationReason + TerminationOutcome from the M4.6.β enrollment_terminated
event. That data lives in the Events table, not in the transition journal;
joining it here would expand scope into cross-table reads. The current
projection exposes the coarse TerminalOutcome label (derived from the
transition's ToStage) which is enough for the M6 Inspector's initial DAG
render. Richer decoration is a dedicated follow-up.
Three sources of truth now carry the terminal-stage list: agent-side
DecisionCore.SessionStageExtensions.IsTerminal, backend-ingest
TelemetryPayloadParser.IsTerminalStage, and now DecisionGraphBuilder's copy.
Adding a new terminal stage = three places to update; flagged in the builder's
docstring so it doesn't get missed.
627/627 backend tests green (+13: 11 builder + 2 transition round-trip).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
f1e89146cb |
feat(backend): M5.c.1 GET /api/sessions/{id}/signals — Inspector SignalLog read
First of three V2 Inspector read endpoints (Plan §M5). Returns the SignalLog
for a session — ordered by SessionSignalOrdinal, bounded by a caller-supplied
maxResults with a hard 5000 cap.
- ISignalRepository.QueryBySessionAsync added alongside the existing
StoreBatchAsync. Uses Azure Tables filter on PartitionKey = {tenant}_{session};
the D19(ordinal) RowKey padding means lex order already matches numeric
order, so the explicit sort on return is a redundancy guard — cheap
invariant check, not a real cost.
- TableSignalRepository.FromEntity — reverse mapping (entity → SignalRecord)
that mirrors ToEntity, including TableStorageChunking.ReassembleProperty
for oversized PayloadJson. Round-trip tested end-to-end.
- GetSessionSignalsFunction.Run — follows the same shape as
GetSessionEventsFunction: RequestContext.TargetTenantId resolved by
PolicyEnforcementMiddleware, Global-Admin cross-tenant fallback via
SessionsIndex when the effective-tenant query comes up empty.
- ParseMaxResults extracted as a pure helper taking the raw query string
(not HttpRequestData) so it's unit-testable without mocking the abstract
Worker HttpRequestData class.
- Endpoint registered in EndpointAccessPolicyCatalog under MemberRead +
TenantScoping.QueryParam (same policy as sessions/{id}/events). Without
this the fail-closed catalog would reject the route with 403 — verified
by the EndpointPolicyCatalogCompletenessTests.
9 new tests (614/614 backend green): ParseMaxResults defaults / garbage /
in-range / cap / negative / null query string (6), plus FromEntity
round-trip (simple payload + oversized-chunked payload).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
260d9afd8d |
feat(backend): wire WhiteGloveSealingPatternIds through /api/agent/config
The V2-agent AgentConfigResponse schema has carried four fields the backend was expected to populate (Plan §M5): LatestAgentSha256, LatestAgentExeSha256, AllowAgentDowngrade, and WhiteGloveSealingPatternIds. The first three were already wired through adminConfig; the last one was declared on the DTO but never populated from storage — V2 agents always received an empty list, effectively leaving the sealing-pattern emission path dead. Adds the missing plumbing: - AdminConfiguration.WhiteGloveSealingPatternIdsJson (string, JSON-serialised List<string>) + GetWhiteGloveSealingPatternIds() helper. Defaults to empty for null/whitespace/invalid JSON so the feature stays off by default (M3-compatible, no regression on tenants that haven't opted in). Global-only, no per-tenant override (plan §M5 M4.4.5.e decision). - TableConfigRepository Store + Map entries for the new column (feedback_table_storage_serialization — new model properties MUST round-trip through the table mapping or the field silently drops on save/load). - GetAgentConfigFunction populates WhiteGloveSealingPatternIds from adminConfig.GetWhiteGloveSealingPatternIds(). ConfigVersion bumped 25 → 26 as a semantic-change marker. 5 new tests mirror the HarmlessEventIds pattern (null / whitespace / invalid JSON / valid round-trip / explicit empty array). 605/605 backend tests green. Agent-side (M4.4.4 / M4.4.5.e): the V2 agent's OnPatternMatched hook already reads WhiteGloveSealingPatternIds from the cached config and re-emits matching IME pattern hits as WhiteGloveSealingPatternDetected DecisionSignals. With this commit the data path is end-to-end. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
0936132a00 |
feat(backend): M5.b.3 payload size-cap guard for /api/agent/telemetry
Defense-in-depth: reject oversized request bodies before buffering them in memory + deserialising. Closes the documented gap from M5.b — the V2 endpoint previously accepted unbounded bodies while legacy /api/agent/ingest has capped NDJSON payloads since day one. - Reuse existing TenantConfiguration.MaxNdjsonPayloadSizeMB (table-only, admin-set, default 5 MB). The property name is historical; scope is shape-agnostic — both NDJSON and JSON-array paths now share the knob. Doc comment updated to reflect that. No new config surface. - New internal static helper IngestTelemetryFunction.ReadBodyWithSizeCapAsync reads the already-gzip-decompressed request stream with a strict byte cap. Short-circuits + returns (exceeded=true, body="") the instant the cap would be exceeded, so a malicious sender can't force a 100 MB allocation. Semantics match legacy NdjsonParser: strict greater-than, equal-to accepted. - Cap exceeded → HTTP 413 Payload Too Large with IngestEventsResponse body (Success=false, Message describes limit). Agent's UploadResult parser classifies 413 as Permanent (not retried), which is correct — the same batch will fail again. The agent handles this by dropping the batch from the spool after the permanent-failure log. 4 new tests: under cap, exactly at cap, over cap (asserting the stream is not fully drained — memory bound), empty stream. 600/600 backend tests green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
f0fc3696f6 |
feat(backend): M5.b IngestTelemetry endpoint — POST /api/agent/telemetry
V2 ingest endpoint consuming a heterogeneous batch of TelemetryItemDtos (Event + Signal + DecisionTransition in one JSON array). gzip request bodies are decoded transparently by the existing UseRequestDecompression middleware — new code only has to read a plain stream. - TelemetryItemDto (Shared): wire-format DTO mirroring the agent's TelemetryItem JSON shape so the backend can deserialise without linking agent assemblies. - IngestTelemetryFunction: security check (cert / rate / hardware whitelist) → device & version kill-switch short-circuits with DeviceBlocked=true body → body parse → tenant-header/body match → partition by Kind → persist via the appropriate repository → 200 with AdminAction + Actions control-signal fields populated from ISessionRepository (matching the /api/agent/ingest parity needed by the M4.6.ε agent-side UploadResult parser). - TelemetryPayloadParser: pure defensive parsing layer (wire → storage record). Malformed PayloadJson on a single item returns null; one poisonous item can't poison the batch. Projects index discriminators (IsTerminal, ClassifierVerdictId, ClassifierHypothesisLevel) off the DecisionTransition payload so the later queue-driven index fan-out reads them straight off the primary row. - EndpointAccessPolicyCatalog: POST agent/telemetry registered under DeviceOrBootstrapAuth (same policy as agent/ingest). Without this the fail-closed catalog rejects the route with 403. Scope boundary — Events get stored via StoreEventsBatchAsync only; the rule engine, app-install aggregation, SignalR notification, and vulnerability correlation remain on the legacy /api/agent/ingest path. Sharing that pipeline is tracked as M5.b.2 (extract EventIngestProcessor so both functions run the same downstream steps) — M7 rollout gates on completing it. Documented inline in PersistItemsAsync. 28 new tests: defensive null-handling per kind, typed-column extraction for Signal (with envelope fallback for missing SessionTraceOrdinal), DecisionTransition dead-end + classifier-verdict nesting, IsTerminal authoritative-list guard (Completed / Failed / WhiteGloveSealed / WhiteGloveCompletedPart2 — mirrors DecisionCore SessionStageExtensions), PartitionKey parser including malformed-shape rejection. The EndpointPolicyCatalogCompletenessTests guard the route registration. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
eeaa0643a2 |
feat(backend): M5.a primary tables — Signals + DecisionTransitions repositories
Groundwork for the V2 ingest path (/api/agent/telemetry). Adds the two primary Decision-Engine tables projected from the agent-side SignalLog and Journal so the Inspector + Reducer-Verifier can replay sessions. - Constants.TableNames: Signals, DecisionTransitions (+ All[] init). - Shared DTOs SignalRecord / DecisionTransitionRecord carry a flat projection plus the agent-serialised PayloadJson for full fidelity. DecisionTransitionRecord eagerly carries index discriminators (IsTerminal, ClassifierVerdictId, ClassifierHypothesisLevel) so the later index fan-out can read them straight off the primary row. - ISignalRepository / IDecisionTransitionRepository: lean interfaces (StoreBatchAsync only — queries land with the read endpoints in M5.c). - TableSignalRepository / TableDecisionTransitionRepository: idempotent UpsertReplace via entity-group-transactions, grouped per (tenant, session), chunked at the 100-op Azure limit. PayloadJson goes through TableStorageChunking so the 32K-char property ceiling can't bite on large Evidence + Payload blobs. - Registered in DataAccessServiceExtensions alongside the existing 13 Table repositories — Cosmos swap remains a single-file change. - 11 pure mapping tests cover key builders (PK tenant_session, RK D19(ordinal) / D10(stepIndex), lex-ordering invariant), full entity projection, chunked PayloadJson handling, dead-end paths, and null-payload tolerance. Index tables (SessionsByTerminal, SessionsByStage, DeadEndsByReason, ClassifierVerdictsByIdLevel, SignalsByKind) land with the queue-driven fan-out in M5.b; the primary schemas are stable. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
941e03cf32 |
refactor(agent-v2): rename telemetry endpoint to /api/agent/telemetry
Place the new V2 ingest route under the existing /api/agent/* namespace (where all other agent-writes live: register-session, ingest, config, upload-url) instead of the orphan /api/telemetry/* bucket. Agent writes vs. admin/web reads is the thematic split used throughout the repo — /api/sessions/* is reserved for read-side and admin action routes. Constant renamed TelemetryBatch → IngestTelemetry to mirror the legacy IngestEvents pattern; semantic successor, heterogeneous TelemetryItem[] payload replacing per-type ingest once Legacy is decommissioned. No wire-format or handler changes — URL string only. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
17063be600 |
feat(agent-v2): M4.6.β terminal dispatch — FinalStatus, SummaryDialog, cleanup, server actions
Closes the second batch of peripheral-parity gaps so the V2 agent actually
terminates like Legacy does: write FinalStatus.json, launch the WPF summary
dialog in the user session, upload diagnostics (respecting mode/outcome),
write the enrollment-complete marker, run self-destruct, and signal shutdown.
All of this is driven off a single typed kernel event so nothing leaks back
into the DecisionEngine.
Terminal-stage detection (kernel hook)
- DecisionCore/State/SessionStageExtensions.IsTerminal() + IsPauseBeforePart2()
— Completed, Failed, WhiteGloveSealed, WhiteGloveCompletedPart2 are terminal;
WhiteGloveSealed is the "pause before Part 2" special case (session resumes
on reboot, cleanup MUST be suppressed).
- DecisionStepProcessor ctor gains Action<DecisionState>? onTerminalStageReached;
fires at most once per processor lifetime, swallows handler exceptions,
suppressed when initial state was already terminal (crash-recovery race).
- EnrollmentOrchestrator.OnDecisionTerminalStage translates the hook into the
public Terminated event with Reason=DecisionTerminalStage + Outcome mapped
from stage (Completed/Part2/Sealed→Succeeded, Failed→Failed, else TimedOut).
MaxLifetime watchdog is cancelled on the real terminal so the two paths can't
race each other.
Peripheral termination (V2.Core/Termination/*)
- FinalStatus DTO — wire-format mirror of SummaryDialog's Models/FinalStatus
(same JSON property names). Lives in V2.Core because the dialog is a
separate net48 WPF exe we can't ProjectReference.
- FinalStatusBuilder — pure function: DecisionState + AppPackageStateList +
TerminatedArgs + agent start time → FinalStatus. Maps outcome string
("succeeded" | "failed" | "timed_out" | "whiteglove_part1" | "whiteglove_part2"),
derives signalsSeen from DecisionState facts, counts apps by AppTargeted
phase (Device/User), tallies device vs user errors.
- SummaryDialogLauncher — writes final-status.json, copies dialog exe + deps
+ status file to a per-launch temp dir (%TEMP%\AutopilotMonitor-Summary\<guid>),
composes the Legacy-compatible CLI (--status-file, --timeout, --cleanup,
optional --branding-url), invokes UserSessionProcessLauncher.LaunchInUserSession
with the configured retry window. Fire-and-forget, swallows all errors.
- EnrollmentTerminationHandler — the Terminated-event handler that sequences:
build FinalStatus → write JSON + launch dialog → upload diagnostics
(respects DiagnosticsUploadMode: Off/Always/OnFailure) → write
enrollment-complete.marker (suppressed on WhiteGloveSealed) → run
CleanupService.ExecuteSelfDestruct (suppressed on WhiteGloveSealed OR when
SelfDestructOnComplete=false) → signal shutdown MRE. Idempotent; each step
is best-effort and logs rather than throws — nothing is allowed to prevent
agent shutdown.
DefaultComponentFactory
- Stores the ImeLogHost it creates and exposes its PackageStates to the
termination pipeline (factory.ImePackageStates) so FinalStatusBuilder has
the live package list without breaking host encapsulation.
Program.cs wiring
- Instantiates DiagnosticsPackageService, EnrollmentTerminationHandler, and
ServerActionDispatcher with all callbacks live:
• rotate_config → RemoteConfigService.FetchConfigAsync()
• request_diagnostics → DiagnosticsPackageService.CreateAndUploadAsync()
• terminate_session → EnrollmentTerminationHandler.Handle() synthesising a
DecisionTerminalStage/Failed termination
- Terminated event routed to the handler; the handler signals the shutdown
MRE so the main thread cleanly calls orchestrator.Stop().
- ServerActionDispatcher is alive but has no ingest-response consumer yet —
V2 batch endpoint is M5 scope, we plumb the API contract here so M5 can
drop in the response parser.
Tests (+47 total)
- DecisionCore.Tests: +15 SessionStageExtensionsTests (IsTerminal + IsPauseBeforePart2)
- V2.Core.Tests: +32 across terminal-stage hook, FinalStatusBuilder, handler
• DecisionStepProcessorTerminalStageTests: fires once per stage, skips
non-terminal, respects initial-terminal-state (recovery), swallows
handler exceptions, works with null callback
• FinalStatusBuilderTests: outcome mapping per stage, uptime, HelloOutcome
fact path, signalsSeen composition, empty package list, device/user
error tallying, JSON property-name contract
• EnrollmentTerminationHandlerTests: writes FinalStatus.json + marker,
runs/skips cleanup per config + WhiteGlove rule, idempotent shutdown
signal, diagnostics mode matrix (Off/Always/OnFailure × Succeeded/Failed),
suffix ("success" | "failure") per outcome
All 4 suites green: 88 DecisionCore + 353 V2.Core + 507 Agent.Core + 557 Functions
= 1505 tests green.
Remaining M4.6 scope: γ (Geo/NTP/Timezone/Emergency/Distress services wiring,
RegistryWatcher admin-action detector, DeliveryOptimizationCollector in
DefaultComponentFactory) + δ (--run-gather-rules + --run-ime-matching modes,
AnalyzerManager + 3 analyzers).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
81b3e053a3 |
refactor(v2core): M4.4.5.e whiteglove sealing pattern IDs injection
Neue AgentConfigResponse.WhiteGloveSealingPatternIds-Property traegt die Tenant-Config-Liste der Pattern-IDs, die vom V2-Agent als WhiteGloveSealingPatternDetected-DecisionSignal re-emittiert werden. Default leer = Feature off = M3-kompatibel. IComponentFactory.CreateCollectorHosts nimmt zusaetzlich whiteGloveSealingPatternIds entgegen. Orchestrator akzeptiert die Liste per Ctor-Param und reicht sie an die Factory weiter, die sie an den ImeLogTrackerAdapter-Ctor uebergibt (M4.4.4 hat den Hook vorbereitet). EnrollmentOrchestratorEventBridgeTests FakeFactory-Signatur nachgezogen. +3 WG-Pattern-Injection-Tests (265 → 268 gruen; null → empty coerce, verbatim passthrough, empty collection erhalten). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
8294f7a8a2 |
refactor(v2): M4.4.4 classifier-tick from session start + hello-outcome forward + pattern-match hook
Three related fixes Plan §4.x M4.4.4 calls out: 1. ClassifierTick armed at SessionStarted (DecisionCore.Shared) Previously the WhiteGlove classifier deadline was armed reactively on the first WG-relevant signal (AttachWhiteGloveClassifierEffects, WhiteGlove.cs). Non-WG sessions and late-WG sessions never got a periodic re-evaluation, which is the M3 re-trigger-lücke. Fix: HandleSessionStartedV1 now schedules the ClassifierTick deadline up-front and emits the matching ScheduleDeadline effect. The existing reactive `hasTick` dedup makes the WG-path arm a no-op once the tick is already present. Classifier-runner anti-loop (snapshot-hash) keeps the tick cheap for non-WG paths until the session reaches a terminal stage and HandleClassifierTickDeadlineFired stops re-arming. Dead-end branch (SessionStarted on active state) is unchanged — no deadline, no effect, state preserved. 2. HelloOutcome now forwards from coordinator (EspAndHelloTrackerAdapter) ReadHelloOutcome() previously returned hardcoded "unknown", so HelloResolved signals always carried that value regardless of what the inner HelloTracker actually observed. The coordinator already exposes a forwarded HelloOutcome property (line 78, since M2) — only the adapter was missing the wiring. One-line fix: `_coordinator.HelloOutcome ?? "unknown"`. 3. ImeLogTracker.OnPatternMatched hook + WhiteGloveSealingPatternDetected emission (ImeLogTracker + ImeLogTrackerAdapter, closing the M4.3.4 tech debt) Added `Action<string> OnPatternMatched` to ImeLogTracker, invoked by HandlePatternMatch after LastMatchedPatternId is set (subscribers can read the ID synchronously). Adapter gains an optional ctor arg `IReadOnlyCollection<string> whiteGloveSealingPatternIds` (default empty, backwards-compatible with pre-M4.4.4 M3 behavior — no emission). When configured and a matching pattern fires, adapter posts WhiteGloveSealingPatternDetected once per session (fire-once). Polling-on- LastMatchedPatternId (alternative A from plan) was rejected; the hook is a real event surface. Orchestrator in M4.4.5 will inject the tenant- configured sealing-pattern-ID list. Bonus: reduced EventSequenceCounterTests thread-safety stress from 6×200 to 4×50 to avoid parallel xUnit test contention on the Windows atomic-rename path (Next() persists to disk per call). Test still catches any counter race via the uniqueness assertion. SmokeTests.DecisionEngine_Reduce_SessionStarted_advancesStep updated to assert the new ClassifierTick effect/deadline instead of `Assert.Empty(step.Effects)`. +18 tests (1350 -> 1368 green, 57 DecisionCore + 218 V2.Core before vs. 73 + 228 after): ClassifierTickFromSessionStartTests (5) · EspAndHelloTrackerAdapter- HelloOutcomeTests (3) · ImeLogTrackerAdapterSealingPatternTests (7) · smoke + harness updates. Bestehende Scenario-Tests grün (Classic/WG1/WG2/ SelfDeploying/Edge) — ClassifierTick-at-start ist additiv, kein Scoring-Impact. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
fb98d43ae5 |
refactor(decisioncore): M4.4.3 reducer handlers for diagnostic signals
DeviceInfoCollected and AutopilotProfileRead previously fell through to HandleUnhandledSignal, writing unhandled_signal_kind:* DeadEnd rows into the journal on every emission — pure noise, no decision value, but distorting the Inspector timeline. Plan §4.x M4.4.3 fix. Both are diagnostic signals: they carry hardware inventory / Autopilot profile payload used only by the Inspector and backend analyzers. They do NOT drive stage or hypotheses. Fix is minimal: neutral Taken transitions via a new shared helper RecordDiagnosticObservation — bookkeeping advances, stage/hypotheses untouched, no effects, no DeadEndReason. Full payload stays in the SignalLog for Inspector rendering (Plan §2.7c — Snapshot/Journal are Cache, SignalLog is Master; no reason to duplicate data into DecisionState). Version-gap protection: dispatcher entries are keyed on (Kind, v1). A future v2 payload still dead-ends with unhandled_signal_kind:*:v2 — the L.6 forward-compat contract requires explicit handler upgrades, not silent recording under the old handler. AppInstallCompleted/AppInstallFailed intentionally stay unwired — no decision consumer in v11 scope, and the collector isn't emitting them yet. +11 tests (1339 -> 1350 green): Taken-without-DeadEnd, stage+hypotheses unchanged, bookkeeping advancement, no effects, existing deadlines preserved, v2-schema-version falls through to unhandled. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
bca53bd43d |
refactor(v2core): M4.4.2 backend telemetry uploader HTTP skeleton
Concrete IBackendTelemetryUploader implementation for the Plan §2.7a
POST /api/telemetry/batch endpoint. Skeleton only — the live backend
endpoint lands in M5; this milestone only needs the client-side contract
testable against HttpMessageHandler fakes.
Design:
- New class BackendTelemetryUploader in Transport/Telemetry/ (NOT added as a
method on the M2-copied Legacy BackendApiClient, which is slated for
removal in M4.5 / v11.1+). Lives next to IBackendTelemetryUploader;
clean layer, no legacy cross-dependency.
- HttpClient injected (not self-constructed). mTLS client-cert wiring stays
with the orchestrator-builder in M4.4.5 via
HttpClientHandler.ClientCertificates; the uploader itself is cert-agnostic,
which makes it trivially testable with a custom handler.
- Ctor deps are flat: baseUrl, tenantId, optional hardware/bootstrap/version
headers. No AgentConfiguration coupling — orchestrator extracts values.
- No retry/back-off here; TelemetryUploadOrchestrator (M4.1) owns that.
- Body: JSON array of TelemetryItem via Newtonsoft (PascalCase; Kind as
string via existing StringEnumConverter annotation). No gzip — evaluate
in M5 once bandwidth profile is known.
- HTTP status -> UploadResult mapping:
2xx -> Ok
401 / 403 -> Permanent ("unauthorized: ...")
408 / 429 -> Transient (rate limit / request timeout)
other 4xx -> Permanent (client bug; retry pointless)
5xx -> Transient
HttpRequestException-> Transient ("network: ...")
TaskCanceledException (HttpClient internal timeout, not caller CTS)
-> Transient ("timeout: ...")
OperationCanceledException with caller-CTS signaled -> rethrow
Empty batch short-circuits with Ok (no HTTP call) to avoid wasted
round-trips when a concurrent drain emptied the spool mid-upload.
- Security headers: X-Tenant-Id (pre-body auth check), hardware headers
(X-Device-Manufacturer/Model/SerialNumber for whitelist + Autopilot
validation), optional X-Bootstrap-Token (pre-MDM OOBE), X-Agent-Version
for kill-switch + diagnostics.
New Constants.ApiEndpoints.TelemetryBatch = "/api/telemetry/batch" — shared
with M5 backend-side routing.
+26 tests (1313 -> 1339 green): URL + header shape, JSON body wire-format
(Kind as string, TelemetryItemId/PK/RK/PayloadJson round-trip), empty-batch
short-circuit, full HTTP status-code -> UploadResult matrix (2xx/401/403/
4xx/408/429/5xx), HttpRequestException + timeout mapping, caller
cancellation propagation, null-arg rejection.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
4b3fd46f69 |
refactor(v2core): M4.2 effect runner (deadline/classifier/emit/snapshot wiring)
Plan §2.5 / §2.7b / L.17 — einziger Side-Channel vom pure Reducer zur Außenwelt.
New in DecisionCore:
- ClassifierVerdictLookup.LookupLastVerdictId(state, classifierId) — mapping
classifier-id → hypothesis-slot. Shared between ClassifierAwareReplayHarness
(refactored) and the production EffectRunner (DRY, L.15).
New in V2.Core.Orchestration:
- IClassifierRegistry + ClassifierRegistry (id→IClassifier, dup-detect)
- ISignalIngressSink — abstraction for posting synthetic signals (real impl M4.4)
- IEventTimelineEmitter — abstraction for EmitEventTimelineEntry (real impl M4.4)
- EffectFailure + EffectRunResult — orchestrator-consumable dispatch result
- IEffectRunner + EffectRunner
- ScheduleDeadline/CancelDeadline → critical class: any exception sets
SessionMustAbort=true with reason=timer_infrastructure_failure, stops
processing subsequent effects
- RunClassifier → optional class: anti-loop via ClassifierVerdictLookup +
IClassifierSnapshot.ComputeInputHash(); exception → Inconclusive verdict
posted via ingress, no abort
- EmitEventTimelineEntry / PersistSnapshot → transient class: retry
100/400/1600ms via IClock.Delay, exhaust → failure recorded but no abort
(§2.7b explicit „State unverändert, weiter im Flow")
New tests (+18, 74 total in V2.Core.Tests):
- Schedule/Cancel forwarding + abort on scheduler exception + stop-subsequent
- Emit retry-then-success + retry exhaust (no abort, failure recorded) +
failure does not block subsequent effects
- Persist retry-then-success
- Classifier happy (verdict signal posted correctly) + anti-loop skip +
exception → Inconclusive verdict + unknown id → failure + missing snapshot
- Batch-dispatch-order test, empty-batch test
Testsuite: 1177 → 1195. V2-isolation clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
d76da2ad75 |
refactor(v2core): M4.0 persistence writers + deadline scheduler skeleton
- TransitionSerializer + StateSerializer in DecisionCore.Serialization (Newtonsoft-ctor-binding, shared between agent writers + backend verifier) - V2.Core.Persistence: SignalLogWriter, JournalWriter (JSONL append-only, WriteThrough+Flush(true) per append — plan L.12 sofort-flush); SnapshotPersistence (SHA256-envelope, tempfile+rename, .quarantine/) - V2.Core.Orchestration: IDeadlineScheduler + DeadlineScheduler (IClock-basiert; past-due fires on ThreadPool; same-name replace; RehydrateFromSnapshot; plan §2.6 / L.7 Timer-Monopol) - V2.Core.Tests (new net48 xUnit project): 28 tests across persistence (roundtrip, monotonicity, tail-corruption tolerance, checksum-mismatch, quarantine) + scheduler (past-due, future, cancel, replace, rehydrate, dispose) - V2-isolation preserved (no reference to Agent.Core) Testsuite: 1121 → 1149 (+28). Foundation for M4.1–M4.5 (Transport, EffectRunner, SignalAdapters, Orchestrator-Wiring, transitional-Cleanup). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
75c98d131f |
refactor(decisioncore): edge scenarios — hybrid reboot, ESP failure, recovery (M3.5)
Final M3 sub-milestone. Closes the plan's §4 M3 scenario matrix:
Handlers (DecisionEngine.Edge.cs):
- EspResumed mid-ESP reboot. Stage preserved; records
SystemRebootUtc on first observation (hash
stable on repeats).
- EspTerminalFailure Stage=Failed + Outcome=EnrollmentFailed,
deadlines cleared, enrollment_failed event
emitted with reason carried from payload.
Definitive — no classifier re-run.
- SystemRebootObserved records SystemRebootUtc so the WhiteGlove
sealing classifier credits the +15 reboot
weight on the next RunClassifier effect.
Scenario fixtures (committed-anonymized):
- hybrid-reboot-v1.jsonl ESP -> mid-phase reboot -> EspResumed ->
AccountSetup -> Hello -> Desktop ->
Completed. No WhiteGloveSealed path.
- esp-terminal-failure-v1.jsonl ESP -> AccountSetup -> terminal failure
-> Failed / reason=
esp_terminal_failure_account_setup.
Tests (EdgeScenarioTests, 5 new):
- Hybrid reboot preserves stage, records reboot fact, completes
normally without triggering WhiteGlove.
- ESP terminal failure transitions straight to Failed.
- Restart-recovery determinism: replaying the same JSONL twice yields
identical FinalStepHash, Stage, Outcome and transition count.
- Prefix-replay check: truncated log ends in the expected intermediate
stage (simulates crash between signals).
- Global sanity: every committed fixture (all 12) ends in a terminal
stage — regression guard for any future reducer change.
Suite totals: 57 DecisionCore (was 52) / 507 Legacy / 557 Functions =
1121 tests green. All 14+ scenarios from plan §R6 Szenario-Matrix pass
through the reducer with the expected terminal outcome.
This closes M3 (DecisionEngine-Reducer + Classifier-Kernel). M4 wires the
SignalAdapter + EffectRunner + TelemetryTransport in the V2 agent runtime.
Plan: plans/REFACTOR_AGENT_V2.md §4 M3 sub-milestone M3.5 — M3 complete.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
8e4faf4251 |
refactor(decisioncore): WhiteGlove Part 2 + Part2CompletionClassifier + 2 scenarios (M3.4)
Plan §4 M3.4 + plan §2.3 WhiteGlove Part-2 state machine: post-reboot user
sign-in is now a first-class path with a dedicated classifier, a 24h safety
deadline, and an explicit Sealed -> AwaitingUserSignIn -> CompletedPart2 /
Failed progression.
Classifier (WhiteGlovePart2CompletionClassifier : IClassifier):
- Scoring rules NEW (no Legacy equivalent): each of the four Part-2 user
signals (+25) — UserAadSignInComplete, HelloResolvedPart2,
DesktopArrivedPart2, AccountSetupCompletedPart2. All four together hit
the Confirmed threshold (100); missing one drops to Weak (75).
- WhiteGlovePart2CompletionSnapshot implements the new IClassifierSnapshot
interface; existing WhiteGloveSealingSnapshot was retrofit to the same
contract so the harness anti-loop is uniform.
Handlers (DecisionEngine.WhiteGlovePart2.cs):
- SessionRecovered (Shared.cs) now bridges a WhiteGloveSealed state into
WhiteGloveAwaitingUserSignIn and arms the 24h WhiteGlovePart2Safety
deadline on the recovered signal's occurredAt.
- Four Part-2 signal handlers (UserAadSignInComplete, HelloResolvedPart2,
DesktopArrivedPart2, AccountSetupCompletedPart2) record facts + emit a
Part-2 RunClassifier effect; facts arriving outside the AwaitingUserSignIn
stage are recorded but no classifier runs (prevents stray Part-2 verdicts
on non-sealed sessions).
- WhiteGlovePart2Safety deadline handler terminates with Failed /
enrollment_failed reason=part2_user_absent.
- ClassifierVerdictIssued router extended to dispatch on classifier id —
Part-2 verdicts route to ApplyWhiteGlovePart2Verdict; Confirmed triggers
Stage=WhiteGloveCompletedPart2 + Outcome=WhiteGlovePart2Complete +
whiteglove_part2_complete event.
DecisionState: four new SignalFact<DateTime>? fields for the Part-2 signals
(full builder parity).
ClassifierAwareReplayHarness: anti-loop lookup now routes per classifier id
(WhiteGloveSealing <-> WhiteGloveSealing.LastClassifierVerdictId;
Part2Completion <-> WhiteGlovePart2Completion.LastClassifierVerdictId).
Snapshot hashing works through the IClassifierSnapshot interface instead
of a WhiteGloveSealingSnapshot type check.
Scenario fixtures (committed-anonymized):
- whiteglove-part2-happy-v1.jsonl seal Part 1, reboot, 4 Part-2 signals
within minutes -> Confirmed + Part2Complete.
- whiteglove-part2-stuck-v1.jsonl seal Part 1, reboot, 24h passes, safety
deadline fires -> Failed / part2_user_absent.
Tests (3 new in WhiteGlovePart2ScenarioTests):
- Happy terminal state + hypotheses + all four facts + no residual deadlines
- Stuck terminal state + Part-1 Confirmed remains, Part-2 Rejected with reason
- Bridge check: after SessionRecovered(Sealed), Part-2 safety is active
Suite totals: 52 DecisionCore (was 49) / 507 Legacy / 557 Functions.
Plan: plans/REFACTOR_AGENT_V2.md §4 M3 sub-milestone M3.4 + §2.3 Part-2 stages.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
f75a8d4375 |
refactor(decisioncore): WhiteGlove Part 1 handlers + sealing classifier + 4 scenarios (M3.3)
Plan §4 M3.3: the first kernel-service classifier goes live (plan §2.4 L.8),
the effect-based classifier-run contract is wired end-to-end, and the anti-loop
via InputHash is proven by a dedicated fixture.
Classifier (WhiteGloveSealingClassifier : IClassifier):
- Scoring ported 1:1 from Legacy WhiteGloveClassifier (same weights, same
thresholds, same asymmetric-conservative rule: only Confirmed drives the
WhiteGloveSealed stage).
- Positive weights: ShellCoreWhiteGloveSuccess=80, SealingPattern=40,
DeviceOnlyDeployment=15, SystemReboot=15.
- Hard excluders (-100 each): AadJoinedWithUser, DesktopArrived, HelloResolved.
Soft exclude: AccountSetupActivity=-40.
- WhiteGloveSealingSnapshot is the classifier input type and computes a
deterministic SHA256-short InputHash over WG-relevant fields only — so
the anti-loop triggers on meaningful fact changes, not on unrelated state.
Handlers (DecisionEngine.WhiteGlove.cs):
- WhiteGloveShellCoreSuccess records fact; emits RunClassifier effect;
arms ClassifierTick if not already.
- WhiteGloveSealingPatternDetected mirror handler for the IME-pattern path.
- ClassifierVerdictIssued applies the verdict to WhiteGloveSealing
hypothesis; on Confirmed transitions to
WhiteGloveSealed stage +
Outcome=WhiteGlovePart1Sealed and emits
the whiteglove_complete event.
- HandleClassifierTickDeadlineFired (shared router) re-emits RunClassifier
and re-arms itself unless terminal.
ClassifierAwareReplayHarness (tests-only):
- Loops per-step effects: any RunClassifier effect is resolved synchronously
via the registered IClassifier; the verdict is wrapped in a
ClassifierVerdictIssued signal and fed back into Reduce. Anti-loop skips
the call when snapshot hash == state.WhiteGloveSealing.LastClassifierVerdictId.
- Exposes ClassifierRunStats for regression tests.
DecisionState extension: two new SignalFact<bool> fields capture the WG
sightings (ShellCoreWhiteGloveSuccessSeen, WhiteGloveSealingPatternSeen). The
classifier reads them via WhiteGloveSealingSnapshot.
Scenario fixtures (4, committed-anonymized):
- whiteglove-inline-v1.jsonl Confirmed on ShellCore alone (80).
- whiteglove-signal-correlated-v1.jsonl Weak after pattern, ClassifierTick
anti-loop-skipped, Confirmed after
ShellCore escalates the score.
- whiteglove-false-positive-v1.jsonl SealingPattern + late-AADJ hard-
excludes -> Rejected; session still
completes via Classic Hello+Desktop.
- whiteglove-antiloop-v1.jsonl Single pattern + two ticks; runs=1,
skipped_by_antiloop=2.
Tests: 4 scenario tests in WhiteGlovePart1ScenarioTests, all passing.
DecisionCore.Tests: 49 (was 45). Legacy 507 + Functions 557 unchanged.
ReducerFailSafeTests + HarnessTests updated to use AppInstallCompleted as
the still-unhandled sentinel (WhiteGloveShellCoreSuccess is now wired).
Plan: plans/REFACTOR_AGENT_V2.md §4 M3 sub-milestone M3.3 + §2.4 classifier kernel.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
0ebfa2fc3c |
refactor(decisioncore): SelfDeploying + Device-Only handlers + 2 scenarios (M3.2)
SelfDeploying-v1 vertical slice: DeviceSetupProvisioningComplete terminal
handler, DeviceOnlyEspDetection deadline wiring, and the
DeviceOnlyDeployment hypothesis progression UserPresent / DeviceOnly /
Ambiguous expressed via Hypothesis.Reason (plan §2.3).
Handlers (DecisionEngine.SelfDeploying.cs):
- DeviceSetupProvisioningComplete terminal. Cancels all deadlines,
finalizes DeviceOnlyDeployment based on
user-presence facts (AadJoinedWithUser,
HelloResolvedUtc, DesktopArrivedUtc),
raises EnrollmentType to Strong when
no AccountSetup was observed before.
- DeviceOnlyEspDetectionFired hypothesis-only update to
DeviceOnlyDeployment=Strong(DeviceOnly).
Stage unchanged; completion still
requires explicit provisioning signal.
Classic.cs extended:
- First EspPhaseChanged(DeviceSetup) arms the DeviceOnlyEspDetection
deadline (5 min window, plan §2.7).
- EspPhaseChanged(AccountSetup) cancels it — UserDriven-v1 path.
Shared DeadlineFired router routes the deadline to the SelfDeploying body.
Scenario fixtures (tests/fixtures/enrollment-sessions/):
- selfdeploying-happy-v1.jsonl — 3 signals, SkipUserStatusPage
path. Terminal Completed with
DeviceOnly hypothesis confirmed
and no user-presence facts.
- selfdeploying-esp-exit-unknown-v1.jsonl — 4 signals, deadline fires at
T+5m and sets DeviceOnly before
provisioning-complete arrives.
Tests (SelfDeployingScenarioTests, 4 new):
- SelfDeployingHappy terminal state + hypotheses
- SelfDeployingHappy deadline cleanup
- Device-Only ESP-exit-unknown deadline -> provisioning flow
- AccountSetup on the Classic path cancels DeviceOnlyEspDetection
(regression guard — UserDriven should never classify as DeviceOnly)
InternalsVisibleTo added to DecisionCore for DecisionCore.Tests so the
internal DeviceOnlyReasons tokens stay hidden from consumers while tests
assert on them directly.
Suite totals: 45 DecisionCore (was 41) / 507 Legacy / 557 Functions.
Plan: plans/REFACTOR_AGENT_V2.md §4 M3 sub-milestone M3.2.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
0210d42c7f |
refactor(decisioncore): Classic UserDriven-v1 handlers + 3 scenarios (M3.1)
First vertical slice of real reducer behavior: six handlers for the Classic
UserDriven-v1 enrollment flow plus the shared DeadlineFired router, with three
committed-anonymized scenario fixtures driving the proof.
Handlers (DecisionEngine.Classic.cs):
- EspPhaseChanged stage progression DeviceSetup -> AccountSetup;
populates CurrentEnrollmentPhase + DeviceSetupEnteredUtc /
AccountSetupEnteredUtc / FinalizingEnteredUtc facts;
raises EnrollmentType hypothesis to Weak on AccountSetup.
- EspExiting records EspFinalExitUtc; schedules HelloSafety (300 s);
transitions to AwaitingHello and emits ScheduleDeadline.
- HelloResolved records HelloResolvedUtc + HelloOutcome; cancels
HelloSafety; completes session if Desktop already arrived.
- DesktopArrived mirror of HelloResolved; completion when Hello already
resolved.
- ImeUserSessionCompleted raises EnrollmentType hypothesis to Strong; records
matched pattern id.
- AadUserJoinedLate HYPOTHESIS-ONLY (project memory
feedback_aad_joined_late_not_completion). Records
AadJoinedWithUser fact + annotates EnrollmentType reason.
Stage unchanged, no terminal event. Regression guard test
verifies From/To stage equality.
Shared router (DecisionEngine.Shared.cs):
- DeadlineFired dispatches on Payload[deadline]. HelloSafety timeout synthesizes
HelloOutcome=Timeout and drives the same completion path as HelloResolved.
Unknown deadline names cancel the deadline and record a neutral transition —
forward-compat with M3.3 (ClassifierTick) / M3.4 (WhiteGlovePart2Safety).
Constants:
- DeadlineNames enumerates the 7 pflicht-deadlines from plan §2.6.
- SignalPayloadKeys standardizes payload keys used by collectors and the
deadline scheduler.
Scenario harness + fixtures (tests/fixtures/enrollment-sessions/):
- ScenarioTestBase locates repo root and wraps FixtureLoader + ReplayHarness.
- userdriven-happy-v1.jsonl — 7 signals, terminal Completed /
EnrollmentComplete / HelloOutcome=Success.
- userdriven-hello-timeout-v1.jsonl — Desktop arrives before Hello, deadline
fires at T+5m -> HelloOutcome=Timeout,
Completed.
- late-aadj-v1.jsonl — Late-AADJ mid-flight does not complete
prematurely; session still completes
normally via Hello + Desktop.
Tests: 4 scenario tests in ClassicScenarioTests + determinism check.
Total suite: 41 DecisionCore.Tests (was 37 at M3.0) / 507 Legacy unchanged /
557 Functions unchanged.
Plan: plans/REFACTOR_AGENT_V2.md §4 M3 sub-milestone M3.1.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
b6a949bc02 |
refactor(decisioncore): engine foundation — dispatch, fail-safe, clock, serialization (M3.0)
M3.0 establishes the reducer infrastructure the scenario sub-milestones
(M3.1–M3.5) will build on. No domain-specific reducer behavior yet; only
kernel plumbing + a live SessionStarted / SessionRecovered / SessionAborted
lifecycle path.
Kernel (plan §2.5 / L.2 / L.3 / L.16):
- DecisionEngine dispatches on (Kind, KindSchemaVersion) to partial-class
handlers. Unhandled kinds produce a DeadEnd transition with a versioned
reason string — never a throw, never a mutation.
- Exception fail-safe (L.16): any handler exception is caught in the
kernel, StepIndex + LastAppliedSignalOrdinal still advance (so the
signal counts as processed and there is no retry loop), the transition
records DeadEndReason="reducer_exception" + SHA256-short StackTraceHash.
DecisionState is returned untouched beyond bookkeeping.
- Shared helpers for Taken / DeadEnd transition construction keep
partial-class handlers short and consistent.
State (plan §2.3 / L.3):
- Immutable DecisionState gets a mutable DecisionStateBuilder companion —
reducer handlers use state.ToBuilder().WithStage(...).Build() to
express "copy with changes" without retyping 20+ ctor arguments.
- Fluent helpers AddDeadline (replace-if-same-name), CancelDeadline,
WithCurrentEnrollmentPhase carry the most common reducer operations.
Time (plan §2.6 / L.7):
- IClock interface + SystemClock (wall-clock wrapper). The only way any
decision-relevant code can observe time going forward. VirtualClock
lives in the test assembly — never in shipping builds.
Serialization (plan §2.8 / §2.15 / L.14):
- Newtonsoft.Json added to DecisionCore (both net48 + net8.0 consume the
same DTOs with the same converters).
- UnknownFallbackEnumConverter<T> maps unrecognized enum strings to a
caller-provided fallback instead of throwing — lets an older backend
read a newer DecisionCore's rows without crashing; the verifier will
flag them as drift_tolerated in §2.10.
- DecisionCoreJsonSettings registers fallback converters for every
DecisionCore enum (DecisionSignalKind, EvidenceKind, HypothesisLevel,
SessionStage, SessionOutcome, DecisionEffectKind, EnrollmentPhase).
- SignalSerializer round-trips DecisionSignal JSONL with enforced
Evidence.Identifier and Evidence.Kind — a line without Evidence throws
just like constructor validation.
Lifecycle (DecisionEngine.Shared.cs):
- SessionStarted: anchors the first journal transition; rejects mid-flight
replay (StepIndex != 0) with a DeadEnd instead of silently resetting
hypotheses.
- SessionRecovered: neutral bookkeeping in M3.0 — WhiteGlove Part 1 → 2
transition lands in M3.4 DecisionEngine.WhiteGlovePart2.cs.
- SessionAborted: terminal, Outcome=Aborted, all deadlines cleared —
orchestrator admin-action path (plan §2.7 audit).
Test coverage (37 total):
- VirtualClockTests (6) — monotonic clock, Delay instant-return
- DecisionStateBuilderTests (5) — immutable semantics, deadline replace/cancel
- SerializationTests (6) — round-trip, UnknownFallback both string + numeric
- ReducerFailSafeTests (6) — null-arg, replay-on-active, unhandled kind,
SessionAborted clears deadlines, ReducerVersion
- SmokeTests (7) — smoke, updated: Reduce(SessionStarted) is a
real transition, not a throw
- HarnessTests (6) — empty-stream determinism, single-signal replay,
unknown-kind DeadEnd at harness level
- VirtualClock helper (instance) covered via Harness integration
Suite totals unchanged elsewhere: 507 Legacy + 557 Functions. Full solution
builds 0 warnings, 0 errors.
Plan: plans/REFACTOR_AGENT_V2.md §4 M3 (sub-milestone M3.0 Foundation).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
830a27bb70 |
refactor(decisioncore): add DecisionCore shared library scaffolding (M1)
New netstandard2.0 project src/Shared/AutopilotMonitor.DecisionCore/ with 14 scaffolding types across Signals/, State/, Classifiers/, Engine/. Agent.Core and Functions reference the library without call-sites; the full solution builds 0 errors, 0 warnings. DecisionEngine.Reduce is an intentional NotImplementedException stub — M3 fills in the partial-class reducer. Invariants wired into the scaffolding: - Immutable DTOs (sealed + get-only + With-methods) per L.3 - Pflicht-Evidence validation in Evidence ctor per L.5 - KindSchemaVersion enforcement per L.6 - AssemblyVersion 2.0.0.0 as ReducerVersion source per §2.10 Architecture principles L.1–L.18 extracted to .claude/architecture-principles.md as persistent Claude-session context. Plan: plans/REFACTOR_AGENT_V2.md §4 M1. Zero runtime risk. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |