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>
7.7 KiB
type, title, description, resource, tags, timestamp
| type | title | description | resource | tags | timestamp | |||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| concept | CMTrace Time Resolution — Per-Line Self-Anchoring | How the agent converts bias-less CMTrace local times to UTC without trusting any process's timezone belief — each provably fresh line anchors its own offset against the agent clock on the 15-minute grid; everything not provably fresh falls back uniformly and says so. | src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Enrollment/Ime |
|
2026-08-20 |
Problem
A CMTrace line carries local time and no offset — IME 1.104 writes DateTime.Now.TimeOfDay
in both of its trace listeners. Converting such a line with the reader's own
TimeZoneInfo.Local silently assumes the writing and reading processes agree on the zone.
They do not: each caches its zone at process start and follows neither a later tzutil nor
a Windows auto-timezone change. The error is Offset_writer − Offset_reader — zero only
when both believe the same thing, right or wrong. Field measurement over 11,068 sessions
found 26 sessions with a real error, from +1 h to −17 h.
Two designs failed before this one:
- Fix the reader's belief (
TimeZoneInfo.ClearCachedData, live OS offset): makes the agent isolated-correct and thereby breaks the majority of sessions, where two equally stale beliefs cancel. - One measured offset per file (agent 2.0.1410, reverted in
04b1a7c6): a single log file holds lines from multiple writer eras.AgentExecutor.logis written by short-lived child processes whose zone belief flips per process — interleaved in one file, and flipping back (fixture:tests/fixtures/cmtrace-logs/agentexecutor-two-writer-eras-v1.cmtrace). Any cross-line anchor — per file, or nearest-in-write-order — applies one era's offset to the other era's lines. In the field this shiftedscript_startedevents by −9 h against their correct completions.
Schema
The resolution rule, in order:
-
Writer-declared bias (
+480suffix): authoritative, used as-is. Never a calibration anchor — it needs no measurement and must not overwrite one. -
Per-line self-anchoring — only for lines read in a provably fresh pass:
offset_line = round((local − agentUtcNow) / 15 min) · 15 min lineUtc = local − offset_lineEvery real UTC offset is a whole multiple of 15 minutes, so the rounding absorbs poll and flush latency. No cross-line state exists, so interleaved eras resolve line by line — the property the fixture test pins. The line's own timestamp still contributes sub-poll precision and ordering (two lines 6 ms apart stay 6 ms apart; a plain
occurredAt = nowcould not do that). Guards: grid residual ≤ 2 min, offset within UTC−12 … UTC+14. -
Uniform reader-zone fallback for everything else: wrong in absolute terms whenever the writer held a different belief, but self-consistent — both ends of a duration are wrong by the same amount, so derived durations stay right. The revert's core lesson: partially corrected is strictly worse than uniformly wrong.
Freshness is what makes rule 2 sound, and it is strict:
- Every poll look at a file stamps
LastCheckedUtc(LogFilePositionTracker.MarkChecked), including "no new data" and an empty first sight — so a file first seen empty (AgentExecutor.logappearing mid-enrollment) anchors from its very first content. - A pass's lines are fresh only if the file was previously observed in this process and
the gap since the last look is ≤
FreshLineMaxAge(30 s — far above the 100 ms poll, far below the 13-minute zone where a line's age could round onto the offset grid). - A restart-restored position bookmark deliberately carries no freshness: the first pass after a restart reads downtime backlog and must never anchor.
Provenance (sourceOffsetOrigin in event DataJson): bias | line-anchored |
reader-zone-fallback (plus retired calibrated from 2.0.1410 events). The applied
offset is always recorded (sourceOffsetMinutes), the raw local time too
(sourceLocalTs), so any event can be re-derived later. The per-file measurement still
runs, purely observationally (measuredWriterOffsetMinutes, Info log when it disagrees
with the process zone).
Constraints
- An anchored line resolves to
now ± 2 minby construction. The accepted edge: a freshly written line carrying a replayed old timestamp whose age is exactly N×15 min ± 2 min anchors to ≈now instead of its replayed past — bounded error, and sub-24 h replays are treated as current by the historic-replay guard anyway. - Snapshot readers (
StallProbeCollector,LogParserCollector) cannot tail and therefore cannot prove freshness — they stay on the marked fallback. This is a documented limit, not an omission; sharing the per-file measurement with them would re-apply exactly what the revert removed. - Mixed pairs remain possible across a restart (start resolved from backlog fallback, completion line-anchored) and are recognizable via provenance.
Backend regression tripwire
The backend watches for the failure mode this design eliminates. On every terminal ingest
batch, the counter reconcile's existing event-partition scan additionally collects
Δ = ReceivedAt − OccurredUtc per event, split by Source == "ImeLogTracker" versus the
rest (CmTraceSkewTripwire, hooked in EventIngestProcessor). If
median(Δ_IME) − median(Δ_other) is a clean 15-minute multiple (residual < 2 min — the
same constants as the anchoring grid guard above) with ≥ 20 samples over ≥ 3 distinct
upload batches per side, a CmTraceTimeSkewRegression ops event fires (category Agent,
warning), carrying all numbers plus the session's sourceOffsetOrigin histogram.
Bias-dominated sessions are suppressed (writer-declared offsets cannot be an anchoring
regression); measuredWriterOffsetMinutes is never consulted (sticky after era
flip-backs). Kill switch: app setting CmTraceSkewTripwireDisabled. Goal state: the event
never fires — any hit is an anchoring case the per-line design misses, or a detector bug.
Device-clock problems (the customer-actionable cousin) are covered separately by the
clock_skew analyze condition behind ANALYZE-DEV-008, which excludes IME-derived events
precisely so an anchoring regression can never surface as a customer finding.
Citations
CmTraceOffsetCalibrator.TryMeasureOffset— the pure grid measurement, shared by anchoring and the observational per-file path.CmTraceSkewTripwire+CmTraceSkewTripwireTests— the backend tripwire above.RuleEngine.ConditionEvaluators.EvaluateClockSkewCondition+RuleEngineClockSkewTests— the customer-facing device-clock metrics (batch-median frames, spool-spread filter, plateau step detection, end-state persistence).ImeLogTracker.LogProcessing.csResolveEntryUtc— the resolution order above.ImeLogTrackerLineAnchoringTests— resolves-to-T across writer beliefs, interleaved eras in one chunk, freshness boundaries (first sight, empty first sight, poll gap, restart restore), the grid-replay edge, and the field fixture replayed at production poll cadence.CmTraceCalibratorFileIsolationTests— pins per-file isolation of the observational measurement (session e9753578 logged a cross-file pairing the committed code could not be shown to produce; the mechanism remains open, the resolution path is immune by design, andCalibrateFromlogs the anchor timestamp as a tripwire).tasks/todo.md(2026-08-20) — full evidence trail: field measurements, the revert, the two-writer-era fixture, and the disproven foreign-binary hypothesis (the SHA mismatch was the known release race against the 5-minute AdminConfiguration cache).