mirror of
https://github.com/okieselbach/AutopilotMonitor.git
synced 2026-10-24 20:53:06 +00:00
fbb338efcca5cd7255661d62d0c4ef84cf04326f
1284 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
fbb338efcc |
Merge pull request #57 from okieselbach/chore/mcp-dep-vulns
chore(mcp): fix Dependabot vulns (protobufjs critical, hono, vite, sdk) |
||
|
|
6bb7daa79d |
Merge pull request #56 from okieselbach/chore/web-dep-vulns
chore(web): fix Dependabot vulns (next/vite/picomatch/flatted/postcss) |
||
|
|
50c002712b |
chore(mcp): fix Dependabot vulns via npm audit fix + @anthropic-ai/sdk major bump
Resolves all 13 Dependabot alerts on src/McpServer/autopilot-monitor-mcp (1 critical, 4 high, 8 medium): npm audit fix (no --force) lifted these transitively, all within existing parent semver ranges: - protobufjs 7.5.4 -> 7.5.5 (CRITICAL: arbitrary code execution, GHSA-xq3m-2v4x-88gg, transitive via @huggingface/transformers -> onnxruntime-web; the over-pinned protobufjs override I had planned turned out to be unnecessary) - hono 4.12.9 -> 4.12.15 (6 advisories: serveStatic bypass, cookie name validation, IP matching, JSX SSR, toSSG path traversal) - @hono/node-server 1.19.11 -> 1.19.14 (serveStatic bypass mirror) - path-to-regexp 8.3.0 -> 8.4.2 (high: ReDoS x2) - vite 8.0.3 -> 8.0.10 (high x3, transitive via vitest) - postcss 8.5.8 -> 8.5.10 (transitive via vite) Also bumped: - @anthropic-ai/sdk ^0.80.0 -> ^0.91.0 (medium: Memory Tool path validation; we don't use the Memory Tool, but bumping anyway to clear the alert. Verified agents/query.ts still compiles -- only standard Messages API used: client.messages.create, Anthropic.Tool, Anthropic.MessageParam, Anthropic.ToolUseBlock, Anthropic.TextBlock.) Verified: - npm audit -> 0 vulnerabilities - npm run build (tsc) -> green - npm test gated on AUTOPILOT_API_TOKEN env var (pre-existing setup, not affected by this change) |
||
|
|
8a773a062f |
chore(web): fix Dependabot vulns via npm audit fix + postcss override
Resolves 6 Dependabot alerts on src/Web/autopilot-monitor-web (next high, vite high x3, picomatch high, flatted high, postcss medium): - next 15.5.12 -> 15.5.15 (closes GHSA-q4gf-8mx6-v5v3, GHSA-3x4c-7xq6-9pq8, GHSA-ggv3-7p47-pfv8 within ^15.1.6 range) - vite 8.0.3 -> 8.0.10 (transitive via vitest) - picomatch, flatted bumped via lockfile resolution - postcss devDep ^8.4.49 -> ^8.5.10 + "overrides" entry to force the copy bundled inside next/node_modules/postcss to >= 8.5.10 (npm refused override-only because the direct range overlapped the vulnerable window; bumped both) Verified: - npm audit -> 0 vulnerabilities - npm run build -> green - npm test -> 179/179 passed |
||
|
|
0305c0d635 |
test(v2): add UserDriven Hello-skipped fixture (anon df7abbc2)
Real-world V2 enrollment-session replay covering a UserDriven-v1 happy variant where Hello-for-Business was enabled but per-user enrollment resolved as "skipped" rather than "Success". Exercises the Hello+Desktop AND-gate's tolerance for any terminal Hello outcome. Asserts Stage=Completed, Outcome=EnrollmentComplete, HelloOutcome=skipped, ScenarioProfile.Mode=Classic (set by ImeUserSessionCompleted). README replaced with pointer to ScenarioTests.cs as source of truth — old "(empty)" placeholder dropped, only the new entry listed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
0e08af8f58 |
refactor(v2): extract AgentRuntimeHost + TerminationPipeline (PR7)
Final PR of the V2 Runtime-Host slice — Phase 7+8 of RunAgent (the EnrollmentOrchestrator-using block, the shutdown synchronisation primitives, the onIngressReady wiring, the post-startup decision-signal posts, the analyzer + probes startup, the shutdown.Wait + finally) moves into Runtime/AgentRuntimeHost.cs and Runtime/TerminationPipeline.cs. AgentRuntimeHost.Run takes the bundles produced by PR2-PR5 (BootstrapResult, BackendAuthBundle, RuntimeConfigBundle, TelemetryClientResult, SessionRegistrationOutcomeResult) plus the pre-computed paths and returns the agent's process exit code. The cadence-clamp helpers (Min/Max/Default Upload* constants + ClampUploadIntervalSeconds + ClampUploadBatchSize) move with it — they are an internal concern of the runtime host, not a Program-level configuration helper. TerminationPipeline.Run owns the finally-block contract: unsubscribe event handlers (Ctrl+C / ProcessExit / Terminated / ThresholdExceeded) to prevent re-entrant shutdown, run orchestrator.Stop, dispose the mTLS HttpClient and the BackendApiClient, then signal shutdownComplete LAST so any ingest-dispatcher thread parked in the ServerActionDispatcher terminate-callback only resumes once a fully-cleaned-up agent is visible. The Plan §6.2 synchronous-shutdown contract is preserved verbatim. RunAgent collapses to a linear ~75-line composer: Phase 1+2 → AgentBootstrap.Run, Phase 3 → BackendClientFactory.BuildAuthClients, Phase 4 → AgentRuntimeConfig.Resolve, Phase 5 → BackendClientFactory.BuildTelemetryClients, Phase 6 → BackendSessionRegistration.Register, Phase 7+8 → AgentRuntimeHost.Run. Every dead alias from the previous PRs is cleared; each phase is a single named call with a clear exit-code result. Program.cs: 1607 LOC (pre-PR1) → 441 LOC (after PR7). RunAgent body: ~770 LOC → ~75 LOC, fully linear. No behavioural change — every construction order, exit code, side effect and synchronisation contract is preserved exactly. Build clean (0 W / 0 E). Full V2.Core suite 766/767 (1 known RecoveryPathTests threading flake, passes solo). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
fb71fb1c9c |
refactor(v2): extract LifecycleEmitters + ServerControlPlane (PR6)
PR6 of the V2 Runtime-Host slice — the heaviest closure-capture block of RunAgent moves into two new factory classes plus the ServerActionSynthesis partial is folded into the new home. LifecycleEmitters (Runtime/LifecycleEmitters.cs): - EmitAgentStarted, EmitVersionCheckIfAny, EmitUnrestrictedModeAuditIfChanged - PostSessionStarted, PostAdminPreemption, PostSystemRebootObserved (decision-signal posters that previously lived as Program statics) - CreateMaxLifetimeEmitter / CreateAuthThresholdHandler — factory methods returning EventHandlers that close over a Func<Post> accessor so the watchdogs can be subscribed BEFORE the live InformationalEventPost is constructed in the orchestrator's onIngressReady hook (the trick that made the inline lambdas hard to read). ServerControlPlane (Runtime/ServerControlPlane.cs): - BuildDispatcher — constructs the live ServerActionDispatcher with its rotate_config / upload_diagnostics / terminate_session callbacks and the 60-second synchronous-shutdown wait - Wire — the EnrollmentOrchestrator.Transport.ServerResponseReceived handler that translates DeviceKillSignal / AdminAction / Actions[] into ServerActions - HandleAdminAction, MapAdminOutcome — moved from Program.cs and the Program.ServerActionSynthesis.cs partial (which is deleted) The terminate_session callback's synchronous-shutdown wait, the dispatcher's reentry short-circuit (Codex Finding 1) and the admin-outcome mapping (Codex Finding 2) are preserved verbatim with identical comments. RunAgent's onIngressReady block shrinks from ~135 lines to ~12 lines of factory invocations. Existing tests (AdminActionOutcomeMappingTests, HandleAdminActionTests, PostSessionStartedSignalTests) migrated to the new namespace (global::AutopilotMonitor.Agent.V2.Runtime.* instead of Program.*). New tests (LifecycleEmittersTests, 9 cases): max-lifetime suppress on null post, no-emit on non-max-lifetime reason, emit-on-max-lifetime, auth-threshold signalShutdown side-effect, agent_shutdown event emission, signalShutdown still fires when post is null, unrestricted-mode no-op gates (null/unchanged/changed). Build clean (0 W / 0 E). Full V2.Core suite 766/767 (1 known RecoveryPathTests threading flake, passes solo). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
79622515f0 |
refactor(v2): extract BackendSessionRegistration from Program.RunAgent (PR5)
Phase 6 (POST /api/agent/register-session with retry, outcome-based exit-code mapping, on-failure mTLS+API-client disposal) moves into Runtime/BackendSessionRegistration.cs. SessionRegistrationOutcomeResult is the discriminated outcome — Continue with the live SessionRegistrationResult (consumed downstream by PostSessionStartedSignal, PostAdminPreemptionSignal, WireTelemetryServerResponse, HandleAdminAction) or Exit with the V1-parity codes (6 = AuthFailed, 7 = anything else; the diag skill keys on the distinction in Scheduled-Task history). Disposal of mtlsHttpClient + auth.BackendApiClient on registration failure is preserved inside the factory, matching the original inline behaviour. RunAgent's Phase 6 block shrinks from ~20 lines to 8 lines (factory call + result destructuring). Tests: 4 new BackendSessionRegistrationTests (Exit-6, Exit-7, Continue, null-arg validation). Existing SessionRegistrationHelperTests cover the underlying retry/outcome behaviour. Full V2.Core suite 757/758 (1 known RecoveryPathTests threading flake, passes solo). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
6b804d38d9 |
refactor(v2): extract AgentRuntimeConfig from Program.RunAgent (PR4)
Phase 4 (RemoteConfigService.FetchConfigAsync, RemoteConfigMerger.Merge, AuthFailureTracker ceiling refresh, logger SetLogLevel, SelfUpdater hash propagation, BinaryIntegrityVerifier check + IntegrityCheckFailed emergency-report dispatch, BootstrapConfigCleanup H-2 mitigation) moves into Runtime/AgentRuntimeConfig.cs. RuntimeConfigBundle exposes the live RemoteConfigService (still needed later by the rotate_config ServerAction callback), the fetched AgentConfigResponse and the RemoteConfigMergeResult diff (consumed by the EmitUnrestrictedModeAuditIfChanged lifecycle hook). VerifyBinaryIntegrity and TryCleanupBootstrapConfig stay private to the factory — they are pure side-effect helpers with no return value needed downstream. The runtime-self-update trigger closure no longer captures Program.GetAgentVersion via implicit access; it receives the agent version as a parameter instead. RunAgent's Phase 4 block shrinks from ~70 lines to 7 lines (factory call + bundle destructuring). No behavioural change: ordering of side effects (Merge before UpdateLimits before SetLogLevel before SelfUpdater hash before VerifyIntegrity before BootstrapCleanup) is preserved exactly. Tests: 4 new AgentRuntimeConfigTests (RuntimeConfigBundle property mapping + 3 null-arg validation cases). Full V2.Core suite 753/754 (1 known RecoveryPathTests threading flake, passes solo). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
1785854ac7 |
refactor(v2): extract BackendClientFactory from Program.RunAgent (PR3)
Phase 3 (BackendApiClient + DistressReporter + EmergencyReporter + AuthFailureTracker + AuthCertificateMissing distress emit) and Phase 5 (mTLS HttpClient + BackendTelemetryUploader) move into Runtime/BackendClientFactory.cs. BackendAuthBundle holds the four clients plus the cached manufacturer/model/serial from HardwareInfo so RunAgent + Phase 5 read the same hardware identity. TelemetryClientResult is the discriminated outcome for Phase 5 — Continue with mTLS+uploader, or Exit with the V1-parity codes (4 = mTLS construction failure, 5 = uploader construction failure). Phase 4 (RemoteConfig fetch + merge + binary-integrity + bootstrap- config cleanup) stays inline in RunAgent for PR4. RunAgent reads the Phase 3 clients via local aliases so Phase 4 wiring is unchanged. RunAgent's Phase 3+5 blocks shrink from ~80 lines to ~12 lines of factory invocations + result destructuring. No behavioural change: constructor argument lists, distress-emit guard, exit-code mapping and the disposal-on-failure semantics are preserved. Tests: 4 new BackendClientFactoryTests (auth-bundle smoke, no-cert-lookup-on-bootstrap-auth, TelemetryClientResult.Exit, TelemetryClientResult.Continue). V2.Core 747/750 without the 3 known EnrollmentOrchestrator threading flakes (pass solo). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
9b9ad61315 |
refactor(v2): extract AgentBootstrap from Program.RunAgent (PR2)
Phase 1+2 of RunAgent (previous-exit classification, persisted-config load, TenantId resolution, AgentConfiguration build, SessionId persistence, completion-marker / emergency-break guards, optional --await-enrollment cert wait) move into Runtime/AgentBootstrap.cs. The static helpers (DetectPreviousExit, TryReadBootstrapConfig, TryReadAwaitEnrollmentConfig, DeleteAwaitEnrollmentConfig, CheckEnrollmentCompleteMarker, CheckSessionAgeEmergencyBreak, BuildAgentConfiguration) stay on Program (Program.Guards.cs) and are called via internal access from the new Runtime namespace. BootstrapResult is the discriminated outcome — ShouldExit + ExitCode for the V1-parity early-return paths (0 = guard handled, 2 = no TenantId, 3 = await timeout), or a Continue payload carrying AgentConfiguration, SessionIdPersistence, PreviousExitSummary, IsWhiteGloveResume and the cleanup-service factory. RunAgent's first ~95 lines collapse to a single AgentBootstrap.Run call + result destructuring. No behavioural change: every helper invocation, exit code and side effect (sessionPersistence.Delete on --new-session, IsWhiteGloveResume snapshot before GetOrCreate, DeleteAwaitEnrollmentConfig on cert success) is preserved. Tests: 4 new AgentBootstrapTests (marker-handled exit, age-emergency exit, continue-with-bootstrap-config, first_run classification); existing 18 ProgramGuardsTests still pass; full V2.Core suite 745/746 (1 known RecoveryPathTests threading flake). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
7abd286774 |
refactor(v2): split DefaultComponentFactory hosts into Hosts/ folder (PR1)
Mechanical extract: 10 nested ICollectorHost classes moved from DefaultComponentFactory.cs (1131 LOC) into individual files under Orchestration/Hosts/. Visibility flips from `private sealed` to `internal sealed`; namespace stays `Orchestration` so existing relative type references keep resolving without new usings. DefaultComponentFactory shrinks to ~280 LOC of pure composition. No behavioural change: all 11 host constructors, Start/Stop/Dispose contracts, and the factory's public surface (ImePackageStates, AllKnownPackageStates, ImeAppTimings, SetTelemetrySpool) are preserved verbatim. PR1 of the V2 Runtime-Host slice (Codex P2 finding); follow-up PRs will extract AgentBootstrap, BackendClientFactory, AgentRuntimeConfig, BackendSessionRegistration, LifecycleEmitters/ServerControlPlane, AgentRuntimeHost/TerminationPipeline from Program.RunAgent. Tests: V2.Core 741 pass + 1 known threading flake (passes on rerun); Agent.Core 507 pass; DecisionCore 232 pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
a775f3288b |
fix(v2): apply remote telemetry cadence + spool perf/observability
P1 — UploadIntervalSeconds and MaxBatchSize were merged into agentConfig by RemoteConfigMerger but never reached the orchestrator: drainInterval fell back to a hardcoded 30s and TelemetryUploadOrchestrator to its default batchSize=100. Pass both from agentConfig at construction with clamped bounds (5..300s, 1..500 items) and log the applied values. P2 step 1 — TelemetrySpool.Enqueue used FileOptions.WriteThrough + Flush(true) per item. Make fsync conditional on RequiresImmediateFlush so periodic snapshot items go through the OS cache only; backend dedupes via (PartitionKey, RowKey), so the worst-case loss on crash is a few non-lifecycle items. P2 step 2 — Replace File.ReadAllLines on every Peek with an in-memory LinkedList tail cache populated once in the ctor and kept in sync by Enqueue/MarkUploaded. Disk JSONL stays the source of truth for crash recovery and diagnostics ZIP forensics; the cache makes drain O(batchSize) instead of O(N). Telemetry — surface spool stats (pendingItemCount, peakPendingItemCount, fileSizeBytes, totalEnqueuedCount) in agent_metrics_snapshot, plumbed via DefaultComponentFactory.SetTelemetrySpool. Emit a one-shot spool_pressure_detected event with ImmediateUpload=true when pending crosses 2000 items or file grows past 5 MB, so upload-stall conditions become queryable on the backend without waiting for the next snapshot. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
1d42332c7b |
fix(v2): count BackendTelemetryUploader requests in NetworkMetrics
Insert a NetworkMetricsRecordingHandler at the head of the mTLS HttpClient pipeline so every outbound request through MtlsHttpClientFactory.Create (in particular BackendTelemetryUploader's POST /api/agent/telemetry, which dominates per-session traffic) is recorded on the shared NetworkMetrics instance. Previously only the legacy BackendApiClient calls (config + gather-rules + emergency) called RecordRequest directly, so agent_metrics_snapshot.net_total_requests undercounted by ~25-30x (observed: ~2/session in V2 vs ~58/session in V1). The handler is wired by passing backendApiClient.NetworkMetrics — the same instance later read by AgentSelfMetricsCollector — into the factory. No double-count: BackendApiClient builds its own HttpClient and is unaffected by the mTLS pipeline. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
fdc0e3e8ce |
fix(v2): normalize timeline event Source values + stop periodic collectors before diag ZIP
Source attribution review on session df7abbc2 surfaced four inconsistencies in the Source field on the Events table — assembly leakage, V1-parity carry-overs, and a CamelCase / snake_case mix on the same emitter. Normalize so every Source is a CamelCase class-name or the stable lifecycle label "Agent": - Program.cs `agent_started` — "AutopilotMonitor.Agent.V2" -> "Agent" (V1 parity, drops assembly leak) - ImeLogTrackerAdapter `script_completed` — "IME" -> "ImeLogTracker" (consistent with the seven sibling events emitted from the same adapter) - StartupEnvironmentProbes (5 sites: device_location, agent_trace, timezone_auto_set, two ntp_time_check shapes) — "Network" -> "StartupEnvironmentProbes" (class-name consistency; eventType already encodes the sub-probe) - EventTimelineEmitter default — "decision_engine" -> "DecisionEngine" (CamelCase, matches the override path used by phase_transition; eliminates the per-event mix on the wire) Tests adjusted: ImeLogTrackerAdapterTests, StartupEnvironmentProbesTests, EventTimelineEmitterTests, InformationalEventPostTests. EnrollmentEvent.Source XML doc updated to reflect the new convention. Same session also showed `performance_snapshot` (seq 220) firing AFTER `diagnostics_collecting` (seq 219), because periodic collectors keep ticking through the termination handler. Fix: - New EnrollmentOrchestrator.StopCollectorHosts() — idempotent; the existing Stop() now delegates to it so we never double-stop. - EnrollmentTerminationHandler gains an optional `stopPeripheralCollectors` callback and invokes it best-effort right before RunUploadDiagnosticsWithEvents (after DelayLateEventGrace). The WhiteGlove Part 1 path returns earlier and is unaffected — the agent stays alive for Part 2. - Program.cs wires `() => orchestrator.StopCollectorHosts()` so all collector hosts (Performance, AgentSelfMetrics, DeliveryOptimization, ESP/Hello, IME log, …) are stopped before the diag ZIP is built. Late `performance_snapshot` / `agent_metrics_snapshot` no longer slip in after the `diagnostics_collecting` event. Tests: V2.Core 732/732, DecisionCore 232/232, Backend 736/736. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
6937ff73b8 |
fix(v2): start drain pump at Start() step 3b instead of after collector boot
Symptom: Web UI showed register-session, then ~6.7s of silence, then a wall of ~100 events at once — visibly different from V1's streaming feel. Confirmed on session 3808befb-…: agent_started enqueued at 18:20:20.5, first BackendTelemetryUploader flush at 18:20:27.2 with firstId=0 lastId=99 (a full default 100-item batch). Root cause: the periodic drain pump was started at Start() step 15, after collector hosts had spun up. Steps 4-14 do meaningful synchronous work — gather-rule compile, Hello event-log subscriptions, PowerShell runspace open in DeliveryOptimizationCollector, ipinfo.io GeoLocation, analyzer scheduling — which on a constrained VM kept the calling thread (or the ThreadPool) busy for ~6.7s. During that window the spool accumulated 100 startup signals, saturating the batchSize cap. Even though immediate-flush wakeups did fire correctly, they only set a TaskCompletionSource that nobody was awaiting yet. Fix: move the drain pump start to step 3b, directly after the spool and the immediate-flush bridge are wired (step 3 + 3a). The pump's loop only references _spool / _transport (both built at step 3) and the wakeup TCS (step 3a) — no collector-state, classifier-state, or ingress dependency — so an early start is safe. An empty Peek returns OK(0) without an HTTP round-trip, so a pump tick that races ahead of any enqueue is a no-op. With the pump live from step 3b onward, the very first immediate-flush wakeup (e.g. agent_started in onIngressReady at step 13b) sets the TCS and the drain loop's WhenAny returns within milliseconds, then DrainAllAsync's peek-flush-peek loop drains whatever the spool has accumulated up to that moment in tight ~100-item batches. Tests: V2.Core 732/732 green; no behaviour-coupling on the old step 15 position. Solution build 0/0. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
4de2677a5a |
fix(v2): 882fef64 followup — restore V1 audit trail on enrollment_complete
V2's reducer-emitted enrollment_complete event landed in the backend with
empty Data{} — V1 had carried a signal audit trail (signalsSeen,
signalTimestamps, completionSource, helloOutcome) that downstream consumers
relied on to reconstruct "which signals drove completion" without re-reading
the agent's local final-status.json.
BuildEnrollmentCompleteEffect now takes (state, completionTrigger) and
attaches a structured TypedPayload. EventTimelineEmitter passes that dict
verbatim into EnrollmentEvent.Data, so the wire shape mirrors V1's audit
trail (plus enrollmentMode/JoinMode from the V2 ScenarioProfile).
Both kernel-driven completion paths now carry the trail:
- DecisionEngine.Classic: DeadlineFired:FinalizingGrace
- DecisionEngine.SelfDeploying: DeviceSetupProvisioningComplete
AdminPreemptionDetected keeps its existing adminAction/source params —
different completion semantics, leave alone.
Tests: DecisionCore 232 (+1), V2.Core 732, V1.Core 507, all green.
Solution build 0 warnings, 0 errors.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
8a9a9ffe82 |
fix(v2): restore V1-parity shadow download_progress on terminal app states
V2's ImeLogTrackerAdapter collapsed V1's parallel-channel emission into one event per state transition, dropping V1's shadow `download_progress` event on Installed/Error transitions. The Web UI's DownloadProgress panel relies on that shadow to close out apps that never produced byte-level progress. WinGet/Store apps (Company Portal, …) emit no `download_progress` ticks at all because they have no DO byte data — their row in the panel was stuck on "started" forever even though the install completed and InstallProgress showed it as installed. Restores V1 parity: when MapAppStateToEventType yields Installed/Error, also emit a shadow `download_progress` (Severity=Debug, ImmediateUpload) carrying the same payload plus `status=completed/failed`. Frontend logic (unchanged) then flips the row to complete. Also caches BuildAppStatePayload across both emissions and constructs the shadow payload via foreach-copy (net48 lacks the IEnumerable+Comparer Dictionary ctor). Tests: V2.Core 732 (+3 — Theory[Installed|Error] + non-terminal guard). Solution build 0/0. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
96191c8740 |
fix(v2): 882fef64 codex review followup — TableStorage force-update CurrentPhase + persist phase package snapshots
Codex review of merged PR3 + PR5 surfaced two correctness gaps that match real-world session traffic. Finding 1 — TableStorageService.Sessions.cs force-update path: The normal status-update path mirrors `update["CurrentPhase"] = Complete` whenever status flips to Succeeded (line ~1019), so V2 enrollment_complete events (which carry Phase=Unknown per feedback_phase_strategy) still land at CurrentPhase=Complete. The ETag-exhaustion / force-update block at line ~1224 was missing the same override, leaving CurrentPhase stuck at the prior in-flight value (or -1) when ETag retries are exhausted exactly at the completion update. 6-line fix mirrors the well-tested normal-path override. Finding 2 — ImeTrackerStateData.PhasePackageSnapshots persistence: GetAllKnownPackageStates() returns the union of live _packageStates and the _phasePackageSnapshots dict (DeviceSetup apps captured before _packageStates.Clear() on the AccountSetup transition). SaveState only persisted the live list, so an agent restart between phases — common on hybrid-join + multi-reboot enrollments — lost the DeviceSetup snapshot and dropped those apps from FinalStatus + app_tracking_summary. ImeTrackerStateData now carries `Dictionary<string, List<PackageStateData>> PhasePackageSnapshots`, SaveState serialises it, LoadState restores it tolerantly (null on old state files → empty dict, no crash). Refactor side-product: extracted ToPackageStateData / FromPackageStateData as private static helpers in ImeLogTracker so the mapping is single-source-of-truth across live + snapshot serialisation. Skipped: Finding 3 (DetectHelloPolicy first-match-wins between Device/User CSP scopes) not fixed — Intune CSPs are exclusive per setting (Device OR User, never both), CSP wins over GPO, the first-match iteration is correct in practice. Saved feedback_csp_scope_exclusive.md so a future review doesn't re-suggest the defensive-aggregation patch. Tests: V2.Core 729 (+2 new for save/load round-trip + old-snapshot backwards-compat), DecisionCore 231, Functions 736, vitest 179, solution build 0/0. No new test for Finding 1's force-update path — the method has no existing test infrastructure (no Azurite fixture, no mock TableClient); the fix mirrors a well-tested normal-path override and risk is essentially zero. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
d0200d061a |
fix(v2): 882fef64 PR5 — peripheral logging follow-up (SelfUpdater + AppPackageState + RemoteConfigMerger)
Plan listed five 0-logger-call hotspots; two (RegistryWatcher, VersionCheckEventBuilder) are already adequately observed via existing call-site logging. The remaining three got targeted log lines where forensic gaps actually exist: - SelfUpdater: 4 new entry/success Info lines (start, ZIP downloaded, integrity verified, extract succeeded) so the happy-path is no longer silent. The failure branches were already logged via WriteSkipMarker / inner helpers. - AppPackageState: new read-only `WasAutoDowngradedToSkipped` flag set when UpdateState reclassifies an Installed-without-prior-download into Skipped via the inverse-detection rule. ImeLogTrackerAdapter.EmitAppState's PR3-D4 DEBUG line now appends `auto-downgraded=true (inverse-detection)` when the flag is set — answers "why is App X marked Skipped without a download" in one line. - RemoteConfigMerger: pre-overwrite snapshots for 7 operator-relevant knobs (SelfDestruct, KeepLog, Reboot, DiagnosticsMode/Enabled, ShowSummary, AgentMaxLifetime). One Info line per knob that actually flipped — bounded spam (Merge runs ~5min). Existing LogLevel + UnrestrictedMode lines unchanged. Skipped: - RegistryWatcher: already exposes Error event + _trace callback wired by both callers (AadJoinWatcher, ProvisioningStatusTracker) into _logger.Trace + _logger.Warning. No silent path. - VersionCheckEventBuilder: caller in Program.cs already logs ParseError (Warning), Event (Info+outcome), Deduped (Debug). No silent path at the caller boundary. Tests: V2.Core 727, DecisionCore 231, Functions 736, solution build 0/0. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
07312ee2a3 |
fix(v2): 882fef64 PR3 — kernel-path observability (A2 + A3 + D1-D5)
Session 882fef64's 12-min agent log carried only `EnrollmentOrchestrator: started.`
and `decision terminal stage reached` between the first event and termination — no
stage-transition trail, no signal cadence, no effect failures. PR3 wires logging
into the kernel hot path so every Decision-Stage transition, every signal accept/
reject, every effect retry/critical-failure, and every IME app state change leaves
a trace at the right severity.
A2 — noise reduction:
- TelemetrySpool `enqueued itemId=...` DEBUG -> VERBOSE (600+ lines/session before)
- IME `impersonation` lines deduplicated by user with 60s rollup
A3 — strengthened existing weak lines:
- `Command line:` shows `(no args)` instead of empty
- `Process exit code: 0` -> `Process completed: <name> -> exit=N, durationMs=T`
- IME session change now carries sessionId+user
- HelloTracker 358/360 suppression includes eventTime+provider
- EnrollmentOrchestrator started/stopping carry sessionId/stage/hosts/signals
D1-D5 — observability into the silent kernel components:
- DecisionStepProcessor: per-step DEBUG (stage transitions + effects) + VERBOSE for no-ops
- SignalIngress: 5 strategic log lines (drop, append-fail, worker-fault, stop summary, processed)
- EffectRunner: 5 strategic log lines (retry, exhausted, critical, anti-loop, classifier throw)
- ImeLogTrackerAdapter: 7 emit-site log lines covering all 7 emission paths
- InformationalEventPost: per-emit DEBUG with eventType/source/immediate/severity/dataKeys
All log statements are null-safe (_logger?.Debug); legacy unit-test rigs without a
logger keep working. New ctor overloads on InformationalEventPost / SignalIngress /
EffectRunner / ImeLogTrackerAdapter are backwards-compatible (logger param defaults
null).
Side fix: BinaryIntegrityVerifierTests pinned to [Collection("SerialThreading")].
Process-global `_runtimeSelfUpdateFired` static raced with parallel mismatch tests;
PR3's logging shifted thread timing enough to surface the race consistently.
Tests: V2.Core 727, DecisionCore 231, Functions 736, vitest 179, solution build 0/0.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
ae330f54d9 |
fix(v2): 882fef64 PR4 — Hello policy → DecisionEngine + 10s grace + mismatch detector
Surface the WHfB / Hello-for-Business policy fact to the decision engine and use it to drive the post-ESP-exit Hello wait cadence. Policy=enabled keeps the 30s default; policy=disabled shortens to 10s as a sanity check against a buggy detector. A `hello_policy_detection_mismatch` Warning event fires when a Hello terminal arrives despite policy=disabled — that is the detector-bug indicator. The completion-gate is intentionally left untouched: completion is orthogonal to Hello status (the Hello+Desktop AND-gate already accepts any terminal Hello outcome). A feedback memory is saved so a future iteration doesn't re-add a completion guard on this fact. Foundation: - DecisionSignalKind.HelloPolicyDetected (v1) - SignalPayloadKeys.HelloEnabled / HelloPolicySource - DecisionState.HelloPolicyEnabled : SignalFact<bool>? (null = unknown) - DecisionStateBuilder.WithHelloPolicyEnabled fluent helper - HandleHelloPolicyDetectedV1 reducer (set-once, no-op on same value, update on different value, DeadEnd on missing/unparseable payload) Behavior: - HelloTracker.HelloWaitTimeoutDisabledSeconds = 10 - StartHelloWaitTimer picks 10s when policy explicitly disabled, 30s otherwise - HelloTracker.HelloPolicyDetected event re-raised through EspAndHelloTracker - EspAndHelloTrackerAdapter posts HelloPolicyDetected signal once per session - EmitHelloPolicyDetectionMismatch on Hello-terminal arrival under policy=disabled (wired into both ProcessHelloEvent and ProcessHelloForBusinessEvent paths) Tests: DecisionCore 231 (+11), V2.Core 727 (+11), Functions 736, vitest 179, solution build 0/0. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
f6d8a286ef |
fix(v2): 882fef64 PR2 — AgentLogger size-cap rotation
Daily log file used to grow without bound under verbose mode. AgentLogger now caps each segment at 50 MB (configurable per-instance) and rolls forward to agent_YYYYMMDD_002.log, _003.log, ... when the cap is reached. Existing rotation suffixes are probed on construction so process restarts mid-day continue at the highest existing segment instead of overwriting older ones. The first line of every rotated segment is an `AgentLogger: rotated` marker for forensic readability. Size check + rotation are best-effort: exceptions are swallowed so the logger never breaks the agent. Tests: V2.Core 716 (+5 new in Logging/AgentLoggerTests.cs), solution build 0/0. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
b243706629 |
fix(v2): 882fef64 PR1 — Company Portal UI, diagnostics ZIP State/Spool, agent_shutting_down V1 parity
- DownloadProgress.tsx: exempt app_download_started from <1024B drop so WinGet/Store apps (Company Portal etc.) appear in the UI; filter logic extracted to pure helpers in lib/downloadProgressFilters.ts for vitest coverage. - DiagnosticsPackageService: ZIP now also packages AgentState/ (snapshot, journal, signal-log, ime-tracker-state, enrollment-complete.marker, final-status, whiteglove-backfill-state + .quarantine), AgentSpool/ (pending uploads + cursor), and AgentMarkers/ (top-level whiteglove.complete + clean-exit). BuildArchiveBytes extracted as internal seam so xUnit asserts ZIP contents without HTTP. - EnrollmentTerminationHandler: agent_shutting_down emission moved to top of Handle() so it fires unconditionally — including SelfDestruct=false dev VMs and WhiteGlove Part-1 exits. Data carries reason/outcome/stage/uptimeMinutes/agentVersion. Tests: V2.Core 711 (+12), DecisionCore 220, Functions 736, vitest 179 (+11), solution build 0/0. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
8b7a5f509e |
fix(v2): debrief 7dd4e593 followups + final-status schema 2 + V2 dialog renderer
F3 — SummaryDialog launch crash: switch temp dir from C:\WINDOWS\SystemTemp\ (unreadable for standard users when agent runs as SYSTEM) to flat %ProgramData%\AutopilotMonitor-Summary\ + wipe-on-launch (V1 parity), and grant BUILTIN\Users delete rights so the dialog's --cleanup self-removal works. F4 — Backend phase classification: classify `phase_transition` events (emitted by V2 DecisionEngine for AppsDevice/AppsUser/FinalizingSetup) as phase updates so CurrentPhase advances during App phases. Also force CurrentPhase=Complete on Status=Succeeded to materialise the terminal phase regardless of the carrier event's Phase value (V2 emits enrollment_ complete with Phase=Unknown per feedback_phase_strategy). F5 — App tracking summary covers all phases: ImeLogTracker now snapshots AppPackageState references (not serialized dicts) before _packageStates. Clear() at the AccountSetup transition, and exposes a deduped union GetAllKnownPackageStates() for the termination summary path. Without this, DeviceSetup apps were silently dropped from app_tracking_summary and the SummaryDialog after the phase transition. Schema 2 / V2 dialog — final-status.json now carries schemaVersion=2 plus failureReason, signalTimestamps, and per-app errorPatternId/errorDetail/ errorCode. SummaryDialog branches on schemaVersion: V1 path is the unchanged legacy renderer (deletable when V1 retires), V2 path renders 6 outcome states (Success / PreProvisioningComplete / TimedOut / Failure / Unknown / WhiteGlovePart2), shows a coloured banner with the agent- supplied failure reason, and per-app rows with duration on the right plus the IME-pattern error detail under the app name when an install failed. V1 agent + V1 dialog rendering remain bit-for-bit identical (no schema field → V1 render path). V1 Agent.Core.Tests 507/507, DecisionCore 220/220. Tests: V2.Core 702/702 (+12 schema-2 builder, signal-timestamps, failure- reason, per-app errors, phase-snapshot dedup, launcher path). Backend 736/736. New SummaryDialog.Tests 45/45 (OutcomeMapper + V1/V2 JSON roundtrip + FormatDuration). Build clean (0 warnings). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
a523192dc7 | backend fix buildtime | ||
|
|
bba25328a5 |
Merge pull request #54 from okieselbach/v2/fix-deadline-keys-and-immediate-flush
fix(v2): first-session regressions — deadline payload key + immediate-flush wakeup + willlaunch noise |
||
|
|
43ee4d2fd0 |
fix(v2): first-session regressions — deadline payload key, immediate-flush wakeup, willlaunch noise
Three independent first-session bugs surfaced while debugging session 9ed7021e
(V2 agent v2.0.333, Classic user-driven enrollment):
P0 — FinalizingGrace deadline dead-ended with "deadline_fired_without_name".
The orchestrator's OnDeadlineFired posted a payload using key "deadlineName"
while the reducer reads under SignalPayloadKeys.Deadline ("deadline"). Every
deadline (FinalizingGrace, HelloSafety, DeviceOnlyEspDetection, ClassifierTick,
WhiteGlovePart2Safety) fell through that key-mismatch and was rejected by the
reducer — in production this left every Classic session hanging in
Stage=Finalizing forever, blocking enrollment_complete, the final snapshot, the
terminal drain, and the diagnostics upload. Fix: forward ActiveDeadline.FiresPayload
verbatim so the per-deadline payload its scheduling callsite already populated
reaches the reducer unchanged.
P1 — "30 s silent gap at session start" in the UI. Every lifecycle-critical
item (agent_started, enrollment_type_detected, esp_phase_changed, hello wizard
events, auth-failure shutdown, enrollment_failed on max-lifetime, …) was
enqueued with RequiresImmediateFlush=true but the drain loop never woke up on
it — it blocked the full Task.Delay(_drainInterval) window (30 s by default)
before the first upload fired. Fix: add ITelemetrySpool.ImmediateFlushRequested
event raised on every immediate-flush enqueue, and race the drain loop's delay
against it via Task.WhenAny on a per-iteration TaskCompletionSource. First
post-start upload now happens in milliseconds; periodic cadence is unchanged.
P2 — hello_provisioning_willlaunch/willnotlaunch (EventID 358/360) noise in
the timeline. Per project memory `project_hello_willlaunch_unreliable` these
are snapshot-only prerequisites-passed flags that flip multiple times per
session (session 9ed7021e: 6× 358, three within 232 ms around desktop arrival)
and the DecisionEngine already treats them as non-evidence. Stop emitting
the backend events entirely — keep a single DEBUG log line per observation
so the sequence is still reconstructible from agent-logs during diagnostics.
Tests
- TelemetrySpoolTests: event fires exactly on immediate enqueues.
- RecoveryPathTests: past-due FinalizingGrace deadline advances Finalizing→Completed
(direct P0 regression guard) + fixed two assertions that encoded the old
buggy key / stale-log emptiness assumption.
- ScenarioTests.Full_pipeline: asserts enrollment_complete reaches the uploader
within 10 s — doubles as the P1 end-to-end wakeup guard.
- EnrollmentOrchestratorTests.Stop_flushes_final_snapshot: relaxed "stepIndex
equals observed" to "snapshot matches stop-time CurrentState + ≥ observed",
now that deadlines cascade correctly after SessionStarted.
- HelloTrackerImmediateUploadTests: 358/360 assertions flipped from "emitted
with ImmediateUpload=false" to "not emitted at all".
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
6a50256d59 |
Merge pull request #53 from okieselbach/v2/test-consolidation-theories
test(v2): consolidate xUnit tests to Theories + extract shared Harness |
||
|
|
2cdebaa7fc |
test(v2): consolidate xUnit tests to Theories + extract shared Harness
Converts repeated Fact-clusters across the V2 Agent and DecisionCore test suites into data-driven Theories, and extracts two oft-duplicated inner fixture/rig classes into reusable Harness helpers. Net: smaller files and more coverage via InlineData rows for edge cases that had no own Fact before. Wave 1 — Fact→Theory consolidations (10 files): - RemoteConfigMergerTests: LogLevel parsing (3 Facts → 1 Theory, 9 rows including new Whitespace/Empty/Null + case-insensitive combos). - ImeLogTrackerAdapterTests: EspPhaseChanged phase-mapping (3 Facts → 1 Theory, 7 rows + Finalizing alias, Complete, preboot). - InformationalEventPostTests: eventType/source validation + evidenceSummary precedence (3 Facts → 2 Theories with 4+4 rows). - ProgramGuardsTests: CheckEnrollmentCompleteMarker matrix (3 Facts → 1 Theory with 4 rows incl. new (no-marker, no-selfdestruct) cell). - AuthFailureTrackerTests: distress dispatch (2 Facts → 1 Theory, 5 rows incl. 403 dedup + 500 catch-all). - SessionIdPersistenceTests: ctor rejection (1 Fact → 3 rows) + LoadSessionCreatedAt null-return (2 Facts → 5 rows incl. empty/whitespace /out-of-range dates). - TelemetrySpoolTests: MarkUploaded regression guard (1 Fact, 3 inline calls → 1 Theory with 5 rows incl. same-value idempotence). - ReducerReplayTests: null-arg rejection (1 Fact, 3 asserts → 1 Theory). - AppInstallSignalHandlerTests: newState → breakdown (4 Facts → 1 Theory, 7 rows incl. case-insensitive lowercase/uppercase + empty). - ClassicAwaitingHelloGuardTests: 8 Facts → 2 Theories with 6 rows each (full skipUser × accountSetupEntered matrix; 4 new cells). Wave 2 — Shared Harness extraction (2 new helpers): - ImeLogTrackerAdapterFixture.cs (60 LOC) replaces 5× duplicated inner Fixture classes across ImeLogTrackerAdapter*Tests.cs (~150 LOC saved). - EnrollmentOrchestratorRig.cs (65 LOC) replaces 6× near-identical Rig/ Build helpers across EnrollmentOrchestrator*Tests.cs (~200 LOC saved). Results: - AutopilotMonitor.Agent.V2.Core.Tests: 679 tests (was 650), all green. - AutopilotMonitor.DecisionCore.Tests: 220 tests (was 164), all green. - 22 files changed, +350 / -825 lines (-475 net). - No production code touched; no V1 code touched. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
f9c0a9d134 |
Merge pull request #52 from okieselbach/v2/codex-05-scenario-profile
v2 codex-05: typed EnrollmentScenarioProfile + Observations + ClassifierOutcomes |
||
|
|
511ed70361 |
v2 codex-post-#51: partial-skipUser guard regression + enum serialization parity
Addresses Codex second-pass on commit
|
||
|
|
51d57153ad |
v2 codex-05: typed ScenarioProfile + Observations + ClassifierOutcomes aggregates
Codex review follow-up #5 (Option 2b, clean-cut). Replaces the legacy 9-field hypothesis/fact mosaic on DecisionState with three structured aggregates that separate concerns cleanly: - EnrollmentScenarioProfile — derived classification (Mode, JoinMode, EspConfig, PreProvisioningSide, Confidence, EvidenceOrdinal, Reason) replacing the Hypothesis EnrollmentType + SkipUser/DeviceEsp facts. - EnrollmentScenarioObservations — raw per-signal evidence (ShellCoreWhiteGloveSuccessSeen, WhiteGloveSealingPatternSeen, AadUserJoinWithUserObserved, SkipUserEsp, SkipDeviceEsp) with set-once semantics, feeding the WhiteGlove sealing classifier snapshot. - ClassifierOutcomes — classifier verdict + anti-loop storage (WhiteGloveSealing, WhiteGlovePart2Completion, DeviceOnlyDeployment) replacing the three Hypothesis fields they used to occupy. EnrollmentScenarioProfileUpdater holds the signal → profile mapping rules with monotonic-confidence invariants. EspConfigDetected keeps its partial- first-signal / second-signal-completes semantic via the observations half- facts; the profile's EspConfig is only derived once both halves are known. Critical Codex correction: AadUserJoinWithUserObserved is NOT a JoinMode signal — it is a late-AADJ user-presence hard-excluder for the WhiteGlove classifier. JoinMode comes strictly from SessionStarted.payload.isHybridJoin. Classifier algorithm, weights, and thresholds unchanged; only snapshot input sources, anti-loop verdict storage, and the AwaitingHello guard migrate. The WhiteGloveSealingSnapshot fields keep their names and types, so ComputeInputHash stays byte-stable and the anti-loop contract is preserved across the refactor. AssemblyVersion stays 2.0.0.0. Suites: DecisionCore 203/203 (+39 new aggregate/updater tests), Agent.V2.Core 650/650, Functions 735/735; 0 warnings. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
5091d5f1ed |
Merge pull request #50 from okieselbach/v2/codex-review-followups
v2: codex-review followups (#1, #2, #3, #4, #6) |
||
|
|
1418eacfe9 |
v2 codex-post-#50-2: rehydrate-failure durable abort + v1 payload contract (#E #F)
Addresses Codex second-pass findings against commit
|
||
|
|
bd3ec4e195 |
v2 codex-post-#50: recovery + abort durability follow-ups (#A #B #C #D)
Addresses Codex review against PR #50: #A (High) EnrollmentOrchestrator branch (c) now distinguishes empty-log (fresh start) from log-has-entries (full-log replay via ReducerReplay) so a crash-before-first-snapshot cannot regress state to StepIndex=0 while the SignalLog preserves older ordinals. #B (High) EffectInfrastructureFailure is now crash-durable. ApplyStep returns EffectRunResult; on SessionMustAbort the phantom snapshot is skipped, and SignalIngress synthesises + durably appends + reduces the terminal failure signal INLINE within the same worker iteration. The old in-memory PostEffectInfrastructureFailureSignal path in EffectRunner is removed. #C (Medium) ReducerReplay.Replay gained an Action<DecisionTransition>? onTransition overload. The orchestrator's recovery path truncates Journal AHEAD-of-seed phantoms before replay and rematerialises BEHIND entries bit-for-bit via the callback, closing the gap between SignalLog.Append and Journal.Append crashes. #D (Medium) ReducerVerifier.ReplaySemantically was seeding DecisionState with swapped (tenantId, sessionId) — tests mirrored the bug so it slipped through. Args fixed; defensive replay_identity_mismatch issue added so future swaps surface as an Error-severity verifier finding. Suite-Baseline all green, 0 warnings: DecisionCore 164/164, Agent.V2.Core 648/648, Functions 735/735. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
ed1c26f98b |
v2 codex-03: forward-link events to their originating transition + signal
Closes Codex review follow-up #3. Two distinct traceability gaps: 1. DecisionTransition.EmittedEventSequences was always empty. The journal record is appended before the effects run, so at construction time the Event.Sequence values haven't been assigned yet. Populating it later would need a journal-reorder or a sidecar mapping file — both more risk than payoff. 2. SessionTraceOrdinal had two independent meanings in the codebase: the reducer-step correlation counter (SessionTraceOrdinalProvider) shared by a signal and its transition, vs. the transport-cursor on TelemetryItem (== TelemetryItemId when session-scoped). The backend parser already prefers the inner (payload) value, but the shared name was misleading. Resolution: - Forward link instead of backward. EnrollmentEvent gets two new nullable columns, CausedByTransitionStepIndex and CausedBySignalOrdinal, populated in EventTimelineEmitter.Emit from currentState.StepIndex and currentState.LastAppliedSignalOrdinal (the reducer already has both, zero new plumbing). Nullable so legacy events and any future non-reducer emits stay valid. Inspector queries "events emitted by step N" = WHERE CausedByTransitionStepIndex = N — trivial index scan. - DecisionTransition.EmittedEventSequences stays on the type for journal backward-compat but the XML doc now states explicitly that it is always empty and points readers at the new forward link. - SessionTraceOrdinal XML docs clarified on DecisionSignal, DecisionTransition (by extension), and TelemetryItem: reducer- correlation counter is authoritative for payload, transport-cursor for envelope, backend parser resolves by preferring inner value. No code change — the two counters were already independent in practice; the name overload was the whole confusion. - Backend TableStorageService persists both new columns in StoreEventAsync + StoreEventsBatchAsync, reads them in MapToEnrollmentEvent. Entity setters guarded by HasValue so legacy agent uploads don't write null-as-zero. Tests (+2 in EventTimelineEmitterTests): - Emit populates CausedByTransitionStepIndex + CausedBySignalOrdinal from a state with StepIndex=7, LastAppliedSignalOrdinal=12. - CausedBySignalOrdinal is null when LastAppliedSignalOrdinal=-1 (fresh initial state sentinel), avoiding a meaningless -1 on the wire. Suites green: DecisionCore 161/161, Agent.V2.Core 645/645 (+2), Functions 734/734. Build 0 warnings. Plan: .claude/plans/codex-03-traceability.md Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
3bfe7d7fdb |
v2 codex-04: wire AppInstall signals as observation-level facts
Closes Codex review follow-up #4. Before this commit the reducer had no handler for DecisionSignalKind.AppInstallCompleted / AppInstallFailed — the signals were real (ImeLogTrackerAdapter posts one terminal per app) but fell through to HandleUnhandledSignal, so the journal accumulated 'unhandled_signal_kind:AppInstallCompleted:v1' dead-ends and the per-app outcomes disappeared. Subsequent ESP / IME classifiers had no structured evidence of which apps blocked. Scope: observation-only, as the Master-Tracker recommended. Neither handler changes Stage or Outcome — they roll the terminal outcome into a new DecisionState.AppInstallFacts aggregate that: - increments CompletedCount (+ Installed / Skipped / Postponed breakdown from the adapter's newState payload) - increments FailedCount and appends the appId to FailedAppIds, deduplicated and capped at MaxFailedAppIds (50) so pathological sessions cannot bloat DecisionState. Every DecisionSignalKind now has at least one handler; the dead-end path is reachable only through an unknown (kind, schemaVersion) pair — the genuine forward-compat failure mode per plan §2.5. The 'Still unwired' dispatch comment is replaced with a 'Fall-through: unknown (kind, schemaVersion)' label. Tests: - new AppInstallSignalHandlerTests (+10): Installed/Skipped/Postponed breakdowns, unknown newState forward-compat, no stage/outcome change, failed count + appId recording, dedup on repeated appId, MaxFailedAppIds cap keeps first-observed entries, mixed-stream integration covering all buckets in one fold. - HarnessTests.Replay_unknownSignalKind_recordsDeadEnd renamed to Replay_unknownSchemaVersion_recordsDeadEnd, switched from the now-wired AppInstallCompleted to a v99 SessionStarted so the dead-end path stays covered through its real-world trigger. - ReducerFailSafeTests.Reduce_unhandledKind same retarget. Suites green: DecisionCore 161/161 (+10), Agent.V2.Core 643/643, Functions 734/734. Build 0 warnings. Fixtures: - tests/fixtures/signal-kinds/app-install-completed-v1.json - tests/fixtures/signal-kinds/app-install-failed-v1.json Plan: .claude/plans/codex-04-appinstall-signals-wiring.md Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
8da8dc649d |
v2 codex-06: ReducerVerifier semantic replay — reducer-agreement CI gate
Closes Codex review follow-up #6. Until now GetSessionReducerVerificationFunction only ran structural checks (ordinal contiguity, step-index contiguity, ReducerVersion drift, orphaned SignalOrdinalRef). That catches corruption but never verifies the backend's reducer agrees with the agent-produced journal under the same ReducerVersion — exactly the invariant the whole (newState, transition, effects) architecture rests on. Unblocked by Codex #1 phase 1: ReducerReplay.Replay exists in DecisionCore, and the backend already references DecisionCore. Flow: 1. Skip preconditions: drift, ordinal gaps, step-index gaps, empty session, or head PayloadJson deserialization failure → record SemanticReplaySkipReason + optionally emit an Info 'replay_skipped' issue. Never throws. 2. Deserialise each SignalRecord.PayloadJson via SignalSerializer. 3. Fold through a transient DecisionEngine: state = Reduce(state, sig). 4. Compare each produced transition against the stored one on (StepIndex, Trigger, FromStage, ToStage, Taken, DeadEndReason). Discrepancies surface as 'replay_divergence' issues (capped at 20 individual entries; overflow message follows the same pattern as the structural gap reports). 5. Compare replayed final state's Stage to the last stored transition's ToStage — mismatch surfaces as 'replay_final_stage_mismatch' at Error severity. Report additions (backward-compatible): - SemanticReplayPerformed - SemanticReplaySkipReason - SemanticReplayFinalStageMatches - ReplayedFinalStage - TransitionDivergenceCount Side change: GetSessionReducerVerificationFunction XML-doc no longer claims "structural only" — now documents both layers and the skip rules. Tests (+7, ReducerVerifierSemanticReplayTests): - Skip on ReducerVersion drift. - Skip on signal-ordinal gap. - Skip on step-index gap. - Perfect agreement (reducer-seeded journal round-trips). - Divergence: tampered Trigger → one replay_divergence issue. - Divergence: tampered last-ToStage → replay_final_stage_mismatch Error. - Mid-stream deserialization failure tolerated without aborting. Existing structural ReducerVerifierTests unchanged — their stub PayloadJson='{}' deserialization failures now silently set SemanticReplaySkipReason='deserialization_failure' without polluting Issues, preserving the happy-path empty-issues assertion. Suites green: DecisionCore 151/151, Agent.V2.Core 643/643, Functions 734/734 (+7). Build 0 warnings. Plan: .claude/plans/codex-06-reducer-verifier-semantic-replay.md Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
386f3e909f |
v2 codex-02: EffectRunner posts EffectInfrastructureFailure on critical fail
Closes Codex review follow-up #2. Before this commit, a DecisionEffectKind.ScheduleDeadline / CancelDeadline failure raised EffectRunResult.SessionMustAbort but the decision world never heard about it — DecisionStepProcessor only logged and continued. A safety deadline could live in DecisionState without ever being armed on the scheduler, leaving the session to hang until the max-lifetime watchdog tripped hours later. Flow now: 1. EffectRunner.TryRunCriticalDeadlineEffect fails. 2. EffectRunner posts a synthetic DecisionSignalKind.EffectInfrastructureFailure signal onto the ingress before returning. Payload: reason + failingEffect. 3. Ingress worker picks it up next iteration. 4. HandleEffectInfrastructureFailureV1 (new, in DecisionEngine.Shared.cs) transitions Stage=Failed, Outcome=EnrollmentFailed, ClearDeadlines, and emits one EmitEventTimelineEntry effect for enrollment_failed with the reason from the payload. 5. DecisionStepProcessor observes the terminal stage and fires the Terminated event; EnrollmentOrchestrator tears down normally. EnrollmentFailed (not Aborted) distinguishes infrastructure-induced failures from user/operator-initiated aborts in forensics. The ingress post is wrapped in a try/catch so a full/closed channel cannot crash the worker — the max-lifetime watchdog remains the last-resort exit. Changes: - DecisionSignalKind: new EffectInfrastructureFailure enum value. - DecisionEngine: dispatch entry (v1). - DecisionEngine.Shared: HandleEffectInfrastructureFailureV1 handler, mirrors HandleSessionAbortedV1 shape but with EnrollmentFailed outcome and a failure timeline effect. - EffectRunner: PostEffectInfrastructureFailureSignal helper, called from the critical-effect path before returning SessionMustAbort. - EffectRunResult: XML-doc clarifies that SessionMustAbort is now observability-only (reducer terminates on its own). - DecisionStepProcessor: comment update — no code change needed, the synthetic signal reaches state naturally. - tests/fixtures/signal-kinds/effect-infrastructure-failure-v1.json: replay fixture per the enum's self-documented contract. Tests (+8): - DecisionCore EffectInfrastructureFailureHandlerTests: transition shape, deadline clearing, enrollment_failed emission with reason, missing-reason fallback, double-post idempotency. - EffectRunnerTests: schedule-fail posts signal with correct payload, cancel-fail same, ingress-post exception tolerated. - FakeSignalIngressSink: ThrowOnPost toggle for the tolerance test. All suites green: DecisionCore 144/144 (+5), Agent.V2.Core 633/633 (+3), Functions 727/727. Build 0 warnings. Plan: .claude/plans/codex-02-effect-failure-to-decision.md. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
96854d20ac |
v2 codex-01 phase 4: correct ISnapshotPersistence doc reference to ReducerReplay
The interface docstring promised rebuild via "Reducer.Replay(signals)" but no such method existed until Phase 1. Point readers at the actual symbol (AutopilotMonitor.DecisionCore.Engine.ReducerReplay.Replay) and note the seed semantics now that Phase 2 wires tail-replay. Off-repo artefacts also updated (not committed — tasks/ + ~/.claude/ are local scope): - tasks/lessons.md: new entry on recovery-anchor-preservation. - .claude/plans/codex-review-followups-master.md: #1 marked Done with commit hashes. - user memory project_v2_single_rail_in_progress: appended Codex follow-up section with phase commits + new invariant. All suites remain green (build 0 warnings). Next: Finding #2. Plan: .claude/plans/codex-01-recovery-signallog-replay.md §3 Phase 4. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
5029bdd4c3 |
v2 codex-01 phase 3: re-arm persisted deadlines after recovery replay
Wires IDeadlineScheduler.RehydrateFromSnapshot (long-present, zero production callers) into EnrollmentOrchestrator.Start so deadlines persisted inside DecisionState survive a restart. Without this any safety-net armed pre-crash (HelloSafety, Part2Safety, FinalizingGrace, ClassifierTick) silently vanishes and a resumed session can hang until the max-lifetime watchdog trips. Placement matters: the rehydrate call MUST run after the Fired event is subscribed (step 12 in Start) AND after _ingress.Start (step 13) because (a) past-due deadlines fire immediately via ThreadPool and would otherwise fall into the void without a subscriber, (b) OnDeadlineFired returns early when _ingress is null and would drop the synthesised DeadlineFired signal. Put at step 13.5 just before the WG Part-2 bridge so rehydrated fires precede fresh collector signals. Rehydrate failures are logged at error but do NOT throw — this path will get a synthetic EffectInfrastructureFailure transition once Codex follow-up #2 lands, at which point the session can terminate cleanly instead of relying on the max-lifetime watchdog. Tests (2 new in RecoveryPathTests): - Snapshot_with_past_due_deadline_fires_DeadlineFired_signal_on_start — end-to-end: scheduler fires via ThreadPool, bridge posts signal, signal reaches the SignalLog within 3s. - Snapshot_with_future_deadline_is_scheduled_but_not_yet_fired — future-due deadline reports IsScheduled=true, no premature fire. All suites green: DecisionCore 146/146, Agent.V2.Core 640/640 (+2), Functions 727/727. Build 0 warnings. Plan: .claude/plans/codex-01-recovery-signallog-replay.md §3 Phase 3. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
216967e51c |
v2 codex-01 phase 2: orchestrator recovery replays SignalLog tail + truncates phantom Journal
Wires ReducerReplay.Replay (Phase 1) into EnrollmentOrchestrator.Start so
recovery honours the SignalLog = truth / Snapshot = cache invariant:
- Snapshot loaded cleanly: replay any SignalLog entries past the
snapshot's LastAppliedSignalOrdinal onto the snapshot seed. Closes
the crash window where Journal flushed step N but Snapshot only
made it to step N-1.
- Snapshot corrupt: quarantine the snapshot alone and rebuild state
by replaying the entire SignalLog. Previously the log was moved
aside together with the snapshot, destroying the only recovery
anchor. Log-head-corrupt remains the only escalation to
total-loss quarantine (SegmentQuarantine.QuarantineAll).
- Post-recovery: if Journal.LastStepIndex >= initialState.StepIndex
the on-disk tail contains phantom transitions from a pre-crash
flush. JournalWriter.TruncateAfter(boundary) moves the suffix to
.quarantine/<ts>/journal-phantom-tail.jsonl and realigns counters.
Without this the next live Append would fail monotonicity and
quarantine-escalate the session.
Changes:
- IJournalWriter: new TruncateAfter(int lastValidStepIndex) method.
- JournalWriter: clock ctor overload (for deterministic quarantine
bucket timestamps); TruncateAfter implementation with write-temp +
atomic rename and forensic phantom-tail preservation.
- FakeJournalWriter: matching in-memory TruncateAfter.
- EnrollmentOrchestrator: three-branch recovery (a/b/c) described
inline, using ReducerReplay.Replay and CollectSignalLogTailAfter.
Snapshot quarantine now scoped to the snapshot file only unless
the log is head-corrupt.
Tests:
- JournalWriterTests: 5 new TruncateAfter cases (drops suffix +
quarantines, boundary=last is no-op, boundary>last throws,
boundary=-1 empties file, empty journal no-op).
- RecoveryPathTests: 3 new integration scenarios (snapshot+pending
tail, corrupt-snapshot-intact-log, journal-ahead-of-state
triggers truncate).
- All suites green: DecisionCore 146/146, Agent.V2.Core 638/638
(+8), Functions 727/727. Build 0 warnings.
Plan: .claude/plans/codex-01-recovery-signallog-replay.md §3 Phase 2.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
084274629c |
v2 codex-01 phase 1: ReducerReplay.Replay pure fold + determinism tests
Codex review follow-up #1 (recovery correctness) — lays the foundation for crash-safe state reconstruction. ReducerReplay.Replay folds a signal stream through IDecisionEngine.Reduce without side effects: no journal append, no effect runner, no telemetry. Enforces strict ordinal monotonicity against the seed state's LastAppliedSignalOrdinal so a tampered or corrupt log never silently regresses state. Phase 2 wires this into EnrollmentOrchestrator recovery; Phase 3 adds post-replay deadline re-arm. Backend Finding #6 (ReducerVerifier semantic replay) also consumes this. Tests: 7 new cases in ReducerReplayTests (empty stream, single signal, multi-signal determinism, snapshot+tail equivalence, ordinal-at-seed regression guard, mid-stream monotonicity violation, null-arg guards). All suites green: DecisionCore 146/146, Agent.V2.Core 630/630, Functions 727/727. Build 0 warnings with TreatWarningsAsErrors. Plan: .claude/plans/codex-01-recovery-signallog-replay.md Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
6a94d4d19d |
Merge pull request #49 from okieselbach/v2/single-rail
v2 Agent: first-session fixes + registry-guard dead-code removal |
||
|
|
fef5834ad0 |
v2 single-rail 13: remove dead Registry-guard in CheckEnrollmentCompleteMarker
The registry-based ghost-restart branch in CheckEnrollmentCompleteMarker (HKLM\SOFTWARE\AutopilotMonitor\Deployed present + !SessionExists) has never fired in production and was structurally unreachable: - session.id is created via SessionIdPersistence.GetOrCreate BEFORE the guard runs (both in V1 LoadConfiguration:131 and V2 Program.cs:216), so the !SessionExists() retry-cleanup arm is never reachable at runtime. - Even if the arm were reachable, the Deployed regkey cannot distinguish "fresh install" from "ghost restart": it is written by --install and intentionally preserved by CleanupService as a bootstrap-script reentry lock (read by scripts/Bootstrap/Install-AutopilotMonitor-v2.ps1 Guard 1 to prevent re-install on production devices). Both scenarios therefore present as "Deployed present + session.id absent". Remove the registry-guard block and its dead retry-cleanup path. Keep the file-based enrollment-complete.marker path intact — it is written by CheckSessionAgeEmergencyBreak before its own retry-cleanup attempt and serves the "cleanup-disabled emergency break" exit-cleanly case. Also drop the now-obsolete "Create SessionId BEFORE guards" justification comment in Program.cs and the matching boot-sequence doc wording — neither remaining guard (file-based marker, session-age watchdog) keys on session.id absence for ghost detection. CheckEnrollmentCompleteMarker loses the now-unused dataDirectory parameter; callsites updated. Tests: ProgramGuardsTests.cs keeps marker-absent / marker-without- selfdestruct / marker-with-selfdestruct-retry coverage; the first-run- after-install regression test is renamed to match its actual scope (returns_false_when_marker_file_is_absent). 630/630 V2.Core tests green. No behavior change — removed path was unreachable. TryWriteDeploymentMarker in Program.InstallMode.cs stays (writes the bootstrap-reentry-lock); CleanupService continues to intentionally leave the regkey in place. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
8fe75a0f4e |
v2 single-rail 12: first-session-fixes PR 4 — data completeness + operability (Fixes 4+5)
Closes the "final-status.json shows only 3 apps", "every app ~30 s" and "agent log silent during uploads" gaps observed in session 30410cd7. Fix 4c — Per-app install-lifecycle timing. New AppInstallTiming model (StartedAtUtc / CompletedAtUtc / DurationSeconds). ImeLogTrackerAdapter tracks timing in a set-once dictionary (first Downloading/Installing/ InProgress stamps StartedAt; first terminal stamps CompletedAt) and exposes it via a new public AppTimings snapshot. app_install_* DataJson carries startedAt / completedAt / durationSeconds when known; omitted otherwise so the UI can distinguish "not tracked" from "stamp=epoch". Fix 4a — FinalStatusBuilder uses timings. Optional appTimings param on Build(); FinalStatusPackageInfo gets StartedAt / CompletedAt / DurationSeconds with NullValueHandling.Ignore. Existing per-targeted phase bucketing preserved — that was correct. Fix 4b — app_tracking_summary terminal event. EnrollmentTerminationHandler. EmitAppTrackingSummary runs once at termination before diagnostics upload with aggregate counts, per-phase breakdown, and per-app records (appId/name/phase/finalState/optional timing). ImmediateUpload=true, Phase=Unknown (feedback_phase_strategy). Skipped on WhiteGlove Part 1 (apps haven't installed yet). Fix 5 — Upload-cadence logging. BackendTelemetryUploader + TelemetrySpool gained optional AgentLogger params. Uploader logs INFO flushing/ingest OK, WARN ingest TRANSIENT (5xx/timeout/network), ERROR ingest PERMANENT (4xx), with items + durationMs. Spool logs DEBUG per-enqueue with kind + immediate flag + pending count. Empty-batch path stays silent (short-circuit). Plumbing: ImeLogHost.AppTimings → DefaultComponentFactory.ImeAppTimings → Program.cs wires appTimingsAccessor into EnrollmentTerminationHandler; logger flows into uploader (Program.cs) and spool (EnrollmentOrchestrator). All new ctor params are nullable-default → zero breakage for existing call sites and tests. Tests: +18 new (7 adapter timing, 2 builder timing, 5 handler summary, 4 uploader logging). DecisionCore 139, Agent.V2.Core 630, Functions 727, Web vitest 168 — all green. Web typecheck clean. Solution build 0/0. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
017c7ca1cd |
v2 single-rail 11: first-session-fixes PR 3 — phase tagging + Finalizing stage (Fixes 2+6)
Fix 6 — Finalizing pseudo-stage with ~5 s grace. New non-terminal SessionStage.Finalizing between "both prerequisites resolved" and Completed + DeadlineNames.FinalizingGrace. HandleHelloResolvedV1 / HandleDesktopArrivedV1 / HandleHelloSafetyDeadlineFired (desktop-already-arrived branch) now route through a shared TransitionToFinalizing helper instead of jumping straight to Completed — the helper arms the 5 s deadline and emits a phase_transition declaration effect (Phase=FinalizingSetup). HandleFinalizingGraceDeadlineFired transitions Finalizing → Completed + enrollment_complete. IsTerminal() deliberately unchanged so DecisionStepProcessor.OnDecisionTerminalStage (and therefore EnrollmentTerminationHandler) fires only on the real terminal, not on Finalizing — giving the reducer-emitted phase_transition + enrollment_complete headroom to reach the backend before spool drain. Fix 2 — Sub-phase declarations AppsDevice / AppsUser. ImeLogTrackerAdapter emits a phase_transition InformationalEvent on the first app activity in a given ESP phase (DeviceSetup → AppsDevice, AccountSetup → AppsUser). Fire-once per ESP phase, ImmediateUpload=true, respects feedback_phase_strategy (only phase_transition / esp_phase_changed / agent_started may carry Phase != Unknown). FinalizingSetup + unmapped ESP phases short-circuit — the reducer owns the FinalizingSetup declaration (see Fix 6) and adapter mustn't duplicate. Test harness race-safety. PostFixture in the integration fixture had a latent race where ApproximateQueueLength==0 + "stepIndex > stepIndexBefore" could be met while the actually-posted signal was still executing ApplyStep. Replaced with authoritative LastAppliedSignalOrdinal >= signalLog.LastOrdinal watermark. ReplayHarness + ClassifierAwareReplayHarness + PostFixture all auto-fire a synthetic DeadlineFired:finalizing_grace at end-of-stream when the reducer is parked in Finalizing — fixtures stay unchanged. Tests: +12 new (6 DecisionCore FinalizingStageTests, 6 ImeLogTrackerAdapter SubPhaseDeclarationTests). DecisionCore 139, Agent.V2.Core 612, Functions 727, Web vitest 168 — all green. Web typecheck clean. Solution build 0/0. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
6fec3e2b0b |
v2 single-rail 10: first-session-fixes PR 2 — event observability (Fixes 1+3)
Fix 1 — ImmediateUpload=true on UX-critical lifecycle events. Policy driven by UI responsiveness, not "lifecycle-sounding": * ImeLogTrackerAdapter: esp_phase_changed, all app-state transitions (started/completed/failed/download-started + download_progress live bar), do_telemetry flip to immediate. download_progress ticks every 3s only while a download is active (bounded by download duration). * HelloTracker: hello_policy_detected flips to immediate. Existing terminal set (shouldTriggerHelloCompleted / hello_provisioning_failed) unchanged. willlaunch/willnotlaunch/pin_status stay batched — snapshot flags that flip multiple times per session (project_hello_willlaunch_unreliable). * ShellCoreTracker + reducer-synthesised terminals were already immediate. Fix 3 — desktop_arrived InformationalEvent. DesktopArrivalDetectorAdapter now dual-emits: existing DecisionSignalKind.DesktopArrived(Part2) for the reducer AND SharedEventTypes.DesktopArrived informational event for the Events table / UI timeline / MCP query_raw_events. Fire-once across both rails. Payload carries detectedAt / detectionSource / part2Mode. Tests: +20 xUnit cases. DecisionCore 133, Agent.V2.Core 606, Functions 727, Web vitest 168 — all green. Solution build 0 warnings, 0 errors. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
7b64f98e63 |
v2 single-rail 9: soft admin-action semantics + shutdown-race fixes
Addresses two tasks/todo.md entries from 2026-04-23:
- Soft Admin-Action Semantik + SignalIngress-Race-Fix
- Backend AdminAction-Trigger fix
Backend — authoritative AdminMarkedAction field:
New Session.AdminMarkedAction property (null | "Succeeded" | "Failed")
replaces the fragile heuristic "Status is terminal AND current event is not
a completion marker", which fired falsely on every post-completion event
the agent sent (agent_shutting_down, diagnostics_uploaded,
enrollment_summary_shown). The backend's AdminAction response to /register
and /ingest now reads the explicit field. ISessionRepository +
TableSessionRepository + TableStorageService.Sessions +
Ingest{Events,Telemetry}Function + MarkSession{Succeeded,Failed}Function +
RegisterSessionFunction + EventIngestProcessor all updated to set / clear /
read the new field coherently.
Agent — Program.cs WireTelemetryServerResponse refactor:
Routes TelemetryUploadOrchestrator.ServerResponseReceived through three
distinct semantic paths instead of one terminate_session synthesis:
1. DeviceKillSignal -> terminate_session with forceSelfDestruct=true
(hard admin kill — overrides SelfDestructOnComplete).
2. AdminAction=Succeeded -> single admin_marked_session timeline entry
(informational; agent continues own path).
3. AdminAction=Failed -> EnrollmentTerminationHandler.Handle(outcome=Failed)
(soft shutdown; same analyzers/diagnostics/summary
as a locally-detected failure).
Plus a shutdown-race guard: if shutdownComplete is already set, short-circuit
instead of crashing deep in the dispatcher with an InvalidOperationException
from a disposed SignalIngress. Backed by new HandleAdminActionTests.
Agent — ServerActionDispatcher ingress-disposed race:
Added InvalidOperationException swallow around InformationalEventPost.Emit
for the narrow window between SignalIngress.Stop and
TelemetryUploadOrchestrator.DrainAllAsync completion. Dropped events are
acceptable during teardown; the alternative was a nested Emit failure that
corrupted the outer termination sequence. Backed by new
ServerActionDispatcherTests case.
Tests all green: DecisionCore 133, Agent.V2.Core 586, Functions 727.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|