mirror of
https://github.com/okieselbach/AutopilotMonitor.git
synced 2026-10-24 12:42:38 +00:00
v2.0.1417
2395 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
ba2a390a83 |
fix(agent): battery-instance fallback for the dead Win32_PowerManagementEvent provider
On Windows 11 24H2/25H2 (observed on builds 26200 and 26220, session 161b838c) the MS_Power_Management_Event_Provider is still registered in the WMI repository but activating the extrinsic push query fails with WBEM_E_NOT_FOUND - the legacy provider no longer loads on exactly the OS builds new devices enroll with, so the power watcher degraded fleet-wide. Arm chain: try the push event first (still alive on older builds), then fall back to the intrinsic __InstanceModificationEvent WITHIN 30 on Win32_Battery. The WITHIN sampling runs inside WinMgmt, not in the agent; the handler path is unchanged (payload ignored, re-probe + diff), worst-case detection latency ~35s incl. debounce. collector_degraded now fires only when BOTH attempts fail; the arm log line names the active mode. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>v2.0.1417 |
||
|
|
dbd7d2ac6f | Page-Size-Option „5" v2.0.1416 | ||
|
|
8f24876682 |
feat(agent): live power-state watcher + battery analyze rules DEV-009/010
Event-driven AC/battery observation during enrollment (no polling): a WMI Win32_PowerManagementEvent (EventCode 10) push subscription re-probes GetSystemPowerStatus and diffs snapshots through a pure tracker into power_state_change events - transition ac_to_battery (Warning, immediate) / battery_to_ac (Info) / threshold_crossed at 50/30/15% (per-level lifetime latch, lowest-crossed-only on jumps, arm-time baseline covers sessions starting low, 5s trailing-edge debounce, emission cap 20). Never armed on devices without a battery; arm failure surfaces as collector_degraded. Kill-switch Collectors.EnablePowerStateWatcher (ConfigVersion 38). power_state_change is classified non-activity on both sides (SignalActivityClassifier + IsPeriodicOrStallEvent) so battery drain can neither keep idle collectors alive nor heal a Stalled session. New analyze rules, both with on_event:power_state_change interim triggers and deliberately single-required-condition (event_data conditions scan events independently - no same-instance join): - ANALYZE-DEV-009 (high): thresholdPercent equals 15 - ANALYZE-DEV-010 (warning): transition equals ac_to_battery Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
2d5ee887d7 |
fix(web): make rule JSON paste-import lossless in both rule editors
The JSON editors serialize the FORM object, but what users paste is usually a rule EXPORT (e.g. received from another tenant). The create/edit whitelists rebuilt collector parameters and evaluateOn from form fields the export does not carry, so a pasted gather rule silently lost every collector parameter and a pasted analyze rule silently reverted to the enrollment-end default. Both rules pages get a single shape-tolerant mapper in types.ts: - gatherRuleToForm unflattens the nested parameters object (authoritative when present, so edit-merge leftovers cannot leak), maps nullable rule fields to form defaults, imports a missing emitMode as 'always' (the rule's actual behavior) instead of the empty-form on_change default, and now also backs startEditing so edit and paste share one mapping. - analyze jsonToForm maps evaluateOn onto the eval* checkboxes (an explicit evaluateOn wins over stale form flags in edit-mode merges); terminal-only exports fall back to the enrollment-end default. - tags and the folded effective markSessionAsFailed now survive the import: both forms carry tags into the create/save payloads, analyze creates carry markSessionAsFailedDefault. notify/notifyChannelIds stay excluded — channel ids are tenant-specific. 9 new vitest cases pin both shapes, the merge precedence, and the round-trips; full web suite 824/824, tsc and eslint clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
81c14bb3ed |
fix(rules): close the rule-ID namespace gaps around the shared global keyspace
Custom rules are tenant-partitioned and functionally collision-free, but three
surfaces still keyed on the bare non-unique ruleId string:
- RuleStats global aggregate (global_{ruleId}) summed same-ID custom rules of
unrelated tenants (title/severity last-writer-wins). The global row is now
catalog-only: analyze gates on IsBuiltIn/IsCommunity, gather gates on the
reserved-namespace pattern (agent events carry no flag; custom rules can
never occupy the reserved pattern).
- The 'never ship a built-in in the CUSTOM namespace' commitment from
RuleIdPolicy was documented but unenforced: combine.js now fails on
duplicate or non-reserved built-in IDs, BuiltInRuleCatalogPolicyTests pins
the same invariants on the embedded catalog, and the GitHub reseed rejects
non-reserved IDs before writing to the global partition.
- DeleteRuleStatesForRuleIdAcrossTenantsAsync (cross-partition delete by bare
RowKey) now refuses IDs outside the reserved namespace with failed=-1 so
callers abort the follow-up global catalog delete.
Verified: prod global partitions (45 analyze + 11 gather) are fully inside the
reserved namespace, so the guards touch no existing data. New OKF doc
docs/rules/rule-id-namespace.md captures the identity/collision model.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
3a0bd74b12 |
fix(backend): sequence the synthetic vulnerability_report event instead of defaulting to 0
The async queue correlation stored the event with Sequence 0, sorting it to the top of the timeline (and tripping the time-jump badge) even though it arrives last. Follow the BuildSessionTimeoutEvent/BuildAgentEmergencyBreakEvent pattern: max+1 so it sorts last; WhiteGlove Part 1 anchors at the Part-1 terminal markers so a late device_setup report can never cross the web timeline's sequence-based Part-1/Part-2 split. Also upsert the EventType index the queue path never wrote, making the event visible to cross-session search. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
50e7590060 |
feat: app install durations measure the status-defining attempt
The IME processes the ESP app list in multiple passes (device-ESP evaluation ending Skipped, real install up to an hour later), and AppInstallSummary folded first observation -> last terminal across them: a 23-second app late in the queue reported 4002 s (session 44862cd3, "Set TimeZone"). Backend ingest: new max-folded LastAttemptStartedAt anchors DurationSeconds (CompletedAt - last attempt start; legacy rows without the anchor keep the span fallback), replay-guarded additive InstallPassCount, attempt-scoped DownloadDurationSeconds pairing, and a weaker-terminal guard (completed with state Skipped/Postponed never overrides Installed/Error, in-batch and in the cross-batch reconcile). StartedAt stays the earliest-wins window/version- ordering key. AppsDashboardProjection carries the new columns. AppVersionRegressionRadar: cutover gate — only attempt-anchored rows enter the duration population on BOTH comparison sides, so mixed-semantics medians can neither mask real regressions nor falsely re-arm active episodes. Time attribution: per-pass segment pairing replaces the first-start -> last-terminal hull. An app's seconds = sum of its ACTIVE segments (failed attempts and evaluation passes count; inter-pass idle gaps don't), occupancy merges the segments, hull endpoints remain for display and what-if bounds. Agent: a terminal->active transition re-arms BOTH timing endpoints, so event payload durationSeconds also describes the attempt. Web/PDF/MCP: labels and tooltips say "install time (final attempt)", the apps sessions table gets a xN multi-pass chip, the time-series chart carries a cutover footnote, MCP tool descriptions document the semantics split between get_app_install_metrics and get_time_attribution. No backfill: pre-cutover rows keep span values (documented in the public changelog). OKF doc: docs/backend/app-install-duration-attempt.md. Suites: Functions 4383, Agent 2407, MCP 564, Web 814 - all green, build 0/0. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
61e53a2e9b |
feat: lead duration cards with median + P90 instead of outlier-skewed average
Enrollment durations are heavily right-skewed; a handful of overnight/WhiteGlove outliers made the Fleet Health and Dashboard average cards read like hours (541 min) and answer no real question. Both cards now lead with the median (robust 'typical enrollment' answer) and keep the tail visible via a P90 subtext. AvgDurationMinutes stays in both DTOs for API compatibility. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
7e7b6f8086 |
fix(backend): declare LastIngestAt in the SessionsIndex manifest
|
||
|
|
2e20476559 |
feat(agent): extend RealmJoin timeout while first deployment is demonstrably active
The 60-min RealmJoinTimeout deadline arms at RJ detection (the RJ agent MSI install during DeviceSetup) and used to cut off monitoring mid-install: report 55e6afd61c9d (session 46a96078) lost the tail of a large package catalog whose last observed package completed 3 seconds before the deadline, with the first deployment only starting 16 minutes into the window. When the deadline now fires with LastDeploymentPhase 100/101 and deployment activity (phase change or package observation, persisted restart-safe in RealmJoinFacts.LastActivityUtc; the detection-time phase capture deliberately does not count) inside the last 60 min, the handler re-arms to lastActivity + 60 min instead of timing out — Info event realmjoin_timeout_extended — clamped at an absolute cap of detection + 4 h. realmjoin_timeout carries a reason payload (hard_timeout / inactivity / absolute_cap); a fire of a deadline incarnation superseded by a re-arm dead-ends via the OccurredAtUtc == DueAtUtc scheduler contract. The idle case (no activity ever) still times out at 60 min, and phase-200/210 activity never extends. Packages are only registry-visible at completion, so the inactivity window keeps the full original 60 min. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>v2.0.1415 |
||
|
|
990641b19e |
feat: P14 - per-upload-batch send time separates spool delay from clock offset
Agent stamps X-Send-Time-Utc (device clock, ISO-8601 round-trip) per upload ATTEMPT; ingest parses it (year>=2000 guard, deliberately no upper clamp) and server-stamps SentAt onto every event of the request. SentAt - OccurredUtc is pure spool delay, ReceivedAt - SentAt is network + device-vs-server clock offset - previously indistinguishable and only separable via median statistics. clock_skew condition: batches carrying SentAt measure the frame directly (spool-immune by construction - no spread cap, IME/clamped exclusions irrelevant, IME-only sessions become measurable; evidence sentAtBatchCount); legacy batches keep the per-event median path unchanged. CmTraceSkewTripwire deliberately untouched: its differential median math cancels clock offset as common mode. Full persistence chain: Store x2 + Map, fields= projection (sentAt), MCP get_session_events field list + rule-authoring-guide. New OKF doc backend/send-time-frame-separation.md. Suites: Functions 4370 (+16), Agent 2407 (+1), MCP vitest 564 + tsc 0. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>v2.0.1414 |
||
|
|
3ca2f3b499 |
refactor(web): collapse time provenance behind an inline toggle
The labeled provenance rows duplicated values already visible in the raw JSON dump and inflated the metadata block for every IME event. The section is now collapsed by default behind a small "time provenance" toggle sitting inline on the Created row (whose value it explains) - zero extra height when collapsed, and the base label column returns to its original width, so the default detail view is pixel-identical to before P13. Expanding reveals the same rows, indented under Created. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
b9fa760404 |
feat(web): P13 - CMTrace time provenance visible in the session timeline
The per-line self-anchoring provenance (sourceLocalTs, applied offset + origin, observational writer offset, derived/rejected markers) reaches the browser but was buried unlabeled in the raw JSON dump. Sequence stays THE sort order - without exception (now pinned by a regression test); the corrected UTC is display-only, and a backwards step of the displayed time within sequence order is marked as information instead of confusing the reader. - lib/timeProvenance.ts: readTimeProvenance (camelCase/snake_case, tolerant of truncated payloads) + classifyTimeJump with cause precedence derived > rejected-source > era-offset; era-offset requires applied offsets on BOTH rows - it never guesses. - TimeJumpBadge on the row AFTER the jump, computed against the previous RENDERED row (filters/phase boundaries respected); sky for known causes, amber otherwise; tooltip carries both times, the delta and the "true write order (sequence)" sentence. Threshold 5 min, field-calibrated on 20 sessions: 12/12 normal sessions show zero badges, the era-mixed regression session shows exactly its 6. - Clamped chip (rare) + TIME PROVENANCE detail section: only the applied offset says "(applied)"; the writer offset is explicitly observational (sticky after era flip-backs); origin chips with forward-compatible fallback for unknown values. - EnrollmentEvent gains timestampClamped/originalTimestamp (wire already carried them); formatUtcOffset joins lib/formatting. Verified: 814 web tests (19 new), tsc/eslint/build clean; Playwright against e9753578 (badges match the pre-measurement), 9f475bb2 (no badge on line-anchored rows, detail section correct, raw mode shows exactly the one predicted jump). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
ab61839685 |
feat(backend): P6 split - CMTrace skew tripwire + customer clock_skew rule
Operator side: CmTraceSkewTripwire piggybacks on the terminal counter
reconcile's partition scan (zero extra reads) and compares median
ReceivedAt-OccurredUtc deltas of IME-derived vs other events. A clean
15-min-grid divergence (residual < 2 min, >=20 samples / >=3 distinct
batches per side) emits the CmTraceTimeSkewRegression ops event
(Agent/Warning, goal: never fires). Bias-dominated sessions are
suppressed; measuredWriterOffsetMinutes is never consulted (sticky
after era flip-backs). Kill switch: CmTraceSkewTripwireDisabled.
Customer side: new clock_skew analyze condition source (skewMetric
clock_jump | sustained_offset, threshold seconds, gt/gte magnitude)
behind built-in ANALYZE-DEV-008 (300 s = the Kerberos/Entra 5-min skew
boundary). Batch-median frames with spool-spread filter, plateau step
detection (a gradual backlog-drain ramp cannot fake a jump) and
end-state persistence; IME-derived events are excluded so a CMTrace
anchoring regression can never fire the customer rule. Evidence carries
field=skewSummary/value for {{skewSummary}} interpolation.
Full mirroring chain: schema enum + skewMetric property, shared
manifest + combine.js regeneration, MCP authoring guide + semantic
lint, web SOURCES/form/card, ops-event dual-register, docs. P5 is
subsumed by the tripwire. PercentileMath is now the single nearest-rank
implementation (Apps/PlatformMetrics delegate).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
34fd40c3ad |
fix(agent): refuse cross-file calibration anchors, make rejections visible
Field forensics from the 1412 verification run (session 067797d8): the label/anchor crossing recurred on a provably genuine binary, and the new anchor-timestamp tripwire delivered — the "+02:00 measured for IntuneManagementExtension.log" anchor (local 19:51:26.948) does not exist in ANY log file on the device, and IME.log never produced its expected first measurement during 24 s of growing passes beforehand. Mechanism still unexplained; per-line anchoring keeps resolution immune (all 58 line-anchored events of the session were correct), but the observational layer must convict the culprit next time: - CalibrateFrom now receives the iteration's ACTUAL file name next to the calibration key. A mismatch — the smoking gun the two field occurrences imply but static analysis says is impossible — logs a Warning with both names plus the anchor, and refuses the anchor, so even the unexplained path can no longer poison the measurement store. - The first calibrator rejection per file logs at Debug (anchor local time + clock). The 24 s of silent non-measurement is exactly what made the phenomenon undiagnosable from the agent log. 232 IME tracker tests green against a verified-clean build. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>v2.0.1413 |
||
|
|
a0eb1b9e35 | Merge branch 'main' of https://github.com/okieselbach/Autopilot-Monitor v2.0.1412 | ||
|
|
9d677db2a8 |
docs(okf): CMTrace time resolution — per-line self-anchoring concept
Why the reader-zone assumption and the per-file offset application both failed, the per-line self-anchoring rule with its strict freshness contract (LastCheckedUtc on every poll look, 30 s window, no freshness across restarts), the provenance vocabulary that keeps every event re-derivable, and the documented limits: the bounded grid-replay edge, snapshot readers staying on the marked fallback, and the still-open e9753578 label/anchor crossing with its tripwire. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
c728a0bd41 |
feat(backend): surface genuine agent binary-integrity mismatches as ops events
IntegrityCheckFailed emergency reports used to land in App Insights only — session e9753578's agent reported a hash mismatch and no product surface showed it, which cost a day of mis-attributed root-causing. They now materialize as an AgentBinaryIntegrityMismatch ops event (Warning, Agent category, dual-registered in the web alert-rule catalog per feedback_ops_event_types_dual_register). With suppression for the KNOWN, accepted release race the operator correctly called out: around every release, agents compare against hash snapshots that do not belong to their own binary — an older agent sees the new release's hash on its next config fetch, and a freshly installed latest agent can receive the PREVIOUS release's hashes from the 5-minute AdminConfiguration cache (e9753578: genuine 1410 exe checked against the cached 1409 hash — proven by the 1409 archive logging "verified OK" with exactly that hash). Both self-heal via the forced self-update and are suppressed at materialization time against the CURRENT config: reported running hash == published exe, or reported version != latest. What remains — an agent claiming the latest version whose binary is NOT the published one — is the real signal: tamper, stale blob, or a build outside the release pipeline. Missing config fails open to recording; visibility beats suppression. Ops event only, deliberately no timeline event: the report is about the binary, not the enrollment, and may precede session registration. Backend suite 4320 green, web tsc clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
64336c0c89 |
feat(agent): resolve fresh CMTrace lines by per-line self-anchoring
The era-aware successor to the reverted per-file offset application
(
|
||
|
|
59c944618b |
test(agent): move CMTrace fixture to tests/fixtures and anonymize it
Two corrections to
|
||
|
|
3c25172dfb |
test(agent): real two-writer-era CMTrace fixture from session e9753578
Captured from ImeLogs/AgentExecutor.log of the session that exposed the regression (agent 2.0.1410; device booted on the Pacific OOBE default, agent set W. Europe at 12:13:22Z). One file, two writer eras: 371 lines rendered at local hour 05 by a writer still on PDT (UTC-7) and 195 at hour 14 by one already on CEST (UTC+2) - nine hours apart, interleaved, and no line carries a bias suffix, so nothing in the file itself says which is which. Exactly the case the per-file calibration could not express, and the test that was missing. Analysing the archive also turned up a SECOND, independent defect the revert commit did not know about: the agent log reports "IntuneManagementExtension.log writer offset measured 02:00:00" while that file contains ONLY hour-05 lines (2022 of them, zero at hour 14). A +2 measurement is impossible from its own content, so the calibration anchor leaked across file boundaries - files are walked in sorted order and AgentExecutor.log precedes it. Two-eras-per-file and anchor-leak are separate bugs; both must be fixed. Extension is .cmtrace.txt, not .log: *.log is globally gitignored, and a fixture has no reason to claim that extension. Fixture only, nothing consumes it yet - the era-aware calibrator is next. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
eda61eaade | Fix Tenant Groups layout overflowing on phones (#132) | ||
|
|
04b1a7c6e7 |
revert(agent): stop applying the measured CMTrace offset — one offset per file is wrong
Field regression, session e9753578 (agent 2.0.1410, PST-defaulted device in DE).
Five script_started events resolved 9 h early while their script_completed
counterparts resolved correctly, inflating every platform-script duration to
~32,400 s. Reverts the application introduced in
v2.0.1411
|
||
|
|
3b59dab2df |
feat(agent): carry timestamp provenance on IME-derived events
Every IME-sourced event now states what the writer wrote, what offset was applied, and how that offset was obtained: sourceLocalTs raw local time, verbatim, NO zone suffix sourceOffsetMinutes the offset subtracted sourceOffsetOrigin bias | calibrated | reader-zone-fallback Why: until now the only record of a conversion was the converted value, so a wrong offset was indistinguishable from a right one. Proving the field defect needed the diagnostics archive — and of the 5,386 sessions that went through a timezone change, essentially none had one, so the analysis had to fall back to median statistics across thousands of sessions to establish what one stored value makes obvious per session. It also makes a stored timestamp recomputable after the fact, without an agent release. No zone suffix on sourceLocalTs on purpose: the value genuinely carries none, and formatting one in would re-introduce the assumption this whole change removes. sourceOffsetMinutes is normalised to one sense (local = UTC + offset) across both origins. The wire bias uses the opposite GetTimeZoneInformation convention (UTC = local + bias), so a "+480" line is reported as -480; without that the field would flip sign depending on where the offset came from and be useless to compare. Added centrally in TagDerivedTimestamp, which all 14 emit sites already call, so this does not scatter through the adapter. DataJson only — no table schema change, so no Store/Map/seed chain and no backend deploy needed to start collecting it. Agent 2393 / backend 4313 green against a verified-clean build. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>v2.0.1410 |
||
|
|
389426f8f2 |
fix(agent): future-skew guard that a one-hour timezone error can actually trip
ResolveOccurredAt rejected a source timestamp when ageHours < -1.0. That bound could not fire for the single most common timezone error: with a one-hour offset mistake the real value is parseLag - 3600 s, i.e. just ABOVE -1.0. It sat exactly on the boundary of the case it was meant to catch, which is why every skewed session sailed past it. It was asymmetric too — a past-shifted error had to exceed 24 h before anything noticed, while the measured field errors reached -17 h. The bound is now derived rather than guessed: writer and reader share one system clock, and the tracker only sees a line after it was written, so a correctly converted source timestamp is always at or behind "now". Anything ahead means the conversion was wrong. MaxFutureSourceSkew is 2 minutes of jitter tolerance — deliberately far below the hour a timezone mistake produces. The rejection log now names which bound tripped and by how much, instead of printing a bare ageHours that reads the same for both directions. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
e9dba11b96 |
fix(agent): resolve IME log timestamps with the writer's measured offset
This is the behaviour change the previous two commits prepared. ImeLogTracker no longer substitutes its own timezone belief for the IME process's; it measures the writer's offset from a freshly appended line and converts with that. Freshness is stricter than "the file grew". GetSafePosition returns 0 for an unknown file, so the first pass reads the whole file and its newest line may be hours old — anchoring there would measure the line's age, not the writer's offset. LogFilePositionTracker.HasSeen(), captured BEFORE GetSafePosition creates the entry, gates calibration on growth against a size we had already observed. Calibration runs at the END of a qualifying pass, so that pass still uses the previously measured offset — at most one poll (100 ms) old, and an offset only moves when a process restarts. Calibrating first would mean buffering the pass, which is not viable: the first pass over AppWorkload.log can be hundreds of MB. The cost is a warm-up of one growing pass, during which lines fall back to the reader zone. Bias-carrying lines are never used as anchors: they already state the writer's offset, so they need no measurement and must not overwrite one. CalibrateFrom logs at Info when the measured offset disagrees with this process's own zone, naming the correction applied. That disagreement is precisely the condition that silently corrupted every IME-derived timestamp until now; agreement stays at Debug so the normal case does not add noise. Tests are deliberately machine-independent: the assertion is always "a line written at instant T resolves to T", asserted across writer beliefs of +2, +1, 0, -7 and +10. That one property covers both field cases — the writer disagreeing with us (the bug) and the writer agreeing with us (the majority of sessions, where two stale beliefs cancel and which a fix to the agent's own cache would have broken). Plus: no calibration on first sight, following a writer restart into a new zone, and a declared bias honoured without being measured. CheckLogFilesAsync is internal as a test seam — calibration depends on the pass structure, which only a real read cycle exercises. Agent 2390 / backend 4313 green against a verified-clean build. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
828cbb5cd0 |
refactor(shared): CMTrace parser stops guessing the writer's timezone
Behaviour-neutral: 2381 agent and 4313 backend tests pass unchanged. The parser resolved a bias-less line with DateTimeStyles.AssumeLocal followed by ToUniversalTime(), i.e. with the READING process's cached zone. That silently assumes reader and writer agree. They do not — each caches its zone at process start and neither follows a later tzutil or Windows auto-timezone change — and the assumption was buried inside a shared helper where no call site could see it. CmTraceLogEntry now exposes what the line actually contains: LocalTimestamp (Kind=Unspecified, because the value is meaningless without knowing the writer's zone) and BiasMinutes. TimestampUtc is populated only when the writer declared its bias, which is the one case where the offset is genuinely known. Timestamp is removed rather than deprecated, so the compiler names every site that has to decide — it immediately turned up ProcessLogMessageForTest, which grep had missed. The defective path still exists, but now as one named function, ResolveUtcAssumingReaderZone(), whose documentation states when it is wrong. All three consumers call it and so keep their current behaviour exactly: - ImeLogTracker routes every use through ResolveEntryUtc(), the single point where the measured offset replaces the fallback in the follow-up change; - StallProbeCollector and LogParserCollector read file snapshots rather than tailing, so they cannot measure an offset and stay on the fallback for now. The parser also no longer invents a timestamp. An unparseable time used to be stamped DateTime.UtcNow inside the parser, making a fabricated value indistinguishable from a real one downstream; it now reports HasTimestamp=false and leaves the fallback to the caller, which alone knows what is safe in context (StallProbeCollector still treats such a line as fresh — calling it stale would hide progress). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
93a42ba5e4 |
feat(agent): measure the CMTrace writer's UTC offset instead of assuming it
A CMTrace line carries local time and no offset — IME 1.104 writes DateTime.Now.TimeOfDay in both of its trace listeners. Converting it with the agent's own TimeZoneInfo.Local assumes both processes agree on the zone. They do not: each caches its zone at process start and neither follows a later tzutil or a Windows auto-timezone change, so the error is Offset_writer - Offset_agent and is zero only by coincidence. Measured over 11,068 sessions: 26 carry a real zone-offset error, +1 h (GMT to W. Europe, session bfff9b1c) through -17 h (a writer still on the Pacific OOBE default while the agent had already moved to E. Australia). Note the sign — this is why "make the agent's own clock honest" is the wrong fix: in the -17 h cases the agent was correct and the writer was stale, and clearing the agent's cache would have broken the majority of sessions where both are stale and the two errors cancel. The tracker tails at a 100 ms poll, so a line that appeared since the previous pass was written essentially "now". That makes the writer's offset measurable: round(lineLocal - agentUtcNow) onto the 15-minute grid every real UTC offset sits on. Per source file, because IntuneManagementExtension.log and AppWorkload.log are written by different processes; re-measured on every fresh anchor, so a writer restart is picked up within one poll. The freshness requirement is the load-bearing part and cannot be replaced by the residual guard: a backlog line exactly 30 minutes old is itself a clean multiple of the grid, rounds with zero residual, and would calibrate the offset half an hour wrong. RejectsBacklogLine_EvenWhenItRoundsCleanly pins that the calibrator cannot detect this on its own — only the caller's freshness rule prevents it. Additive only; no call site uses it yet. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
aaacc6ecb2 |
refactor(backend): order rule evaluation by Sequence, not Timestamp
Sequence is a persisted, strictly monotonic per-session counter assigned at emit time (EventSequenceCounter: locked, saved before return, survives restarts). It is immune to every clock problem. Timestamp carries the device clock frame plus, for CMTrace-derived events, the writing process's timezone belief. EvaluateAppInstallDurationCondition and EvaluateEventCorrelationCondition both ordered by Timestamp and expressed "A happened before B" as a timestamp comparison. Both now order by Sequence and compare Sequence for ordering. Timestamp deliberately stays where an actual ELAPSED DURATION is meant: durationSeconds, the evidence fields, and TimeWindowSeconds. Scope, honestly: this is hardening, not a live bug fix. BuiltInAnalyzeRules.cs contains zero event_correlation conditions, so the cross-frame join can only be reached by a self-authored rule. And the app-install duration evaluator pairs app_install_started with app_install_completed — both IME-sourced, one frame — so its arithmetic was already correct. (The measured 68-vs-8-minute inflation comes from EventIngestProcessor.AppInstall, which folds DO-collector download_progress together with IME app_install_completed; that is fixed at the root in the agent.) The TimeWindowSeconds check stays exposed: the window IS a duration, so there is no alternative to a timestamp there. A +1 h skew on B makes any window below 3600 s fail to match. Only the agent-side calibration closes that. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
bbc7a1d8b2 |
fix(backend): agent-silence sweep must compare clock frames it owns
LastEventAt is the maximum AGENT-supplied event timestamp and therefore lives in the device clock frame. The silence sweep compared it against a server-derived cutoff, mixing two frames. Measured over 11,068 sessions: 26 carry a genuine CMTrace timezone skew on their IME-derived events, from -17 h to +1 h (a further 27 carry plain device-clock offsets). At -17 h a live agent looks silent the moment it starts and is marked Stalled with a customer-visible "Agent silent for 1020min"; at +1 h the sweep is blind for an hour. Adds SessionSummary.LastIngestAt, stamped from the server clock in IncrementSessionEventCountAsync — reaching that method IS contact from the agent, so it is written unconditionally rather than gated on latestEventTimestamp. The sweep's server-side filter now spans both frames (LastIngestAt lt X or LastEventAt lt X): filtering on LastIngestAt alone would silently drop every row written before the field existed, because Table Storage does not match a filter against a missing property. The authoritative decision moves into ServerFrameLastContact() (LastIngestAt ?? LastEventAt ?? StartedAt) and is applied in both stages — before the Stalled marker and in the stage-2 alive guard. Deliberately not mirrored into SessionsIndex: the sweep reads Sessions, and the index manifest is pinned bidirectionally by tests. shared-manifests.json + .generated.ts regenerated — SharedManifestParityTests is the drift brake and correctly went red on the new field. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
5f00124a86 |
fix(web): keep PDF section titles with their content, show full exit-code text
Section headings now reserve keep-with-next space (charts reserve their full height), so a title never strands at a page bottom while its table starts on the next page. Table cells wrap up to 4 lines instead of single-line ellipsis, and the exit-code column carries the full error description (the 24-char slice is gone) - that text is what the app owner acts on. Failure-code column widths rebalanced (Code 42 / Description 62 / Exit 60 / Count 16). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
d43cfe2bb3 | updated last modified page v2.0.1409 | ||
|
|
78cc038426 |
fix(agent): record real bytes-down when AutomaticDecompression strips Content-Length
The pipeline's AutomaticDecompression (GZip|Deflate) removes the response Content-Length header on virtually every backend JSON response, so net_total_bytes_down in agent_metrics_snapshot read 0 on healthy sessions. When the header is absent, buffer the content (LoadIntoBufferAsync, 16 MB cap) and record the DECOMPRESSED payload size — HttpClient buffers responses anyway (default ResponseContentRead), so no extra memory cost. Fail-soft: a buffering error keeps 0 and leaves the failure for the caller to observe. AutomaticDecompression itself stays untouched (pinned by MtlsHttpClientFactoryTests). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
ff8b847dc0 |
fix(backend): fold do* fields from download_progress — DO data survives IME 1.104
IME 1.104 removed the [DO TEL] log line, leaving the agent's DO collector as the only do_telemetry source — and its gate only fires when a 3s poll itself observes the completed download. Fast, interrupted or between-poll-completed downloads never emit one, so DoDownloadMode stayed -1 and DoAggregator dropped the whole row: DO bytes read as "practically null" on 1.104 fleets even though download_progress events carried the full do* set on every poll. - Shared ApplyDoFields for both writers: download_progress folds byte counters and file size via Math.Max (monotonic, replay-idempotent) and sets the mode only when >= 0; do_telemetry stays authoritative (last-write-wins) but never regresses DoDownloadMode to the -1 unset sentinel. - DownloadBytes now prefers actually-transferred bytes over doFileSize (the file size used to inflate the transfer measure). Intended effect after deploy: DO aggregates count previously-invisible incomplete downloads, so app counts/bytes rise. No backfill of historic rows. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
aab7ae12ff |
feat(mcp): get_agent_efficiency_metrics tool + honest truncated + full platform pass-through
- New GA-only tool get_agent_efficiency_metrics: near-pass-through of the server-side per-version aggregation (120s timeout, default 500 sessions, optional tenantId). Added to GA_FULL + PLATFORM_ONLY catalog snapshots. - platformWindowEcho decides truncated on the backend's sessionsScanned (sessions actually covered) and only falls back to the has-snapshots count for old backends — the previous comparison read false even when the cap was hit exactly, whenever some scanned sessions carried no snapshots. - get_platform_metrics: 90s timeout (backend fans out one storage query per session); spool block, crashRate, deliveryLatency, computedAt, computeDurationMs and fromCache are no longer dropped on the MCP side; description clarifies totalBytesDown is the agent's own HTTP traffic. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
935f0e8c71 |
feat(backend): agent-efficiency aggregate + honest scan count + filtered metrics fetch
- New GET /api/global/metrics/agent-efficiency (GlobalReadOrAdmin, ?tenantId=): per-agent-version percentiles (CPU, working set, private bytes, threads, handles, spool, API latency/requests), crash rates per version, top offender sessions per dimension, 5-min cache keyed (days, limit, tenant). First backend consumption of agent_thread_count/agent_handle_count. - PlatformAgentMetricsResponse.SessionsScanned: sessions the scan actually covered — the honest basis for the caller's truncated flag (Sessions.Count only counts sessions WITH snapshots). - Per-session fetch no longer drains the full Events partition: new GetSessionEventsByTypesAsync (multi-type OData filter + column projection + real maxResults enforcement); the 20 delivery-latency sample sessions keep the full fetch so latency deltas stay computed over all events. - Shared AgentMetricsAggregation (crash tally, payload getters, bounded fan-out, metrics event types) so the two services cannot drift. - GetAllSessionsPageAsync gains an optional select projection (default null, existing callers unchanged), pinned by an equivalence test. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
541dc32ffd |
feat(web): PDF export for the app install report + usage telemetry
Adds an "Export PDF" button to the app detail page that renders the full 90/30/7-day install report (KPIs, both charts, failure codes, device model correlation, version breakdown, detection-rule/regression callouts) as a branded A4 PDF, drawn data-driven with jsPDF — no DOM capture, so the output is light-palette and theme-independent. jsPDF stays out of the route bundle via on-click dynamic import (verified: separate async chunk). Sample messages and the affected-sessions list are deliberately excluded: the PDF is meant to be emailed outside the org. Tracks app_report_pdf_exported in Application Insights (windowDays, scope, durationMs, pageCount, success/errorName — chunk-load failures included). Generic chrome (header/footer/tables/KPI cells/callouts) lives in utils/pdf/pdfPrimitives.ts for reuse by future report exports. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
022cc0354b |
fix(engine): let the observed user-ESP skip stand in for the exit evidence
The five stuck sits-d Cloud PCs carried SkipUserStatusPage=True — verified across three agent processes (10+ registry reads, True every time) and six hours of live Shell-Core watching with zero 62407: Windows never rendered a user ESP page, so no exit and no AccountSetup provisioning registry can ever exist on this flow. Arm B of ShouldTransitionToAwaitingHello was open from the bootstrap EspConfigDetected onward, but every evaluation site hangs off a signal the flow cannot produce, and the AdvisoryCompletion backstop arms exclusively from an EspExiting signal — an open gate nobody knocks on, six hours to the max-lifetime watchdog. The earlier lost-exit-through-reboot diagnosis is retired by the same evidence. HandleImeUserSessionCompletedV1's completion attempt now accepts the observed skip in place of IsPostAccountSetupFinalExit (anchor + genuine IME user-session completion + real-user desktop stay mandatory; Device Preparation excluded). The knock deliberately lives on the IME edge, not on desktop arrival: Fix 10's AccountSetup bounce-back would undo a desktop-side promotion two signals later, while the IME phase line always precedes the user-session-complete line in log order, so a bounced promotion is re-knocked by the very next re-emission — restart re-parse included. Docs and comments no longer claim an ESP-exit recovery that does not exist (stray duplicate summary in ShellCoreTracker, ImeLogHost restore-callback comment); the OKF doc is reframed on the evidence and names the residual SkipUser=false exit-in-downtime gap honestly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
3aa8b95676 |
fix(agent): narrow the Shell-Core replay to the Hello-wizard rail
Round-four review, confirmed — and the cause sits a layer below where I had been fixing. The IsBackfill return added last round only guarded the coordinator's own completion logic. Upstream of it, ShellCoreTracker still raised FinalizingSetupPhaseTriggered, the coordinator still forwarded EspExited to the adapter before that return, and the adapter discards the args entirely (OnEspExited(...) => EmitEspExiting()). So a replayed exit still arrived at the reducer as an ordinary, unmarked EspExiting. There it is decisive: HandleEspExitingV1 passes espFinalExitInFlight: true for every arriving exit, and IsPostAccountSetupFinalExit orders exits by ingest ordinal rather than timestamp — so a historic exit replayed today reads as post-AccountSetup by construction. With state restored from the snapshot (AccountSetup entered, a genuine IME user session, desktop arrived) arm C of ShouldTransitionToAwaitingHello opens on the Device→Account handoff. The reducer is itself a completion gate and cannot tell the two apart, so the fix has to be upstream. The replay is therefore narrowed to what is provably safe: the Hello-wizard start (62404). That fact is conservative — it vetoes a premature "Hello is disabled" skip and can never by itself complete a session, so replaying it can only make the agent wait longer. It is also the observation session 772fe502 wrote the replay for. A 62407 is no longer raised at all, neither exit nor failure: the exit because Windows writes the identical description for the intermediate and the final one and nothing downstream can order it, the failure because re-injecting it as fresh can fail a session that recovered on retry (ANALYZE-ESP-006). Skipped records are reported once as an agent_trace with counts and bounds — silently dropping evidence is how this class of bug survives. The cross-layer proof is now a pair, as the review asked: ClassicEspExitingOnRestoredStateTests pins that the reducer opens on any arriving exit (including one with an hour-old source timestamp), and ShellCoreTrackerReplayScopeTests pins that the replay never produces one. Dead machinery removed with it: EspExitedEventArgs.IsBackfill, allowEspExit, the newest-exit selection, the IsBackfill return. The method is now called BackfillRecentHelloWizardStart. One pre-existing integration test drove EspExited through HandleBackfillRecord because the replay is easier to call than the event-log watcher, and its class doc claimed that was the production path — it never was, the replay had no caller when that test was written. Switched to the live path and the doc corrected. Agent V2.Core 2361, DecisionCore 538, Backend 4287, all green. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
ec945a93d2 |
fix(agent): bar replayed ESP exits from the completion gate entirely
Round-three review, confirmed. The previous round stopped a replayed exit from arming the DEFERRED re-check but left the immediate synthesis attempt right below it untouched. My justification for that — "pre-existing behaviour" — was simply wrong: the immediate attempt on a replayed exit never existed, because the backfill had no caller. I introduced it, and by moving the replay behind the IME state restore I made the dangerous state reachable by design: if the newest 62407 in the window is only the intermediate DeviceSetup→AccountSetup exit while the restored app states are already terminal, that attempt opens the strong AccountSetup gate before the final exit has even happened. A test was pinning the faulty assumption as expected behaviour. A replayed exit now returns before touching completion at all — no deferred edge, no immediate attempt. Nothing available to it can establish its position in time: Shell-Core writes the identical description for both exits, and every probe that could order it reads state as it is now rather than at the event's time. The replay keeps the job it was written for in session 772fe502 — feeding EspExiting, FinalizingSetup and the Hello wizard to the reducer for the window in which no agent process was observing. Completion is the exclusive business of exits this agent watched live. With the synthesis attempt gone, the replay no longer needs restored app states, so it moves back into EspAndHelloHost.Start(); that also keeps this host's startup independent of the IME host's. ImeLogHost.onStateRestored goes back to doing just the re-check, which is still needed for a live exit observed before the restore finishes. The test asserting the faulty assumption is inverted, plus coverage that the replay is not neutered (EspExiting still forwards) and that a live exit following a replayed one completes normally. Agent V2.Core 2364, DecisionCore 533, Backend 4287, all green. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
b36cf8ff36 |
fix(agent): order the ESP-exit replay by evidence, size its window by measurement
Round-two review, both findings confirmed against the code. A replayed exit was being confirmed as post-AccountSetup by reading the registry at replay time. That read reflects NOW, not the event's time, so an agent that was down across the DeviceSetup→AccountSetup transition would confirm the stale intermediate exit and, once the apps settled, synthesise a completion while the AccountSetup page was still up. Windows offers no way to tell the two apart from the event itself — Shell-Core writes the identical description CommercialOOBE_ESPProgress_Page_Exiting for both. So the deferred edge is now armed by LIVE exits only (EspExitedEventArgs.IsBackfill): only for those is the continuous-observation argument valid. A replayed exit keeps the single immediate synthesis attempt it always had. For that attempt to be worth anything it must see real data, so the replay moved out of EspAndHelloHost.Start() into ReplayEspExitBackfill(), invoked from ImeLogHost.onStateRestored — after LoadState() has restored the package states. That is what makes "the agent came back long after the ESP finished" recoverable. The five-minute window for non-reboot restarts was unsound: the scheduled task carries a BootTrigger only, with no restart-on-failure, so after an exception_crash the agent does not return until the next boot — hours later, long after Windows wrote the final exit. The window is now measured rather than guessed: back to the last moment the previous run is known to have been alive, taking the wider of the snapshot mtime (rewritten every decision step, available for every exit type) and LastBootUtc. The five-minute default is the floor, ClampLookbackMinutes the ceiling, and a future timestamp from clock skew is ignored instead of producing a negative window. Also switched the backfill tests to the real Shell-Core wording — the invented string matched the regex but hid the fact that both exits look identical. 9 new tests. Agent V2.Core 2362, DecisionCore 533, Backend 4287, all green. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
c6ee7808f1 |
fix(agent): scope the ESP-exit replay to restart recovery only
Follow-up to the review hardening: the replay ran on EVERY agent start with a 5-minute window, first run included. On a first run there is no gap to recover — no earlier agent process existed — and a 62407 already sitting in the Shell-Core log belongs to a phase transition this agent was never meant to observe. Replaying it pushes an EspExiting into the reducer that the pre-fix agent never saw, i.e. a happy-path behaviour change for no gain. ResolveEspExitBackfillLookbackMinutes now keys the window to the previous exit type: first_run (or unknown) disables the replay entirely, clean/hard_kill/ exception_crash keep the 5-minute default because the process is back within seconds, and reboot_kill widens to "since LastBootUtc" because that gap spans the forced restart until the post-reboot logon. The result is still clamped to BackfillLookbackMaxMinutes, so policy and backstop together cannot build an unbounded event-log query. With this the happy path is byte-identical to pre-fix behaviour: on a first run nothing replays, no edge is remembered, and the deferred re-check returns on a volatile bool read. No other backfill is affected — the Hello, MDM-reboot, Windows-Update and ModernDeployment trackers each own a private backfill invoked from their own Start(), none of which this change touches. 13 policy tests including clock skew across the reboot, a missing boot timestamp and the clamp backstop. Agent V2.Core 2353, DecisionCore 533, Backend 4287. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
b61d7023bc |
fix(agent): harden post-reboot completion recovery after review
Three real regressions, all confirmed against the code before touching it — two of them introduced by the recovery itself. The remembered exit edge was unqualified. Shell-Core raises 62407 at every ESP phase transition, so OnEspExited also latched the DeviceSetup→AccountSetup one; the deferred re-check would then open the strong AccountSetup gate as soon as the last user app settled, while the AccountSetup page was still up and its other subcategories still running. IsConfirmedPostAccountSetupExit() now demands positive evidence (AccountSetup activity observed, or SkipUser == true) and is deliberately stricter than the existing IsIntermediateDeviceEspExit() forward guard: an unknown SkipUser counts as not confirmed. Erring strict costs the pre-fix stall; erring loose costs a premature Succeeded. The immediate on-edge synthesis keeps its previous behaviour verbatim. The replay picked the oldest exit, not the final one. The reader walks oldest-first and the exit branch is fire-once, so the widened downtime window — which routinely holds both the intermediate and the final exit — handed over the intermediate one and the fire-once guard swallowed the exit that matters. ReplayBackfillRecords buffers the batch and lets only the newest exit-matching record through; ESP failures and the Hello-wizard rail keep replaying chronologically. The replay ran before the IME state restore. ImeLogTracker.Start() restores the persisted package states via LoadState() without raising OnAppStateChanged, and ImeLogHost starts after EspAndHelloHost, so the replay evaluated an empty app list — leaving the sharpest case unfixed: apps already terminal before the reboot, only the final exit lost to the downtime. ImeLogHost gained an onStateRestored callback wired to the re-check, and the host order is now pinned by DefaultComponentFactoryOrderingTests so a reshuffle fails loudly. 11 new tests. Agent V2.Core 2340, DecisionCore 533, Backend 4287, all green. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
e1b03de5b3 |
fix(agent): recover ESP completion after a forced mid-ESP reboot
Seven sits-d Cloud PCs enrolled the same day with identical configuration (~138 required apps at the user-ESP gate, DeviceGuard/DmaGuard device-assigned). The two that finished AccountSetup before the coalesced reboot landed succeeded; the five the reboot killed in Stage=EspAccountSetup never derived a completion again and ended Incomplete on healthy devices. Two defects behind that: ShellCoreTracker.BackfillRecentEspExitEvents() has existed since session 772fe502 but no caller ever invoked it, so the Shell-Core watcher only saw records written after Start(). Every agent restart silently dropped an ESP exit (62407) or Hello-wizard start (62404) from its downtime. EspAndHelloHost.Start() now calls it, and the fixed 5-minute window is sized to the actual downtime when the previous run died to a reboot (LastBootUtc, clamped to 360 min). The user-apps-settled synthesis ran only on the EspExited edge. When the ESP page exits with apps still in flight the single attempt misses and nothing re-checks — 8110e262 and a89aac2d both reached 138/138 apps, 0 failed, and still never completed. The exit edge is now recorded and the synthesis re-evaluated from ImeLogTracker.OnAppStateChanged. No new completion path: the gate conditions are unchanged, this only gives the existing conservative check a second look. The fire-once claim moves to Interlocked (edge and re-check run on different threads) and the re-check does not re-emit app_install_starved. Rules: ANALYZE-ESP-005 gains on_event:session_stalled so a hung session produces a finding at the 60-minute stall instead of staying finding-less for hours (verified against 8110e262 — fired, confidence 95). New ANALYZE-APP-016 turns the already-emitted app_install_starved events into a customer finding and, unlike ANALYZE-APP-015, fires even on a succeeded session: 3b9291aa was recorded Succeeded with 99 required apps that never started installing. ANALYZE-APP-015 and ANALYZE-ESP-001 were considered for the stall trigger and deliberately left alone — APP-015 is the documented worked example of the terminal-precondition trap, and ESP-001's phase_duration gate on a still-open phase is unverified mid-run. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
25bac34acb |
fix(ci): upload raw bytes, not a stringified array
The first CI run overwrote all four customer-facing scripts on the download
host and the legacy mirror with their own byte values as decimal text
(Install-AutopilotMonitor.ps1 went from 27331 to 95679 bytes).
PowerShell unrolls an array returned from a function into the output stream, so
the caller receives Object[] rather than byte[]. Invoke-RestMethod serialises an
Object[] body as space-separated decimals. Every hash check stayed green because
[byte[]] parameter coercion repairs the type everywhere except on the wire.
build-agent.yml's Send-Blob was never affected: it read the bytes inside the
function. Proven with a local HttpListener: the old shape puts 50 bytes starting
54,48,32,51,53 ("60 35 13 1...") on the wire where 17 bytes starting 60,35,13,10
("<#\r\n.SYN") are expected.
Fixes:
- ',' on both byte-array returns, plus an explicit [byte[]] at the upload site.
- Read each blob back from storage right after its PUT and abort before the
mirror and the remaining files. Verifying only at the end left every blob
corrupted.
- The version-bump guard tolerates an unparseable published copy with a warning
instead of throwing: that is the state a republish exists to repair, and a
guard that blocks the repair is worse than no guard.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
c0be389a95 |
ci(scripts): version-bump guard compares behaviour, not bytes
Byte comparison made the guard fire on comment fixes, typos in comments and
reflow -- changes no consumer of the version can act on, so forcing a bump for
them is friction without a payoff.
The guard now compares a code fingerprint: the PowerShell token stream with
comments, newlines and line continuations removed. A behaviour change under an
unchanged $ScriptVersion still fails; comment and formatting changes publish
with a warning. That is also the rule the source file's own comment on
$ScriptVersion already states ("bump on meaningful changes").
Tokenising rather than regex-stripping keeps a '#' inside a string literal from
being mistaken for a comment.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
91f9d656ae |
ci(scripts): publish customer scripts on push, decoupled from the agent release
The scripts behind download.autopilotmonitor.com/agent/ rotated only on a publish_as_stable run of build-agent.yml, so a bootstrap fix sat unpublished until the next agent cutover -- while the WDP bootstrap MSI downloads the published copy on every enrolling device. Publish-BootstrapScripts.ps1 becomes the single owner of the chain: dev render with anchor check, $ScriptVersion bump guard, uploads (eu + fail-soft legacy mirror, no-cache), the two version oracles, and verification through the alias. publish-scripts.yml calls it on every push to main touching the sources; the publish_as_stable branch of build-agent.yml calls the same code, so a release still lands a consistent set. Blobs are published in a deterministic CRLF/UTF-8-without-BOM form. Git normalises line endings on checkout, so raw working-copy bytes would differ per environment and make every byte comparison -- guard and verify alike -- depend on where the workflow ran. The form reproduces the currently published bytes exactly. The Pester (PS 5.1) and ASCII gates move into the reusable bootstrap-script-gates.yml and now block the upload; pester.yml keeps the pull-request view and drops its push trigger. AdminConfiguration .LatestBootstrapV2ScriptVersion gets exactly one writer. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
b1e7077d0a |
fix(agent): spool pending-bytes drift guard + WDP backstop fallback test
Codex findings on d8c84f41/a369ff99: (1) MarkUploaded now recomputes _pendingBytes from the pending list when accounting drifts negative or stays non-zero on an empty list — a silent negative would disarm the spool-pressure trigger. (2) New test covers the DevicePrepCompletion handler''s AwaitingDesktop fallback (stale/replayed timer without a desktop fact): synthetic Hello timeout, park, and completion on the later real desktop arrival. Third finding (HashingCappedReadStream maxBytes:0) refuted — the 0=uncapped contract is implemented in the same class and exercised by the heal-path tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>v2.0.1408 |
||
|
|
d8c84f41c1 |
fix(agent): spool pressure trigger on pending backlog, not file size
99/100 spool_pressure_detected events in the field were file-size-only triggers on a near-empty backlog (append-only spool.jsonl grows past 5 MB in healthy long sessions). TelemetrySpool now tracks PendingBytes (exact serialized size of the not-yet-uploaded tail, maintained on enqueue / mark-uploaded / restart rehydrate) and the collector thresholds on that instead of SpoolFileSizeBytes, which stays as an informational metric. Snapshot gains spool_pending_bytes; pressure event data carries pendingBytes + pendingBytesThreshold. Also: startup recovery scan streams via File.ReadLines instead of materializing the whole spool with File.ReadAllLines (corrupt-tail break semantics unchanged). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>v2.0.1407 |
||
|
|
12b51bbc24 |
feat(landing): scenario coverage chips in hero + MSI channel in about claims
Hero gets a quiet checkmark chip row (user-driven, pre-provisioning, self-deploying, Device Preparation, Windows 365, Entra ID & Hybrid join); about page deployment claim now includes the MSI LOB channel for WDP. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |