mirror of
https://github.com/okieselbach/AutopilotMonitor.git
synced 2026-10-24 12:42:38 +00:00
3bfe7d7fdbb7a12fdc97e45de592ca37da4e3204
1247 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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. |
||
|
|
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. |
||
|
|
c2c55a191e |
v2 single-rail 5.3a: migrate ServerActionDispatcher to Signal-Post + ActionId dedup
Plan §5.3 + §6.2 — ServerActionDispatcher now emits server_action_received/executed/failed through InformationalEventPost (single-rail pipe), not the legacy Action<EnrollmentEvent> bypass. Adds dedup of at-least-once re-deliveries from the backend PendingActions queue: composite key (Type|RuleId|QueuedAt) maps every re-delivery of the same server-queued action to the same slot, squelching duplicates before any telemetry or handler side effect fires. Fixes the 77× terminate_session storm observed in the PR-motivating session without rejecting two genuinely distinct admin-issued terminates. Program.cs: ServerActionDispatcher construction moved inside orchestrator.Start's onIngressReady hook so the live lifecyclePost backs it; WireTelemetryServerResponse picks it up from the outer-scope local after Start returns. Tests: 8 new ServerActionDispatcherTests cover success / failure / unknown- type emission plus the dedup matrix (same key twice, same-batch dup, distinct QueuedAt, distinct RuleId, distinct Type with identical QueuedAt). V2.Core test suite: 527 → 535, all green. Architecture baseline unchanged — ServerActionDispatcher still holds no structural dependency on TelemetryEventEmitter. |
||
|
|
b02313b7ea |
v2 single-rail 5.2: migrate StartupEnvironmentProbes to Signal-Post (B7)
StartupEnvironmentProbes.RunAsync no longer takes a TelemetryEventEmitter
— it now routes device_location, timezone_auto_set, ntp_time_check, and
agent_trace (geo-failure) events through InformationalEventPost. The
existing Source labels ("Network") survive via the Emit(EnrollmentEvent)
overload's payload decomposition.
- Parameter rename: emitter → post throughout RunAsync / RunGeoAndTimezone /
RunNtpCheck / SafeEmit. Bodies unchanged apart from the dispatched Emit.
- Unused using removed (Telemetry.Events namespace no longer referenced
from this file — the probes live outside the emitter-internal zone).
- Program.cs passes lifecyclePost (the single-rail helper instance
established in PR #1) to the background Task.Run invocation, which runs
after orchestrator.Start has completed so the hook has already assigned
the variable.
- Architecture baseline test (PR #0 §4.4) now expects 3 dependents
instead of 4 — StartupEnvironmentProbes dropped out. Comment updated
to mirror the migration inventory so future PRs see the correct
removal sequence.
527 V2.Core tests + 108 DecisionCore tests still pass. No behavior
change on the wire: device_location / ntp / timezone_auto_set events
carry the same EventType / Source / DataJson as before.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
8b413d59a8 |
v2 single-rail 5.1c: migrate Program.cs lifecycle events to Signal-Post
Shifts every direct orchestrator.EventEmitter.Emit(...) in Program.cs (B2-B6 in the plan inventory) onto InformationalEventPost. Fixes the Parity Issue #1 ordering regression: agent_started now lands on the signal log as Seq=1 because it is posted from inside the orchestrator's onIngressReady hook, before any collector-generated signal reaches the ingress. Call sites migrated: - agent_started, agent_version_check, agent_unrestricted_mode_changed: emitted inside the new orchestrator.Start(ingress => { ... }) hook, in the canonical boot order. - enrollment_failed (max_lifetime watchdog), agent_shutdown (auth_failure watchdog): emitted from their event-handler lambdas via a lifecyclePost variable captured by reference — the handlers are registered before Start but only fire later, by which time the callback has assigned the live helper. Three private helpers (EmitAgentStartedEvent, EmitVersionCheckEventIfAny, EmitUnrestrictedModeAuditIfChanged) switched from an EnrollmentOrchestrator parameter to an InformationalEventPost parameter; bodies unchanged apart from the final Emit call. Side fixes: - enrollment_failed (max_lifetime) no longer declares Phase=Complete (plan §1.4 phase invariant — only phase-declaration events may set a non-Unknown phase). The UI bucketing behavior is unchanged; the legacy setting produced a phantom phase in the timeline. - Null-guard on the lifecyclePost variable: if a watchdog fires before Start's hook has assigned the helper, emission is skipped with a warning rather than NRE. In practice impossible (handlers only fire after Terminated / ThresholdExceeded which come later), but defensive. Left for later PRs (plan §2 inventory): - line ~575 ServerActionDispatcher emitEvent callback → PR #3 (B28). - orchestrator.EventEmitter property itself → PR #10 enforcement step. Tests: 527 V2.Core + 108 DecisionCore pass. Full solution builds clean. Functional impact on a real session: the V2 parity audit's Parity Issue #1 (agent_started = Seq=13) is structurally fixed — verification will be via an E2E session after this lands on a ring device. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
3acac2ee71 |
v2 single-rail 5.1b: add Emit(EnrollmentEvent) overload to InformationalEventPost
Migration convenience for PR #1 call sites that already assemble an EnrollmentEvent inline. Decomposes the event into the primary Emit(eventType, source, ...) signature so a call-site diff stays to a single line change. - Overload maps EnrollmentEvent fields onto SignalPayloadKeys: EventType, Source, Message (when non-empty), Severity, ImmediateUpload, phase (only when != Unknown). Transport-managed fields (Sequence, RowKey, SessionId, TenantId) are ignored — the engine assigns them once the event reaches TelemetryEventEmitter again. - Data (Dictionary<string,object>) is flattened to Dictionary<string,string> with invariant-culture formatting; DateTime values use the "o" roundtrip format so wire shape matches the legacy Newtonsoft output. - Timestamp is forwarded as occurredAtUtc — EnrollmentEvent()'s ctor always sets it to DateTime.UtcNow, so there is no default to fall back from. Unspecified Kind is treated as UTC since the wire contract is UTC-only. 6 new xUnit tests cover: full-field decomposition, non-Unknown phase declaration, Timestamp-forwarding with Kind normalization, empty-message omission, DateTime round-trip format, null-argument rejection. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
f3a2cc721f |
v2 single-rail 5.1a: add onIngressReady hook to EnrollmentOrchestrator.Start
Add an optional caller hook that fires after _ingress.Start() and the WhiteGlove Part-2 recovery bridge (step 13a) but before any collector host is started (step 14). Program.cs uses this slot in the next commit to post agent_started / agent_version_check / agent_unrestricted_mode_changed signals so they land on the signal log with sequence numbers lower than anything the collectors produce — fixes the Seq=13 ordering regression from the V2 parity audit (plan Parity Issue #1). - Start(Action<ISignalIngressSink>? onIngressReady = null). Default null preserves the existing signature 1:1 so all existing callers compile unchanged. - Hook fires synchronously on the caller thread. Exceptions are caught and logged so a malformed hook cannot abort Start or prevent collectors from running. - 5 xUnit tests cover: backward-compatible no-hook Start, synchronous invocation with live ISignalIngressSink, ingress.Post works from inside the hook without throwing, exception isolation, and idempotency (second Start still throws, hook runs exactly once). Program.cs migration lands in the next commit so this change can be reviewed in isolation. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
d1b4ceb561 |
v2 single-rail 4.4: add architecture baseline test for emitter callers
Living documentation of every type in V2.Core that structurally depends on TelemetryEventEmitter today. Shrinks with each migration PR; PR #10 tightens the assertion to the PR10 permitted set (EventTimelineEmitter + BackPressureEventObserver), at which point a new bypass attempt fails the build. - Reflection over fields, properties, constructor params, and method params on types in AutopilotMonitor.Agent.V2.Core. IL-level call-site detection is deliberately avoided: structural dependencies are sufficient to prove reachability and need no extra packages. - ExpectedCurrentBaseline enumerates the four known dependents today: Rail A (EventTimelineEmitter), the meta exception (BackPressureEventObserver), the EnrollmentOrchestrator field + EventEmitter property, and StartupEnvironmentProbes' static methods. - Second test asserts the PR10 permitted set is already a subset of the current baseline — guards against accidentally losing Rail A or the meta exception while migrating. - Program.cs is in the V2 executable assembly, outside this test's scope; it reaches the emitter via orchestrator.EventEmitter, which PR #10 internalizes. PR #0 is now functionally complete: InformationalEvent signal + reducer case + emitter parameter overrides + helper + baseline test, all with dedicated coverage. Build green, 624 tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
428b14cd60 |
v2 single-rail 4.3: add InformationalEventPost helper for signal posting
Introduce the migration helper that lets peripheral collectors post an InformationalEvent signal without assembling the payload dict, the evidence record, or the signal kind themselves. Every PR #1-#9 call site in the single-rail refactor will go through this helper instead of writing raw TelemetryEventEmitter.Emit calls. - InformationalEventPost in V2.Core.Orchestration, constructor takes ISignalIngressSink + IClock (matches the existing adapter pattern). - Emit(eventType, source, ...) writes eventType + source mandatorily and folds optional message / severity / immediateUpload / phase / data into the payload using SignalPayloadKeys constants. - Data entries that collide with reserved top-level keys are silently dropped so a caller-supplied "source" in data cannot rewrite the explicit source label on the wire. - Evidence summary falls back to explicit arg → message → eventType in that order. - Timestamp defaults to IClock.UtcNow; callers can override via occurredAtUtc for deterministic replay harnesses. 13 xUnit tests cover payload composition, optional-field omission, data-collision guard, sourceOrigin/evidence fallback chain, argument validation, and timestamp semantics with VirtualClock. The helper lives outside Telemetry.Events so it stays reachable from callers once TelemetryEventEmitter becomes internal in PR #10 (enforcement step). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
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>
|
||
|
|
c2d3a433ed |
v2 single-rail 4.2b: extend EventTimelineEmitter parameter overrides
Follow-up to 4.2 needed before the InformationalEvent reducer case can
land: the emitter must let the reducer pass through the originating
collector's Source, Severity, Message, and ImmediateUpload values,
otherwise single-rail migration would collapse every event to
Source="decision_engine" / Severity=Info-by-suffix and break UI filters.
- New parameter keys: source, severity, message, immediateUpload.
Reason also gets a named constant (it was already consumed by
BuildMessage but as a string literal).
- Defaults preserve today's behavior exactly:
* Missing source → "decision_engine" (SourceId)
* Missing / unparsable severity → DeriveSeverity(eventType) suffix rule
* Missing message → "{eventType}: {reason}" fallback (or bare eventType)
* Missing / unparsable immediateUpload → true
- BuildDataDict strips all top-level parameter keys so the wire format
is self-consistent; reason stays in Data by design.
- Eight new tests for the overrides + their fallback semantics, plus a
regression guard that immediateUpload defaults to true when absent.
No reducer case emits any of these new keys yet — behavior change is
gated on the subsequent HandleInformationalEventV1 commit (4.1b).
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> |
||
|
|
715527eda9 |
v2 single-rail 4.2: add phase-override to EventTimelineEmitter
Add an optional "phase" parameter to the EmitEventTimelineEntry effect contract so explicit phase-declaration events (agent_started, esp_phase_changed) can carry their phase through the reducer pipe without breaking the Phase=Unknown default invariant for all other events (feedback_phase_strategy). - New PhaseParamKey constant and ParsePhase helper; unknown / unparsable values fall back to EnrollmentPhase.Unknown, never throw. - BuildDataDict skips the phase key so the top-level field is not duplicated in Data (parallel to the existing eventType skip). - Tests cover: valid enum name applied, invalid value falls back, case-sensitive match required, phase not duplicated in Data, plus the existing "no phase param -> Unknown" case. Part of the PR #0 foundation in the V2 single-rail refactor (see plan §1.4). No behavior change for today's reducers — they do not yet pass a phase parameter. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
23aa51a3f2 | fix warnings and fix v2 agent hash compare | ||
|
|
42ff4284d8 | added version info to backend | ||
|
|
2e82dd2ba6 | fix folder structure v2 agent | ||
|
|
2ecbefce73 | backend route fix and v2 agent version check fix | ||
|
|
06bbf13dbe |
Merge branch 'v2-parity-port': V2-idiomatic refactor of parity ports
Three commits route lifecycle telemetry through the V2 reducer rather than synthesising events from Program.cs. Also wires the pre-existing LogReplay infrastructure to the CLI. - |
||
|
|
1bb659cac5 | chore: bump build counters after V2 parity refactor tests | ||
|
|
f149eef3fe |
feat(agent-v2): wire --replay-log-dir CLI flag to ImeLogTracker SimulationMode
V2 had the complete replay infrastructure in place but never wired up: ImeLogTracker.SimulationMode / SpeedFactor properties (ImeLogTracker.cs:66-67) and the ApplySimulationDelay compression loop (ImeLogTracker.Handlers.cs:529-546) were fully implemented, and AgentConfiguration had ReplayLogDir + ReplaySpeedFactor fields (L.128, L.134), but nothing set them from the CLI and nothing read them in the factory. This commit adds: - CLI args --replay-log-dir <path> and --replay-speed-factor <n> in Program.BuildAgentConfiguration (invariant-culture parse, zero/malformed rejected). - DefaultComponentFactory.ImeLogHost ctor accepts a simulation mode + speed factor and applies them to the inner tracker when ReplayLogDir is non-empty. - PrintUsage documents the flags under a new "Dev / Test" section. For the deterministic signal-level replay (V2 ReplayHarness + signal-log.jsonl fixtures) no wiring is needed - that is already the primary V2 test path. The IME- log replay activated here is the V1 compatibility dev-mode for support cases where only raw IME logs are available. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |