Files
AutopilotMonitor/docs/log.md
T
2026-08-23 00:50:41 +02:00

67 KiB
Raw Blame History

Log

2026-08-23

  • Update: backend/tenant-activation-welcome-mail.md — new "Template overrides" section: Global Admins can replace the welcome/farewell HTML in the portal (Settings → Email Templates); EmailTemplateService resolves override-or-built-in per send (PreviewConfig table, partition EmailTemplates, 5-min cache, {{domainName}} placeholder, 30k limit, fail-soft send path), GA endpoints global/email-templates/{kind} (+/test to the caller's tenant contact address). Also: the mail header is image-free (CSS-drawn brand mark, gradient stripes and clip-path edge as progressive enhancement).

2026-08-22

  • Update: backend/tenant-activation-welcome-mail.md — new "Mail transport" section: transactional mail now goes through the provider-neutral EmailService (IEmailService + IOffboardFarewellEmailSender) with the provider confined to one private transport method and the Email:* settings (ApiKey, Endpoint, FromAddress, FromName); current provider Mailchimp Transactional (Mandrill) replacing Resend (NuGet dropped, typed HttpClient + ResiliencePolicies.Notification). Result-array semantics (sent/queued/scheduled ok, rejected/invalid warn), open/click tracking explicitly off because the privacy/trust claim depends on it.

  • New: backend/plan-downgrade-retention-grace.md — plan downgrade policy: every Pro entitlement gates read-time and reversibly on downgrade ("gate, don't destroy"), except retention, which now gets a 30-day grace (FeatureEntitlementCatalog.RetentionDowngradeGraceDays) before the Community cap starts hard-deleting the 90–365-day band. Anchor model: ProDowngradedUtc stamped/cleared by the plan endpoint's pure ApplyPlanChanges core on EFFECTIVE-edition transitions (covers explicitly ended trials; planTier downgrade under an active trial deliberately doesn't stamp), trial expiry anchors read-time via TrialExpiresUtc. New Warning ops events TenantPlanDowngraded (PATCH handler) and TenantRetentionGraceExpiring/TenantRetentionGraceEnded (TrialExpirySweep window mechanics, only when stored retention exceeds the Community cap). Field is patch-denied + revert-protected; retention WRITE cap deliberately ignores the grace.

2026-08-21

  • New: agent/do-group-id-auto-set.md — optional DO peering-group seed (EnableDoGroupIdAutoSet, default off, ConfigVersion 40): DoGroupIdService derives a deterministic GUID from the default gateway's IPv4 + SendARP MAC in RealmJoin's exact byte layout (cross-product peering) and writes DOGroupId in the 64-bit GPO Policies store with readback verification. Conservative by design: no random-GUID fallback (skip on missing gateway/MAC), DOGroupIdSource/MDM-managed values/foreign GPO values always win (skip reasons in the do_group_id_auto_set event), own stale values are recognized via the written:<guid> StartupEventGate fingerprint and updated after a gateway change. Gateway heuristic extracted into the shared NetworkInterfaceLocator (dedup of NetworkChangeDetector + DeviceInfoCollector copies).

  • New: agent/system-timeline-watcher.md — clock-step and sleep-episode ground truth from the System event log: one EventLogWatcher (Kernel-General 1, Power-Troubleshooter 1, Kernel-Power 507; 506 deliberately unconsumed) emits system_clock_changed (oldTime/newTime/signed timeDeltaMs/reason/process; 2 s micro-slew floor, ≥5 min = Warning+immediate) and system_sleep_episode (kind sleep/hibernate/modern_standby, authoritative enteredAt/exitedAt payload instants, 60 s Modern-Standby floor). WindowsUpdateTracker-style backfill (24 h default, system-timeline-watermark.json) recovers pre-agent records; ConfigVersion 39 (Collectors.EnableSystemTimelineWatcher). Consumers: TimeJumpBadge cause clock-set (ground truth beats inference), SessionInfoCard standby note next to the unchanged wall-clock duration, SleepSpans in time attribution (AttributionVersion 3, RebootSpan clone), and ANALYZE-DEV-012 (info, single, ≥5 min episode; deliberately the only new rule — clock issues stay with DEV-008).

  • New: agent/power-state-watcher.md — live AC/battery observation during enrollment: PowerStateWatcherHost subscribes to the WMI push Win32_PowerManagementEvent EventCode 10 (no polling; re-probes GetSystemPowerStatus per push, 5 s trailing-edge debounce, never armed on battery-less devices, Collectors.EnablePowerStateWatcher kill-switch, ConfigVersion 38). The pure PowerStateTransitionTracker emits power_state_change with transition = ac_to_battery (Warning, immediate) / battery_to_ac (Info) / threshold_crossed at 50/30/15% (each level latched once per lifetime, lowest-crossed-only on jumps, baseline-below-level covered, emission cap 20). Classified non-activity on both sides (SignalActivityClassifier + IsPeriodicOrStallEvent). New analyze rules ANALYZE-DEV-009 (thresholdPercent=15, high) and ANALYZE-DEV-010 (transition=ac_to_battery, warning), both with on_event:power_state_change interim triggers and deliberately single-required-condition (event_data conditions have no same-instance join).

  • Update: rules/rule-id-namespace.md — the export→paste round-trip is no longer lossy: the JSON editors now map rule-shaped exports into the form (gatherRuleToForm unflattens the nested gather parameters object, analyze jsonToForm maps evaluateOn onto the eval checkboxes; tags and the effective markSessionAsFailed carry into the created rule). Both editors still accept the serialized-form shape; edit-mode pastes merge, with an explicit parameters/evaluateOn winning over stale flat fields.

  • New: rules/rule-id-namespace.md — rule identity model: human-readable ruleId strings (no GUIDs), tenant isolation via Table Storage partitioning (global = built-in/community, tenantId = custom), the reserved built-in namespace from RuleIdPolicy, and the four collision guards: catalog-only global RuleStats aggregate (same-ID custom rules of unrelated tenants no longer sum into one global_{ruleId} row), combine.js duplicate/namespace fail, GitHub-reseed rejection of non-reserved IDs into the global partition, and the namespace gate on the cross-tenant RuleState GC.

  • New: backend/app-install-duration-attempt.md — attempt-scoped app install durations: DurationSeconds = status-defining (last) attempt anchored on the new max-folded LastAttemptStartedAt (StartedAt stays the earliest-wins window/version-ordering key; legacy rows without the anchor keep the span fallback); InstallPassCount with replay-guarded additive fold; weaker-terminal guard (completed-with-Skipped never overrides Installed/Error, in-batch and cross-batch); attempt-scoped DownloadDurationSeconds pairing; the version-regression radar's both-sides cutover gate; time attribution counts the union of per-pass active segments (occupancy merges segments, hulls stay for display/what-if); agent payload timing re-arms both endpoints on terminal→active. No backfill — pre-cutover rows keep span semantics.

  • Update: agent/decision-engine.md — activity-based RealmJoin timeout extension (report 55e6afd61c9d: a large RJ package catalog outlived the 60-min window mid-install — the timer arms at RJ-agent-MSI detection, so the first deployment had effectively ~44 min). When the RealmJoinTimeout deadline fires with LastDeploymentPhase 100/101 and deployment activity (phase change or package observation, persisted in RealmJoinFacts.LastActivityUtc; the detection-time phase capture deliberately excluded) inside the last 60 min, the deadline re-arms to lastActivity + 60 min (Info event realmjoin_timeout_extended), clamped at detection + 4 h. realmjoin_timeout now carries reason = hard_timeout / inactivity / absolute_cap; a fire of a superseded deadline incarnation dead-ends. Idle detection and phase-200/210 sessions keep the original 60-min behavior.

  • New: backend/send-time-frame-separation.md — P14: the agent stamps one device-clock send timestamp per upload attempt (X-Send-Time-Utc), the ingest function server-stamps it as SentAt on every event of the request. SentAt − OccurredUtc is pure spool delay, ReceivedAt − SentAt is network + device-vs-server clock offset — the two were indistinguishable before and had to be separated statistically. The clock_skew condition measures SentAt-carrying batches directly (spool-immune, no spread cap, IME-only sessions become measurable; evidence carries sentAtBatchCount), legacy batches keep the median path; the CMTrace tripwire deliberately stays on its differential math (common-mode clock offset cancels there).

2026-08-20

  • Update: agent/cmtrace-time-resolution.md — new "Backend regression tripwire" section: the P6 split. Operator side: CmTraceSkewTripwire piggybacks on the terminal counter reconcile's partition scan (Δ = ReceivedAt − OccurredUtc per source, 15-min grid + 2-min residual, ≥20 samples / ≥3 distinct batches per side, bias-dominated sessions suppressed, measuredWriterOffsetMinutes never consulted) and emits the CmTraceTimeSkewRegression ops event — goal state: never fires; kill switch CmTraceSkewTripwireDisabled. Customer side: new clock_skew analyze condition source (skewMetric: clock_jump | sustained_offset, threshold in seconds, batch-median frames with spool-spread filter, plateau step detection, end-state persistence, IME-derived events excluded) behind the new built-in ANALYZE-DEV-008 (300 s = the Kerberos/Entra 5-minute skew boundary) — the during-enrollment complement of ANALYZE-DEV-007's one-shot NTP check. P5 (agent-side observability event) is subsumed by the tripwire.

  • New: agent/cmtrace-time-resolution.md — the era-aware answer to the CMTrace timezone skew: a bias-less line read in a provably fresh pass anchors its own UTC offset against the agent clock (15-minute grid, 2-minute residual), so interleaved writer eras in one file resolve line by line; everything not provably fresh keeps the uniform reader-zone fallback and says so via sourceOffsetOrigin. Freshness is a per-file LastCheckedUtc stamped on every poll look (empty first sights included) with a 30-second window, never restored across restarts. Replaces the reverted per-file offset application (04b1a7c6); the per-file measurement stays observational, now with the anchor timestamp logged as a tripwire for the still-unexplained e9753578 label/anchor crossing.

  • Update: agent/post-reboot-completion-recovery.md — retitled to "ESP Completion Starvation — the sits-d Cloud-PC complex" and reframed on the evidence: the reboot was a red herring. The five stuck Cloud PCs carried SkipUserStatusPage=True (read 10+ times across three agent processes, True every time) and Windows honoured it — no user ESP page, hence no Shell-Core 62407 and no AccountSetup provisioning registry, verified over a six-hour uninterrupted post-reboot run with the watcher armed. Arm B of ShouldTransitionToAwaitingHello was open from the bootstrap onward, but every evaluation site hangs off a signal a skip-user flow cannot produce, and the AdvisoryCompletion backstop arms exclusively from an EspExiting signal — an open gate nobody knocks on. Fix: HandleImeUserSessionCompletedV1 accepts the observed skip in place of the final-exit evidence (skipUserExitEquivalent; anchor + genuine IME completion + real-user desktop stay mandatory, WDP excluded). The knock deliberately lives on the IME edge, not desktop arrival — ordering-robust against Fix 10's AccountSetup bounce-back and semantically "user-phase work finished" rather than mid-install. ClassicSkipUserEspUserSessionCompletionTests replays the real 8110e262 ordering end to end. Docs and comments no longer claim an ESP-exit recovery that does not exist (stray duplicate summary in ShellCoreTracker, ImeLogHost restore-callback comment); the residual SkipUser=false exit-in-downtime gap is named honestly and surfaced by ANALYZE-ESP-005 via session_stalled instead of being patched with an unorderable replay.

2026-08-19

  • New: agent/post-reboot-completion-recovery.md — a forced mid-ESP reboot (DeviceGuard/DmaGuard) used to cost the session its AccountSetup completion permanently. Two defects: ShellCoreTracker.BackfillRecentEspExitEvents() existed but had no caller, so every agent restart dropped an ESP exit (62407) written while it was down; and the user-apps-settled synthesis was edge-triggered on EspExited only, so apps settling afterwards never re-opened the gate. EspAndHelloHost.Start() now backfills with a lookback sized to the downtime (previousExit=reboot_kill → since LastBootUtc, clamped to 360 min), and ReevaluateUserAppsSettledSynthesis() is chained into ImeLogTracker.OnAppStateChanged. No new completion path — the gate conditions are unchanged. Evidence: sits-d Cloud PCs, five Incomplete sessions on healthy devices, two of them at 138/138 apps, 0 failed. Hardened over two review rounds: the remembered edge must be a live exit that is confirmed post-AccountSetup (a replay carries no evidence of its own position — Shell-Core writes the same CommercialOOBE_ESPProgress_Page_Exiting for the intermediate and the final exit, and the registry check reads now, not the event's time); the replay hands over the newest exit rather than the oldest; the replay is narrowed to the Hello-wizard start (62404) alone — a conservative fact that can only make the agent wait longer. A replayed ESP exit is never re-raised at all: Windows writes the identical description for the intermediate and the final exit, the reducer orders exits by ingest ordinal (fresher than reality for a replay) and HandleEspExitingV1 passes espFinalExitInFlight: true for every arriving exit, so arm C of ShouldTransitionToAwaitingHello would open on a historic handoff once the restored state carries AccountSetupEntered + IME user session + desktop. A replayed ESP failure is likewise dropped (it could fail a session that recovered). Skipped records are reported as an agent_trace. The window is measured from the previous run's last known liveness (snapshot mtime, widened by LastBootUtc) instead of a constant — the scheduled task has a BootTrigger only and no restart-on-failure, so a crashed agent returns hours later and five minutes would lose the exit every time. On first_run the replay stays off, leaving the happy path untouched.

  • Update: rules/analyze-rule-triggers.md — on_event:session_stalled documented as an established interim trigger for the never-terminal stuck session (one emission per session, no throttle needed). ANALYZE-ESP-005 carries it: both its conditions are monotonic and it has no terminal precondition, and it fires at confidence 95 on the sits-d stalled stream. ANALYZE-APP-015 deliberately does NOT — it is the worked example of the terminal-precondition trap (lint 1) — and ANALYZE-ESP-001 does not either, because its phase_duration gate on a still-open phase is unverified mid-run. New ANALYZE-APP-016 ("Required Apps Never Started — ESP Apps Gate Starved") turns the existing app_install_starved events into a customer-facing finding and, unlike ANALYZE-APP-015, fires even when the session succeeded — session 3b9291aa was recorded Succeeded with 99 required apps that never started installing.

  • New: script-publishing.md — the customer script publishing chain. .github/scripts/Publish-BootstrapScripts.ps1 becomes the single owner (dev render, version-bump guard, uploads, version oracles, alias verification), called both by the new publish-scripts.yml (push to main on scripts/Bootstrap/** and scripts/CustomerSetup/**) and by the publish_as_stable branch of build-agent.yml. Script publishing no longer waits for an agent release. Blobs are published in a deterministic CRLF/UTF-8-no-BOM form so byte comparisons do not depend on the checkout; the Pester + ASCII gates move into the reusable bootstrap-script-gates.yml and block the upload; AdminConfiguration.LatestBootstrapV2ScriptVersion now has exactly one writer. The version-bump guard compares a code fingerprint (PowerShell token stream without comments, newlines and line continuations), so only behaviour changes require a $ScriptVersion bump — comment and formatting changes publish with a warning.

2026-08-18

  • New: agent/wdp-scenario-gates.md — WDP scenario gates (afee7ae0 audit): deterministic ExecutionContext marker as the only gate key (fail-toward-Classic), suppressed Autopilot/ESP-only emissions and hosts (autopilot_profile*/ZTD probe, esp_config unknown null-gate, EspPolicyProviderStallHost, ProvisioningStatusTracker, ModernDeployment noise IDs 100/417/418/1005 + error-backfill skip, starved sweep), and the WDP completion model (High-confidence v2 seed via enrollmentTypeDeterministic, desktop-based arm D, 30-min device_prep_completion backstop, parked-tripwire WDP branch).

  • Update: backend/time-attribution.md — AttributionVersion 1 → 2: new duration-critical quality flag PriorEnrollmentResidue (fires on historic_ime_replay_detected; a re-enrollment without wipe pulls the phase anchors ahead of the real ESP page, so the segments are excluded from fleet stats; registry_app_baseline successes deliberately not a trigger — by-design normal for WDP). Added the quality-flags section.

  • Update: agent/logs-and-persistence.md, agent/overview.md, agent/gather-rule-debug-log.md — log files renamed to unified kebab-case: agent.log (date rotation dropped; size segments agent-NNN.log), bootstrap-script.log (was bootstrap_agent.log), bootstrap-msi.log (was msi-bootstrap.log), crash-*.log, gather-rules-debug.log, ime-pattern-matches.log. Old names still appear in archives from pre-rename agents.

  • New: agent/registry-app-state-pillar.md — the registry second pillar for IME app state (audit follow-up): snapshot-and-diff watcher on HKLM\...\IntuneManagementExtension (Win32Apps / EspTrackingWin32Apps / StatusServiceReports), registry_app_state + app_state_reconciliation events, silent startup baseline, WDP reuse notes.

2026-08-17

  • Update: mcp/tool-telemetry.md — the tool_call stderr line now carries errorMessage (capped at 300 chars; thrown message or the leading toolError text, absent on success), so error drilldowns name the failure class instead of guessing from args. Same change: get_rule_stats omits the per-rule daily trend rows by default (they drove 50% of calls over the response cap at platform scope); includeTrends=true opts back in, the response carries a trendsNote marker when stripped.

2026-08-14 (later)

  • Update: rules/analyze-rule-triggers.md — status SPEC → IMPLEMENTED. Shipped: evaluateOn on the analyze-rule schema (grammar-validated server-side and in validate_rule), AnalyzeRunContext-driven engine runs (Terminal/Interim/Reanalyze) with the RuleResult lifecycle columns (FirstDetectedAt/LastEvaluatedAt/IsInterim/ResolvedAt/NotifiedAt), interim rule filtering + KO/stat suppression, resolve/finalize update-semantics, the NotifiedAt one-send-per-(session,rule) notification dedupe (preserved across the manual reanalyze rebuild), the ingest-side InterimTriggerRegistry (5-min TTL, fail-soft) gating interim_trigger / whiteglove_sealed enqueues, the three MCP authoring lints, web preliminary/resolved rendering, and ANALYZE-ID-004 as the first interim-enabled rule.

2026-08-14

  • Creation: Added rules/analyze-rule-triggers.md — SPEC for opt-in evaluateOn interim evaluation of analyze rules: enrollment_end default preserves today's behavior, whiteglove_sealed + on_event:<type> close the WhiteGlove notification gap and the never-terminal stuck-session gap; RuleResults update-semantics (FirstDetectedAt/IsInterim/ResolvedAt/NotifiedAt) replace the permanent dedupe freeze; interim runs suppress KO + stats, notifications fire on first detection; three new validate_rule lints (terminal-precondition trap, unknown trigger type, KO-on-interim).

2026-08-13 (late evening)

  • Creation: Added drift-guards.md — the fragility-audit follow-up contracts (P5.x + P7): SignalR message-name catalog in Constants.SignalRMessages, exported through the shared manifest and typed into the web's SignalRContext.on/off union; the RowKeyCodec single encoder for inverted-tick RowKeys (persisted widths pinned, incl. UserActivity's legacy D20); DeletionRowDump.Table on tombstone rows with DeletionTombstoneTables.Resolve replacing the duplicated Contains('_') heuristic (legacy fallback retained one retention cycle); the shrink-only OkAsyncBaselineGuardTests per-file baseline (44 sites frozen); the Settings per-section PATCH (section field map + diff-based config/{tenantId}/fields writes on the TenantAdmin caller tier); and the web's scopedApi endpoint-pair builders, single admin-scope hook, navVisibility matrix and guard-consistency ratchet.

  • Update: versioning.md — the web portal joins the version contract commit-only: public/version.json generated at build time from GITHUB_SHA, served with Cache-Control: no-store, polled by deploy-web.yml until the deployed commit matches before versions/web.json is published.

2026-08-13 (evening)

  • Creation: Added backend/lifecycle-manifests-and-session-scope.md — the fragility-audit P5/P6 completeness nets: (1) TableLifecycleBucketTests classifies every TableNames constant into offboarding-wipe / per-session-cascade / kept-by-design (fixed en route: UsageMetrics wiped via the new RowKey-anchored SafeWipe Variant D — the exact-PK wipe matched 0 rows; PlatformStats tenant partition + ScriptNameCache added to the tenant-PK bucket); (2) SessionIndexFieldManifest as the declared SessionsIndex column set with bidirectional builder/reader test pinning, Replace-mode full rebuilds, and a fail-soft merge-key guard; (3) ResolveSessionScopeAsync as the single session-scope fallback (16 former copies), backed by the new SessionTenantLookup point-read table (scan fallback + self-heal, cascade step before the tombstone), plus the RecomputeTriggerGate 403 for reanalyze/rescan by read-only roles; (4) server-side dashboard free-text search (q=) with page-backfill loop and fingerprint binding, replacing the client's full-history loadAll sweep.

  • Creation: Added backend/app-version-regression-radar.md — app-version duration regression radar: per (tenant, app) the newest version's median measured install duration (nearest-rank; population Succeeded + non-skip + HasMeasuredDuration + no AppId collision + non-empty AppVersion) is compared against the previous version (first-seen ordering, never string sort); fires at median lift ≥2× AND ≥300s absolute with ≥10 measured installs on both sides. Episodes live as appversionregression|{app}|{version} tracker rows (one bell app_version_duration_regression + one AppVersionDurationRegression ops event per episode, refresh keeps FirstNotifiedAt, re-arm on drain/median-recovery/30d retention). Surfaces: versionRegressions[] + per-version measuredInstalls/medianDurationSeconds/p95DurationSeconds in the apps analytics responses, amber banner + duration-by-version chart on the apps detail page. Kill switch AppVersionRegressionRadarDisabled.

2026-08-13

  • Update: backend/session-annotations.md — the /annotations portal overview page now carries the standard tenant switcher (GA/Reader: "All tenants" aggregate + per-tenant drill-down via GET global/session-annotations; delegated: managed subset), with a tenant column and tenant-qualified session deep links. The global list endpoint now derives excludeGlobalAdminLane from HasGlobalScope, so delegated-rescue callers on their managed ?tenantId= path never receive the platform-internal globaladmin lane (endpoint-test-pinned).

2026-08-12

  • Update: agent/autopilot-ztd-diagnostics.md — Windows 365 Cloud PC exception documented: on Cloud PCs ProfileAvailable=0 is expected (provisioned by the Windows 365 service, not Autopilot), so autopilot_profile_missing is emitted as Info with a Cloud-PC-specific message and an isCloudPc payload marker (ResolveAutopilotProfileMissingPresentation); the portal's Autopilot Profile card renders a neutral note instead of the amber warning for Cloud PC sessions (session.isCloudPc or the event marker).

2026-08-11 (later)

  • Creation: Added backend/session-annotations.md — session annotations: the new SessionAnnotations table (PK=tenantId, RK={sessionId}_{lane}, lanes operator/tenantadmin/globaladmin) storing structured verdicts (root_cause_confirmed | analysis_wrong | different_problem | inconclusive) + notes with server-stamped authorship and a fired-rule-id snapshot for join-free rule-quality evaluation. Per-lane write matrix re-gated in-function with own-tenant binding (a GA writes only the platform-internal globaladmin lane cross-tenant; tenant callers never see that lane). Surfaces: portal SessionAnnotationsCard (session detail, after Analysis), MCP list_session_annotations (ga) + annotate_session (strictGa, always the GA lane) + annotations key in get_session_summary. Lifecycle: per-session cascade steps 17–19, offboarding wipe, critical-backup member.

2026-08-11

  • Update: rules/gather-rule-guardrails.md — hard-block mirroring closed across all pre-flight layers: rules/guardrails.json now also mirrors blockedCommandPatterns + maxCommandLength and carries C:\Windows\System32\config in blockedFilePrefixes; portal (guardValidation.ts) and MCP (rule-validation.ts) apply them in agent order (before the unrestricted-mode early-return), so a command the agent hard-blocks no longer shows "Allowed" in unrestricted tenants. GuardrailsParityTests pins the JSON mirror to the C# constants. MCP validate_rule additionally validates json/xml targets with file semantics (the schema's collectorType enum was missing both) and rejects activePhases+activeFromPhase set together (backend 400 parity). Author stamping extended to PUT-upserts: tenant and global gather-rule updates stamp the caller's JWT display name before the service call, so an upsert of a non-existing ruleId can no longer store a payload-supplied author (true updates still preserve the original author).

2026-08-10

  • Update: rules/gather-rule-guardrails.md — WMI matcher generalized from literal prefix matching to shape parsing: SELECT <* | property-list> FROM <class> is allowed when the class is derived from the existing SELECT * FROM <class> entries (a projection is a strict subset; motivated by SELECT BatteryStatus FROM Win32_Battery + on_change battery polling that SELECT * defeats via fluctuating charge %). Same algorithm mirrored agent/portal/MCP; guardrails.json unchanged. Portal now also gates enabling on the client-side validation (invalid-target custom rules create disabled, cannot be toggled on, force-disable on invalid edit, "Blocked on devices" badge) — UX only, the agent guard stays the boundary. Gather-rule Author is now stamped from the creator's JWT (display name → UPN) at create and immutable on update.

  • New: agent/continue-anyway-observation.md — Continue-Anyway observation mode (EnableEspContinueAnywayObservation, ConfigVersion 37, operator-set): Device-phase ESP terminal failures on Continue-Anyway profiles are defanged into a 60-min observation window (Classic/Unknown, non-pre-prov only); real-user desktop + Hello gate complete the session as Succeeded + EspSoftFailure/CompletionSource (amber "with issues" badge + detail banner), category recovery resolves clean, expiry un-defangs to the original esp_terminal_failure. Registered in the Agent index section. Related backend fix (no doc): the auto-analyze enqueue now carries a 30 s visibilityTimeout so rule evaluation no longer races the agent's terminal flush (ANALYZE-ESP-004's {{appName}} placeholder).

2026-08-07 (later)

  • Update: backend/cloudpc-device-validation.md — third IsCloudPc transport rail: the pre-auth distress channel. DistressReport.IsCloudPc (CloudPcDetector verdict captured at DistressReporter construction) → DistressReportEntry/table column → "Devices Not Registered" aggregation (sticky-true per serial, same OR semantics as the Sessions merge) → portal Cloud PC badge + actionable hint ("enable Windows 365 Cloud PC Validation + W365CloudPcValidation add-on"). Motivated by the first W365 field run: the rejected Cloud PC showed up as anonymous "Microsoft Corporation / Virtual Machine" with no clue that the fix is the Cloud PC validator, not Autopilot registration. UNVERIFIED like all distress fields; ≤18 bytes, fits the 1536-byte cap.

2026-08-07

  • Update: backend/cloudpc-device-validation.md — the "deliberately out of scope" follow-up landed in the same PR: agent-side IsCloudPc flag (CloudPcDetector, bootstrap-verified marker AND: Windows365 key + CloudManagedDesktopExtension service key, SKIP-safe) rides two rails — SessionRegistration → Sessions/Index (sticky-true) → SessionSummary + isCloudPc search filter (portal/raw/MCP), and the EnrollmentFactsObserved payload → EnrollmentScenarioObservations.CloudPc (set-once, both values) with profile reason cloud_pc_first_connect:no_device_esp_expected + cloud_pc_marker in the audit-trail census. Reason+observation only: no completion arm depends on DeviceSetup, Mode stays AccountSetup/IME-classified, EspConfig keeps configured-FirstSync semantics. Never an auth input.

2026-08-06

  • Creation: Added backend/cloudpc-device-validation.md — the CloudPc fallback validator (ValidatorType.CloudPc = 5, tenant flag ValidateCloudPcDevice): W365 Cloud PCs are structurally never Autopilot-registered, so stage 4 gains a third stage that binds the chain-validated MDM cert's Subject CN (= Intune device id, field-verified on a real Cloud PC) to the tenant's virtualEndpoint/cloudPCs inventory — admits exactly the service-provisioned Cloud PCs, no device-type routing, local W365 markers never consulted for auth. New optional Graph add-on W365CloudPcValidation → CloudPC.Read.All (catalog + grant script now pinned by GraphFeatureCatalogSyncTests; Graph 403 = definitive "grant missing", not transient). Bootstrap dev script v2.3-dev.3 exempts guard 4 (uptime) while the Cloud-PC relax is active — the fresh-profile window is the time anchor at first connect, not boot uptime. Follow-up deliberately out of scope: agent-side IsCloudPc classification + decision-engine "no Device ESP" expectation.

2026-08-05 (later)

  • Update: rules/rule-authoring-surface.md — new section POST /api/rules/gather/test-pattern: the logparser pattern tester (agent-exact .NET matching: no IgnoreCase, first-match-per-line, cmtrace vs text mode, verbatim format=text hint), motivated by a field incident where a PHP-verified regex behaved differently under .NET. CmTraceLogParser moved Agent → AutopilotMonitor.Shared.Logging so agent and backend share the ONE parsing implementation (timestamp caveat: AssumeLocal = server TZ on backend, endpoint reports no timestamps). MCP: new test_log_pattern tool, logparser lint in validate_rule, guide gains the case-sensitivity/engine-mismatch pitfalls.

2026-08-05

  • Creation: Added mcp/tool-telemetry.md — the three MCP tool-telemetry layers (App Insights McpToolName, per-user usage table, MCP_TOOL_LOGGING stderr lines) and the new quality signals: soft-error capture (toolError returns instead of throwing, so backend telemetry counted those calls as successes), resultChars/overCap vs. the inline-size hint, bounded args summaries, caller scope, search_zero_hit on the three search tools (unmet-demand signal), and tool_call_rejected for JSON-RPC-level Zod/unknown-tool rejections sniffed at the /mcp route (previously invisible everywhere). Bicep default for toolLoggingEnabled flipped to true (documented desired state; live flag set via az containerapp update).
  • Creation: Added rules/rule-authoring-surface.md — the AI-assisted rule-authoring stack: three deterministic MCP resources (rule_authoring_guide hand-written, rule_schemas + rule_guardrails generated into rule-authoring.generated.ts by a new combine.js emitter, drift-guarded twice), the MCP-local validate_rule (ajv 2020-12 against the single-rule $defs, guardrail pre-flight mirroring GatherRuleGuards matching semantics incl. hard blocks, semantic lint for schema-valid-but-dead rules), and the side-effect-free POST /api/rules/analyze/dryrun (TenantAdminOrGlobalReader; RuleEngine.DryRun.cs trace twin sharing the production evaluators, all-conditions trace with per-condition evidence + matchingEventCount, parity and side-effect-freedom pinned by RuleEngineDryRunTests). MCP test_analyze_rule wraps the endpoint with {{token}}-interpolated previews; author-rule prompt drives the draft → validate → dry-run loop. Deliberately absent: gather-rule remote execution, rule writes via MCP.

2026-08-04

  • Update: rules/gather-rule-phase-scoping.md — new section "Which phase feeds the triggers": GatherRuleExecutorHost now drives phase_change/phase_exit/on_event gather triggers (and the phase-scope gates) from the post-reduce TimelineEventStream (every emitted event's (eventType, phase) published by EventTimelineEmitter) instead of the pre-reduce SignalIngress.SignalPosted payloads. Trigger: session 32312a32 (RealmJoin tenant rsneuffen.de) — a Phase-Start rule on FinalizingSetup fired at the raw ESP-exit EspPhaseChanged(FinalizingSetup) signal, 7 minutes before the engine's RealmJoin-gated phase_transition(FinalizingSetup), reading HKLM\SOFTWARE\RealmJoin\Custom\BIOS before the RealmJoin package created it. Phase triggers now fire exactly when the timeline shows the phase; on_event additionally sees engine-emitted event types (enrollment_complete, realmjoin_resolved, …) for the first time in V2, restoring V1 semantics and the documented end-of-enrollment pattern. Other raw-signal hosts (DeviceInfo, keep-awake, PPKG) deliberately unchanged.

2026-08-02

  • Creation: Added backend/tenant-activation-welcome-mail.md — triggered by the first production auto-approve (activation at signup+64 s, notification address saved at +69 s → welcome mail silently skipped, since the approval path is the only sender and reads the address once). Fix documented in the doc: the notification-email save path now also triggers the send (PUT /api/preview/notification-email → fresh approval check IsApprovedFreshAsync, bypassing the 30 s negative cache), and a conditional-insert welcome-email-sent marker in PreviewWhitelist makes the send once-only across both racing paths (marker consumed strictly after the address check; revoke clears it, offboarding wipes the partition, the GA resend endpoint sends unconditionally and consumes it best-effort).

2026-07-29

  • Update: agent/gather-rule-debug-log.md — three trace enrichments: (1) new phase stage writing an enrollment-phase transition marker (phase change: Old -> New, ruleId -) so every rule-level line reads against the current phase; (2) logparser now writes one line per regex match — line number, matched text, capture groups — capped at 10 per file per run (the per-file summary keeps the full count); (3) the invalid-regex error line includes the offending pattern string.

  • Update: url-registry.md (+ web/portal-navigation-prefetch.md context) — the portal is now a full static export (output: 'export', trailingSlash: true): the SWA serves plain files, the managed SSR runtime — the portal's last cold-start source ("Backend call failure" > 45 s) — no longer exists. Detail routes moved from path params to query strings (/sessions?id=, /sessions/inspector?id=, /diagnosis?id=, /apps/detail?name=, /admin/backups/detail?id=, /admin/customs-archive/detail?tenantId=&rowKey=) with all 21 link sites going through the new single-definition builders in lib/routes.ts; the 9 [section] families prerender via generateStaticParams server shells (37 pages, dynamicParams=false), server redirect() index pages became ClientRedirect shells. Legacy PATH-shaped deep links (sent Teams/Slack/webhook notifications hold /sessions/{id} forever) are preserved permanently: SWA wildcard rewrites serve the family page and the globally mounted LegacyPathRedirect rewrites to the canonical query URL (hash + query preserved; mapping pinned by lib/__tests__/legacyPathRedirect.test.ts). middleware.ts host routing became the client-side HostRoutingGuard (prefix list moved to lib/hostRouting.ts); the legacy /go/[code] route + bootstrapValidation.ts are deleted (BootstrapSessions table verified free of pre-cutover active sessions). staticwebapp.config.json now carries the 19 docs/marketing redirects (order-sensitive), the legacy rewrites, 404.html override and ALL security headers incl. the literalized prod CSP — guarded by the new swaConfig.guard.test.ts; maintenance mode changed from whole-file swap to a first-routes-entry insert (README rewritten). Backend notification deep links go through the new Constants.PortalSessionUrl (query shape, pinned). Deploy: deploy-web.yml uploads out/ with is_static_export: true.

  • Creation: Added backend/oobe-bootstrap-go-flow.md (and a Bootstrap go-URL section + web-registry update in url-registry.md) — the OOBE bootstrap script endpoint moved from the portal's Next.js /go/[code] SSR route into the Function App (GET /api/bootstrap/go/{code}, GetBootstrapScriptFunction), unblocking the portal's static export; the customer URL became go.autopilotmonitor.com/{code} (Front Door custom domain, /* route with origin-path rewrite, caching disabled — the script inlines the bearer token). New Constants.BootstrapGoBaseUrl (single producer via CreateBootstrapSessionFunction) + web BOOTSTRAP_GO_URL (display only); both guard host lists extended. Security invariants ported to C#: BootstrapScriptValueValidator (exact-GUID tenantId/token, https + host-allowlist + /agent/….zip path, expiry ≤ 14 d) and OobeBootstrapScriptGenerator (Replace-token raw-string template, single-quoted PS literals, 200-char error cap before quote-doubling; always-HTTP-200 text/plain contract for irm | iex). Endpoint is PublicAnonymous in the catalog, NoStoreCacheMiddleware-covered, self-rate-limited bootstrap-script:{ip} 20/min (rate-limit reply is also a 200 error script). Tests: golden byte-parity against the TS template, validator invariants incl. hostile-URL cases, format-gate table, and the previously missing BootstrapUrl shape pin. Transition: the legacy www route + middleware.ts /go prefix stay alive for pre-migration URLs (validity ≤ 168 h) and are removed in the static-export PR no earlier than 168 h after the last www URL was issued. Customer docs network-endpoints.md gained the go. host (device-facing, Bootstrap-Sessions-only).

  • Update: agent/autopilot-ztd-diagnostics.md — presentation layer for the startup error backfill (customer feedback: an unannounced wall of pre-agent Error events with a single identical timestamp read like a live failure). Agent: the backfill scan now buffers the capped record set, emits an Info summary event first (modern_deployment_log, backfillSummary: true + replayedCount/droppedOverCap/lookbackMinutes), then replays oldest-first so the timeline sequence follows event-log chronology. Web: EventTimeline.tsx badges backfilled: true events („⟲ Backfilled", tooltip) and renders the original timeCreated as the row time with a "(reported …)" suffix plus a Recorded metadata line — works retroactively for already-stored events. Severity is deliberately not downgraded.

  • Creation: Added agent/gather-rule-debug-log.md — the new per-tenant setting EnableGatherRuleDebugLog (ConfigVersion 35 → 36, portal toggle under Settings → Agent, EnableImeMatchLog-style bool→path projection in RemoteConfigMerger). When enabled, the agent writes %ProgramData%\AutopilotMonitor\Logs\gather_rules_debug.log: one pipe-delimited line per evaluation decision across ten previously silent "produced nothing" exits — disabled rules, interval without intervalSeconds (never runs), the first-tick-after-one-full-interval window, phase-scope skips incl. currentPhase=Unknown, per-(rule, phase) dedup, empty collector results (emitOnlyIfExists), on_change hash suppression (hash prefix + streak), guard blocks, logparser per-file outcomes (position skips, parse failures with a format=text hint when every line fails CMTrace parsing, maxLines/wildcard caps), and full exception stacks that the agent log drops. 10 MB cap + one .old generation; no hot-reload (per-enrollment lifecycle); flag survives the offline config cache; --run-gather-rules always traces and echoes under --console; zero-rules delivery leaves a one-line file instead of nothing. Side fix: GatherRuleExecutorHost now passes the logger into InformationalEventPost, so gather-event emissions appear in the agent DEBUG log.

  • Correction + Update: web/portal-navigation-prefetch.md. The HTTP/1.1 claim in the 2026-07-27 entry below is wrong and has been struck from the document: portal.autopilotmonitor.com negotiates h2, verified by openssl s_client -alpn h2,http/1.1 and a .NET HttpClient returning HTTP/2.0. The false reading came from the Windows/Schannel curl build in this environment, which has no HTTP/2 support and therefore always reports 1.1 — do not use it to determine HTTP versions. There is no six-connection cap in play; the queue is server-side in the SWA SSR runtime, which does not change the fix or the evidence, only the mechanism. Added an Outcome section with 20 h of post-change telemetry: the Free → Standard plan change (16:02:44 UTC) produced no measurable latency gain once stratified by the gap to the previous request (p50 Free → Standard: <10s 85 → 95 ms, 5-30min 429 → 441 ms; tail rates 1.64 %/0.374 % over 2 s/10 s vs 6 and 1 events at n = 258 against 4.2 and 1.0 expected; browserTimings Mann-Whitney z = −0.24, p = 0.81). The freezes were removed by prefetch={false} (16:34:13 UTC): worst browserTimings.totalDuration 225,277 → 4,856 ms. The prefetch storm was also an accidental keep-alive — requests under 10 s apart fell from 85 % to 46 %, and on the identical never-cacheable /sessions/{id} route the median rose 87 → 211 ms and p90 465 → 1,480 ms, with one 11,441 ms abandoned request (resultCode 0) after 517 minutes idle. Synthetic bursts warm the runtime and must not be used to benchmark this (round 1 p50 288 ms vs rounds 4–25 p50 113 ms).

2026-07-27

  • Creation: Added web/portal-navigation-prefetch.md (and a Web Portal section to the index) — root cause of the minute-long portal freezes. GlobalSidebar and Navbar render the whole navigation on every page, so the App Router's default viewport prefetch issued one ?_rsc= request per visible link against portal.autopilotmonitor.com (~10,400 in 14 days). (The original entry attributed the freeze to HTTP/1.1 and a six-connection-per-origin cap — see the 2026-07-28 correction; the queue is server-side.) When the Static Web Apps managed runtime queued the prefetches (p50 91 ms, p95 896 ms, but max 234 s / 21 samples over 30 s), the navigation the user wanted was stuck behind that queue. Measured on the 2026-07-27 17:25 CEST incident: /admin/presence?_rsc= 151 s, three siblings finishing within 340 ms of each other at ~37.5 s, all 200, while the backend API answered in 1.6–2.2 s and browserTimings showed networkDuration 16 ms against processingDuration 225 s. Every navigation Link now sets prefetch={false} — these routes are client components behind ProtectedRoute that fetch their own data, so the prefetched RSC payload carried no benefit. The doc also records that browser telemetry lives in a separate App Insights resource (AUTOPILOT_MONITOR_WEB_APPINSIGHTS_ID) that the MCP query_backend_logs tool cannot reach, which is why the freezes left no trace in the backend logs.

  • Creation: Added backend/rule-regression-radar.md — F3 regression radar (analyze rules only; gather rules wait for their per-batch stats dedup fix): per (tenant, rule) daily hit rate from RuleStats, firing only when the trailing 7-day window has ≥5 hit sessions and ≥20 evaluated sessions, the rate is ≥2× the prior 28-day baseline AND the Wilson 95 % intervals are disjoint (deterministic MetricsMath primitive with hand-computed test vectors). Suppressed for deleted rules, rules younger than baseline+window (CreatedAt grace), rules edited inside the window (UpdatedAt), empty baselines, and the RuleRegressionRadarDisabled app-setting kill switch. Alert episodes live as ruleregression|{ruleId} tracker rows (one bell + one RuleFrequencyRegression ops event per episode; numbers refreshed while active; re-armed under 1.5× baseline, on fire-stop, or by the 30-day tracker retention). On-fire dimension correlation over osBuild/model/agentVersion/imeVersion with the "correlated, not necessarily causal" wording contract. Surfaces: regressions[] in the rule-stats response (MCP inherits), the analyze-rules page's 30-day sparkline and regression badge.

  • Creation: Added backend/device-journeys.md — F2 device history / First-Time-Right: the device key (trim + case-fold serial, junk/placeholder exclusion incl. the agent's Unknown WMI sentinel), journey grouping over the terminal set Succeeded/Failed/Incomplete (first success ends a journey, a >30-day gap from the previous attempt's CompletedAt starts one, WhiteGlove's single session row is ONE attempt, open sessions are never attempts), the DeviceHistories chain (cap 20, written inline at the terminal seam, healed by the sweep, deleted-session refs pruned tombstone-driven, deliberately not in the per-session deletion manifest) and the additive DeviceJourneyAggregates daily rows (window rate = ratio of sums — no rolling row needed, 180-day retention). Surfaces: the session-detail "Attempt N" banner with server-computed attempt numbers (virtual-attempt rule for live sessions), the Fleet Health First-time-right section (≥20-journey gate, attempt histogram, repeat-devices violator list), and the MCP get_device_history tool (serial mode / fleet FTR mode).

2026-07-25

  • Update: agent/secureboot-ca2023-detection.md — review follow-up on 90012d9 hardened the privilege scope, the repo's only privilege-adjustment code and therefore the precedent for any future one. The process token moved from a raw IntPtr to a SafeHandle assigned by the P/Invoke marshaler, so the CLR owns it from the moment OpenProcessToken returns and the critical finalizer releases it even when Dispose never runs — previously an async exception between the successful open and the caller's using leaked the handle with the privilege enabled and no owner left to restore it. Dispose became idempotent and now releases the handle in a finally, so a throwing restore cannot strand it; the restore obligation is recorded immediately after the successful AdjustTokenPrivileges with nothing throwable in between. The whole enable → read → restore sequence runs under a static gate because the privilege is adjusted on the process token: a concurrent reader would see "already enabled", correctly skip its own restore, and then lose the privilege mid-read when the first reader disposed, reporting privilege_denied on a machine that holds it.

2026-07-24

  • Creation: Added backend/server-actions-on-demand-diagnostics.md — the ServerAction channel (queue on Sessions.PendingActionsJson, deliver piggy-backed on the next ingest response, at-least-once with Type|RuleId|QueuedAt dedup and sequential dispatch) and the on-demand log collection built on it: the portal's always-visible Collect Logs button, the new EndpointPolicy.TenantAdminOrOperator tier with the mandatory in-function per-type re-gate (Operators queue only request_diagnostics), the member-readable diagnosticsUploadConfigured feature flag, the admin quick-config path that must queue rotate_config before request_diagnostics (works against the deployed fleet — a one-time override would not, the mode gate is agent-side), sequence-based progress matching immune to agent clock skew, and the offboarding cascade's hosted-prefix sweep closing the latest-wins orphan-blob gap.

  • Update: agent/esp-policy-provider-stall.md — the customer's follow-up on issue #106 corrected the field-case mechanics: TrackingPoliciesCreated=1 WAS set under the foreign ConfigMgr provider, so the ESP ignores a foreign provider's completion values and waits on the Intune-registered Sidecar provider by name (the rename to Sidecar creates no tracking entries yet unblocks instantly). The completeness-keyed detector would have missed exactly this — the incident it was built for. The tripwire is now two-armed under one event type discriminated by reason: provider_incomplete (unchanged dwell on continuously-incomplete providers) and sidecar_provider_missing (≥ 1 Setup/Apps provider registered without Sidecar, completeness deliberately ignored, clock reset when Sidecar registers later). Sidecar-missing wins the reason when both arms cross in one round; the cross-restart fingerprint gains a sidecarMissing sentinel; payload adds reason and sidecarMissingForMinutes. Evidence for the name-keyed wait: Niehaus' documented Intune WAP payload always registers Sidecar (ConfigMgr only via a co-management settings policy) and Microsoft's ESP troubleshooting models app tracking exclusively through the Sidecar subkey.

  • Creation: Added agent/esp-policy-provider-stall.md — a customer diagnosed that pre-provisioned devices with the ConfigMgr client as a blocking app occasionally park the user ESP at "Apps (Identifying)" for days: only a ConfigMgr provider is registered under EnrollmentStatusTracking\Device\Setup\Apps\PolicyProviders (no Sidecar = IME) and TrackingPoliciesCreated never becomes 1 — a wait the CSP defines without any timeout; renaming the key to Sidecar unblocks immediately. The agent now ships the always-on esp_policy_provider_stalled tripwire (60-s tick, 15-min continuous-incompleteness dwell, one-shot per provider, StartupEventGate fingerprint across restarts, purely observational via the InformationalEvent rail — no toggle, no ConfigVersion bump), generalized to any provider in both Setup/Apps and DevicePreparation kinds rather than hardcoding the ConfigMgr case. Companion change: SOFTWARE\Microsoft\Windows\Autopilot joins the gather-rule registry allowlist so tenants can target the subtree with custom rules (e.g. not_exists on the Sidecar key).

  • Creation: Added agent/secureboot-ca2023-detection.md — ANALYZE-SEC-001 warned on devices whose firmware already carried the 2023 certificate, because the agent only read the SecureBoot\Servicing\UEFICA2023Status registry value, which mirrors the Windows Update rollout rather than the firmware (customer report). The agent now reads the UEFI db/KEK variables natively (GetFirmwareEnvironmentVariableExW with SeSystemEnvironmentPrivilege enabled on the process token — first privilege-adjustment code in the repo) and emits per-certificate booleans on secureboot_status. Suppression rides on a one-sided marker, uefiCa2023FirmwareConfirmed, emitted only on positive confirmation: the engine's not_exists precondition fails on any present value, so a two-sided boolean could not distinguish "confirmed" from "checked and missing", while the marker's absence keeps old agents and firmware-missing devices on the v2 path. Rule v3.0.0 is therefore safe to seed before the agent release.

  • Update: url-registry.md — the first release after the alias migration surfaced Front Door caching as a correctness hazard: the alias served the previous version.json (X-Cache: TCP_REMOTE_HIT) after the origin had rotated, and since manifest and ZIP are independent cache entries, a stale manifest with a fresh ZIP fails the bootstrap SHA-256 check. Route caching on /agent/* is now disabled and every blob upload stamps Cache-Control: no-cache (CI Send-Blob, local build.ps1, versions/*.json manifests) — all agent-container blobs rotate in place, so nothing there is safely cacheable.

  • Update: agent/decision-engine.md — the RealmJoin completion gate has a third terminal, FirstDeploymentIncomplete. Session 224b2087 showed why: an interactive logon 17 s after DeploymentPhase 101 makes RealmJoin reclassify the run as a secondary-user deployment, whose branch writes 200/210 and never 110 — the gate stayed closed until the 60-min hard timeout although Hello and desktop were long resolved. realmjoin_phase_changed is now a typed RealmJoinPhaseChanged signal, so RealmJoinFacts.LastDeploymentPhase is persisted across agent restarts (this also fixes every realmjoin_timeout event claiming "last phase: 0"); a transition out of 100/101 into 200/210 releases the gate with the realmjoin_first_deployment_incomplete Warning, and the RealmJoinDetected replay path evaluates the same rule for restarts that swallow the transition. A first observation of 210 deliberately does not release — session 6f1959c0 started at 210 and only deployed afterwards.

  • Creation: Added url-registry.md — all well-known hosts (own subdomains, both blob accounts, API origin, Graph, Entra login) are now defined once per component (Constants.cs, web utils/config.ts, MCP src/config.ts) and referenced everywhere else; three guard tests fail the build on any literal outside the registries. Written because the EU cutover missed two hardcoded copies of the legacy blob host — LatestVersionsService (feeds the portal's version badge) and ValidateBootstrapCodeFunction — which kept working only through the fail-soft legacy mirror in build-agent.yml; those two plus the agent SelfUpdater now use the download.autopilotmonitor.com alias (verified serving all four artifacts first). The portal's bootstrap allow-list accepts alias AND legacy host during the transition, which imposes a deploy order: web before (or with) backend. The sweep also surfaced a fourth copy of the www/portal host pair in middleware.ts (now derived from the registry via hostRouting.ts) and a backend short-link built on the bare apex domain, which only worked through a registrar-level 301.

2026-07-22

  • Creation: Added versioning.md — agent, backend and MCP now share one version shape (<major>.<minor>.<build> + 7-char commit + build timestamp) and one implementation of the build-number reservation (.github/scripts/Request-BuildNumber.ps1, previously copy-pasted between build-agent.yml and build.ps1). The backend had been pinned at a hand-written 1.0.0 since inception and now starts at 1.5.1000; the MCP server's package.json version was bumped by hand and now starts at 1.5.300, and it reports its own commit on /health instead of only the documentation bundle's. Both deploy workflows poll their health endpoint until it reports the build just deployed and publish versions/<component>.json only then, so a manifest that disagrees with the live endpoint means the instance drifted rather than the pipeline failing halfway. agent/build-counter-blob.md keeps the agent-specific parts and now points at the shared protocol; its counter URL was still the pre-cutover storage account.

  • Creation: Added rules/gather-rule-guardrails.md — the gather-rule allowlists are enforced only on the agent (the backend validates no collectorType, target, or parameters), so a collector that forgets to call its guard is the vulnerability. Written because two had: LogParserCollector read any path in rule.Target, bypassing even the C:\Users hard block that every other file collector honours, and EventLogCollector read any channel including Security — while the customer-facing documentation already promised both were allowlisted. Both now guard, with logparser checking the directory for wildcard targets (Path.GetFullPath throws on *) and re-checking each resolved path. Added an event-log channel allowlist (eventLogChannels) plus hard-blocked channels held in C# rather than JSON, so a parse error cannot lift them. Also fixed the command timeout, which was inert: ReadToEnd() blocks until the pipe closes, so WaitForExit(30000) was only reached after the process had already exited and a hung command pinned its worker forever — now read asynchronously and killed at the timeout.

  • Update: Added .github/workflows/ci.yml — the full suite (backend, agent V2 Core, DecisionCore, SummaryDialog, web, MCP) now runs on every pull request and every push to main, plus a job that fails if the generated guardrail files drift from rules/guardrails.json. Previously no workflow ran tests on a PR: the deploy workflows build without testing, and build-agent.yml tested only DecisionCore.Tests on manual dispatch — while trust/security-faq.md already told customers that automated tests run in CI. Added CodeQL (C# and TypeScript) and Dependabot (nuget, npm ×2, actions). Marked OfficeBinaryWatcherTests [Collection("SerialThreading")]: its bounded re-probe races the shared ThreadPool and failed once under full-suite load.

2026-07-21

  • Update: search_docs and search_knowledge now state in their tool descriptions that queries must be in English. Both corpora and all-MiniLM-L6-v2 are English-only, and the failure is total rather than gradual — a German question returns zero results, not weak ones. The caller is a language model, so translating the question before the call and answering in the user's language costs nothing; a multilingual embedding model would be the alternative but is much larger and would have to be measured against the eval harness first, since it changes English retrieval too.

  • Update: Added scripts/eval-docs-search.ts, an offline retrieval evaluation harness (2465 queries in three classes: chunk headings as a regression detector, rare identifiers, and hand-written questions as the reality anchor), and used it to close a third search_docs gap found during the live test. The keyword fallback only fired when semantic search returned fewer than topK; a pasted identifier like WOW6432Node instead returned three chunks at ~0.29 that did not contain the word while the one chunk that did was never considered. It now also fires on a weak semantic head (<0.35), letting exact matches take the front while keeping one semantic result. The threshold is measured, not guessed: 0.35 fixes 335 queries and breaks 0 (identifier recall 66.2% → 81.1%), while 0.40 fixes more but starts demoting hand-written questions. The measurement also resized the problem — identifier queries were only succeeding 66.2% of the time, so the reported case was one of roughly 750.

  • Update: Closed the two measured recall gaps in search_docs (mcp/docs-corpus.md). (1) Token windows — 87 of 324 chunks exceeded the model's 256-token context and 13% of all tokens never reached the ranking; a character budget cannot fix this (the corpus ranges 2.43–5.81 chars/token), so long text is now embedded as 250-token windows with 40 overlap and scored by its BEST window (embeddings: number[][], max not mean). Token coverage 87% → 100%; "proxy configuration blocking app downloads" went from absent to 0.559 at rank 1; the ten reference queries kept byte-identical scores. Costs +31% vectors, index 2.63 → 3.12 MB. The rules corpus gained the same treatment (128 rules → 172 vectors). (2) Keyword fallback — when the semantic pass returns fewer than topK, results are topped up via literal lexicalMatch on distinctive query terms, requiring two matching terms for corroboration (one incidental substring match returned a page about LLM providers for a sourdough query). Fuzzy matching was measured and rejected: at a threshold low enough to surface "RealmJoin" it invents 5 hits for "SCEPman", which the corpus never mentions. Also sharpened the tool description with the measured context argument for preferring it over fetching the docs site.

  • Creation: Added mcp/docs-corpus.md (new mcp/ section) — the MCP server gains search_docs, a third semantic corpus built from the published customer documentation repository (autopilotmonitor-docs, checked out into the Docker build context by the deploy workflow). Documents the h2/h3 + <details> chunking with breadcrumb prefixes, the two deliberate size exceptions (tables never split, unbroken changelog lists split at line level), the CRLF precondition (. does not match \r in JS — headings silently stop matching, which produced 286 wrong-but-plausible chunks before it was found), the optional docs section in search-index.json and its validation asymmetry, embedding rounding to 6 decimals, and the cross-repo coupling: a docs edit reaches clients only after an MCP redeploy, made checkable via DOCS_COMMIT on /health. Container resources raised to 0.5 vCPU / 1.0 GiB for the cold-start query-embedder load.

2026-07-20

  • Update: rules/gather-rule-phase-scoping.md — the gather-rule form now shows the phase-scope and emit-mode controls only where they can change behavior (supportsPhaseScope / supportsEmitMode), nulls hidden-control state out of the payload, and rejects a scope mode selected but left unfilled. Offering all three "when" controls unconditionally allowed combinations that could only suppress a one-shot rule, plus a silent no-op state.

  • Update: Extended rules/gather-rule-phase-scoping.md with the phase_exit trigger — the closing bookend to phase_change, for one-shot collection when a phase is LEFT (e.g. "state at the end of Account Setup"). Documents the evaluation-before-phase-advance rule (triggerPhase match and scope gate see the phase being left), firing into Failed, the disjoint exit: dedup key space, and why no ConfigVersion bump is needed (no new field — old agents simply never fire the rule).

  • Update: Reworked agent/mdm-reboot-coalescing.md after the first real enrollment (session b2e890c1): 2800 records are now aggregated per burst into ONE neutral Info event (rebootUris[]/uriCount/firstRebootUri; watermark persisted at flush so a pre-emit reboot kill loses nothing); the raw event no longer claims a reboot — ANALYZE-ESP-005 is gated on an actually-observed system_reboot_detected. Removed the Shell-Core esp_reboot_coalescing event entirely: the "RebootCoalescing" token appears in the routine SubcategoryProcessing_Started bootstrap marker on every enrollment (false positive by construction). Verified 2800 description shape recorded.

  • Creation: Added agent/mdm-reboot-coalescing.md — the agent now attributes the "unexpected mid-ESP reboot + second sign-in" pattern (PMPC research) to the device-assigned policy URIs that forced it: new MdmRebootPolicyTracker (DM-Enterprise EventID 2800, per-URI mdm_policy_reboot_required events, cross-restart RecordId watermark), a RebootCoalescing branch in ShellCoreTracker (esp_reboot_coalescing, backfill-emitting by design), and the ANALYZE-ESP-005 advisory (reassign device → user groups). ConfigVersion 35; event types deliberately distinct from system_reboot_detected so RebootCount stays uninflated.

  • Creation: Added backend/business-timestamps.md (new backend/ section) — the 2026-07-18 migration reset every row's system Timestamp, exposing that AuditLogs/OpsEvents/Events had (unknowingly) displayed and filtered on it all along; documents the OccurredUtc column, the RowKey decoders and RowKey-range date/retention clauses in BusinessTimestamp, the GUID-guard trap, the audit/ops backfill endpoint, and the "system Timestamp is never authoritative" migration rule.

  • Creation: Added rules/gather-rule-phase-scoping.md (new rules/ section) — gather rules gain activePhases (run only during listed phases), activeFromPhase (sticky latch from a phase onwards, never via Failed) and emitMode: "on_change" (canonical-hash result dedup with suppressedPolls observability), ConfigVersion 34. Includes the prerequisite backend fix: the custom-rule toggle PUT no longer wipes the stored rule definition (partial-merge in GatherRuleService.UpdateRuleAsync).

2026-07-18

  • Creation: Added agent/endpoint-migration.md — config-channel agent re-home (MigrateToApiBaseUrl on GET agent/config, ConfigVersion 33): backend or per-tenant API-URL moves without DNS indirection (impossible due to mTLS/Flex-TLS). Live-fetch-only with cache strip, AgentEndpointMigrationRules allowlist validated on both sides, one hop, kill-switch wins. Built as the lesson from the 2026-07-18 WEU→GWC cutover, which hard-cut all in-flight agents.

2026-07-26

  • Creation: Added backend/time-attribution.md — the F1 enrollment time attribution: exact-partition invariant against the session's authoritative DurationSeconds, two-window WhiteGlove model, segment mapping from phase-declaration events, positive-evidence ESP-blocking join with "up to" what-if bounds, reboot spans from event gaps around lastBootUtc, and the SessionTimeBreakdowns / TimeAttributionAggregates tables with their terminal-seam + maintenance-sweep compute paths and read surfaces (web lane, fleet panel, MCP get_time_attribution).

2026-07-17

  • Creation: Added agent/build-counter-blob.md — agent build numbers now come from a shared public counter blob (buildcounter-v{N}.txt, reserve-before-build with ETag-CAS) so local build.ps1 and CI build-agent.yml builds can never mint the same number. Documents the CI parity fixes (SummaryDialog.exe.config in the ZIP, EXE-vs-manifest version check, hash-oracle update gated on stable cutover) and the Sigstore provenance attestation on CI-built ZIPs.

2026-07-16

  • Creation: Added agent/autopilot-ztd-diagnostics.md — reference for Windows' own ZTD diagnostic surfaces (ModernDeployment Autopilot event IDs incl. 807/809/815/908, the Diagnostics\Autopilot registry key, deployment-service endpoints, known-issue error-code map with KB fixes). Backs the agent's ZtdEvidence collector (ztdVerdict on autopilot_profile_missing) and the backend known-issue rules; sources carry a re-check RSS feed.

2026-07-13

  • Removal: Moved customer-graph-add-on-permissions.md out of this contributor bundle into the customer documentation repository (reference/optional-graph-permissions.md). It was customer-facing product content and did not belong in the tech-docs bundle; removed the entry from index.md.

  • Creation: Added agent/hello-wizard-unskip.md — session 772fe502 fix: the HelloWizardStarted decision signal (prevention veto + un-skip cure for the policy-disabled Hello shortcut) and the HelloTracker confirmation second read against flip-flopping WHfB CSP values.

2026-07-11

  • Creation: Added the agent/ section — three concept documents on the V2 agent: agent/overview.md (runtime, collectors, telemetry pipeline), agent/decision-engine.md (DecisionCore reducer, signals, A/B/C completion arms, invariants), agent/logs-and-persistence.md (persisted artifacts, signal log vs spool, recovery/replay flow) plus agent/index.md.

  • Creation: Converted docs/ into an OKF v0.1 knowledge bundle — added YAML frontmatter to architecture.md, mcp-oauth-flow.md, and customer-graph-add-on-permissions.md; created index.md and this log.