Audit findings #4 (SessionSummary mapper x3) and #3 dual-write Phase 1.
- Collapse the two ~99%-identical SessionSummary mappers into one core mapper
taking the resolved sessionId (index vs primary differ only in that source).
- BuildSessionIndexEntity is now the single SessionsIndex projection manifest and
a SUPERSET of every field the merge call sites write, so a StartedAt-shift full
upsert can no longer drop a merged field (the recurring drift class, cf ab90423b).
- Fix two real bugs surfaced while grounding the design: ImeAgentVersion was never
written to the index although search/MCP push an OData filter against it (filter
matched nothing); UpdateSessionImeAgentVersionAsync now mirrors it. AdminMarkedAction
(dashboard list badge) was merged but missing from the projection -> now included.
- SyncSessionIndexAsync funnels the upsert-vs-merge + IndexRowKey-resolution decision
that was duplicated across the 3 StartedAt-shift branch sites.
- Correct the DeletionState doc-comment (it is primary-only, not index-surfaced).
The SessionsIndex is a full denormalized mirror of Sessions (different RowKey sort)
serving list/search/stats/raw directly; lean-drop would break /api/raw/sessions.
Backend build 0 warnings; full xUnit suite 2172/2172 (incl. new index-projection +
ImeAgentVersion-mirror tests). BuildSessionIndexEntity made internal for the test.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Hourly TimerFunction queries Azure Monitor for SignalR ConnectionCount (max
over last hour) and MessageCount (sum since UTC midnight) and emits Warning
(>=80%) / Error (>=95%) OpsEvents when the free-tier limits (20 connections,
20k messages/day) are approached. Mirror of the cert-expiry pattern - global
TenantId=null events, dedup'd to one emission per EventType per UTC day.
Fail-soft: skips silently when SignalRResourceId app setting is unset, logs
warning on Azure Monitor auth/query errors. Function App's managed identity
needs 'Monitoring Reader' on the SignalR resource.
Also called from the manual maintenance trigger so 'Run Maintenance Now'
exercises every health check.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Re-applies 7e21591b after the chain-validation outage and adds the missing
trust anchors. The first attempt embedded only the 2030 root and the 2026
intermediate; current production device certs chain via the 2026 root +
the 2028 intermediate, so chain.Build() returned "unable to get local
issuer certificate" for every legitimate agent and broke enrolment.
What is different in v2:
- Bundle expanded with both Intune roots (exp 2026-08-12 and 2030-09-15)
and both currently-issuing MDM Device CA intermediates (exp 2026-05-05
and 2028-09-15). Real device certs chain via 2028-intermediate -> 2030-
root today; the 2026 generation is kept for any older device certs
still in flight.
- Loader now globs intune-root-*.pem and intune-intermediate-*.pem from
embedded resources and dedupes by thumbprint, so a future Intune root
rotation is a drop-in PEM with no code change. Legacy generic-named
files (intune-root-ca.pem, intune-intermediate-ca.pem) are duplicates
of the new ones and harmlessly de-dup at load.
- Adds a happy-path integration test against a real Intune-issued device
cert (public-key only, copied from a test VM). This is the regression
test that would have caught the v1 outage and gates any future bundle
drift in CI.
- Fixes a pre-existing TZ bug in the expiry check: X509Certificate2's
NotBefore/NotAfter return Local time, were being compared raw against
DateTime.UtcNow. Surfaced by the integration test on a CEST dev box;
unobservable in prod because Linux Flex Consumption runs in UTC.
Trust model is unchanged from v1: X509ChainTrustMode.CustomRootTrust
pinned to the embedded Intune roots, OS trust store ignored, fail-closed
when no roots are loaded. Self-signed-with-Intune-DN bypass remains shut.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reverts:
- 8dacce5c "chore(backend): bump optional NuGet patches + fix SYSLIB0057"
- 2f68fe69 "chore(backend): upgrade Functions from .NET 8 to .NET 10 isolated"
After deploy succeeded and the Flex Consumption stack was switched to
.NET 10 Isolated in the portal, the dotnet-isolated worker died on every
boot with: dotnet exited with code 150 (FrameworkMissingFailure) and
"Exceeded language worker restart retry count for runtime:dotnet-isolated".
The deployed package was correct (TFM net10.0, FrameworkReference 10.0.0,
all package bumps applied) and the runtime stack property was set to
dotnet-isolated 10.0, but the .NET 10 runtime is not present on the
Flex Consumption stamp in West Europe even though Microsoft lists
DOTNET-ISOLATED|10.0 as supported. Restart and stop+start did not help.
Recovery sequence:
1. Stack switched back to .NET 8 Isolated (manual portal action).
2. Re-deployed last known-good net8.0 commit 637bd9a1 via temp branch
rollback-net8 (now safe to delete).
3. /api/health back to 200.
Follow-ups:
- Re-apply security fixes (DataProtection 10.0.7 pin GHSA-9mv3-2cwr-p262,
SYSLIB0057 X509CertificateLoader migration) on net8.0 in a separate PR.
- Open Microsoft support: ".NET 10 Isolated worker on Flex Consumption
WestEurope fails with FrameworkMissingFailure despite stack listed as
supported."
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Consumes the telemetry-index-reconcile queue (M5.d.2) and fans each
envelope out into 0–3 index-table rows via IIndexTableRepository
(M5.d.1). Closes the producer→consumer loop; once the
EnableIndexDualWrite flag is flipped, primary commits drive
eventual-consistent index upserts asynchronously.
Components:
- Microsoft.Azure.Functions.Worker.Extensions.Storage.Queues 5.5.4
package for the isolated-worker QueueTrigger binding.
- IndexReconcileHandler (plain class — not interface-abstracted per
Plan §M5.d design: DAL abstraction lives at IIndexTableRepository,
not here). Routing:
Signal → SignalsByKind (always, 1 row).
DecisionTransition:
IsTerminal=true → SessionsByTerminal
Taken=true → SessionsByStage
Taken=false + reason → DeadEndsByReason
ClassifierVerdictId!=null → ClassifierVerdictsByIdLevel
Up to 3 rows per DecisionTransition envelope (a Taken+terminal+
classifier transition fans to SessionsByTerminal + SessionsByStage
+ ClassifierVerdictsByIdLevel).
- IndexReconcileFunction thin QueueTrigger wrapper
(Connection = AzureTableStorageConnectionString). Malformed JSON
logged + dropped to avoid poison-queue retry loops; transient
repo errors propagate so the platform retries.
- DI: handler registered as Singleton in Program.cs.
Failure semantics:
- Required-field-missing envelopes (Signal without SessionSignalOrdinal
or SignalKind; DecisionTransition without StepIndex) → log warn,
drop. These won't become valid on retry — avoiding the retry loop.
- Transient Table Storage errors → rethrow. QueueTrigger platform
retries up to 5 times by default, then moves to
telemetry-index-reconcile-poison for manual investigation.
Deferred to M5.d.4:
- SessionsByStage stale-row pruning: when a session moves from
Stage-X→Stage-Y, the old Stage-X row stays. Either query-side
dedupe (per SessionId, max-Ticks RK) or reconcile-timer pruning
handles this. Deliberately not solved in the handler to avoid
a query+delete round-trip per Taken transition.
- End-to-end dual-write verification tests (Azurite harness) are
the M5.d.4 release gate before the flag is flipped.
Tests (+10 in Functions) via capture-only FakeIndexTableRepository:
- Signal happy path (1 SignalsByKind row).
- Signal missing SessionSignalOrdinal / SignalKind → 0 rows (x2).
- DecisionTransition Terminal+Taken+Classifier → 3 rows.
- DecisionTransition Taken non-terminal no-classifier → 1 row
(SessionsByStage only).
- DecisionTransition dead-end with classifier → 2 rows
(DeadEndsByReason + ClassifierVerdictsByIdLevel).
- DecisionTransition dead-end without reason → 0 rows (PK-collision
defense).
- DecisionTransition missing StepIndex → 0 rows (all paths gated).
- Unknown SourceKind → 0 rows.
- Null envelope → 0 rows.
Full Functions suite: 682 green (prev 672 + 10 new). Full solution
build green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the async fan-out producer path from Plan §2.8 / §M5.d so primary
Signals + DecisionTransitions rows can be projected onto their index
tables without blocking the hot ingest loop.
Components:
- Constants.QueueNames.TelemetryIndexReconcile (azure-queue-name-safe,
all lowercase + hyphens).
- IndexReconcileEnvelope (Shared DTO, flat shape with nullable fields
per SourceKind="Signal"/"DecisionTransition"). Avoids polymorphic
Newtonsoft deserialization — see M5.c.3 finding.
- IndexReconcileEnvelopeFactory pure static helper (FromSignal /
FromDecisionTransition / BuildBatch) — state-less projection.
- IIndexReconcileProducer (Shared interface) contract: no-op when
the feature flag is off; never rethrows underlying-queue failures.
- AzureQueueIndexReconcileProducer (Functions) — Base64-encoded JSON
messages, lazy CreateIfNotExistsAsync guarded by Interlocked flag,
Managed-Identity-or-connection-string construction mirroring
BlobStorageService / TableStorageService.
- IngestTelemetryFunction ctor gains IIndexReconcileProducer; after
primary Signals + DecisionTransitions commit it builds envelopes
and enqueues them. Event path is untouched (events don't feed the
index tables per §2.8).
- Program.cs registers the producer as a singleton.
Feature gate (AdminConfiguration.EnableIndexDualWrite, default false):
- New bool field in AdminConfiguration.
- TableConfigRepository Store + Map extension (per
feedback_table_storage_serialization).
- Producer checks config each call via AdminConfigurationService's
5-min memory cache. Flag off → primary behaviour is bit-exact.
Fault tolerance: queue-side exceptions (network, throttling, missing
queue before first CreateIfNotExists) are logged per envelope and
swallowed. The primary row is the source of truth; the 2h reconcile
timer (M5.d.4) is the safety-net recovery path.
Tests (+11 in Functions):
- 5 IndexReconcileEnvelopeFactoryTests: Signal/DecisionTransition
projection, dead-end preservation, batch ordering, null/empty.
- 3 IndexReconcileEnvelopeSerializationTests: Newtonsoft round-trip
(Signal, full DecisionTransition, minimal-envelope tolerance).
- 3 AdminConfigurationEnableIndexDualWriteTests: default-false on
new config + CreateDefault, persists when set.
Deferred to later sub-milestones:
- M5.d.3: queue-triggered consumer that maps envelopes to 0–3 index
rows using M5.d.1 TableIndexRepository.
- M5.d.4: 2h reconcile timer + release-gate dual-write verification
tests, then flip EnableIndexDualWrite=true in prod admin config.
Full Functions suite: 672 green (prev 661 + 11 new). Full solution
build green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New netstandard2.0 project src/Shared/AutopilotMonitor.DecisionCore/ with
14 scaffolding types across Signals/, State/, Classifiers/, Engine/.
Agent.Core and Functions reference the library without call-sites; the
full solution builds 0 errors, 0 warnings. DecisionEngine.Reduce is an
intentional NotImplementedException stub — M3 fills in the partial-class
reducer.
Invariants wired into the scaffolding:
- Immutable DTOs (sealed + get-only + With-methods) per L.3
- Pflicht-Evidence validation in Evidence ctor per L.5
- KindSchemaVersion enforcement per L.6
- AssemblyVersion 2.0.0.0 as ReducerVersion source per §2.10
Architecture principles L.1–L.18 extracted to
.claude/architecture-principles.md as persistent Claude-session context.
Plan: plans/REFACTOR_AGENT_V2.md §4 M1. Zero runtime risk.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
## Rules as GitHub single source of truth
- Add 75 individual JSON rule files (2 gather, 18 analyze, 55 IME patterns)
under rules/gather/, rules/analyze/, rules/ime-log-patterns/
- Add JSON Schemas for all three rule types with VS Code IntelliSense support
- Add CI combine script (rules/scripts/combine.js) + GitHub Action that
validates schemas with ajv-cli and generates rules/dist/ combined files
- Convert BuiltIn*.cs from hardcoded C# objects to embedded JSON resource loaders
- Add GitHubRuleRepository service to fetch dist/ files from raw.githubusercontent.com
- Add ReseedFromGitHubFunction: POST /api/rules/reseed-from-github?type=all|gather|analyze|ime
## IME Log Patterns
- Extend ImeLogPattern model with Description and IsBuiltIn properties
- Add ImeLogPatterns table + API constants
- Add TableStorageService CRUD methods for IME log patterns
- Add ImeLogPatternService (CRUD, merge, auto-seed from embedded JSON)
- Add ImeLogPatternsFunction: GET/PUT /api/rules/ime-log-patterns (Phase 1: read-only
for tenants, Galactic Admin can enable/disable/edit globally)
- Add IME Log Patterns management page (/ime-log-patterns)
- Wire GetAgentConfigFunction to use ImeLogPatternService instead of static class
## API route consolidation
- All rule endpoints now under /api/rules/* for single certificate exclusion path:
/api/rules/gather, /api/rules/analyze, /api/rules/ime-log-patterns,
/api/rules/reseed-from-github
## Export buttons
- Add per-rule and export-all JSON download buttons to gather-rules,
analyze-rules, and ime-log-patterns pages
## Admin configuration
- Replace old individual reseed buttons with unified "Fetch & Reseed from GitHub"
(calls new GitHub endpoint) plus compact individual-type reseed buttons
- Add CDN cache hint: GitHub raw.githubusercontent.com caches up to 5 minutes
## Preview mode
- ?preview=1 URL param seeds localStorage.previewMode (persists across navigation)
- ?preview=0 clears it; toggle in Navbar settings dropdown (indigo, eye icon)
- IME Log Patterns nav link gated behind galacticAdminMode OR previewMode
- Preview badge shown on nav link when active for normal users
## Tests
- Add xUnit test project (net8.0) with 5 tests for embedded resource loading,
known IME actions validation, and valid categories check
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>