mirror of
https://github.com/okieselbach/AutopilotMonitor.git
synced 2026-10-24 12:42:38 +00:00
96191c874035bc4e0779a02663d3cc8a51af52f7
1266 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
96191c8740 |
fix(v2): 882fef64 codex review followup — TableStorage force-update CurrentPhase + persist phase package snapshots
Codex review of merged PR3 + PR5 surfaced two correctness gaps that match real-world session traffic. Finding 1 — TableStorageService.Sessions.cs force-update path: The normal status-update path mirrors `update["CurrentPhase"] = Complete` whenever status flips to Succeeded (line ~1019), so V2 enrollment_complete events (which carry Phase=Unknown per feedback_phase_strategy) still land at CurrentPhase=Complete. The ETag-exhaustion / force-update block at line ~1224 was missing the same override, leaving CurrentPhase stuck at the prior in-flight value (or -1) when ETag retries are exhausted exactly at the completion update. 6-line fix mirrors the well-tested normal-path override. Finding 2 — ImeTrackerStateData.PhasePackageSnapshots persistence: GetAllKnownPackageStates() returns the union of live _packageStates and the _phasePackageSnapshots dict (DeviceSetup apps captured before _packageStates.Clear() on the AccountSetup transition). SaveState only persisted the live list, so an agent restart between phases — common on hybrid-join + multi-reboot enrollments — lost the DeviceSetup snapshot and dropped those apps from FinalStatus + app_tracking_summary. ImeTrackerStateData now carries `Dictionary<string, List<PackageStateData>> PhasePackageSnapshots`, SaveState serialises it, LoadState restores it tolerantly (null on old state files → empty dict, no crash). Refactor side-product: extracted ToPackageStateData / FromPackageStateData as private static helpers in ImeLogTracker so the mapping is single-source-of-truth across live + snapshot serialisation. Skipped: Finding 3 (DetectHelloPolicy first-match-wins between Device/User CSP scopes) not fixed — Intune CSPs are exclusive per setting (Device OR User, never both), CSP wins over GPO, the first-match iteration is correct in practice. Saved feedback_csp_scope_exclusive.md so a future review doesn't re-suggest the defensive-aggregation patch. Tests: V2.Core 729 (+2 new for save/load round-trip + old-snapshot backwards-compat), DecisionCore 231, Functions 736, vitest 179, solution build 0/0. No new test for Finding 1's force-update path — the method has no existing test infrastructure (no Azurite fixture, no mock TableClient); the fix mirrors a well-tested normal-path override and risk is essentially zero. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
d0200d061a |
fix(v2): 882fef64 PR5 — peripheral logging follow-up (SelfUpdater + AppPackageState + RemoteConfigMerger)
Plan listed five 0-logger-call hotspots; two (RegistryWatcher, VersionCheckEventBuilder) are already adequately observed via existing call-site logging. The remaining three got targeted log lines where forensic gaps actually exist: - SelfUpdater: 4 new entry/success Info lines (start, ZIP downloaded, integrity verified, extract succeeded) so the happy-path is no longer silent. The failure branches were already logged via WriteSkipMarker / inner helpers. - AppPackageState: new read-only `WasAutoDowngradedToSkipped` flag set when UpdateState reclassifies an Installed-without-prior-download into Skipped via the inverse-detection rule. ImeLogTrackerAdapter.EmitAppState's PR3-D4 DEBUG line now appends `auto-downgraded=true (inverse-detection)` when the flag is set — answers "why is App X marked Skipped without a download" in one line. - RemoteConfigMerger: pre-overwrite snapshots for 7 operator-relevant knobs (SelfDestruct, KeepLog, Reboot, DiagnosticsMode/Enabled, ShowSummary, AgentMaxLifetime). One Info line per knob that actually flipped — bounded spam (Merge runs ~5min). Existing LogLevel + UnrestrictedMode lines unchanged. Skipped: - RegistryWatcher: already exposes Error event + _trace callback wired by both callers (AadJoinWatcher, ProvisioningStatusTracker) into _logger.Trace + _logger.Warning. No silent path. - VersionCheckEventBuilder: caller in Program.cs already logs ParseError (Warning), Event (Info+outcome), Deduped (Debug). No silent path at the caller boundary. Tests: V2.Core 727, DecisionCore 231, Functions 736, solution build 0/0. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
07312ee2a3 |
fix(v2): 882fef64 PR3 — kernel-path observability (A2 + A3 + D1-D5)
Session 882fef64's 12-min agent log carried only `EnrollmentOrchestrator: started.`
and `decision terminal stage reached` between the first event and termination — no
stage-transition trail, no signal cadence, no effect failures. PR3 wires logging
into the kernel hot path so every Decision-Stage transition, every signal accept/
reject, every effect retry/critical-failure, and every IME app state change leaves
a trace at the right severity.
A2 — noise reduction:
- TelemetrySpool `enqueued itemId=...` DEBUG -> VERBOSE (600+ lines/session before)
- IME `impersonation` lines deduplicated by user with 60s rollup
A3 — strengthened existing weak lines:
- `Command line:` shows `(no args)` instead of empty
- `Process exit code: 0` -> `Process completed: <name> -> exit=N, durationMs=T`
- IME session change now carries sessionId+user
- HelloTracker 358/360 suppression includes eventTime+provider
- EnrollmentOrchestrator started/stopping carry sessionId/stage/hosts/signals
D1-D5 — observability into the silent kernel components:
- DecisionStepProcessor: per-step DEBUG (stage transitions + effects) + VERBOSE for no-ops
- SignalIngress: 5 strategic log lines (drop, append-fail, worker-fault, stop summary, processed)
- EffectRunner: 5 strategic log lines (retry, exhausted, critical, anti-loop, classifier throw)
- ImeLogTrackerAdapter: 7 emit-site log lines covering all 7 emission paths
- InformationalEventPost: per-emit DEBUG with eventType/source/immediate/severity/dataKeys
All log statements are null-safe (_logger?.Debug); legacy unit-test rigs without a
logger keep working. New ctor overloads on InformationalEventPost / SignalIngress /
EffectRunner / ImeLogTrackerAdapter are backwards-compatible (logger param defaults
null).
Side fix: BinaryIntegrityVerifierTests pinned to [Collection("SerialThreading")].
Process-global `_runtimeSelfUpdateFired` static raced with parallel mismatch tests;
PR3's logging shifted thread timing enough to surface the race consistently.
Tests: V2.Core 727, DecisionCore 231, Functions 736, vitest 179, solution build 0/0.
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> |
||
|
|
f6d8a286ef |
fix(v2): 882fef64 PR2 — AgentLogger size-cap rotation
Daily log file used to grow without bound under verbose mode. AgentLogger now caps each segment at 50 MB (configurable per-instance) and rolls forward to agent_YYYYMMDD_002.log, _003.log, ... when the cap is reached. Existing rotation suffixes are probed on construction so process restarts mid-day continue at the highest existing segment instead of overwriting older ones. The first line of every rotated segment is an `AgentLogger: rotated` marker for forensic readability. Size check + rotation are best-effort: exceptions are swallowed so the logger never breaks the agent. Tests: V2.Core 716 (+5 new in Logging/AgentLoggerTests.cs), solution build 0/0. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
b243706629 |
fix(v2): 882fef64 PR1 — Company Portal UI, diagnostics ZIP State/Spool, agent_shutting_down V1 parity
- DownloadProgress.tsx: exempt app_download_started from <1024B drop so WinGet/Store apps (Company Portal etc.) appear in the UI; filter logic extracted to pure helpers in lib/downloadProgressFilters.ts for vitest coverage. - DiagnosticsPackageService: ZIP now also packages AgentState/ (snapshot, journal, signal-log, ime-tracker-state, enrollment-complete.marker, final-status, whiteglove-backfill-state + .quarantine), AgentSpool/ (pending uploads + cursor), and AgentMarkers/ (top-level whiteglove.complete + clean-exit). BuildArchiveBytes extracted as internal seam so xUnit asserts ZIP contents without HTTP. - EnrollmentTerminationHandler: agent_shutting_down emission moved to top of Handle() so it fires unconditionally — including SelfDestruct=false dev VMs and WhiteGlove Part-1 exits. Data carries reason/outcome/stage/uptimeMinutes/agentVersion. Tests: V2.Core 711 (+12), DecisionCore 220, Functions 736, vitest 179 (+11), solution build 0/0. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
8b7a5f509e |
fix(v2): debrief 7dd4e593 followups + final-status schema 2 + V2 dialog renderer
F3 — SummaryDialog launch crash: switch temp dir from C:\WINDOWS\SystemTemp\ (unreadable for standard users when agent runs as SYSTEM) to flat %ProgramData%\AutopilotMonitor-Summary\ + wipe-on-launch (V1 parity), and grant BUILTIN\Users delete rights so the dialog's --cleanup self-removal works. F4 — Backend phase classification: classify `phase_transition` events (emitted by V2 DecisionEngine for AppsDevice/AppsUser/FinalizingSetup) as phase updates so CurrentPhase advances during App phases. Also force CurrentPhase=Complete on Status=Succeeded to materialise the terminal phase regardless of the carrier event's Phase value (V2 emits enrollment_ complete with Phase=Unknown per feedback_phase_strategy). F5 — App tracking summary covers all phases: ImeLogTracker now snapshots AppPackageState references (not serialized dicts) before _packageStates. Clear() at the AccountSetup transition, and exposes a deduped union GetAllKnownPackageStates() for the termination summary path. Without this, DeviceSetup apps were silently dropped from app_tracking_summary and the SummaryDialog after the phase transition. Schema 2 / V2 dialog — final-status.json now carries schemaVersion=2 plus failureReason, signalTimestamps, and per-app errorPatternId/errorDetail/ errorCode. SummaryDialog branches on schemaVersion: V1 path is the unchanged legacy renderer (deletable when V1 retires), V2 path renders 6 outcome states (Success / PreProvisioningComplete / TimedOut / Failure / Unknown / WhiteGlovePart2), shows a coloured banner with the agent- supplied failure reason, and per-app rows with duration on the right plus the IME-pattern error detail under the app name when an install failed. V1 agent + V1 dialog rendering remain bit-for-bit identical (no schema field → V1 render path). V1 Agent.Core.Tests 507/507, DecisionCore 220/220. Tests: V2.Core 702/702 (+12 schema-2 builder, signal-timestamps, failure- reason, per-app errors, phase-snapshot dedup, launcher path). Backend 736/736. New SummaryDialog.Tests 45/45 (OutcomeMapper + V1/V2 JSON roundtrip + FormatDuration). Build clean (0 warnings). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
a523192dc7 | backend fix buildtime | ||
|
|
bba25328a5 |
Merge pull request #54 from okieselbach/v2/fix-deadline-keys-and-immediate-flush
fix(v2): first-session regressions — deadline payload key + immediate-flush wakeup + willlaunch noise |
||
|
|
43ee4d2fd0 |
fix(v2): first-session regressions — deadline payload key, immediate-flush wakeup, willlaunch noise
Three independent first-session bugs surfaced while debugging session 9ed7021e
(V2 agent v2.0.333, Classic user-driven enrollment):
P0 — FinalizingGrace deadline dead-ended with "deadline_fired_without_name".
The orchestrator's OnDeadlineFired posted a payload using key "deadlineName"
while the reducer reads under SignalPayloadKeys.Deadline ("deadline"). Every
deadline (FinalizingGrace, HelloSafety, DeviceOnlyEspDetection, ClassifierTick,
WhiteGlovePart2Safety) fell through that key-mismatch and was rejected by the
reducer — in production this left every Classic session hanging in
Stage=Finalizing forever, blocking enrollment_complete, the final snapshot, the
terminal drain, and the diagnostics upload. Fix: forward ActiveDeadline.FiresPayload
verbatim so the per-deadline payload its scheduling callsite already populated
reaches the reducer unchanged.
P1 — "30 s silent gap at session start" in the UI. Every lifecycle-critical
item (agent_started, enrollment_type_detected, esp_phase_changed, hello wizard
events, auth-failure shutdown, enrollment_failed on max-lifetime, …) was
enqueued with RequiresImmediateFlush=true but the drain loop never woke up on
it — it blocked the full Task.Delay(_drainInterval) window (30 s by default)
before the first upload fired. Fix: add ITelemetrySpool.ImmediateFlushRequested
event raised on every immediate-flush enqueue, and race the drain loop's delay
against it via Task.WhenAny on a per-iteration TaskCompletionSource. First
post-start upload now happens in milliseconds; periodic cadence is unchanged.
P2 — hello_provisioning_willlaunch/willnotlaunch (EventID 358/360) noise in
the timeline. Per project memory `project_hello_willlaunch_unreliable` these
are snapshot-only prerequisites-passed flags that flip multiple times per
session (session 9ed7021e: 6× 358, three within 232 ms around desktop arrival)
and the DecisionEngine already treats them as non-evidence. Stop emitting
the backend events entirely — keep a single DEBUG log line per observation
so the sequence is still reconstructible from agent-logs during diagnostics.
Tests
- TelemetrySpoolTests: event fires exactly on immediate enqueues.
- RecoveryPathTests: past-due FinalizingGrace deadline advances Finalizing→Completed
(direct P0 regression guard) + fixed two assertions that encoded the old
buggy key / stale-log emptiness assumption.
- ScenarioTests.Full_pipeline: asserts enrollment_complete reaches the uploader
within 10 s — doubles as the P1 end-to-end wakeup guard.
- EnrollmentOrchestratorTests.Stop_flushes_final_snapshot: relaxed "stepIndex
equals observed" to "snapshot matches stop-time CurrentState + ≥ observed",
now that deadlines cascade correctly after SessionStarted.
- HelloTrackerImmediateUploadTests: 358/360 assertions flipped from "emitted
with ImmediateUpload=false" to "not emitted at all".
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
6a50256d59 |
Merge pull request #53 from okieselbach/v2/test-consolidation-theories
test(v2): consolidate xUnit tests to Theories + extract shared Harness |
||
|
|
2cdebaa7fc |
test(v2): consolidate xUnit tests to Theories + extract shared Harness
Converts repeated Fact-clusters across the V2 Agent and DecisionCore test suites into data-driven Theories, and extracts two oft-duplicated inner fixture/rig classes into reusable Harness helpers. Net: smaller files and more coverage via InlineData rows for edge cases that had no own Fact before. Wave 1 — Fact→Theory consolidations (10 files): - RemoteConfigMergerTests: LogLevel parsing (3 Facts → 1 Theory, 9 rows including new Whitespace/Empty/Null + case-insensitive combos). - ImeLogTrackerAdapterTests: EspPhaseChanged phase-mapping (3 Facts → 1 Theory, 7 rows + Finalizing alias, Complete, preboot). - InformationalEventPostTests: eventType/source validation + evidenceSummary precedence (3 Facts → 2 Theories with 4+4 rows). - ProgramGuardsTests: CheckEnrollmentCompleteMarker matrix (3 Facts → 1 Theory with 4 rows incl. new (no-marker, no-selfdestruct) cell). - AuthFailureTrackerTests: distress dispatch (2 Facts → 1 Theory, 5 rows incl. 403 dedup + 500 catch-all). - SessionIdPersistenceTests: ctor rejection (1 Fact → 3 rows) + LoadSessionCreatedAt null-return (2 Facts → 5 rows incl. empty/whitespace /out-of-range dates). - TelemetrySpoolTests: MarkUploaded regression guard (1 Fact, 3 inline calls → 1 Theory with 5 rows incl. same-value idempotence). - ReducerReplayTests: null-arg rejection (1 Fact, 3 asserts → 1 Theory). - AppInstallSignalHandlerTests: newState → breakdown (4 Facts → 1 Theory, 7 rows incl. case-insensitive lowercase/uppercase + empty). - ClassicAwaitingHelloGuardTests: 8 Facts → 2 Theories with 6 rows each (full skipUser × accountSetupEntered matrix; 4 new cells). Wave 2 — Shared Harness extraction (2 new helpers): - ImeLogTrackerAdapterFixture.cs (60 LOC) replaces 5× duplicated inner Fixture classes across ImeLogTrackerAdapter*Tests.cs (~150 LOC saved). - EnrollmentOrchestratorRig.cs (65 LOC) replaces 6× near-identical Rig/ Build helpers across EnrollmentOrchestrator*Tests.cs (~200 LOC saved). Results: - AutopilotMonitor.Agent.V2.Core.Tests: 679 tests (was 650), all green. - AutopilotMonitor.DecisionCore.Tests: 220 tests (was 164), all green. - 22 files changed, +350 / -825 lines (-475 net). - No production code touched; no V1 code touched. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
f9c0a9d134 |
Merge pull request #52 from okieselbach/v2/codex-05-scenario-profile
v2 codex-05: typed EnrollmentScenarioProfile + Observations + ClassifierOutcomes |
||
|
|
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> |
||
|
|
5091d5f1ed |
Merge pull request #50 from okieselbach/v2/codex-review-followups
v2: codex-review followups (#1, #2, #3, #4, #6) |
||
|
|
1418eacfe9 |
v2 codex-post-#50-2: rehydrate-failure durable abort + v1 payload contract (#E #F)
Addresses Codex second-pass findings against commit
|
||
|
|
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> |
||
|
|
96854d20ac |
v2 codex-01 phase 4: correct ISnapshotPersistence doc reference to ReducerReplay
The interface docstring promised rebuild via "Reducer.Replay(signals)" but no such method existed until Phase 1. Point readers at the actual symbol (AutopilotMonitor.DecisionCore.Engine.ReducerReplay.Replay) and note the seed semantics now that Phase 2 wires tail-replay. Off-repo artefacts also updated (not committed — tasks/ + ~/.claude/ are local scope): - tasks/lessons.md: new entry on recovery-anchor-preservation. - .claude/plans/codex-review-followups-master.md: #1 marked Done with commit hashes. - user memory project_v2_single_rail_in_progress: appended Codex follow-up section with phase commits + new invariant. All suites remain green (build 0 warnings). Next: Finding #2. Plan: .claude/plans/codex-01-recovery-signallog-replay.md §3 Phase 4. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
5029bdd4c3 |
v2 codex-01 phase 3: re-arm persisted deadlines after recovery replay
Wires IDeadlineScheduler.RehydrateFromSnapshot (long-present, zero production callers) into EnrollmentOrchestrator.Start so deadlines persisted inside DecisionState survive a restart. Without this any safety-net armed pre-crash (HelloSafety, Part2Safety, FinalizingGrace, ClassifierTick) silently vanishes and a resumed session can hang until the max-lifetime watchdog trips. Placement matters: the rehydrate call MUST run after the Fired event is subscribed (step 12 in Start) AND after _ingress.Start (step 13) because (a) past-due deadlines fire immediately via ThreadPool and would otherwise fall into the void without a subscriber, (b) OnDeadlineFired returns early when _ingress is null and would drop the synthesised DeadlineFired signal. Put at step 13.5 just before the WG Part-2 bridge so rehydrated fires precede fresh collector signals. Rehydrate failures are logged at error but do NOT throw — this path will get a synthetic EffectInfrastructureFailure transition once Codex follow-up #2 lands, at which point the session can terminate cleanly instead of relying on the max-lifetime watchdog. Tests (2 new in RecoveryPathTests): - Snapshot_with_past_due_deadline_fires_DeadlineFired_signal_on_start — end-to-end: scheduler fires via ThreadPool, bridge posts signal, signal reaches the SignalLog within 3s. - Snapshot_with_future_deadline_is_scheduled_but_not_yet_fired — future-due deadline reports IsScheduled=true, no premature fire. All suites green: DecisionCore 146/146, Agent.V2.Core 640/640 (+2), Functions 727/727. Build 0 warnings. Plan: .claude/plans/codex-01-recovery-signallog-replay.md §3 Phase 3. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
216967e51c |
v2 codex-01 phase 2: orchestrator recovery replays SignalLog tail + truncates phantom Journal
Wires ReducerReplay.Replay (Phase 1) into EnrollmentOrchestrator.Start so
recovery honours the SignalLog = truth / Snapshot = cache invariant:
- Snapshot loaded cleanly: replay any SignalLog entries past the
snapshot's LastAppliedSignalOrdinal onto the snapshot seed. Closes
the crash window where Journal flushed step N but Snapshot only
made it to step N-1.
- Snapshot corrupt: quarantine the snapshot alone and rebuild state
by replaying the entire SignalLog. Previously the log was moved
aside together with the snapshot, destroying the only recovery
anchor. Log-head-corrupt remains the only escalation to
total-loss quarantine (SegmentQuarantine.QuarantineAll).
- Post-recovery: if Journal.LastStepIndex >= initialState.StepIndex
the on-disk tail contains phantom transitions from a pre-crash
flush. JournalWriter.TruncateAfter(boundary) moves the suffix to
.quarantine/<ts>/journal-phantom-tail.jsonl and realigns counters.
Without this the next live Append would fail monotonicity and
quarantine-escalate the session.
Changes:
- IJournalWriter: new TruncateAfter(int lastValidStepIndex) method.
- JournalWriter: clock ctor overload (for deterministic quarantine
bucket timestamps); TruncateAfter implementation with write-temp +
atomic rename and forensic phantom-tail preservation.
- FakeJournalWriter: matching in-memory TruncateAfter.
- EnrollmentOrchestrator: three-branch recovery (a/b/c) described
inline, using ReducerReplay.Replay and CollectSignalLogTailAfter.
Snapshot quarantine now scoped to the snapshot file only unless
the log is head-corrupt.
Tests:
- JournalWriterTests: 5 new TruncateAfter cases (drops suffix +
quarantines, boundary=last is no-op, boundary>last throws,
boundary=-1 empties file, empty journal no-op).
- RecoveryPathTests: 3 new integration scenarios (snapshot+pending
tail, corrupt-snapshot-intact-log, journal-ahead-of-state
triggers truncate).
- All suites green: DecisionCore 146/146, Agent.V2.Core 638/638
(+8), Functions 727/727. Build 0 warnings.
Plan: .claude/plans/codex-01-recovery-signallog-replay.md §3 Phase 2.
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> |
||
|
|
6a94d4d19d |
Merge pull request #49 from okieselbach/v2/single-rail
v2 Agent: first-session fixes + registry-guard dead-code removal |
||
|
|
fef5834ad0 |
v2 single-rail 13: remove dead Registry-guard in CheckEnrollmentCompleteMarker
The registry-based ghost-restart branch in CheckEnrollmentCompleteMarker (HKLM\SOFTWARE\AutopilotMonitor\Deployed present + !SessionExists) has never fired in production and was structurally unreachable: - session.id is created via SessionIdPersistence.GetOrCreate BEFORE the guard runs (both in V1 LoadConfiguration:131 and V2 Program.cs:216), so the !SessionExists() retry-cleanup arm is never reachable at runtime. - Even if the arm were reachable, the Deployed regkey cannot distinguish "fresh install" from "ghost restart": it is written by --install and intentionally preserved by CleanupService as a bootstrap-script reentry lock (read by scripts/Bootstrap/Install-AutopilotMonitor-v2.ps1 Guard 1 to prevent re-install on production devices). Both scenarios therefore present as "Deployed present + session.id absent". Remove the registry-guard block and its dead retry-cleanup path. Keep the file-based enrollment-complete.marker path intact — it is written by CheckSessionAgeEmergencyBreak before its own retry-cleanup attempt and serves the "cleanup-disabled emergency break" exit-cleanly case. Also drop the now-obsolete "Create SessionId BEFORE guards" justification comment in Program.cs and the matching boot-sequence doc wording — neither remaining guard (file-based marker, session-age watchdog) keys on session.id absence for ghost detection. CheckEnrollmentCompleteMarker loses the now-unused dataDirectory parameter; callsites updated. Tests: ProgramGuardsTests.cs keeps marker-absent / marker-without- selfdestruct / marker-with-selfdestruct-retry coverage; the first-run- after-install regression test is renamed to match its actual scope (returns_false_when_marker_file_is_absent). 630/630 V2.Core tests green. No behavior change — removed path was unreachable. TryWriteDeploymentMarker in Program.InstallMode.cs stays (writes the bootstrap-reentry-lock); CleanupService continues to intentionally leave the regkey in place. 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> |
||
|
|
6fec3e2b0b |
v2 single-rail 10: first-session-fixes PR 2 — event observability (Fixes 1+3)
Fix 1 — ImmediateUpload=true on UX-critical lifecycle events. Policy driven by UI responsiveness, not "lifecycle-sounding": * ImeLogTrackerAdapter: esp_phase_changed, all app-state transitions (started/completed/failed/download-started + download_progress live bar), do_telemetry flip to immediate. download_progress ticks every 3s only while a download is active (bounded by download duration). * HelloTracker: hello_policy_detected flips to immediate. Existing terminal set (shouldTriggerHelloCompleted / hello_provisioning_failed) unchanged. willlaunch/willnotlaunch/pin_status stay batched — snapshot flags that flip multiple times per session (project_hello_willlaunch_unreliable). * ShellCoreTracker + reducer-synthesised terminals were already immediate. Fix 3 — desktop_arrived InformationalEvent. DesktopArrivalDetectorAdapter now dual-emits: existing DecisionSignalKind.DesktopArrived(Part2) for the reducer AND SharedEventTypes.DesktopArrived informational event for the Events table / UI timeline / MCP query_raw_events. Fire-once across both rails. Payload carries detectedAt / detectionSource / part2Mode. Tests: +20 xUnit cases. DecisionCore 133, Agent.V2.Core 606, Functions 727, Web vitest 168 — all green. Solution build 0 warnings, 0 errors. 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>
|
||
|
|
0c58465095 |
v2 single-rail 8.1: codex PR-1 pass-1 — SkipUser race + late-fill fix
Codex static review of
|
||
|
|
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>
|
||
|
|
05c92196cc |
Merge pull request #48 from okieselbach/v2/single-rail
v2 Agent: single-rail refactor + premature completion fix |
||
|
|
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> |
||
|
|
96dc1482c7 |
chore: drop local-only tooling + fixtures from repo
- tests/fixtures/enrollment-sessions-local/ is raw/pre-anonymization session fixture staging — keep untracked; remove .gitkeep + README.md from the index and simplify the gitignore to ignore the whole folder. - tools/export-session-fixture.mjs is a local conversion helper; add to gitignore next to the existing tools/orphan-analysis.py entry. Files remain on disk locally; only the Git index is pruned. 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> |
||
|
|
877523594f |
v2 single-rail 5.10: enforce single-rail — internalize emitters + shrink baseline to 2 callers
Plan §5.10 — final PR of the single-rail refactor. Rail B is gone; Invariant 1 (single caller set) + Invariant 2 (compile-time enforcement) are now active. Classes internalized: - TelemetryEventEmitter (public sealed → internal sealed; Emit → internal) - EventTimelineEmitter (public sealed → internal sealed) - BackPressureEventObserver (public sealed → internal sealed) Consumers of these three live in the same Telemetry.Events namespace (plus InternalsVisibleTo for the test assembly). Any future attempt to construct or reference them from outside the assembly becomes a compile error — the single-rail invariant is now load-bearing on the type system, not review. EnrollmentOrchestrator scrubbed: - Deleted the `public TelemetryEventEmitter EventEmitter` property (no external consumer after PR #1–#9). - Deleted the `BuildTelemetryEventSink()` Action<EnrollmentEvent> factory (last caller removed in step 14 of Start, where it wired collectors; those now post via ISignalIngressSink exclusively). - Dropped the `_eventEmitter` field — the emitter is built as a local in Start and handed to EventTimelineEmitter + BackPressureEventObserver inline. After Start returns, nothing in the orchestrator references it. Architecture baseline test shrinks by one as a direct consequence. IComponentFactory cleanup: - Removed `Action<EnrollmentEvent> onEnrollmentEvent` parameter from CreateCollectorHosts (interface + DefaultComponentFactory + call-site in EnrollmentOrchestrator.Start). - DefaultComponentFactory.ImeLogHost constructor drops the now-dead onEnrollmentEvent parameter (declared but never used — relic from the pre-migration wire). - ICollectorHost + IComponentFactory doc comments updated to reflect the new "post-via-ingress" contract. Gather-rule internals (GatherRuleExecutor, GatherRuleContext, CommandCollector, LogParserCollector) keep their internal Action<EnrollmentEvent> signature — they are the orthogonal standalone CLI mode (plan §9), not a kernel path; the session-mode GatherRuleExecutorHost already bridges the Action to an InformationalEventPost in PR #6. Architecture baseline test: - `TelemetryEventEmitterCallersBaselineTests` promoted from transitional baseline (3 dependents) to strict enforcement: exactly the two permitted callers EventTimelineEmitter + BackPressureEventObserver. A third entry now fails the build — the review signal for any reintroduced bypass is loud. - Dropped the `Current_baseline_is_a_superset_of_the_PR10_permitted_callers` sanity test (redundant once the enforcement set IS the permitted set). Test fixtures updated: - EnrollmentOrchestratorEventBridgeTests rewritten — the "Action<EnrollmentEvent> bridge" contract the old tests exercised is gone, so the tests covering it (Emit-from-host-reaches-transport, emitter-exception-does-not-propagate etc.) were removed. Remaining coverage: Start without factory, Start with factory spawns + starts hosts, factory receives ingress + clock, Stop stops + disposes hosts. - EnrollmentOrchestratorWhiteGlovePatternTests fake factory updated to the new IComponentFactory signature. - EnrollmentOrchestratorTests + EnrollmentOrchestratorOnIngressReadyTests: removed the `sut.EventEmitter` / `orchestrator.EventEmitter` assertions — the property no longer exists. Verification: dotnet build AutopilotMonitor.sln clean; V2.Core test suite 559 / 559 green (down from 562 — 3 obsolete bridge tests removed, 0 new failures); DecisionCore 108 / 108 green. Soll-Zustand per plan §8 now holds in code: - Single-rail Emission: exactly 2 callers of TelemetryEventEmitter.Emit. - Compile-time enforcement: emitter class + method are internal; any bypass attempt breaks the build instead of slipping through review. - No Action<EnrollmentEvent> parameter on ICollectorHost / IComponentFactory. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
791eadcd7a |
v2 single-rail 5.9b: ImeLogTrackerAdapter wire missing ImeLog callbacks (B29 part 2)
Plan §5.9 / PR #9 — V1 Parity Issue #3 completion: wire the three tracker callbacks the adapter previously did not touch so the full V1-compatible wire shape reaches the Events table via Signal-Post (Rail A single-rail). Adapter additions: - OnImeAgentVersion → ime_agent_version InformationalEvent (Source=ImeLogTracker); no dedup — matches V1 behavior where the same version fires multiple times per session at each pattern-match ingress. - OnDoTelemetryReceived → do_telemetry InformationalEvent carrying the full DO payload (doFileSize / doTotalBytesDownloaded / doBytesFromPeers / doPercentPeerCaching / doDownloadMode / doDownloadDuration / doBytesFromHttp + app state/intent/targeted/runAs/progress). Tracker guards with HasDoTelemetry, so each app fires at most once per "DO complete" pattern match. - OnScriptCompleted → script_completed InformationalEvent with Source="IME" (not "ImeLogTracker" — matches V1 contract observed in the Events table for both platform and remediation scripts); Severity auto-derives to Error when Result="Failed" or ExitCode != 0. Supports both scriptType=platform (Result + exitCode) and scriptType=remediation (ScriptPart + ComplianceResult). All three callbacks preserve prior handlers via the existing chain-invoke pattern and are restored on Dispose. Tracker's LastMatchedPatternId flows into the payload's "patternId" key to match the V1 shape. Tests: +11 new adapter tests covering each callback (payload/fields/severity, null-guards, prior-handler chaining, Dispose restore). Suite passes 48/48 for ImeLogTrackerAdapterTests and 562/562 for the V2.Core project. Full solution build clean. Parity Issue #3 (V1 reference e99a6805 → V2 Beleg-Session e259c121): the IME-driven events app_install_started / app_install_completed / app_install_failed / app_download_started / download_progress / do_telemetry / script_completed / ime_agent_version / ime_user_session_completed / esp_phase_changed are now emitted by V2 via the single-rail Signal-Post path. 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> |
||
|
|
be9070a68f |
v2 single-rail 5.8: DeviceInfoCollector + new DeviceInfoHost (B27, fixes Parity Issue #2)
V1 Parity Issue #2: DeviceInfoCollector existed in V2.Core but was not wired into any host, leaving the Web "Device Details" section empty on every V2 session. This PR migrates the collector to Signal-Post and adds a new DeviceInfoHost that schedules CollectAll() on a background thread at orchestrator startup. Collector (3 partials — .cs, .HardwareSpec.cs, .NetworkAndSecurity.cs): * Constructor Action<EnrollmentEvent> emitEvent -> InformationalEventPost post. 14 emit-sites flow through the shared EmitDeviceInfoEvent helper plus one direct enrollment_type_detected emit: os_info, boot_time, network_adapters, dns_configuration, proxy_configuration, autopilot_profile, enrollment_type_detected, esp_config_detected, secureboot_status, bitlocker_status, tpm_status, aad_join_status, network_interface_info, hardware_spec, wifi_signal_info. New DeviceInfoHost (nested in DefaultComponentFactory): * Kernel host (not remote-config-gated) — Device Details are essential for every enrollment session, not optional telemetry. * Constructs InformationalEventPost from (ingress, clock). * Start() schedules CollectAll() on Task.Run — WMI queries can take several seconds and must not block orchestrator.Start. Exceptions are logged at Warning but never crash the host. * Stop() is a no-op; the background task completes on its own. Follow-up (tracked in plan §5.8 TODO): Legacy EnrollmentTracker also called CollectAtEnrollmentStart on first DeviceSetup phase detection (re-fetches AAD join / autopilot profile / ESP config / TPM once MDM enrollment has populated them) and CollectAtEnd at termination (re-fetches BitLocker + active NIC). Hooking those into the V2 signal timeline requires a reducer-driven subscription that does not exist yet. This PR solves the dominant parity case; the re-collections are deferred. Tests: 538 V2.Core + 108 DecisionCore = 646 green. No DeviceInfoCollector unit tests exist today (the collector is deeply tied to live WMI + registry); the behavior change is verified structurally via the factory registration. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
818fa53728 |
v2 single-rail 5.7: migrate Analyzer trio + AgentAnalyzerManager to Signal-Post (B24-B26)
Full migration — the 3 analyzer classes and their manager all move to
InformationalEventPost, no Action<EnrollmentEvent>-bypass remains in the
analyzer surface (no dual-use concerns; no standalone CLI mode touches
these types).
Analyzers (1 Emit-site each):
* LocalAdminAnalyzer (B24): local_admin_analysis.
* SoftwareInventoryAnalyzer (B25): software_inventory_analysis
(startup baseline + shutdown delta; WG-part discriminator unchanged).
* IntegrityBypassAnalyzer (B26): integrity_bypass_analysis.
Manager:
* AgentAnalyzerManager stores InformationalEventPost _post and passes
it to all 3 analyzer constructors in Initialize().
Program.cs wiring:
* AgentAnalyzerManager construction deferred into orchestrator.Start's
onIngressReady hook so lifecyclePost is non-null at construction time.
The outer-scope `AgentAnalyzerManager analyzerManager = null;`
declaration is captured by both the later RunStartup() call (after
Start returns) and the terminationHandler constructor (inside the
same hook).
Tests:
* AgentAnalyzerManagerTests helper NewPost() returns a throwaway
InformationalEventPost(new FakeSignalIngressSink(), VirtualClock)
for the 7 ctor sites including the null-arg test.
* 538 V2.Core + 108 DecisionCore = 646 green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
8e3e62f4d3 |
v2 single-rail 5.6: route session-mode GatherRules through Signal-Post (B20-B23)
GatherRuleExecutorHost now constructs an InformationalEventPost from (ingress, clock) and wraps post.Emit into the Action<EnrollmentEvent> that GatherRuleExecutor + GatherRuleContext + LogParserCollector + CommandCollector still consume internally. The factory's onEnrollmentEvent parameter is dropped from the host constructor; the CreateCollectorHosts call-site passes ingress + clock instead. Design rationale — the 4 internal Gather types keep their Action<EnrollmentEvent> signature (no ICollectorHost / IComponentFactory contract forces migration, and the standalone --run-gather-rules CLI mode in Program.GatherRulesMode.cs still needs to collect raw EnrollmentEvents in-memory for its direct BackendApiClient.IngestEvents upload path, which is the orthogonal world from plan §9). Session mode now routes all gather events through the InformationalEvent ingress pipe before hitting the telemetry spool — Rail-A semantics for ordering and replay determinism. Standalone mode unchanged. Tests: 538 V2.Core + 108 DecisionCore = 646 green. No Gather-specific tests exist in V2.Core.Tests today; the behavior change is verified structurally via the factory signature. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
2fc405fb8b |
v2 single-rail 5.5c: migrate ESP+Hello trackers to Signal-Post (B14-B17, B19)
ESP+Hello sub-tracker bundle migration — they share the coordinator's
delegate and must be migrated together to keep the build buildable.
Sub-trackers (constructor param Action<EnrollmentEvent> onEventCollected
-> InformationalEventPost post):
* HelloTracker (B17, 7 emit-sites): hello_policy_detected, hello_*
UDR/HelloForBusiness events, hello_wait_timeout (x2),
hello_completion_timeout, SetPolicyForTest helper.
* ShellCoreTracker (B14, 1 emit-site): esp_exiting / esp_failure /
whiteglove_complete / hello_wizard_started via ProcessEvent.
* ProvisioningStatusTracker (B15, 3 emit-sites): fallback
esp_provisioning_status, EmitProvisioningEvent, EmitRawRegistryDump.
* ModernDeploymentTracker (B16, 2 emit-sites): ProcessEvent dispatch
(modern_deployment_log/warning/error) + HandleWhiteGloveStart.
Coordinator (B19):
* EspAndHelloTracker stores InformationalEventPost _post and forwards
it to all four sub-trackers in Start(). The per-tracker
onEventCollected forwarder parameter is retired; the single-rail
post flows through the coordinator unchanged.
Factory wiring:
* EspAndHelloHost drops onEnrollmentEvent and constructs the post from
(ingress, clock) before building the coordinator. The adapter still
gets the shared ingress directly for its DecisionSignal surface.
Adapter tests (5 files):
* ShellCoreTrackerAdapterTests, ProvisioningStatusTrackerAdapterTests,
HelloTrackerAdapterTests, EspAndHelloTrackerAdapterTests,
EspAndHelloTrackerAdapterHelloOutcomeTests fixture rigs use a
separate throwaway FakeSignalIngressSink for tracker emissions so
adapter DecisionSignal assertions on Ingress.Posted remain clean.
Tests: 538 V2.Core + 108 DecisionCore = 646 tests green.
PR #5 complete: all 7 tracker classes (B13-B19) now post through
InformationalEventPost. Architecture baseline unchanged (trackers never
had structural TelemetryEventEmitter deps; the reach went through the
Action<EnrollmentEvent> delegate chain from EnrollmentOrchestrator).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
14b57ac4b8 |
v2 single-rail 5.5b: migrate ImeProcessWatcher to Signal-Post (B18)
ImeProcessWatcher.OnImeExited now emits `ime_process_exited` via an InformationalEventPost built from (ingress, clock) inside ImeLogHost's constructor. Constructor parameter rename Action<EnrollmentEvent> onEventCollected -> InformationalEventPost post. No test file existed for ImeProcessWatcher; Windows-only Process.Exited semantics make it non-trivial to unit-test. Coverage stays via the E2E smoke test. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
cc9c927227 |
v2 single-rail 5.5a: migrate StallProbeCollector to Signal-Post (B13)
StallProbeCollector now takes InformationalEventPost post instead of Action<EnrollmentEvent> onEventCollected. Three emit-sites (EmitProbeResultEvent, EmitProbeCheckTraceEvent, EmitSessionStalledEvent) route through the single-rail ingress. StallProbeHost in DefaultComponentFactory constructs the post from (ingress, clock) and drops the previous onEnrollmentEvent parameter. Adapter test fixture uses a separate throwaway FakeSignalIngressSink for the tracker's informational emissions so DecisionSignal assertions on the adapter's shared Ingress remain uncontaminated. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
908255f57b |
v2 single-rail 5.4: migrate periodic collectors to Signal-Post (B8-B12)
Plan §5.4 PR-Block #4 — migrates the five peripheral periodic collectors from the Action<EnrollmentEvent> bypass onto the single-rail pipe. CollectorBase: ctor parameter Action<EnrollmentEvent> emitEvent becomes InformationalEventPost post; protected EmitEvent field becomes Post. Every subclass's emit site switches from EmitEvent(new EnrollmentEvent{...}) to Post.Emit(new EnrollmentEvent{...}) — the EnrollmentEvent overload keeps the on-wire shape identical while routing through ingress → reducer → EmitEventTimelineEntry effect. - PerformanceCollector (B8) — performance_snapshot - AgentSelfMetricsCollector (B9) — agent_metrics_snapshot - NetworkChangeDetector (B10) — network_state_change + network_connectivity_check - DeliveryOptimizationCollector (B11) — download_progress + do_telemetry - PeriodicCollectorLifecycleHost.EmitIdleStopped (B12) — performance_collector_stopped + agent_metrics_collector_stopped PeriodicCollectorLifecycleHost now owns its own InformationalEventPost built from an IdleActivityObservingIngressSink wrapper. The wrapper peeks each signal's eventType payload and bumps _lastRealEventTimeUtc for any non-periodic event, preserving the V1-parity idle-timeout window semantics the legacy WrapEventSink had — just at the ingress layer now. Wrapper is a nested sealed class; observer exceptions are swallowed so they cannot break the ingress pipe. DefaultComponentFactory: PeriodicCollectorLifecycleHost, NetworkChangeHost, and DeliveryOptimizationHost now take ingress + clock instead of onEnrollmentEvent. The remaining hosts keep onEnrollmentEvent for now — PR #5 through #9 migrate them in batch. Tests: V2.Core 538, DecisionCore 108, all green. Architecture baseline unchanged (still 3 permitted dependents) — none of the five migrated types holds a structural reference to TelemetryEventEmitter anymore. |
||
|
|
0bc04c3a9a |
v2 single-rail 5.3c: Program.cs terminate callback waits for shutdown completion
Plan §6.2 synchronous-shutdown — the ServerActionDispatcher terminate callback now blocks until the main-thread cleanup sequence (orchestrator stop, client disposal) has fully run, instead of returning Task.CompletedTask immediately after terminationHandler.Handle. Mechanism: a second ManualResetEventSlim `shutdownComplete` is Set() at the very end of the finally block (after orchestrator.Stop + HTTP-client disposal). The terminate callback waits on it for up to 60s after terminationHandler.Handle returns. If the bound expires we log and return rather than deadlock the ingest-dispatcher thread forever. Effect: no fresh ingest call can happen between terminationHandler.Handle returning and the process actually exiting, which would otherwise give the backend another window to re-deliver terminate_session. Combined with the PR #3a dedup this closes the 77×-storm loop from the root. Non-server-action termination paths (decision-engine enrollment_complete, max-lifetime, auth-failure, Ctrl+C) are unaffected — they already drive shutdown from the main thread itself. V2.Core: 538, DecisionCore: 108, all green. |