mirror of
https://github.com/okieselbach/AutopilotMonitor.git
synced 2026-10-24 12:42:38 +00:00
a3f98abcb519fcc55bb9b905aef2ae4fccd0e4bc
33 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
2f9a404323 |
Record session start and end on the server clock (no reader yet)
Session durations come from device times, and a device clock can be off by hours for a whole session or change in the middle of one. Every upload carries the device send time next to the server receive time; their difference is the device clock error. This change records the session's start, WhiteGlove Part 2 start and end on the server clock next to the device values. Nothing reads them yet, so no stored value and no display changes. - ServerTime: the rule, defined by tests/fixtures/server-time/cases.json and run by both the backend (ServerTime.cs) and the web (lib/serverTime.ts). - Ingest: ServerTime.Tracker, the rule fed one upload at a time, keeps its state in the primary-only column ServerTimeState and writes StartedAtServer and ResumedAtServer in the counter merge (no extra read or write). It follows a session from its first upload only, and only when that upload carries agent_started; sessions already under way when this build arrives get their values at completion. On every upload prefix of every followed case it gives the rule's start and Part 2 start; a retried upload with agent_started is not counted as a restart. - Terminal: the counter reconcile, which every terminal path runs, also computes the rule over the same event scan and writes StartedAtServer, ResumedAtServer and CompletedAtServer. Fail-soft: a rule failure never costs the counter reconcile or the ingest write. - Re-registration carries the four columns from the fresh CAS read through the Replace (the agent's first upload often races its registration). - SessionSummary gets the three fields; the index mirrors them, the index merge drops primary-only columns; manifests and wire types regenerated. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> |
||
|
|
c6fda24843 |
Show the OOBE quality update live on the session page and in the Progress Portal (D-322)
The time attribution shows the OOBE quality update only once a session ended. While it runs, the web now derives it from the events it already holds: - lib/osUpdateLive.ts ports the calculator's update interval for the current window and labels it from positive evidence that the update finished: running (restarting), waiting for sign-in from 2 min after its end, or failed/skipped without a restart. A package waiting for its restart keeps the update running whatever the page reported. - Session page: a line under the phase timeline, shown after the first complete walk of the event pages, with a note when the device has been silent for 15 minutes. - Progress Portal: the headline says that Windows is installing updates and asks the user to sign in after the update's restart. - OobeUpdateVocabulary (Shared) is the one vocabulary of the agent, the calculator and the web (manifest section oobeUpdate); same strings on the wire, no behaviour change. - tests/fixtures/os-update-live/cases.json pins the calculator and the port (TimeAttributionOsUpdateLiveParityTests, lib/__tests__/osUpdateLive.test.ts). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> |
||
|
|
08455cc5f9 |
OOBE quality update: install events from the System log, servicing steps, a bounded engine phase in Shadow mode (D-310)
Report 3a2207978e4c: a session was failed 30 min after the ESP exit while Windows installed its OOBE quality update. The update watcher had been blind since July and the engine had no notion of the phase. - Windows Update client: 19/20/43/44 come from the System log (provider-filtered XPath), as the provider manifest routes them; 25/26/31/41 from Operational via their own config list. Updates are classified os/defender/store/other: only Windows/.NET updates become rows, the rest is counted into windows_update_activity_summary. Per-channel watermarks persist a record only after its row and engine signal were handed to the ingress. - Servicing: CBS steps from the Setup log (windows_update_servicing), client UpdateAgentLCU. - Engine: OsUpdateActivity and OsUpdateFacts (event-time ordered) anchor an OOBE update phase after the ESP exit. Each state has its own bound: the update runs at most 3 h (then Failed oobe_update_not_finished), after it the user has 60 min to sign in (then the engine stops waiting without a verdict), after the sign-in one window of grace. - The phase acts only in OobeUpdatePhaseMode=Active. Shadow, the default, reports what it would decide as oobe_update_phase_shadow right before today's verdict. The agent stamps the mode on EnrollmentFactsObserved at every start; a later stamp can take Active away, never grant it. - Agent: a stop without a verdict ends the session like the max-lifetime watchdog (agent_shutting_down reason=stopped_waiting); the backend classifies it honestly (verdict origin stopwait), never Failed. - Measurement: oobe_update_page (the update page and its ExpeditedUpdate_* names; a 62407 value only as boolean, integer or HRESULT) and oobe_update_state (NDUP and ESP-policy registry values), both Debug and bounded per run. - ANALYZE-DEV-004 1.2.0 counts install and download failures only, never scan failures. - Event types in Constants, MCP catalog and web; shared manifests regenerated; privacy page names Windows updates installed during the enrollment. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> |
||
|
|
c60e07dfd4 |
Rule notifications resolve {{token}} placeholders before they are sent
A stored rule result keeps the rule's template; only the web and the MCP
server resolved it. The notification builders sent the explanation as
stored, so a channel card showed {{reason}}, {{appName}},
{{failedSubcategory}} and {{errorCode}} although the session had recorded
all four values.
- RuleTemplateInterpolator: the backend copy of the resolution, applied in
BuildRuleFiredAlert and AddRuleResultSections before the text is cut.
In a notification a list item whose value was not recorded is dropped
and a token in running text becomes an en dash.
- The cut lands on a line, sentence or word boundary instead of a
character count; the limits are unchanged.
- tests/fixtures/rule-template-interpolation/cases.json: one case file
run by the backend, the web and the MCP server.
- The engine's evidence auto-field list is one constant. The web, the MCP
interpolator and the validate_rule token lint gain timeoutHours, which
the engine already recorded.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
||
|
|
8cb5d42edc |
Seal pre-provisioning despite IME's AccountSetup line; report why a session never registered
WhiteGlove seal: IME reports "EspPhase: AccountSetup" from the device session as soon as the device apps are done when the user ESP is enabled. The sealing classifier counted that as user activity (-40) and left pre-provisioning Part 1 unsealed, so no whiteglove.complete marker was written and the Part-2 boot hit the 48-hour emergency break. The excluder now also needs the ESP registry to show AccountSetup progress: the adapter stamps accountSetupProgress on WhiteGloveShellCoreSuccess, only after the tracker has actually read the AccountSetup subcategories (unread is never "untouched"). The new excluder is a subset of the old one, so a verdict can only rise; signals without the key score as before. WhiteGloveShellCoreSuccess v1 gets its replay fixture. The Part-1 exit writes whiteglove.complete as its first step instead of after analyzers, diagnostics upload, grace and drain, where a reseal could kill the process before it. Registration failures: a run that exits because register-session failed records outcome, last error, network link at start and end, the wall-clock attempt window and the config-fetch outcome in registration-failure.json. The emergency break sends the record as AgentErrorReport.PriorRegistrationFailure (null for older agents); the backend bounds it and adds it to the ops details, the App Insights dimensions and the timeline data. Retry behaviour is unchanged. The raw registry trigger whiteglove_signal_* is renamed save_result_step_*: the step exists in every 25H2 enrollment and the old name read as a pre-provisioning signal. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> |
||
|
|
89aa9e1f10 |
fix(mcp): CIMD fetch checks the address inside the socket's own DNS lookup
The SSRF gate resolved the client_id host, checked the answer and then handed the hostname to fetch(), which resolved it again at connect time. A DNS-rebinding host could pass the check with a public answer and be connected to an internal one. The metadata fetch now runs through a dedicated undici dispatcher whose connect.lookup is the gate: one resolution per connection, every address must be public, the socket connects only to those addresses. The global fetch is untouched. The address classifier now refuses every IANA special-purpose block (IPv4), allows only global unicast 2000::/3 minus its special-purpose blocks (IPv6), and refuses zone ids and IPv4-mapped addresses. The tests enumerate the unedited IANA registries (tests/fixtures/ iana-special-purpose-addresses) and prove on loopback sockets that a refused host opens no socket and that an allowed one is connected to exactly the address the gate returned. Adds undici ^8.11.2 (engines >=22.19.0). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> |
||
|
|
d99e16314b |
agent+backend: one telemetry wire class, 422 poison for rejected items, agent responses in the typed regime (D-200/D-201)
TelemetryItemKind moves to Shared; TelemetryItem.ToWire() maps onto TelemetryItemDto and the uploader serialises the DTO, so both ends share one wire class. The spool line keeps writing Kind by name (StringEnumConverter registered in TelemetryItemSerializer). IngestTelemetryFunction.PartitionBatch routes by exact kind name and collects every item it cannot route or parse; a non-empty rejection list answers the batch with 422 + TelemetryItemsRejectedResponse (poison, rejectedRowKeys, reason) before anything is persisted, logs a Warning and writes the new TelemetryItemsRejected ops event. The agent's existing poison protocol drops exactly the named RowKeys and re-uploads the rest; before this the ingest skipped the items, answered 200 and the agent cleared its spool. Contract tests from both sides: TelemetryWireContractTests (agent) freezes the real batch body under tests/fixtures/telemetry-wire/ and parses the pinned poison body; TelemetryWireFixtureTests (backend) dispatches every frozen body with zero rejections, requires an arm per kind and pins the poison body. Agent-facing responses implement IApiResponse and go through ResponseHelper, request types carry [WireContract]; plain ingest refusals use the error envelope, the register-session and upload-url refusals keep their DTO bodies (the agent reads ErrorCode/Message). Manifests and generated wire types regenerated (also picks up four types HEAD had left out of the manifest). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
3c8ef02191 |
feat(agent): RealmJoin self-update is observed and reported as realmjoin_autoupdate_detected — realmjoin_detected read the version once from RealmJoin.exe, but the tray AppUpdater often replaces the MSI-shipped binary on its first start minutes after detection (gktatooine 946ccbd6: 4.21.4 -> 4.21.18, whole deployment ran on the new build while the timeline and RealmJoinFacts kept 4.21.4); the updater's only registry write is HKLM\...\Uninstall\RealmJoin (DisplayVersion/InstallDate), so RealmJoinWatcher arms a RegNotifyChangeKeyValue watcher there once Detected fired (appearance-watch on the Uninstall parent until the tray creates the entry), re-reads the binary version after a 2 s debounce (DisplayVersion fallback while the file is locked, channel from baseline) and emits the Info event {previousVersion,newVersion,releaseChannel} plus the typed RealmJoinAutoUpdateDetected signal, whose reducer overrides RealmJoinFacts.ProductVersion/ReleaseChannel (WithVersionOverride) and touches neither gate, deadline nor LastActivityUtc; unreadable baseline at detection is seeded silently; portal device card prefers the updated version ("auto-updated from X"); event registered in Constants/resource-catalog/shared-manifests with replay fixture and watcher/adapter/reducer tests
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
59c944618b |
test(agent): move CMTrace fixture to tests/fixtures and anonymize it
Two corrections to
|
||
|
|
c48f95b88c |
tests: cross-language parity manifest + agent safety nets (audit P3/P4)
P3 — shared manifest infrastructure: SharedManifestParityTests reflects the wire-relevant catalogs out of AutopilotMonitor.Shared into a committed JSON manifest (regenerate: AM_WRITE_SHARED_MANIFESTS=1 dotnet test); a web codegen lifts it into literal types; sharedManifestChecks.ts enforces AdminConfiguration field parity at COMPILE time and 12 vitest tests pin the value catalogs (sources/operators, lanes/verdicts, severities incl. the missing Trace=-1, phases both directions, terminal statuses, event-type typo guard). KnownSources/KnownOperators made internal for the export. P4 — agent safety nets: DecisionState builder/serializer completeness (4-site sync tripwire), signal-kind fixture coverage with a shrink-only debt set (found and fixed realmjoin-timeout-v1.json, which never deserialized), RemoteConfigMerger completeness tripwire, DefaultComponentFactory ordering invariants as real construction tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
b2d2d37c6f |
feat(realmjoin): detect aborted RJ ESP (missing 110) and release the completion gate
An interactive logon during the RealmJoin first deployment (phase 100/101)
makes RJ reclassify the run as a secondary-user deployment, whose branch
writes 200/210 and never CompletedFirstDeployment (110) — no crash, no
reboot request. The V2 completion gate waited for exactly 110, so these
sessions hung until the 60-min hard timeout (session 224b2087; siblings
558bf067/3cc18724 delayed ~50 min the same way).
- promote realmjoin_phase_changed to a typed RealmJoinPhaseChanged signal
(dual-emitted by the adapter) so RealmJoinFacts.LastDeploymentPhase is
persisted across agent restarts
- abort rule: prev in {100,101} and current in {200,210} with no outcome
yet -> emit realmjoin_first_deployment_incomplete (Warning), cancel the
hard-timeout deadline, release the gate with outcome
FirstDeploymentIncomplete; also evaluated on the RealmJoinDetected
replay path (restart swallows the transition)
- deliberately NOT triggered by 101->0 (RJ service restart may still
complete the first deployment) nor by a first observation of 210
(session 6f1959c0: RJ already completed before agent boot)
- fix realmjoin_timeout always claiming "last phase: 0" - the persisted
phase now feeds the message
- backend EnrollmentTimeoutClassifier counts the new event as a gate
terminal; MCP resource catalog dual-synced
- CompleteIfDeferredOrBookkeep widened to a leading-effect list
Verified by replaying session 224b2087's signal log: gate releases at
15:05:17 (101->200) instead of hanging until the 15:54 deadline.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
977ad37463 |
fix(agent): HelloWizardStarted signal — never complete while the Hello wizard is running (session 772fe502)
A flip-flopping user-scoped WHfB CSP read "disabled" once, the arm-C completion synthesized HelloOutcome="Skipped" and the session completed 5s later while the user sat inside the Hello wizard that had started 230ms earlier (Shell-Core 62404, previously InformationalEvent-only). Layer 1 — engine (DecisionCore): - New DecisionSignalKind.HelloWizardStarted + set-once fact DecisionState.HelloWizardStartedUtc (additive-nullable, census + snapshot wired). - Prevention: centralized HelloSatisfiedForCompletion / HelloPolicyDisabledWithoutWizard predicates veto the policy-disabled stand-in at all five Skipped-synthesis sites and in the completion_waiting prerequisites; the AdvisoryCompletion handler gets a HelloWizardObservedPromote branch (its fallthrough is re-arm-or-fail). - Cure: HandleHelloWizardStartedV1 retracts an engine-synthesized "Skipped" (exact-case discriminator vs the lowercase tracker vocabulary; first fact-nulling reducer, stage-agnostic because the RealmJoin gate-defer parks synthetic skips pre-Finalizing), cancels finalizing_grace, returns to AwaitingHello and arms HelloSafety. - Agent path: ShellCoreTracker raises HelloWizardStarted before FinalizingSetupPhaseTriggered; 62404 included in the startup backfill (fire-once); coordinator timestamp mirror; adapter emit (fire-once). Layer 2 — HelloTracker confirmation read: - ApplyPolicyRead state machine: "disabled" commits only after a second confirming read (~10s poll); "enabled" commits immediately; disabled→enabled flags flipFlopDetected=true; pending keeps the policy-unknown wait cadence. Audit fields confirmedReads / flipFlopDetected on hello_policy_detected. Fixtures: signal-kinds/hello-wizard-started-v1.json + enrollment-sessions/userdriven-hello-wizard-unskip-v1.jsonl (772fe502 ordering, ends Completed with HelloOutcome=completed). Tests: DecisionCore 462/462, V2.Core 1775/1775. Docs: OKF concept doc docs/agent/hello-wizard-unskip.md. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
c6b6e4504a |
feat(decision-core): proactive completion arm C from 4-fact post-ESP user-session evidence
Session a4537c36: Windows closed the User-ESP page normally (Shell-Core 62407, errorCode=0) while the AccountSetup registry never wrote categorySucceeded - the strong gate (arm A) was unsatisfiable by construction and the session only completed because the Hello policy happened to flip to enabled after a reboot. ShouldTransitionToAwaitingHello gains arm C: AccountSetup entered + normal ESP final exit (post-AccountSetup by INGEST ordinal - backdated CMTrace timestamps are irrelevant, sequence is canonical) + genuine IME user-session completion (defaultuser0 guard: at-or-after the AccountSetup anchor, extracted as shared IsImeUserSessionGenuine and reused by the AdvisoryCompletion backstop) + DAD- validated real-user desktop. ALL FOUR mandatory; Hello gating stays at the call sites - arm C never bypasses an enabled-unresolved Hello. Wiring per signal ordering: desktop last via the fast-path (desktopArrived- InFlight), exit last via HandleEspExitingV1 (espFinalExitInFlight + deferred- completion parity mirroring the caa6cf50 branch), IME last via new completion/ promote wiring in HandleImeUserSessionCompletedV1. completion_waiting stops reporting account_setup_provisioning_complete once the evidence set holds. AdvisoryCompletion stays as the 30-min backstop for incomplete evidence. The 08c99638 regression guard (3-of-4, no IME fact -> parked) stays green unmodified; the L5 backdated-exit test now asserts the eager completion. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
eb132b11fd |
feat(agent): complete self-deploying/kiosk sessions despite IME AccountSetup false positive
Kiosk devices (self-deploying profile) never completed: the IME logs an AccountSetup phase line for the kioskUser0 autologon session although user ESP never runs, which cancelled/blocked the DeviceOnlyEspDetection deadline - the sole SelfDeploying terminal path - and the session idled to the 5h backend timeout (reference session 320b3bf7; 436 false Failed platform-wide, 90% of all self-deploying failures). Platform sweep (8983 sessions / 63 tenants): CloudAssignedOobeConfig bits 0x20|0x40 are exclusive to self-deploying/kiosk/shared profiles (0 of 6785 user-driven sessions carry them, bits always co-occur). The fix seeds Mode=SelfDeploying@High from that registry marker via EnrollmentFactsObserved and waives the AccountSetup veto at the four existing decision points (DSPC short-circuit, deadline race guard B, EspPhaseChanged cancel, RealmJoin deferred-release re-check) - only when the seeded profile holds AND user ESP made no real progress. Bits absent = byte-identical behaviour. Also registers IsSelfDeployingProfile on SessionRegistration (display metadata; backend persistence follows separately). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
caa573678f |
fix(decisioncore,agent/v2): defang ESP terminal failure when ContinueAnyway + AccountSetup entered
Session 4fa5a2d4 in tenant c9787ba2 hit 199/202 false-positive enrollment_failed events: ESP
profile sets BlockInStatusPage=6 (ContinueAnyway=true), Windows transitioned to AccountSetup,
then wrote DeviceSetupCategory=failed (Certificates) 13 min later — the reducer terminated
even though the device demonstrably kept progressing. PR1 Tier-1 defang downgrades these
EspTerminalFailure signals to a non-terminal esp_failure_advisory event while keeping
monitoring armed (Hello/Desktop/IME completion paths continue to drive the session).
DecisionEngine:
- New SignalFact<DateTime>? EspAdvisoryFailureRecordedUtc on DecisionState as the fire-once
gate (additive ctor param, optional at end — preserves Newtonsoft positional-binding for
pre-PR1 v4 snapshots).
- HandleEspTerminalFailureV1: when ScenarioObservations.EspAllowContinueAnyway==true AND
AccountSetupEnteredUtc!=null, emit esp_failure_advisory (severity=Warning,
advisoryReason=esp_failure_defanged_continueanyway_with_accountsetup) instead of
transitioning to Failed. Stage / deadlines / outcome stay untouched.
- Duplicate handling: a second EspTerminalFailure dead-ends UNLESS the duplicate carries
registry-specific keys (errorCode/failedSubcategory/category) — those only appear in the
ProvisioningStatusTracker path and indicate enrichment of an earlier ShellCoreTracker
advisory. failureType alone is NOT a registry marker (both adapters emit it).
- New BuildEspFailureAuditTrail helper merges a whitelisted set of failure-context keys into
the audit-trail typedPayload so HRESULT/subcategory survive the wire. Closes a pre-existing
gap on the enrollment_failed path too.
Bootstrap:
- EnrollmentOrchestrator.PostEspConfigDetectedBootstrap switched from EspSkipConfigurationProbe.
Read() (tuple SkipUser/SkipDevice) to ReadFull() so EspAllowContinueAnyway +
EspSyncFailureTimeoutMinutes are synchronously available before any EspTerminalFailure
signal — defeats the fire-and-forget DeviceInfoCollector.CollectAll race that would have
left Tier 1 with ScenarioObservations.EspAllowContinueAnyway==null.
Tests:
- EspFailureAdvisoryDefangTests (9 new): advisory path, fire-once idempotency, enrichment
on registry-detail second-fire, dead-end on ShellCore-only duplicate, three negative paths
(ContinueAnyway false / unknown / no AccountSetup), TypedPayload whitelist merge on both
advisory + Failed paths.
- SerializationTests: backcompat test for old v4 snapshots without EspAdvisoryFailureRecordedUtc.
- DecisionStateSnapshotBuilderTests: fact count bumped from 14 to 15.
- EdgeScenarioTests + esp-failure-advisory-continueanyway-v1.jsonl: end-to-end replay against
the canonical Session 4fa5a2d4 signal stream including a duplicate dead-end.
- EnrollmentOrchestratorEspConfigBootstrapTests: 4 existing tests migrated to ScopedFullOverride,
4 new tests cover ContinueAnyway + SyncFailureTimeout propagation, class gated behind
[Collection("SerialThreading")] since the override targets a static probe slot.
UI:
- EventTimeline renders esp_failure_advisory with an amber "Advisory (ContinueAnyway)" badge
alongside the HRESULT/subcategory chips so analysts can distinguish defanged from real
failures at a glance.
- gather-rules/eventTypes.ts mirrors the new constant for autocomplete.
Build: dotnet build AutopilotMonitor.sln green; xUnit Backend(1937) + V2.Core(1139) +
Agent.Core(538) + DecisionCore(357) + SummaryDialog(44) = 4015 passed, 0 failed.
Web tsc clean, vitest 384/384 passed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
5eb95868d8 |
fix(decisioncore): defang SelfDeploying terminal via 5-min deadline + UI phase coverage
Session 88a53223-9795-4188-8352-7df9f0af9bb7 (v1 Classic UserDriven, AAD Joined, SkipUserStatusPage=false) was wrongly classified as mode=SelfDeploying after the ProvisioningStatusTracker 30-s fallback fired DeviceSetupProvisioningComplete before Windows transitioned DeviceSetup -> AccountSetup. The signal alone was terminal in the old reducer; the absence of AccountSetupEnteredUtc at signal time was sufficient to commit terminal SelfDeploying. New rule: DeviceSetupProvisioningComplete is no longer terminal. It records a new DeviceSetupResolvedUtc anchor and (cancel-then-)arms the DeviceOnlyEspDetection deadline for 5 min. The deadline-fired handler is the sole SelfDeploying-terminal entry point and re-checks six guards (stale: no-anchor / not-armed / DueAtUtc-mismatch; race: Stage-terminal / AccountSetup-entered / monotonic Mode conflict) before classifying. Any dead-end after the DueAtUtc match state-side CancelDeadline()s the past-due deadline so snapshot reload doesn't re-arm via the scheduler. RealmJoin-deferred release (CompleteIfDeferredOrBookkeep) re-checks the same guards, resets DeviceOnlyDeployment hypothesis to Unknown on bailout, and operates on preparedBuilder.* (not state.*) so WithResolved / WithTimeoutOutcome facts survive the bailout. UI phase coverage closed: phase_transition(FinalizingSetup) and phase_transition(Complete) emitted at every terminal-success site (Classic FinalizingGrace, SelfDeploying deadline-terminal, AdminPreemption-Succeeded, RJ-deferred completion) so the Web timeline opens Finalizing + Complete bars universally - was missing entirely for SelfDeploying / AdminPreemption paths. Other changes: - Removed ApplyDeviceSetupProvisioningComplete (dead code under defang), added ApplySelfDeployingDeadlineConfirmed with monotonic guard. - New RealmJoinFacts.ClearSelfDeployingDeferred() for the RJ-bailout path. - DecisionState schema bumped v3 -> v4 with additive nullable DeviceSetupResolvedUtc field. StateSerializer v3-deserialize-compat verified. - DeviceSetupProvisioningComplete added to DecisionStateSignalCensus so the enrollment_complete audit trail surfaces the anchor. Test coverage: - New SelfDeployingDeadlineTerminalTests.cs (12 scenarios): happy path, all six guards including snapshot-migration rollout race + DueAtUtc cancel-rearm race, replay idempotency, audit-census wiring, RJ-deferred release races (AccountSetup-arrives / monotonic / Timeout-variant). - Updated 9 existing test files for new semantics; 2 fixtures now drive DeadlineFired terminal explicitly. - ReplayHarness + ClassifierAwareReplayHarness set AgentBootUtc to the first signal's OccurredAtUtc so fixture scenarios run in their own time-bubble (replay-safety floor still tested directly by BootAnchorDeadlineFloorTests). Tests: DecisionCore 345, V2.Core 1135, Backend 1937 - all green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
bc02dad6bc |
test(decisioncore,agent/v2): RealmJoin gate, adapter, SID resolver tests + signal-kind fixtures (Phase D)
- DecisionCore.Tests/RealmJoinGateTests.cs (9 tests): baseline regression guard,
RealmJoinDetected arms 60-min timeout, gate defers Finalizing on Classic AND-gate,
RealmJoinResolved + Timeout both release through CompleteIfDeferredOrBookkeep,
SelfDeploying terminal defers + direct-complete via SelfDeployingDeferredCompletion,
per-package facts mutation, and audit-trail attaches realmjoin* fields.
- V2.Core.Tests/SignalAdapters/RealmJoinWatcherAdapterTests.cs (6 tests): each of the
5 watcher events maps to the correct DecisionSignalKind + dual InformationalEvent;
phase-changed is timeline-only; DisplayName clamped to 256 chars.
- V2.Core.Tests/Monitoring/Runtime/UserSidResolverTests.cs (4 tests): well-known
BUILTIN\Administrators + NT AUTHORITY\SYSTEM SID round-trips, invalid/empty inputs
return false.
- tests/fixtures/signal-kinds/realmjoin-{detected,resolved,timeout,package_started,
package_completed}-v1.json: replay fixtures fulfilling the merge-block contract
declared on DecisionSignalKind.cs:8.
Verified: 9/9 reducer + 10/10 adapter/SID tests green; no regressions in adjacent
suites.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
bb4f663ace |
fix(agent/v2,decisioncore): gate AwaitingHello on AccountSetup truly succeeded
Session 330f73f3 completed via DeadlineFired:finalizing_grace (helloOutcome= Timeout) while ESP AccountSetup was still at "4 of 5 subcategories completed" and 3 of 8 apps still pending. ShouldTransitionToAwaitingHello promoted on AccountSetupEnteredUtc != null alone, but Windows CXH event 62407 fires at every ESP-page transition — the first occurrence (Device→Account handoff, 9 s after AccountSetup entered) was treated as the genuine final exit, armed HelloSafety against an in-progress AccountSetup, fired its 5-min synthetic timeout, and FinalizingGrace declared the session terminal mid-flight. Introduce a strong "AccountSetup truly succeeded" fact sourced from ProvisioningStatusTracker (categorySucceeded=true OR fallback after 30 s of all subcategories at succeeded/notRequired — same shape as DeviceSetup). Forwarded by EspAndHelloTracker, posted as new DecisionSignalKind. AccountSetupProvisioningComplete by the V2 adapter, lands as DecisionState. AccountSetupProvisioningSucceededUtc. The reducer's ShouldTransitionToAwaitingHello now requires this fact (or SkipUserEsp); the old AccountSetupEnteredUtc gate becomes an entry-time marker only. HandleAccountSetupProvisioningCompleteV1 has a deferred-promotion path: when the strong fact arrives AFTER a guard-blocked EspExiting (EspFinalExitUtc set) OR after a guard-blocked EspPhaseChanged(Finalizing- Setup) (FinalizingEnteredUtc set, ShellCoreTracker adapter fire-once- dedupes), the handler performs the deferred promotion + arms HelloSafety anchored at the AccountSetup-succeeded instant. The new fact is registered in DecisionStateSignalCensus so enrollment_* audit trails and final-status.json show whether the strong gate was satisfied at termination — load-bearing for diagnosing future false-negative timeouts under the new gate. Side cleanup: ProvisioningStatusTracker's fallback timer callbacks (new AccountSetup AND pre-existing DeviceSetup parallel path) now reset the timer field on every abort path AND in the outer exception catch. Without this, a transient subcategory flicker between fallback arm and timer fire could permanently prevent re-arming even when the state re-stabilises. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
d16b60f14c |
feat(v2): WG Part-2 architectural cleanup + install-mode handoff fix
Bundled change covering three orthogonal pieces of work that all landed in
the same working tree before the next PR-style commit could be split out.
## WG Part-2 architectural cleanup (PR-A through PR-E)
Plan: ~/.claude/plans/wg-part2-architectural-cleanup.md.
The V2 agent carried a full separate "Part-2 apparatus" (4 special signal
kinds, 2 special stages, a special outcome, a special classifier, a 24h
safety deadline, a dedicated reducer file, adapter `part2Mode` switches,
a SessionRecovered bridge). This did not match OS reality: post-reseal
Part 2 is just a normal user-driven OOBE running through the Classic
flow. The apparatus also carried a real risk (24h false-positive
enrollment_failed mid-success) and was not even active in practice
(observed in session bdb3cf9d-541b-4c44-aebe-5b9e72743b0a, 2026-05-04).
V2 is now V1-symmetric:
PR-A — Orchestrator Resume Mechanic. EnrollmentOrchestrator detects a
persisted Stage=WhiteGloveSealed snapshot at Start(), archives the
reducer-state segments to state/.part1-<utc-ts>/, runs as a fresh
first-boot through the normal recovery branches, and posts a single
whiteglove_resumed InformationalEvent after the onIngressReady hook.
New helper class StateArchiver.cs + StateArchiverTests +
EnrollmentOrchestratorPart2EmissionTests.
PR-B — DecisionCore Cleanup. Removed:
- DecisionSignalKind.UserAadSignInComplete / HelloResolvedPart2 /
DesktopArrivedPart2 / AccountSetupCompletedPart2 / SessionRecovered
- SessionStage.WhiteGloveAwaitingUserSignIn / WhiteGloveCompletedPart2
- SessionOutcome.WhiteGlovePart2Complete
- DeadlineNames.WhiteGlovePart2Safety
- DecisionState's 4 Part-2 SignalFacts (+ builder + snapshot slots)
- WhiteGlovePart2CompletionClassifier + WhiteGlovePart2CompletionSnapshot
- DecisionEngine.WhiteGlovePart2.cs (entire reducer file)
- HandleSessionRecoveredV1 dispatcher + handler
PR-C — Adapter Cleanup. Dropped the inert `part2Mode` ctor parameter
from AadJoinWatcherAdapter, DesktopArrivalDetectorAdapter,
HelloTrackerAdapter, EspAndHelloTrackerAdapter (and their tests).
AadJoinHost is the only production caller using a named-arg form,
unaffected.
PR-D — Backend + Web. Removed `WhiteGloveCompletedPart2` cases from
TelemetryPayloadParser.IsTerminalStage and DecisionGraphBuilder
(IsTerminalStage + DeriveTerminalOutcome) plus their tests. Dropped
the dead `UserAadSignInComplete` constant from Shared.Constants.
Extracted pure helper computeWhiteGloveSplitSequence(events) in
app/sessions/[sessionId]/utils/eventHelpers.ts; useSessionDerivedData +
utils/sessionExportUtils now share it. New 6-case vitest covering
splitter behaviour (no marker, primary trigger via whiteglove_resumed,
agent_started fallback, Pending-only variants, primary-vs-fallback
race). PhaseTimeline + eventTypes were already on the new model.
PR-E — Tests + Fixtures Final Cleanup. Deleted the two
whiteglove-part2-*.jsonl fixtures + EdgeScenarioTests references;
removed dead `whiteglove_part2` case + test from SummaryDialog
OutcomeMapper; trimmed PR-B history breadcrumbs that still embedded
removed symbol names so the final grep is clean. Updated MEMORY
entries (project_v2_completion_location, project_wg_part2_simple_model,
project_wg_signal_validation_pending, project_bdb3cf9d_wg_followups,
MEMORY.md index).
What stayed: SessionStage.WhiteGloveSealed (Part-1 marker), the
`_isWhiteGlovePart2` Hint-Bool (V1-symmetric, plan §11) wired only into
EnrollmentTerminationHandler / SoftwareInventoryAnalyzer for shutdown-
analyzer phase=2 tagging, the `whiteglove_part1_complete` event-type +
`whiteglove_part1` outcome-string, `PausedForPart2` terminal-outcome
label for the Inspector, the `whiteglove_resumed` event as UI-splitter
trigger.
Final acceptance: grep for the removed symbol set in src/ excluding V1
(Agent.Core/) is empty. V2.Core.Tests 991 / DecisionCore.Tests 282 /
Functions.Tests 862 / SummaryDialog.Tests 44 / Web vitest 227 — all
green. Solution build clean.
## Resume-path correctness fixes (Codex review of the cleanup)
Three findings on the new resume mechanic:
P1a — AgentRuntimeHost was still skipping LifecycleEmitters
PostEnrollmentFactsObserved + PostSessionStarted on isWhiteGloveResume.
After PR-A's archive-and-reset the decision state is empty, so the
Classic flow needs both anchors (otherwise ScenarioProfile stays at
default and the Hello+Desktop AND-gate misclassifies). Fixed: post
unconditionally for facts; SessionStarted is now only gated by an
AdminAction, not by the resume flag. The orchestrator's
whiteglove_resumed lands first (smaller sequence) so the Web splitter
(`splitSequence = resumed.sequence - 1`) still puts SessionStarted
into the Part-2 timeline block.
P1b — StateArchiver was archiving event-sequence.json along with the
reducer state. Part 2 reuses the same SessionId and the backend orders
events session-wide by (SessionId, Sequence); the Web split also uses
`splitSequence = resumed.sequence - 1`. Resetting the counter at the
resume boundary would collide Part-1 and Part-2 sequences and break
the splitter. event-sequence.json is now intentionally preserved; the
three reducer-state segments still archive. StateArchiverTests +
RecoveryPathTests asserting the new contract.
P2 — V1 reset the local session-age clock on Part-2 resume
(SessionPersistence.ResetSessionCreatedAt, MonitoringService.cs:530).
V2 was clearing only the WhiteGlove marker, so a crash/reboot during
Part-2 AccountSetup would let the next start run the emergency-break
watchdog against the original Part-1 timestamp and trip too early.
AgentRuntimeHost now also calls SessionIdPersistence.SaveSessionCreatedAt
(DateTime.UtcNow) right after ClearWhiteGloveComplete. New
ProgramGuardsTests test asserting the V1-symmetric resume contract.
## Install-mode handoff hardening
Plan reference (memory): project_v2_install_runtime_handoff.md.
Bootstrap script + Program.InstallMode + InstallModeTaskXmlTests.
PR1 — Replace `schtasks /Run` with WMI Win32_Process.Create for the
install-time runtime handoff. schtasks /Run reports SUCCESS as soon
as the run is queued; the same battery / AV / OOBE-defer paths that
motivate PR2 also queue a manual /Run (observed WinPE 2026-05-04,
task stuck in Queued state). WMI also gives the runtime process the
same isolation Task Scheduler uses, without the Job-Object inheritance
risk of a direct Process.Start.
PR2 — Register the BootTrigger Scheduled Task from a hardened XML
definition rather than `schtasks /Create`. The CLI form inherits XML
schema defaults DisallowStartIfOnBatteries=true and
StopIfGoingOnBatteries=true, which queue the BootTrigger run
indefinitely on a laptop that boots on battery in OOBE / WinPE. The
XML disables both, sets StartWhenAvailable, and pins
ExecutionTimeLimit=PT0S so the long-running agent is never killed at
the schtasks 72h CLI default.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
579291272e |
fix(v2): split EnrollmentFactsObserved from SessionStarted to fix JoinMode race
V2 tracker / Backend-register-session race could land SessionStarted after ESP/Tracker signals had already advanced Stage past SessionStarted. The HandleSessionStartedV1 dead-end-Wache then swallowed the registry-derived profile facts (enrollmentType + isHybridJoin), leaving JoinMode=Unknown on every affected enrollment_complete payload (Session 10c8e0bf). Root cause was a concept mix in SessionStarted: a lifecycle anchor doing double duty as a static-facts seed. Split into two signals: - SessionStarted: pure lifecycle anchor (Stage / ClassifierTick / step). - EnrollmentFactsObserved: stage-agnostic, idempotent, monotonic facts carrier. New reducer handler delegates to ApplyEnrollmentFactsObserved (renamed from ApplySessionStarted, with set-once reason for v1 to honour the documented idempotency contract). AwaitEnrollment is transparently covered: AgentBootstrap blocks until the MDM cert exists, so the registry is guaranteed populated by the time PostEnrollmentFactsObserved runs; a single post per agent run suffices. Tests: - DecisionCore: 234/234 (4 new fact-handler tests + idempotency + monotonicity) - Agent.V2.Core: 771/771 (3 new producer tests, 1 reshaped) - Agent.Core (V1): 507/507 sanity-check unchanged Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
0305c0d635 |
test(v2): add UserDriven Hello-skipped fixture (anon df7abbc2)
Real-world V2 enrollment-session replay covering a UserDriven-v1 happy variant where Hello-for-Business was enabled but per-user enrollment resolved as "skipped" rather than "Success". Exercises the Hello+Desktop AND-gate's tolerance for any terminal Hello outcome. Asserts Stage=Completed, Outcome=EnrollmentComplete, HelloOutcome=skipped, ScenarioProfile.Mode=Classic (set by ImeUserSessionCompleted). README replaced with pointer to ScenarioTests.cs as source of truth — old "(empty)" placeholder dropped, only the new entry listed. 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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
5b7f424d19 |
refactor(agent-v2): route whiteglove_resumed / system_reboot_detected / admin preemption through reducer effects
V2 parity cleanup - the three lifecycle events were emitted from Program.cs
directly while the corresponding reducer handlers (HandleWhiteGlovePart1To2Bridge,
HandleSystemRebootObservedV1) returned Array.Empty<DecisionEffect>(). Admin
preemption synthesised an EnrollmentTerminatedEventArgs and invoked
terminationHandler.Handle directly, bypassing the reducer entirely.
This commit unifies all three flows: the reducer is the single state owner, the
telemetry event appears as an EmitEventTimelineEntry effect of the state
transition, and the orchestrator's Terminated event fires automatically when
stage goes terminal - no more synthetic bypass.
- B1: HandleWhiteGlovePart1To2Bridge emits whiteglove_resumed timeline entry.
- B2: HandleSystemRebootObservedV1 emits system_reboot_detected with payload
keys previousExitType/lastBootUtc.
- B3: new DecisionSignalKind.AdminPreemptionDetected + new SessionOutcome.AdminPreempted.
Reducer HandleAdminPreemptionDetectedV1 transitions to Completed/Failed based on
adminOutcome payload, emits enrollment_complete/_failed timeline entry with
adminAction+source data keys. Program.cs posts the signal; terminationHandler
still subscribes to orchestrator.Terminated (no direct Handle calls on the
startup-preemption path).
Fixtures: tests/fixtures/signal-kinds/{system-reboot-observed,admin-preemption-detected}-v1.json.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
75c98d131f |
refactor(decisioncore): edge scenarios — hybrid reboot, ESP failure, recovery (M3.5)
Final M3 sub-milestone. Closes the plan's §4 M3 scenario matrix:
Handlers (DecisionEngine.Edge.cs):
- EspResumed mid-ESP reboot. Stage preserved; records
SystemRebootUtc on first observation (hash
stable on repeats).
- EspTerminalFailure Stage=Failed + Outcome=EnrollmentFailed,
deadlines cleared, enrollment_failed event
emitted with reason carried from payload.
Definitive — no classifier re-run.
- SystemRebootObserved records SystemRebootUtc so the WhiteGlove
sealing classifier credits the +15 reboot
weight on the next RunClassifier effect.
Scenario fixtures (committed-anonymized):
- hybrid-reboot-v1.jsonl ESP -> mid-phase reboot -> EspResumed ->
AccountSetup -> Hello -> Desktop ->
Completed. No WhiteGloveSealed path.
- esp-terminal-failure-v1.jsonl ESP -> AccountSetup -> terminal failure
-> Failed / reason=
esp_terminal_failure_account_setup.
Tests (EdgeScenarioTests, 5 new):
- Hybrid reboot preserves stage, records reboot fact, completes
normally without triggering WhiteGlove.
- ESP terminal failure transitions straight to Failed.
- Restart-recovery determinism: replaying the same JSONL twice yields
identical FinalStepHash, Stage, Outcome and transition count.
- Prefix-replay check: truncated log ends in the expected intermediate
stage (simulates crash between signals).
- Global sanity: every committed fixture (all 12) ends in a terminal
stage — regression guard for any future reducer change.
Suite totals: 57 DecisionCore (was 52) / 507 Legacy / 557 Functions =
1121 tests green. All 14+ scenarios from plan §R6 Szenario-Matrix pass
through the reducer with the expected terminal outcome.
This closes M3 (DecisionEngine-Reducer + Classifier-Kernel). M4 wires the
SignalAdapter + EffectRunner + TelemetryTransport in the V2 agent runtime.
Plan: plans/REFACTOR_AGENT_V2.md §4 M3 sub-milestone M3.5 — M3 complete.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
8e4faf4251 |
refactor(decisioncore): WhiteGlove Part 2 + Part2CompletionClassifier + 2 scenarios (M3.4)
Plan §4 M3.4 + plan §2.3 WhiteGlove Part-2 state machine: post-reboot user
sign-in is now a first-class path with a dedicated classifier, a 24h safety
deadline, and an explicit Sealed -> AwaitingUserSignIn -> CompletedPart2 /
Failed progression.
Classifier (WhiteGlovePart2CompletionClassifier : IClassifier):
- Scoring rules NEW (no Legacy equivalent): each of the four Part-2 user
signals (+25) — UserAadSignInComplete, HelloResolvedPart2,
DesktopArrivedPart2, AccountSetupCompletedPart2. All four together hit
the Confirmed threshold (100); missing one drops to Weak (75).
- WhiteGlovePart2CompletionSnapshot implements the new IClassifierSnapshot
interface; existing WhiteGloveSealingSnapshot was retrofit to the same
contract so the harness anti-loop is uniform.
Handlers (DecisionEngine.WhiteGlovePart2.cs):
- SessionRecovered (Shared.cs) now bridges a WhiteGloveSealed state into
WhiteGloveAwaitingUserSignIn and arms the 24h WhiteGlovePart2Safety
deadline on the recovered signal's occurredAt.
- Four Part-2 signal handlers (UserAadSignInComplete, HelloResolvedPart2,
DesktopArrivedPart2, AccountSetupCompletedPart2) record facts + emit a
Part-2 RunClassifier effect; facts arriving outside the AwaitingUserSignIn
stage are recorded but no classifier runs (prevents stray Part-2 verdicts
on non-sealed sessions).
- WhiteGlovePart2Safety deadline handler terminates with Failed /
enrollment_failed reason=part2_user_absent.
- ClassifierVerdictIssued router extended to dispatch on classifier id —
Part-2 verdicts route to ApplyWhiteGlovePart2Verdict; Confirmed triggers
Stage=WhiteGloveCompletedPart2 + Outcome=WhiteGlovePart2Complete +
whiteglove_part2_complete event.
DecisionState: four new SignalFact<DateTime>? fields for the Part-2 signals
(full builder parity).
ClassifierAwareReplayHarness: anti-loop lookup now routes per classifier id
(WhiteGloveSealing <-> WhiteGloveSealing.LastClassifierVerdictId;
Part2Completion <-> WhiteGlovePart2Completion.LastClassifierVerdictId).
Snapshot hashing works through the IClassifierSnapshot interface instead
of a WhiteGloveSealingSnapshot type check.
Scenario fixtures (committed-anonymized):
- whiteglove-part2-happy-v1.jsonl seal Part 1, reboot, 4 Part-2 signals
within minutes -> Confirmed + Part2Complete.
- whiteglove-part2-stuck-v1.jsonl seal Part 1, reboot, 24h passes, safety
deadline fires -> Failed / part2_user_absent.
Tests (3 new in WhiteGlovePart2ScenarioTests):
- Happy terminal state + hypotheses + all four facts + no residual deadlines
- Stuck terminal state + Part-1 Confirmed remains, Part-2 Rejected with reason
- Bridge check: after SessionRecovered(Sealed), Part-2 safety is active
Suite totals: 52 DecisionCore (was 49) / 507 Legacy / 557 Functions.
Plan: plans/REFACTOR_AGENT_V2.md §4 M3 sub-milestone M3.4 + §2.3 Part-2 stages.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
f75a8d4375 |
refactor(decisioncore): WhiteGlove Part 1 handlers + sealing classifier + 4 scenarios (M3.3)
Plan §4 M3.3: the first kernel-service classifier goes live (plan §2.4 L.8),
the effect-based classifier-run contract is wired end-to-end, and the anti-loop
via InputHash is proven by a dedicated fixture.
Classifier (WhiteGloveSealingClassifier : IClassifier):
- Scoring ported 1:1 from Legacy WhiteGloveClassifier (same weights, same
thresholds, same asymmetric-conservative rule: only Confirmed drives the
WhiteGloveSealed stage).
- Positive weights: ShellCoreWhiteGloveSuccess=80, SealingPattern=40,
DeviceOnlyDeployment=15, SystemReboot=15.
- Hard excluders (-100 each): AadJoinedWithUser, DesktopArrived, HelloResolved.
Soft exclude: AccountSetupActivity=-40.
- WhiteGloveSealingSnapshot is the classifier input type and computes a
deterministic SHA256-short InputHash over WG-relevant fields only — so
the anti-loop triggers on meaningful fact changes, not on unrelated state.
Handlers (DecisionEngine.WhiteGlove.cs):
- WhiteGloveShellCoreSuccess records fact; emits RunClassifier effect;
arms ClassifierTick if not already.
- WhiteGloveSealingPatternDetected mirror handler for the IME-pattern path.
- ClassifierVerdictIssued applies the verdict to WhiteGloveSealing
hypothesis; on Confirmed transitions to
WhiteGloveSealed stage +
Outcome=WhiteGlovePart1Sealed and emits
the whiteglove_complete event.
- HandleClassifierTickDeadlineFired (shared router) re-emits RunClassifier
and re-arms itself unless terminal.
ClassifierAwareReplayHarness (tests-only):
- Loops per-step effects: any RunClassifier effect is resolved synchronously
via the registered IClassifier; the verdict is wrapped in a
ClassifierVerdictIssued signal and fed back into Reduce. Anti-loop skips
the call when snapshot hash == state.WhiteGloveSealing.LastClassifierVerdictId.
- Exposes ClassifierRunStats for regression tests.
DecisionState extension: two new SignalFact<bool> fields capture the WG
sightings (ShellCoreWhiteGloveSuccessSeen, WhiteGloveSealingPatternSeen). The
classifier reads them via WhiteGloveSealingSnapshot.
Scenario fixtures (4, committed-anonymized):
- whiteglove-inline-v1.jsonl Confirmed on ShellCore alone (80).
- whiteglove-signal-correlated-v1.jsonl Weak after pattern, ClassifierTick
anti-loop-skipped, Confirmed after
ShellCore escalates the score.
- whiteglove-false-positive-v1.jsonl SealingPattern + late-AADJ hard-
excludes -> Rejected; session still
completes via Classic Hello+Desktop.
- whiteglove-antiloop-v1.jsonl Single pattern + two ticks; runs=1,
skipped_by_antiloop=2.
Tests: 4 scenario tests in WhiteGlovePart1ScenarioTests, all passing.
DecisionCore.Tests: 49 (was 45). Legacy 507 + Functions 557 unchanged.
ReducerFailSafeTests + HarnessTests updated to use AppInstallCompleted as
the still-unhandled sentinel (WhiteGloveShellCoreSuccess is now wired).
Plan: plans/REFACTOR_AGENT_V2.md §4 M3 sub-milestone M3.3 + §2.4 classifier kernel.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
0ebfa2fc3c |
refactor(decisioncore): SelfDeploying + Device-Only handlers + 2 scenarios (M3.2)
SelfDeploying-v1 vertical slice: DeviceSetupProvisioningComplete terminal
handler, DeviceOnlyEspDetection deadline wiring, and the
DeviceOnlyDeployment hypothesis progression UserPresent / DeviceOnly /
Ambiguous expressed via Hypothesis.Reason (plan §2.3).
Handlers (DecisionEngine.SelfDeploying.cs):
- DeviceSetupProvisioningComplete terminal. Cancels all deadlines,
finalizes DeviceOnlyDeployment based on
user-presence facts (AadJoinedWithUser,
HelloResolvedUtc, DesktopArrivedUtc),
raises EnrollmentType to Strong when
no AccountSetup was observed before.
- DeviceOnlyEspDetectionFired hypothesis-only update to
DeviceOnlyDeployment=Strong(DeviceOnly).
Stage unchanged; completion still
requires explicit provisioning signal.
Classic.cs extended:
- First EspPhaseChanged(DeviceSetup) arms the DeviceOnlyEspDetection
deadline (5 min window, plan §2.7).
- EspPhaseChanged(AccountSetup) cancels it — UserDriven-v1 path.
Shared DeadlineFired router routes the deadline to the SelfDeploying body.
Scenario fixtures (tests/fixtures/enrollment-sessions/):
- selfdeploying-happy-v1.jsonl — 3 signals, SkipUserStatusPage
path. Terminal Completed with
DeviceOnly hypothesis confirmed
and no user-presence facts.
- selfdeploying-esp-exit-unknown-v1.jsonl — 4 signals, deadline fires at
T+5m and sets DeviceOnly before
provisioning-complete arrives.
Tests (SelfDeployingScenarioTests, 4 new):
- SelfDeployingHappy terminal state + hypotheses
- SelfDeployingHappy deadline cleanup
- Device-Only ESP-exit-unknown deadline -> provisioning flow
- AccountSetup on the Classic path cancels DeviceOnlyEspDetection
(regression guard — UserDriven should never classify as DeviceOnly)
InternalsVisibleTo added to DecisionCore for DecisionCore.Tests so the
internal DeviceOnlyReasons tokens stay hidden from consumers while tests
assert on them directly.
Suite totals: 45 DecisionCore (was 41) / 507 Legacy / 557 Functions.
Plan: plans/REFACTOR_AGENT_V2.md §4 M3 sub-milestone M3.2.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
0210d42c7f |
refactor(decisioncore): Classic UserDriven-v1 handlers + 3 scenarios (M3.1)
First vertical slice of real reducer behavior: six handlers for the Classic
UserDriven-v1 enrollment flow plus the shared DeadlineFired router, with three
committed-anonymized scenario fixtures driving the proof.
Handlers (DecisionEngine.Classic.cs):
- EspPhaseChanged stage progression DeviceSetup -> AccountSetup;
populates CurrentEnrollmentPhase + DeviceSetupEnteredUtc /
AccountSetupEnteredUtc / FinalizingEnteredUtc facts;
raises EnrollmentType hypothesis to Weak on AccountSetup.
- EspExiting records EspFinalExitUtc; schedules HelloSafety (300 s);
transitions to AwaitingHello and emits ScheduleDeadline.
- HelloResolved records HelloResolvedUtc + HelloOutcome; cancels
HelloSafety; completes session if Desktop already arrived.
- DesktopArrived mirror of HelloResolved; completion when Hello already
resolved.
- ImeUserSessionCompleted raises EnrollmentType hypothesis to Strong; records
matched pattern id.
- AadUserJoinedLate HYPOTHESIS-ONLY (project memory
feedback_aad_joined_late_not_completion). Records
AadJoinedWithUser fact + annotates EnrollmentType reason.
Stage unchanged, no terminal event. Regression guard test
verifies From/To stage equality.
Shared router (DecisionEngine.Shared.cs):
- DeadlineFired dispatches on Payload[deadline]. HelloSafety timeout synthesizes
HelloOutcome=Timeout and drives the same completion path as HelloResolved.
Unknown deadline names cancel the deadline and record a neutral transition —
forward-compat with M3.3 (ClassifierTick) / M3.4 (WhiteGlovePart2Safety).
Constants:
- DeadlineNames enumerates the 7 pflicht-deadlines from plan §2.6.
- SignalPayloadKeys standardizes payload keys used by collectors and the
deadline scheduler.
Scenario harness + fixtures (tests/fixtures/enrollment-sessions/):
- ScenarioTestBase locates repo root and wraps FixtureLoader + ReplayHarness.
- userdriven-happy-v1.jsonl — 7 signals, terminal Completed /
EnrollmentComplete / HelloOutcome=Success.
- userdriven-hello-timeout-v1.jsonl — Desktop arrives before Hello, deadline
fires at T+5m -> HelloOutcome=Timeout,
Completed.
- late-aadj-v1.jsonl — Late-AADJ mid-flight does not complete
prematurely; session still completes
normally via Hello + Desktop.
Tests: 4 scenario tests in ClassicScenarioTests + determinism check.
Total suite: 41 DecisionCore.Tests (was 37 at M3.0) / 507 Legacy unchanged /
557 Functions unchanged.
Plan: plans/REFACTOR_AGENT_V2.md §4 M3 sub-milestone M3.1.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
710504549b |
refactor(agent): V2-Agent-Split skeleton + infrastructure copy (M2)
Three new projects as a clean-slate V2 fork of the Legacy Agent:
AutopilotMonitor.Agent.V2 — net48 exe, minimal Program.cs,
buildcounter 2.0.N, version-v2.json
AutopilotMonitor.Agent.V2.Core — net48 library, 94 files copied from
Agent.Core via bulk sed namespace
rewrite; same folder layout
AutopilotMonitor.DecisionCore.Tests — xUnit net8.0 with ReplayHarness
+ 13 smoke/harness tests
V2 isolation is build-time-enforced via V2Isolation.targets imported from
both V2 csproj files — any Legacy ProjectReference fails the build with a
clear error. Grep-verified at commit time: zero `using
AutopilotMonitor.Agent.Core` statements and zero Legacy ProjectReferences
in the V2 tree.
Per plan §2.11, kernel items (CompletionStateMachine, CompletionGuards,
EnrollmentTracker*, Flows/*, WhiteGloveClassifier, WhiteGloveSignals,
EnrollmentStatePersistence) are transitionally retained so the V2 tree
compiles; M4 removes them when SignalAdapters + V2Orchestrator take over.
Per user direction (feedback_v2_kernel_only_refactor): the V2 refactor
replaces only the decision kernel. All peripheral capabilities — gather
rules, analyzers, diagnostics upload, timezone, server-driven actions,
summary dialog launch, env-var resolution, version checks, registry
watchers, network/DO/performance telemetry, self-update, bootstrap→cert
handover, self-destruct — are preserved verbatim via the bulk copy.
Supporting M3 bring-up platform:
- ReplayHarness (empty-stream deterministic, SHA256 hash over terminal
state fields) in DecisionCore.Tests/Harness/
- Fixture scaffold under tests/fixtures/: committed anonymized directory
with commit-policy README, plus gitignored enrollment-sessions-local/
for raw exports
- tools/export-session-fixture.mjs with --help / --dry-run / --validate
modes; real MCP wiring marked TODO(v2-m3)
V2 bootstrap scripts scripts/Bootstrap/Install-AutopilotMonitor-v2(.|-dev).ps1
(committed, pure ASCII verified per feedback_ps1_ascii_only). V2 deployment
scripts under scripts/Deployment/V2/ are gitignored (analog to Legacy).
Test totals at M2 gate: 13 DecisionCore + 507 Agent.Core (unchanged) +
557 Functions (unchanged) = 1077 green.
Plan: plans/REFACTOR_AGENT_V2.md §4 M2 + §2.16 V2-Agent-Split.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|