diff --git a/docs/design/enrollment-status-reclassification-plan.md b/docs/design/enrollment-status-reclassification-plan.md deleted file mode 100644 index 604d1f8f..00000000 --- a/docs/design/enrollment-status-reclassification-plan.md +++ /dev/null @@ -1,140 +0,0 @@ -# Implementation Plan — Enrollment Status Reclassification - -> Working copy lives in `tasks/todo.md` (gitignored); this is the tracked, buildable checklist. - -Design note: [`docs/design/enrollment-status-reclassification.md`](./enrollment-status-reclassification.md) -Branch: `claude/crcins-enrollment-failures-ac943m` - -Goal: stop the 5h sweep from labelling silent-but-provisioned sessions `Failed`; add a -third terminal state `Incomplete/Unknown`; reconcile late completions to `Succeeded`. -Backend + stats only — no agent changes, no heartbeat. - ---- - -## PR1 — Status model + ESP rollup extraction (foundation) ✅ done - -- [x] Add `AwaitingUser` and `Incomplete` to `SessionStatus` enum - (`SessionApiModels.cs`, appended — existing ordinals unchanged). -- [x] Extend `FailureSnapshotBuilder` to embed the ESP rollup - (`deviceSetupAllSucceeded` / `accountSetup{SucceededCount,Total,AllSucceeded}`), - schema v2. Gated on the subcategory rollup, NOT `categorySucceeded`. -- [x] Pure `EnrollmentTimeoutClassifier.ExtractRollup` + `ClassifyTimedOutSession` - (new `EnrollmentTimeoutClassifier.cs`) implementing the decision table. -- [x] Unit tests `EnrollmentTimeoutClassifierTests` (12) — rollup extraction (0/5, 1–4/5, - 5/5, fallback, missing) + every classification branch. **Full Functions.Tests: 2670 pass.** - -## PR2 — Sweep uses the classifier + grace window ✅ done - -- [x] `TenantConfiguration.SessionGraceHours` (default 72) + default-config plumbing. -- [x] `MarkStalledSessionsAsTimedOutAsync` (Stage 2): replaced hard-coded `Failed` with - `EnrollmentTimeoutClassifier.ClassifyTimedOutSession(...)`. Fast-path skips within-grace - `AwaitingUser` sessions with NO event read (grace is time-based). Per-outcome counters + - audit/ops only when the pass did something. -- [x] Synthetic `session_timeout` event + analyze enqueue kept ONLY for `Failed`/`Incomplete` - terminal transitions (not `AwaitingUser`/`Succeeded`). -- [x] `GetStalledSessionsAsync` now includes `AwaitingUser` so grace→`Incomplete` fires later. - `GetAgentSilentSessionsAsync` left InProgress-only (no regress). -- [x] `UpdateSessionStatusAsync`: `Incomplete` treated as terminal (idempotency guard); - `AwaitingUser` never regresses a terminal; `CompletedAt` stamped for `Incomplete` (no - duration); reason + snapshot persisted for `Incomplete`/`AwaitingUser`. -- [x] Decision logic + grace boundary covered by `EnrollmentTimeoutClassifierTests` (pure — - the sweep itself has ~17 deps, same rationale as `MaintenanceServiceSessionTimeoutEventTests`). - **Full Functions.Tests: 2670 pass.** End-to-end validated via the live forward-only watch. - -## PR-EB — Agent emergency-break event (transparency: close the silent-48h blind spot) - -The agent's 48h absolute session-age break (`Program.Guards.CheckSessionAgeEmergencyBreak`) -writes only a LOCAL marker + cleans up + exits — silent to the backend. Make it visible in the -timeline AND let the backend terminalize precisely instead of guessing with grace. - -- [x] `Constants.EventTypes.AgentEmergencyBreak` + `AgentErrorType.SessionAgeEmergencyBreak`. -- [x] Classifier consumes it: `ExtractRollup.HasAgentEmergencyBreak`; a session carrying the break - classifies as `Incomplete` NOW (skips the AwaitingUser grace) unless it actually completed. - Tested. **This is the load-bearing backend consumer.** -- [x] **Part B (backend): materialize the timeline event.** `ReportAgentErrorFunction` now injects - `ISessionRepository` and, on `SessionAgeEmergencyBreak`, writes an `agent_emergency_break` - `EnrollmentEvent` (Sequence = max+1, idempotent, best-effort) into the stream. Pure - `BuildAgentEmergencyBreakEvent` helper (Warning, non-terminal) + tests. Suite: 2685 pass. -- [x] **Part C (agent, net48 — CI/local build only): best-effort emit.** Implemented via a decoupled - callback so the guard stays pure and its unit tests are untouched: - - `CheckSessionAgeEmergencyBreak` gains an optional `Action onBreakFired = null`, invoked once when - the break is confirmed, BEFORE the marker/cleanup, fully swallowed. - - New `EmergencyBreakReporter.TrySend` builds a throwaway auth bundle (`BackendClientFactory`) and - fires a `SessionAgeEmergencyBreak` `AgentErrorReport` over the resilient emergency channel, bounded - to a 5s wait. `AgentBootstrap` wires it (`GetAgentVersion` made `internal`). - - Lost if the device is fully offline — acceptable (that case falls back to the grace window). - ⚠️ **Not compiled here** (agent targets net48; this container has only the .NET 8 SDK). The Shared - `AgentErrorType.SessionAgeEmergencyBreak` value IS verified (it compiled in the Functions build). - **@okieselbach to run the net48 agent build locally.** - -## PR3 — Reconciliation of late completions ✅ done - -- [x] `UpdateSessionStatusAsync` terminal-guard replaced by pure `IsTerminalTransitionAllowed`: - a genuine completion (`Succeeded`) UPGRADES a prior `Failed`/`Incomplete`/`AwaitingUser` - verdict (late reconcile); silent-terminal verdicts + `AwaitingUser` never overwrite a terminal. -- [x] On `Succeeded`, clear stale `FailureReason` + `FailureSnapshotJson` (reconcile hygiene). -- [x] Admin-marked terminal decisions are preserved — a late completion does NOT auto-override a - hand-marked `Failed`/`Incomplete`. -- [x] Ingest already routes late `enrollment_complete`/`gather` → `Succeeded` with no pre-terminal - skip, so the relaxed guard makes reconcile work end-to-end (no ingest change needed). -- [x] Tests `SessionStatusTransitionTests` (20, full matrix). Suite: 2705 pass (one pre-existing - timing-flaky maintenance test, green on retry/isolation — unrelated). - -## PR4 — Stats / metrics expose 3 states ✅ done (Fleet Health deferred to PR5) - -- [x] `SessionStatusBuckets` (`MetricsMath.cs`): explicit `AwaitingUser` + `Incomplete` buckets - (out of `Other`); buckets still reconcile to Total by construction. -- [x] Metrics summary (`GetMetricsSummaryAsync`): surface `awaitingUser` + `incomplete`; **failure - rate now `Failed / (Succeeded + Failed)`** — Incomplete + non-terminal excluded from the denominator. -- [x] `SessionStats` (`SessionApiModels.cs`): add `IncompleteLastNDays`; dashboard `SuccessRatePct` - was already terminal-only (`Succeeded/(Succeeded+Failed)`), so Incomplete is excluded there too. -- [x] Daily aggregation (`ComputeUsageMetricsSnapshotAsync`) already uses `Succeeded+Failed` as the - denominator — no change needed (Incomplete already excluded). -- [x] Tests `SessionStatusBucketsTests` (bucketing + terminal-only failure-rate). Suite: 2705 pass. -- [x] Fleet Health payload Incomplete count — done in PR5 (`FleetHealthStats.Incomplete` + slate card). - Its SuccessRate stays "over all sessions" (documented), only the Incomplete count was added. - -## PR5 — Web surface ✅ done (typecheck clean) - -- [x] `utils/sessionStatus.ts`: `isTerminalStatus` includes `Incomplete`; docstring notes AwaitingUser non-terminal. -- [x] `SessionStatusBadge.tsx`: `Incomplete` = neutral slate (clearly not red), `AwaitingUser` = sky/blue - ("still going"); ⏱️ affordance extended to Incomplete (the silence family). -- [x] Fleet Health: `FleetHealthStats.Incomplete` (C# model + `MetricsMath.BuildFleetHealthPayload`) + - web interface + a dedicated slate "Incomplete" stat card (`FleetStatCard` gained a `slate` color); - grid widened to 5. -- [x] Main dashboard KPI row left as-is (its Success Rate is already the honest terminal rate); the third - state is surfaced via the badge everywhere, the Fleet Health card, and the metrics summary `incomplete`. -- [x] Verified: `tsc --noEmit` clean; backend suite 2709 pass. - -## PR6 — Backfill (optional, one-shot) - -- [ ] Manual maintenance path to re-classify historical `Failed` 5h-timeouts using the same - classifier, so existing stats (crcins.com + platform) heal retroactively. -- [ ] Dry-run counters logged before any write; gated behind an explicit admin trigger. - ---- - -## Verification -- [ ] `dotnet test` green across Functions.Tests + Shared/DecisionCore tests. -- [ ] Re-run the crcins.com window (via MCP `query_raw_events` / `search_sessions`) and confirm - the "after" table in the design note within rounding (Failed ~122, Incomplete ~115, - +25 reconciled, failure rate ~4–5 %). -- [ ] Confirm no `AwaitingUser` session ever emits a `session_timeout`/analyze `Failed` artifact. - -## Review (fill in after implementation) -- _pending_ - -## Decisions (2026-07-08) -- **Third state label:** `Incomplete` (enum member `SessionStatus.Incomplete`, badge "Incomplete"). -- **Scope:** forward-only first (PR1–PR5) to watch how the first live sessions classify. - **PR6 backfill is deferred** — run it once forward-only looks good, to heal historical - crcins.com + platform stats. Remembered follow-up. -- **`SessionGraceHours`:** now **auto-derived** from the agent's absolute cap, not a magic 72h. - `effectiveGrace = max(AbsoluteMaxSessionHours + 3h buffer, override)` = **51h** by default (48 + 3) - (`EnrollmentTimeoutClassifier.ResolveGraceHours`). Rationale: the 48h agent emergency break is - *silent* to the backend, so grace must be ≥ that cap and only slightly beyond it. `SessionGraceHours=0` - means auto; an override can only raise the floor. `AbsoluteMaxSessionHours` mirrored into TenantConfiguration, - but until the agent honors it (follow-up below) the derivation clamps the assumed cap to ≥ the real agent - default (48), so a lower override can never drag the grace below the agent's actual runtime. - - **Follow-up:** wire `TenantConfiguration.AbsoluteMaxSessionHours` down into `AgentConfigResponse` + - RemoteConfigMerger so a tenant override reaches the agent too (today the agent still uses its own default 48). diff --git a/docs/design/enrollment-status-reclassification.md b/docs/design/enrollment-status-reclassification.md deleted file mode 100644 index 3ca6b52f..00000000 --- a/docs/design/enrollment-status-reclassification.md +++ /dev/null @@ -1,236 +0,0 @@ -# Enrollment Status Reclassification — Timeout ≠ Failure - -**Status:** Proposal · **Owner:** Backend · **Trigger tenant:** crcins.com (`ca9e3c59-d8ec-4ee6-9544-05f62f85ac98`) - -## Problem - -The 5‑hour maintenance sweep marks every silent, non‑terminal session as **`Failed`** -([`MaintenanceService.MarkStalledSessionsAsTimedOutAsync`](../../src/Backend/AutopilotMonitor.Functions/Services/MaintenanceService.cs) — Stage 2 hard‑codes `SessionStatus.Failed`). This conflates -*"the agent stopped sending telemetry"* with *"the enrollment failed"* and produces a -massively inflated failure rate. - -For crcins.com this is dramatic, but the mechanism is platform‑wide. - -## Evidence (crcins.com, 90‑day window) - -Reported failure rate **57.2 %** (1321 / 2310). Measured over the full failed population -(n = 1311) via `query_raw_events(eventType=esp_provisioning_status)` + `search_sessions`: - -| Failure reason | Count | Share | -|---|---:|---:| -| **`Session timed out after 5 hours`** (no terminal event) | **1189** | **90.7 %** | -| ESP `WhiteGlove_Failed` | 37 | 2.8 % | -| Agent max‑lifetime | 35 | 2.7 % | -| `esp_terminal_failure` | 27 | 2.1 % | -| `Autopilot enrollment failed: Provisioning…` | 19 | 1.4 % | -| `esp_exit_without_completion` | 4 | 0.3 % | - -**Explicit, real failures ≈ 87 → true failure rate ≈ 3.8 % (max ~5.3 %), not 57.2 %.** - -### What the "5h timeouts" actually were - -Last observed ESP state across the 1189 timeouts (1077 with captured -`esp_provisioning_status`, 90.6 % coverage): - -| Last observed state | Count | Verdict | -|---|---:|---| -| **DeviceSetup 4/4 = fully provisioned (AADJ+MDM)** | **1067 / 1077 (99.1 %)** | device is enrolled | -| AccountSetup `0/5` — user phase never observed | 1024 (95.1 %) | **AwaitingUser** — not a failure | -| AccountSetup `1–4/5` — mid user phase, agent gone | 25 (2.3 %) | **AwaitingUser** | -| AccountSetup `5/5` — complete | 25 (2.3 %) | **reconcile → Succeeded** | -| no `esp_provisioning_status` at all | 112 (9.4 %) | **Incomplete/Unknown** | - -Not a single 5h‑timeout carries an explicit `enrollment_failed` / `esp_failure` event. - -### Two key data facts (drive the design) - -1. **`categorySucceeded` is unreliable.** In every `esp_provisioning_status` payload — - even at DeviceSetup 4/4 and AccountSetup 5/5 — Windows reports - `categorySucceeded = "in_progress"`. The agent already compensates with a 30 s - "all subcategories succeeded → treat complete" fallback for DeviceSetup - ([`ProvisioningStatusTracker` / `EspAndHelloTracker`](../../src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Enrollment/SystemSignals/)). - → **Gate on the subcategory rollup, never on `categorySucceeded`.** -2. **Desktop arrival ≠ complete.** In User‑driven ESP the desktop appears while the - Account/user phase is still running. `desktop_arrived` is a hint, not a terminal. - The authoritative terminal is **AccountSetup subcategory rollup = all succeeded** - (which precedes the agent's own `enrollment_complete`). - -## Root cause - -The agent captures exactly the right signal (`esp_provisioning_status` with per‑category -subcategory rollups incl. AccountSetup), but: - -- In ~95 % of timeouts the agent goes dark at the DeviceSetup→AccountSetup reboot, - **before** the user logs in, so AccountSetup never progresses in telemetry (0/5). The - user phase legitimately completes hours/days later (proven by session - `cfa820da-…`: AccountSetup 5/5 + `enrollment_complete` the **next day**). -- The backend's 5h sweep fires first, hard‑labels `Failed`, and **never reconciles** when - the late completion arrives. - -So the fix is **classification + reconciliation in the backend** — no new agent load, -no heartbeat. The signals are already in the event stream. - -## Design - -### 1. Status model — a third terminal bucket - -`SessionStatus` ([`SessionApiModels.cs:426`](../../src/Shared/AutopilotMonitor.Shared/Models/SessionApiModels.cs)) -today: `InProgress, Pending, Stalled, Succeeded, Failed, Unknown`. - -Add: - -- **`AwaitingUser`** (non‑terminal): DeviceSetup succeeded, user/account phase not yet - complete, agent silent but within grace. Can heal to `Succeeded` or expire to `Incomplete`. -- **`Incomplete`** (terminal, **non‑failure**): grace expired with no terminal evidence, - or silence before DeviceSetup completed with no explicit failure. This is the - "success/failed/**incomplete‑unknown**" third state for the stats. (Existing `Unknown` - is folded into the Incomplete bucket for reporting.) - -### 2. Classification at the sweep (deterministic, from data we already load) - -The sweep already loads up to 1000 events and builds a snapshot -([`FailureSnapshotBuilder`](../../src/Backend/AutopilotMonitor.Functions/Services/FailureSnapshotBuilder.cs)). -Extend the snapshot to extract the **last DeviceSetup and AccountSetup subcategory rollups** -from `esp_provisioning_status` events, then decide the target status instead of hard‑coding -`Failed`: - -``` -Given a non-terminal session past the inactivity window: - -1. Explicit enrollment_failed / esp_failure present → Failed (real) -2. AccountSetup rollup = ALL subcategories succeeded - OR enrollment_complete / whiteglove_complete present → Succeeded (reconcile) -3. agent_emergency_break present (agent provably gone) → Incomplete (skip grace) -4. desktop_arrived AND ( positive Hello terminal - (hello_provisioning_completed / hello_skipped) present - OR hello policy disabled AND User ESP skipped ) → Succeeded (reconcile) -5. DeviceSetup rollup = ALL subcategories succeeded (or 30s - fallback) AND no failure event: - a. within grace (now - StartedAt < SessionGraceHours) → AwaitingUser (non-terminal) - b. grace expired → Incomplete (terminal, non-failure) -6. else (silence before DeviceSetup complete, no failure) → Incomplete -``` - -Step 4 (added 2026-07-10, trigger session `294ab5b4`): desktop arrival **plus** a positive -Hello terminal is exactly the agent's own Classic completion AND‑gate — once both are in, -the only thing that can still block the agent's `enrollment_complete` is the RealmJoin -completion gate, which self‑releases via a 60‑min hard timeout. A session silent long -enough to reach the sweep is past every agent‑side wait: the enrollment succeeded and only -the completion report never left the device (shutdown / process kill / egress cut). -Labeling it `AwaitingUser` is factually wrong — the user was provably there. The reason -string distinguishes "RealmJoin deployment never reported completion" (RJ detected but -unresolved) from the generic "agent went silent before reporting completion". The failure -snapshot (schema v3) carries the evidence: `helloResolved`, `skipUserEsp`, -`realmJoinDetected`, `realmJoinResolved`. - -The Hello‑disabled branch mirrors the agent's Hello‑disabled fast‑path exactly: with -`hello_policy_detected(helloEnabled=false)` **and** `esp_config_detected(skipUserStatusPage=true)` -no Hello terminal can ever exist, and the agent completes on desktop arrival in that -configuration. Hello disabled **without** SkipUserEsp does NOT qualify — the agent's strong -post‑AccountSetup gate (session `08c99638`) requires the AccountSetup rollup there, which -step 2 already covers. Note `esp_exiting` (Shell‑Core 62407) is deliberately NOT used as -success evidence anywhere in this table: it is a page‑lifecycle event that fires with -`errorCode=0` even while AccountSetup apps are unfinished (the very lesson of `08c99638`), -unlike the Hello events, which are outcome terminals. - -Rationale for each bucket sizing is in the Evidence table above. -⚠️ Gate step 2/3 on the **subcategory rollup**, not `categorySucceeded`. - -### 3. Grace window (backend owns the wait — zero client cost) - -Two‑stage terminalization in the same sweep that already runs: - -- At `SessionTimeoutHours` (5h): non‑terminal → **`AwaitingUser`** (was `Failed`). -- At the grace window: `AwaitingUser` → **`Incomplete`** (unless already reconciled). - -**The grace is anchored to the agent, not a magic number.** The agent has two brakes: -a **6h per‑run** max‑lifetime (`AgentMaxLifetimeMinutes`, re‑armed each reboot) that emits an -explicit `enrollment_failed(max_lifetime)` → the session is already `Failed` at ~6h; and a -**48h absolute session‑age emergency break** (`AbsoluteMaxSessionHours`, cumulative across -reboots, skipped while WhiteGlove‑paused) that is **silent to the backend** — it writes a local -marker, cleans up and exits without any terminal event. So a session only stays *non‑terminal* -past 48h precisely when the agent hit that silent break and is now provably gone. - -Therefore the grace must be **≥ the agent's absolute cap** (never terminalize `Incomplete` -while the agent could still legitimately be enrolling) and only slightly beyond it (once the -cap has fired + last spooled telemetry has had time to land, silence = dead): - -``` -effectiveGrace = max( AbsoluteMaxSessionHours + bufferHours , SessionGraceHours override ) - = max( 48 + 3 , 0 ) = 51h (defaults) -``` - -The buffer is deliberately **small (3h)**, not a long straggler‑wait: a completion can only ever -arrive ≤ the cap (the agent self‑destructs at the cap and sends nothing after), so the buffer only -has to span one 2h maintenance‑sweep cycle + absorb minor clock skew. The reconcile path heals any -prematurely‑set `Incomplete` back to `Succeeded`, so a tight buffer costs at most a brief flicker. - -`SessionGraceHours` defaults to `0` (auto‑derive) and can only *raise* the grace above the -floor, never below it (`EnrollmentTimeoutClassifier.ResolveGraceHours`). `AbsoluteMaxSessionHours` -is mirrored into `TenantConfiguration` so the floor follows any tenant override of the agent cap. - -**Agent emergency‑break signal (closes the blind spot).** The 48h break is otherwise silent to the -backend. The agent best‑effort sends an emergency‑break report (resilient channel — may be lost if -the device is fully offline); the backend materializes an `agent_emergency_break` timeline event -from it and treats it as a definitive "agent is gone" signal → terminalize **now** by the ESP rollup -(Account Setup all‑succeeded → `Succeeded`, else `Incomplete`), skipping the grace wait. With this -signal the grace/buffer only governs the *truly silent* death (bluescreen / power‑cut with no -network), where no event can ever be sent. - -The waiting session is just a table row compared against a timestamp — no process, no -telemetry, no agent change. - -### 4. Reconciliation on late events - -When a session in `Failed` / `Incomplete` / `AwaitingUser` later receives a genuine terminal -signal (`enrollment_complete`, `whiteglove_complete`, or AccountSetup rollup = all -succeeded), flip to **`Succeeded`** -([`EventIngestProcessor.Classification.cs`](../../src/Backend/AutopilotMonitor.Functions/Services/EventIngestProcessor.Classification.cs) -already routes `enrollment_complete` → `Succeeded`; the `UpdateSessionStatusAsync` guard must -permit an already‑terminal → `Succeeded` upgrade for these reconcile signals only). - -### 5. Stats / metrics — expose 3 states, exclude Incomplete from failure rate - -- `SessionStatusBuckets` ([`MetricsMath.cs:317`](../../src/Backend/AutopilotMonitor.Functions/Helpers/MetricsMath.cs)) - gains explicit `AwaitingUser` and `Incomplete` buckets (they currently fall into `Other`). -- `SessionStats` / Fleet Health: add `IncompleteLastNDays`; **failure rate denominator = - `Succeeded + Failed` only** (Incomplete and AwaitingUser excluded). Headline becomes - Success / Failed / Incomplete‑Unknown. -- Same `Incomplete` bucket added to the other reporting surfaces that read session status: - the live usage metrics (`UsageMetricsService.SessionMetrics`, both platform + tenant paths, - feeding `get_usage_metrics` and the usage dashboards) and the persisted daily - `UsageMetricsSnapshot` (`SessionsIncomplete`, stored + mapped in `TableStorageService.Metrics`). - Their success rate already used the `Succeeded + Failed` denominator; only the count bucket - was missing, so terminal Incomplete sessions were silently absent from the breakdown. -- Web: `SessionStatus` mirror + badge + `isTerminalStatus` - ([`utils/sessionStatus.ts`](../../src/Web/autopilot-monitor-web/utils/sessionStatus.ts), - [`SessionStatusBadge.tsx`](../../src/Web/autopilot-monitor-web/components/SessionStatusBadge.tsx)). - -## Expected impact (crcins.com, from the measured buckets) - -| | today | after | -|---|---:|---:| -| Succeeded | 599 | ~624 (+25 reconciled) | -| Failed | 1311 | ~122 (explicit failures + max‑lifetime) | -| AwaitingUser (new, transient) | – | ~1052 | -| Incomplete/Unknown (new) | – | ~115 | -| **Failure rate** | **57.2 %** | **~4–5 %** | - -Platform‑wide the same reclassification de‑noises every tenant's failure rate. - -## Non‑goals / explicitly rejected - -- **Agent heartbeat** — rejected: continuous client load + zombie risk; the completion - truth is already persisted (registry/ESP tracking survives reboots) and the wait belongs - in the backend. -- **`desktop_arrived` ALONE as completion** — rejected: fires while the user phase is still - running. (`desktop_arrived` **+ a positive Hello terminal** is accepted since 2026-07-10 — - that pair is the agent's own Classic completion AND‑gate, see classification step 4.) - -## Acceptance criteria - -- No non‑terminal session is marked `Failed` by the sweep without an explicit failure event. -- A session reaching AccountSetup all‑succeeded (or `enrollment_complete`) after a prior - sweep ends as `Succeeded`. -- `Failed` count only contains explicit‑failure sessions; failure‑rate excludes Incomplete. -- Reprocessing the crcins.com window reproduces the "after" table within rounding. diff --git a/docs/design/mcp-oauth-flow.md b/docs/mcp-oauth-flow.md similarity index 100% rename from docs/design/mcp-oauth-flow.md rename to docs/mcp-oauth-flow.md diff --git a/src/Agent/AutopilotMonitor.Agent.V2/Program.Guards.cs b/src/Agent/AutopilotMonitor.Agent.V2/Program.Guards.cs index a08dea2b..4f0ac68b 100644 --- a/src/Agent/AutopilotMonitor.Agent.V2/Program.Guards.cs +++ b/src/Agent/AutopilotMonitor.Agent.V2/Program.Guards.cs @@ -290,7 +290,7 @@ namespace AutopilotMonitor.Agent.V2 // Best-effort: tell the backend the agent is emergency-breaking so the otherwise-silent // 48h absolute cap is no longer a blind spot in the timeline - // (docs/design/enrollment-status-reclassification.md). Fired BEFORE cleanup, while the + // (tasks/enrollment-status-reclassification.md). Fired BEFORE cleanup, while the // session state and network are still intact. Never throws — a send failure (e.g. no // network) must not block the cleanup/exit that is the whole point of this guard. try { onBreakFired?.Invoke(); } diff --git a/src/Agent/AutopilotMonitor.Agent.V2/Program.cs b/src/Agent/AutopilotMonitor.Agent.V2/Program.cs index d0055676..a909c59e 100644 --- a/src/Agent/AutopilotMonitor.Agent.V2/Program.cs +++ b/src/Agent/AutopilotMonitor.Agent.V2/Program.cs @@ -464,7 +464,7 @@ namespace AutopilotMonitor.Agent.V2 } // internal (not private) so AgentBootstrap can stamp the best-effort emergency-break report - // with the running agent version (docs/design/enrollment-status-reclassification.md). + // with the running agent version (tasks/enrollment-status-reclassification.md). internal static string GetAgentVersion() { try diff --git a/src/Agent/AutopilotMonitor.Agent.V2/Runtime/AgentBootstrap.cs b/src/Agent/AutopilotMonitor.Agent.V2/Runtime/AgentBootstrap.cs index 1e4adbce..546060a5 100644 --- a/src/Agent/AutopilotMonitor.Agent.V2/Runtime/AgentBootstrap.cs +++ b/src/Agent/AutopilotMonitor.Agent.V2/Runtime/AgentBootstrap.cs @@ -85,7 +85,7 @@ namespace AutopilotMonitor.Agent.V2.Runtime agentConfig.AbsoluteMaxSessionHours, agentConfig.SelfDestructOnComplete, cleanupServiceFactory, logger, consoleMode, // Best-effort: surface the otherwise-silent 48h break to the backend before cleanup - // (docs/design/enrollment-status-reclassification.md). Never blocks the exit. + // (tasks/enrollment-status-reclassification.md). Never blocks the exit. onBreakFired: () => EmergencyBreakReporter.TrySend(agentConfig, Program.GetAgentVersion(), logger))) { logger.Info("Emergency break fired — agent exiting."); diff --git a/src/Agent/AutopilotMonitor.Agent.V2/Runtime/EmergencyBreakReporter.cs b/src/Agent/AutopilotMonitor.Agent.V2/Runtime/EmergencyBreakReporter.cs index 8309bae2..9db297dc 100644 --- a/src/Agent/AutopilotMonitor.Agent.V2/Runtime/EmergencyBreakReporter.cs +++ b/src/Agent/AutopilotMonitor.Agent.V2/Runtime/EmergencyBreakReporter.cs @@ -14,7 +14,7 @@ namespace AutopilotMonitor.Agent.V2.Runtime /// (ReportAgentErrorFunction), closing the silent-48h blind spot and letting the timeout /// classifier terminalize the session instead of waiting out the grace. Fully swallowed — a send /// failure (no network, no cert) must never delay the cleanup/exit the break exists to perform. - /// See docs/design/enrollment-status-reclassification.md. + /// See tasks/enrollment-status-reclassification.md. /// internal static class EmergencyBreakReporter { diff --git a/src/Backend/AutopilotMonitor.Functions.Tests/EnrollmentTimeoutClassifierTests.cs b/src/Backend/AutopilotMonitor.Functions.Tests/EnrollmentTimeoutClassifierTests.cs index f89233c4..19c6ad57 100644 --- a/src/Backend/AutopilotMonitor.Functions.Tests/EnrollmentTimeoutClassifierTests.cs +++ b/src/Backend/AutopilotMonitor.Functions.Tests/EnrollmentTimeoutClassifierTests.cs @@ -6,7 +6,7 @@ using AutopilotMonitor.Shared.Models; namespace AutopilotMonitor.Functions.Tests; /// -/// Tests for the timeout reclassification (docs/design/enrollment-status-reclassification.md). +/// Tests for the timeout reclassification (tasks/enrollment-status-reclassification.md). /// The maintenance sweep must stop labelling every silent session Failed: a session whose /// Account Setup rollup reached all-succeeded reconciles to Succeeded; one that finished /// Device Setup but whose user phase never completed is AwaitingUser (within grace) then diff --git a/src/Backend/AutopilotMonitor.Functions.Tests/ReportAgentErrorFunctionTests.cs b/src/Backend/AutopilotMonitor.Functions.Tests/ReportAgentErrorFunctionTests.cs index 641a32b4..3cf2d963 100644 --- a/src/Backend/AutopilotMonitor.Functions.Tests/ReportAgentErrorFunctionTests.cs +++ b/src/Backend/AutopilotMonitor.Functions.Tests/ReportAgentErrorFunctionTests.cs @@ -8,7 +8,7 @@ namespace AutopilotMonitor.Functions.Tests; /// /// Pins — the backend-materialized /// timeline event synthesized from the agent's best-effort 48h emergency-break report -/// (docs/design/enrollment-status-reclassification.md). The load-bearing bits are the Sequence assignment +/// (tasks/enrollment-status-reclassification.md). The load-bearing bits are the Sequence assignment /// (must sort AFTER the session's last event) and that it is a Warning, non-terminal marker — the timeout /// classifier, not this event, decides the real verdict. /// diff --git a/src/Backend/AutopilotMonitor.Functions.Tests/SessionStatusBucketsTests.cs b/src/Backend/AutopilotMonitor.Functions.Tests/SessionStatusBucketsTests.cs index 42bcaeca..46cc60ec 100644 --- a/src/Backend/AutopilotMonitor.Functions.Tests/SessionStatusBucketsTests.cs +++ b/src/Backend/AutopilotMonitor.Functions.Tests/SessionStatusBucketsTests.cs @@ -3,7 +3,7 @@ using AutopilotMonitor.Functions.Helpers; namespace AutopilotMonitor.Functions.Tests; /// -/// Pins the third-state stats bucketing (docs/design/enrollment-status-reclassification.md): +/// Pins the third-state stats bucketing (tasks/enrollment-status-reclassification.md): /// AwaitingUser and Incomplete get their own buckets instead of hiding in Other, and the buckets /// always reconcile to Total by construction — so a session in either state can never be silently /// miscounted as a failure. diff --git a/src/Backend/AutopilotMonitor.Functions.Tests/SessionStatusTransitionTests.cs b/src/Backend/AutopilotMonitor.Functions.Tests/SessionStatusTransitionTests.cs index 03302d16..59ad51c8 100644 --- a/src/Backend/AutopilotMonitor.Functions.Tests/SessionStatusTransitionTests.cs +++ b/src/Backend/AutopilotMonitor.Functions.Tests/SessionStatusTransitionTests.cs @@ -4,7 +4,7 @@ using AutopilotMonitor.Shared.Models; namespace AutopilotMonitor.Functions.Tests; /// -/// Pins the terminal/reconcile transition matrix (docs/design/enrollment-status-reclassification.md): +/// Pins the terminal/reconcile transition matrix (tasks/enrollment-status-reclassification.md): /// a genuine completion may upgrade a prior Failed/Incomplete/AwaitingUser verdict (late reconcile), /// while the silent-terminal verdicts never overwrite an existing terminal. This is the pure core of /// PR3 — the reconcile that lets a device which completed after a sweep-timeout heal to Succeeded. diff --git a/src/Backend/AutopilotMonitor.Functions.Tests/UsageMetricsServiceWindowTests.cs b/src/Backend/AutopilotMonitor.Functions.Tests/UsageMetricsServiceWindowTests.cs index 4423a373..c6507b63 100644 --- a/src/Backend/AutopilotMonitor.Functions.Tests/UsageMetricsServiceWindowTests.cs +++ b/src/Backend/AutopilotMonitor.Functions.Tests/UsageMetricsServiceWindowTests.cs @@ -118,7 +118,7 @@ public class UsageMetricsServiceWindowTests [Fact] public async Task Incomplete_sessions_get_their_own_bucket_and_are_excluded_from_success_rate() { - // The reclassification shape (docs/design/enrollment-status-reclassification.md): timed-out + // The reclassification shape (tasks/enrollment-status-reclassification.md): timed-out // sessions become terminal Incomplete, not Failed. They must be counted in their own bucket // (previously invisible: in Total but no breakdown) and kept out of the failure-rate // denominator — Succeeded + Failed only. 1 succeeded, 1 failed, 8 incomplete -> 50%, not 10%. diff --git a/src/Backend/AutopilotMonitor.Functions/Functions/Ingest/ReportAgentErrorFunction.cs b/src/Backend/AutopilotMonitor.Functions/Functions/Ingest/ReportAgentErrorFunction.cs index 83a8a9a0..20141749 100644 --- a/src/Backend/AutopilotMonitor.Functions/Functions/Ingest/ReportAgentErrorFunction.cs +++ b/src/Backend/AutopilotMonitor.Functions/Functions/Ingest/ReportAgentErrorFunction.cs @@ -170,7 +170,7 @@ namespace AutopilotMonitor.Functions.Functions.Ingest }); // Materialize the agent's silent 48h emergency break as a timeline event so it shows in the - // session and the timeout classifier can see it (docs/design/enrollment-status-reclassification.md). + // session and the timeout classifier can see it (tasks/enrollment-status-reclassification.md). // Best-effort — a failure here must never turn the always-200 emergency channel into a retry loop. if (report.ErrorType == AgentErrorType.SessionAgeEmergencyBreak && !string.IsNullOrEmpty(report.SessionId)) diff --git a/src/Backend/AutopilotMonitor.Functions/Services/EnrollmentTimeoutClassifier.cs b/src/Backend/AutopilotMonitor.Functions/Services/EnrollmentTimeoutClassifier.cs index 499e1c39..c3161e68 100644 --- a/src/Backend/AutopilotMonitor.Functions/Services/EnrollmentTimeoutClassifier.cs +++ b/src/Backend/AutopilotMonitor.Functions/Services/EnrollmentTimeoutClassifier.cs @@ -7,7 +7,7 @@ namespace AutopilotMonitor.Functions.Services { /// /// Pure classification of a session that the maintenance sweep is about to terminalize - /// (docs/design/enrollment-status-reclassification.md). Instead of hard-coding every + /// (tasks/enrollment-status-reclassification.md). Instead of hard-coding every /// silent session to , we read the ESP subcategory /// rollup the agent already emits and decide the honest target state. /// diff --git a/src/Backend/AutopilotMonitor.Functions/Services/FailureSnapshotBuilder.cs b/src/Backend/AutopilotMonitor.Functions/Services/FailureSnapshotBuilder.cs index 64fb29af..228448cf 100644 --- a/src/Backend/AutopilotMonitor.Functions/Services/FailureSnapshotBuilder.cs +++ b/src/Backend/AutopilotMonitor.Functions/Services/FailureSnapshotBuilder.cs @@ -26,7 +26,7 @@ namespace AutopilotMonitor.Functions.Services { // v2 (2026-07-08): added the ESP DeviceSetup/AccountSetup subcategory rollup fields // (deviceSetupAllSucceeded / accountSetup*) that the timeout reclassification uses — - // see docs/design/enrollment-status-reclassification.md. + // see tasks/enrollment-status-reclassification.md. // v3 (2026-07-10): added helloResolved + realmJoinDetected/realmJoinResolved + // skipUserEsp — the evidence behind the "user completed setup" reconcile rule // (session 294ab5b4: user reached desktop + finished Hello, only the RealmJoin gate diff --git a/src/Backend/AutopilotMonitor.Functions/Services/MaintenanceService.cs b/src/Backend/AutopilotMonitor.Functions/Services/MaintenanceService.cs index f00acae7..8048cf4f 100644 --- a/src/Backend/AutopilotMonitor.Functions/Services/MaintenanceService.cs +++ b/src/Backend/AutopilotMonitor.Functions/Services/MaintenanceService.cs @@ -367,7 +367,7 @@ namespace AutopilotMonitor.Functions.Services // Timeout ≠ failure. Classify from the ESP subcategory rollup the agent already // emits instead of blindly failing every silent session - // (docs/design/enrollment-status-reclassification.md): explicit failure → Failed; + // (tasks/enrollment-status-reclassification.md): explicit failure → Failed; // Account Setup all-succeeded / enrollment_complete → Succeeded (reconcile); // desktop + positive Hello terminal observed → Succeeded (user provably finished // setup; only the completion report never left the device — session 294ab5b4); diff --git a/src/Backend/AutopilotMonitor.Functions/Services/TableStorageService.Maintenance.cs b/src/Backend/AutopilotMonitor.Functions/Services/TableStorageService.Maintenance.cs index 16ff3a45..30a5fa30 100644 --- a/src/Backend/AutopilotMonitor.Functions/Services/TableStorageService.Maintenance.cs +++ b/src/Backend/AutopilotMonitor.Functions/Services/TableStorageService.Maintenance.cs @@ -564,7 +564,7 @@ namespace AutopilotMonitor.Functions.Services // At the 5h mark the session is reclassified rather than blindly failed — see // EnrollmentTimeoutClassifier. AwaitingUser is included too so it keeps being // re-evaluated each pass and graduates to Incomplete once SessionGraceHours elapses - // (docs/design/enrollment-status-reclassification.md). + // (tasks/enrollment-status-reclassification.md). // IsPreProvisioned ne true → WhiteGlove sessions are intentionally long-lived and // must never be timed out, even after months in storage. var filter = $"PartitionKey eq '{tenantId}' " + diff --git a/src/Backend/AutopilotMonitor.Functions/Services/TableStorageService.Sessions.cs b/src/Backend/AutopilotMonitor.Functions/Services/TableStorageService.Sessions.cs index c3898db5..a271d26b 100644 --- a/src/Backend/AutopilotMonitor.Functions/Services/TableStorageService.Sessions.cs +++ b/src/Backend/AutopilotMonitor.Functions/Services/TableStorageService.Sessions.cs @@ -1383,7 +1383,7 @@ namespace AutopilotMonitor.Functions.Services /// reflects when the session went silent rather than when the operator clicked the button. /// /// - /// Pure gate for the terminal/reconcile status rules (docs/design/enrollment-status-reclassification.md). + /// Pure gate for the terminal/reconcile status rules (tasks/enrollment-status-reclassification.md). /// Returns whether a write to may proceed given the persisted /// string: /// @@ -1427,7 +1427,7 @@ namespace AutopilotMonitor.Functions.Services var entity = await tableClient.GetEntityAsync(tenantId, sessionId); var session = entity.Value; - // Terminal / reconcile transition gate (docs/design/enrollment-status-reclassification.md): + // Terminal / reconcile transition gate (tasks/enrollment-status-reclassification.md): // a genuine completion (Succeeded) may UPGRADE a prior Failed/Incomplete/AwaitingUser // verdict — a device that reached the desktop is enrolled regardless of an earlier timeout // guess — but the silent-terminal verdicts (Failed/Incomplete) and AwaitingUser never diff --git a/src/Shared/AutopilotMonitor.Shared/Constants.cs b/src/Shared/AutopilotMonitor.Shared/Constants.cs index 40578d18..06fe95a1 100644 --- a/src/Shared/AutopilotMonitor.Shared/Constants.cs +++ b/src/Shared/AutopilotMonitor.Shared/Constants.cs @@ -306,7 +306,7 @@ namespace AutopilotMonitor.Shared public const string AgentStarted = "agent_started"; // Lifecycle anchor — fired Seq=1 at agent boot. PR1: replaces hardcoded string-literals at emit sites. public const string AgentShuttingDown = "agent_shutting_down"; // V2 single-rail plan §6.2 — terminate-hygiene acknowledgement emitted before CleanupService tears down // Backend-materialized from the agent's best-effort SessionAgeEmergencyBreak error report - // (docs/design/enrollment-status-reclassification.md). The agent's 48h absolute session-age + // (tasks/enrollment-status-reclassification.md). The agent's 48h absolute session-age // break (Program.Guards.CheckSessionAgeEmergencyBreak) is otherwise silent to the backend; this // event closes that blind spot in the timeline AND tells the timeout classifier the agent is // definitively gone, so the session is terminalized now (by ESP rollup) instead of waiting out grace. diff --git a/src/Shared/AutopilotMonitor.Shared/Models/Config/TenantConfiguration.cs b/src/Shared/AutopilotMonitor.Shared/Models/Config/TenantConfiguration.cs index 8cabca49..0189fa9c 100644 --- a/src/Shared/AutopilotMonitor.Shared/Models/Config/TenantConfiguration.cs +++ b/src/Shared/AutopilotMonitor.Shared/Models/Config/TenantConfiguration.cs @@ -178,7 +178,7 @@ namespace AutopilotMonitor.Shared.Models /// /// Session inactivity timeout in hours. Once an "InProgress" session is idle past this, the /// maintenance sweep reclassifies it out of "InProgress" (see - /// docs/design/enrollment-status-reclassification.md): if Device Setup already finished it + /// tasks/enrollment-status-reclassification.md): if Device Setup already finished it /// becomes AwaitingUser (non-terminal), otherwise it eventually settles as the terminal, /// non-failure Incomplete state once elapses — it is NOT /// counted as a failure. This prevents stalled sessions from running indefinitely and skewing statistics. @@ -189,7 +189,7 @@ namespace AutopilotMonitor.Shared.Models /// /// Grace window in hours for a session that reached the inactivity timeout with Device Setup - /// already provisioned but no completion signal yet (docs/design/enrollment-status-reclassification.md). + /// already provisioned but no completion signal yet (tasks/enrollment-status-reclassification.md). /// At such a session becomes AwaitingUser (non-terminal) instead /// of Failed; only after this window elapses without a completion does it graduate to the terminal, /// non-failure Incomplete state. diff --git a/src/Shared/AutopilotMonitor.Shared/Models/Diagnostics/AgentErrorReport.cs b/src/Shared/AutopilotMonitor.Shared/Models/Diagnostics/AgentErrorReport.cs index 9465b368..e668ef70 100644 --- a/src/Shared/AutopilotMonitor.Shared/Models/Diagnostics/AgentErrorReport.cs +++ b/src/Shared/AutopilotMonitor.Shared/Models/Diagnostics/AgentErrorReport.cs @@ -35,7 +35,7 @@ namespace AutopilotMonitor.Shared.Models /// cleaning up and exiting. Best-effort report over the resilient emergency channel (may be /// lost if the device is fully offline) so the backend can materialize an /// agent_emergency_break timeline event and terminalize the session instead of waiting - /// out the silence grace. See docs/design/enrollment-status-reclassification.md. + /// out the silence grace. See tasks/enrollment-status-reclassification.md. /// SessionAgeEmergencyBreak = 4, } diff --git a/src/Shared/AutopilotMonitor.Shared/Models/Metrics/UsageMetrics.cs b/src/Shared/AutopilotMonitor.Shared/Models/Metrics/UsageMetrics.cs index 675ea330..5b783b8d 100644 --- a/src/Shared/AutopilotMonitor.Shared/Models/Metrics/UsageMetrics.cs +++ b/src/Shared/AutopilotMonitor.Shared/Models/Metrics/UsageMetrics.cs @@ -83,7 +83,7 @@ namespace AutopilotMonitor.Shared.Models /// Terminal, non-failure sessions (timeout reclassification): the sweep classified them as /// Incomplete instead of Failed. Surfaced as its own count and deliberately excluded from /// (denominator = Succeeded + Failed only), mirroring SessionStats - /// and FleetHealthStats. See docs/design/enrollment-status-reclassification.md §5. + /// and FleetHealthStats. See tasks/enrollment-status-reclassification.md §5. /// public int Incomplete { get; set; } public double SuccessRate { get; set; } diff --git a/src/Shared/AutopilotMonitor.Shared/Models/Metrics/UsageMetricsSnapshot.cs b/src/Shared/AutopilotMonitor.Shared/Models/Metrics/UsageMetricsSnapshot.cs index 707162d9..45a5b51f 100644 --- a/src/Shared/AutopilotMonitor.Shared/Models/Metrics/UsageMetricsSnapshot.cs +++ b/src/Shared/AutopilotMonitor.Shared/Models/Metrics/UsageMetricsSnapshot.cs @@ -56,7 +56,7 @@ namespace AutopilotMonitor.Shared.Models /// Terminal, non-failure sessions (timeout reclassification): classified Incomplete rather /// than Failed. Tracked separately and excluded from /// (denominator = Succeeded + Failed). Historical rows written before this field existed - /// deserialize to 0. See docs/design/enrollment-status-reclassification.md §5. + /// deserialize to 0. See tasks/enrollment-status-reclassification.md §5. /// public int SessionsIncomplete { get; set; } diff --git a/src/Shared/AutopilotMonitor.Shared/Models/SessionApiModels.cs b/src/Shared/AutopilotMonitor.Shared/Models/SessionApiModels.cs index 0adc5ffa..b71b7dd1 100644 --- a/src/Shared/AutopilotMonitor.Shared/Models/SessionApiModels.cs +++ b/src/Shared/AutopilotMonitor.Shared/Models/SessionApiModels.cs @@ -431,7 +431,7 @@ namespace AutopilotMonitor.Shared.Models Succeeded, Failed, Unknown, - // Appended 2026-07-08 (docs/design/enrollment-status-reclassification.md). New members + // Appended 2026-07-08 (tasks/enrollment-status-reclassification.md). New members // MUST stay appended so existing persisted ordinals never shift. AwaitingUser, // Device Setup (ESP DeviceSetup) fully succeeded but the user/Account-Setup // phase has not completed and the agent went silent within the grace window. @@ -464,7 +464,7 @@ namespace AutopilotMonitor.Shared.Models public int FailedLastNDays { get; set; } /// - /// Terminal, non-failure sessions in the window (docs/design/enrollment-status-reclassification.md): + /// Terminal, non-failure sessions in the window (tasks/enrollment-status-reclassification.md): /// the sweep saw no completion or explicit failure. Reported as the third headline bucket and /// deliberately excluded from (which is over Succeeded + Failed only). /// diff --git a/src/Web/autopilot-monitor-web/utils/sessionStatus.ts b/src/Web/autopilot-monitor-web/utils/sessionStatus.ts index a9b6846f..2a83e65c 100644 --- a/src/Web/autopilot-monitor-web/utils/sessionStatus.ts +++ b/src/Web/autopilot-monitor-web/utils/sessionStatus.ts @@ -5,7 +5,7 @@ // // AwaitingUser (Device Setup done, user/Account-Setup phase pending) and Incomplete // (terminal, non-failure — no completion or explicit failure was observed) come from the -// timeout reclassification, see docs/design/enrollment-status-reclassification.md. +// timeout reclassification, see tasks/enrollment-status-reclassification.md. export function isTerminalStatus(status: string | null | undefined): boolean { return status === "Succeeded" || status === "Failed" || status === "Incomplete";