From 6ae7e755434cb66fa2d2ff11d90df7f116f58639 Mon Sep 17 00:00:00 2001 From: Agent 0 Date: Mon, 23 Mar 2026 12:24:39 -0400 Subject: [PATCH 1/9] mirrordaemon: derive health peers_known from peer list --- src/mirror-service/mirror_service.test.ts | 11 +++++++++++ src/mirror-service/mirror_service.ts | 4 ++-- src/mirrordaemon/runtime_state.test.ts | 15 ++++++++++++++- src/mirrordaemon/runtime_state.ts | 8 ++++++-- 4 files changed, 33 insertions(+), 5 deletions(-) diff --git a/src/mirror-service/mirror_service.test.ts b/src/mirror-service/mirror_service.test.ts index 0374011a63..4ee282d007 100644 --- a/src/mirror-service/mirror_service.test.ts +++ b/src/mirror-service/mirror_service.test.ts @@ -1407,6 +1407,7 @@ describe("mirror service", () => { daemon_session_id: string; uptime_ms: number; service: { node_id: string; port: number }; + sync: { peers_known: number }; event_stream: { sse_available: boolean; ws_available: boolean }; correlation: { trace_id: boolean; session_id: boolean }; }; @@ -1416,6 +1417,7 @@ describe("mirror service", () => { version: string; daemon_session_id: string; service: { node_id: string; port: number }; + sync: { peers_known: number }; }; expect(health.ok).toBe(true); @@ -1425,6 +1427,7 @@ describe("mirror service", () => { expect(health.uptime_ms).toBeGreaterThanOrEqual(0); expect(health.service.node_id).toBe("health-node"); expect(health.service.port).toBe(service.port); + expect(health.sync.peers_known).toBe(0); expect(health.event_stream.sse_available).toBe(true); expect(health.event_stream.ws_available).toBe(true); expect(health.correlation.trace_id).toBe(true); @@ -1435,6 +1438,7 @@ describe("mirror service", () => { expect(status.daemon_session_id).toBe(health.daemon_session_id); expect(status.service.node_id).toBe(health.service.node_id); expect(status.service.port).toBe(health.service.port); + expect(status.sync.peers_known).toBe(health.sync.peers_known); } finally { await service.shutdown(); } @@ -1595,6 +1599,11 @@ describe("mirror service", () => { }, }); + const health = (await requestJsonFromApp(service.app, "GET", "/mirror/health")) as { + ok: boolean; + sync: { peers_known: number }; + }; + const sync = (await requestJsonFromApp(service.app, "GET", "/mirror/sync")) as { ok: boolean; daemon_session_id: string; @@ -1609,6 +1618,8 @@ describe("mirror service", () => { }>; }; + expect(health.ok).toBe(true); + expect(health.sync.peers_known).toBe(1); expect(sync.ok).toBe(true); expect(sync.daemon_session_id.length).toBeGreaterThan(0); expect(sync.peers_known).toBe(1); diff --git a/src/mirror-service/mirror_service.ts b/src/mirror-service/mirror_service.ts index 53a87d666c..42aa02a879 100644 --- a/src/mirror-service/mirror_service.ts +++ b/src/mirror-service/mirror_service.ts @@ -126,7 +126,7 @@ export async function startMirrorService( wsConnections: runtimeWebSocket.getConnectionCount(), sseAvailable: true, wsAvailable: true, - peersKnown: observability.getMetrics().gauges.peers_known || syncManager.listPeers().length, + peers: syncManager.listPeers(), }); daemon.publishRuntimeEvent("runtime.health.requested", { path: "/mirror/health", @@ -150,7 +150,7 @@ export async function startMirrorService( getMirrordaemonHealthState(daemon, { port: boundPort, baseUrl: syncManager.getLocalBaseUrl(), - peersKnown: observability.getMetrics().gauges.peers_known || syncManager.listPeers().length, + peers: syncManager.listPeers(), }), getBaseUrl: () => syncManager.getLocalBaseUrl(), }); diff --git a/src/mirrordaemon/runtime_state.test.ts b/src/mirrordaemon/runtime_state.test.ts index 0afbea3cb7..197783eb98 100644 --- a/src/mirrordaemon/runtime_state.test.ts +++ b/src/mirrordaemon/runtime_state.test.ts @@ -204,7 +204,20 @@ describe("mirrordaemon runtime state", () => { wsConnections: 3, sseAvailable: true, wsAvailable: true, - peersKnown: 2, + peers: [ + { + peer_id: "peer-1", + base_url: "http://127.0.0.1:7999", + last_seen_at: "2026-03-13T00:01:00.000Z", + sync_status: "idle", + }, + { + peer_id: "peer-2", + base_url: "http://127.0.0.1:8000", + last_seen_at: "2026-03-13T00:02:00.000Z", + sync_status: "ok", + }, + ], }); expect(health.event_stream).toEqual(runtime.event_stream); diff --git a/src/mirrordaemon/runtime_state.ts b/src/mirrordaemon/runtime_state.ts index e6a3603bd2..260905be59 100644 --- a/src/mirrordaemon/runtime_state.ts +++ b/src/mirrordaemon/runtime_state.ts @@ -23,6 +23,10 @@ type RuntimeStateOverrides = { wsAvailable?: boolean; }; +type HealthStateOverrides = RuntimeStateOverrides & { + peers?: MirrorSyncPeer[]; +}; + function buildCorrelationCapabilities() { return { trace_id: true as const, @@ -180,7 +184,7 @@ export function buildRuntimeSummary( export function buildHealthSummary( daemon: Mirrordaemon, - overrides: RuntimeStateOverrides & { peersKnown?: number } = {}, + overrides: HealthStateOverrides = {}, ): MirrordaemonHealthSummary { const runtime = buildRuntimeSummary(daemon, overrides); const boot = daemon.getBootSnapshot(); @@ -205,7 +209,7 @@ export function buildHealthSummary( fallback_available: boot.readiness.provider.fallback_available, }, sync: { - peers_known: overrides.peersKnown ?? metrics.gauges.peers_known ?? 0, + peers_known: overrides.peers?.length ?? metrics.gauges.peers_known ?? 0, }, observability: { metrics_available: true, From 8958bdb1712c84fa33df3de127a5edfb6ab36c66 Mon Sep 17 00:00:00 2001 From: Agent 0 Date: Mon, 23 Mar 2026 12:30:16 -0400 Subject: [PATCH 2/9] mirrordaemon: pass peer lists through health summary callers --- src/mirror/status/status.ts | 5 ++--- src/mirrordaemon/mirrordaemon.test.ts | 15 ++++++++++++++- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/mirror/status/status.ts b/src/mirror/status/status.ts index ce34e1414c..7487c58af0 100644 --- a/src/mirror/status/status.ts +++ b/src/mirror/status/status.ts @@ -51,8 +51,7 @@ export type GetMirrorStatusOptions = { export async function getMirrorStatus(opts: GetMirrorStatusOptions): Promise { const daemon = opts.runtimeHost.daemon; const boot = daemon.getBootSnapshot(); - const metrics = daemon.getObservability().getMetrics(); - const peersKnown = metrics.gauges.peers_known || opts.runtimeHost.syncManager.listPeers().length; + const peers = opts.runtimeHost.syncManager.listPeers(); const baseUrl = opts.runtimeHost.syncManager.getLocalBaseUrl(); const runtime = getMirrordaemonRuntimeState(daemon, { port: opts.runtimeHost.config.port, @@ -61,7 +60,7 @@ export async function getMirrorStatus(opts: GetMirrorStatusOptions): Promise { const health = getMirrordaemonHealthState(daemon, { port: 7788, baseUrl: "http://127.0.0.1:7788", - peersKnown: 2, + peers: [ + { + peer_id: "peer-1", + base_url: "http://127.0.0.1:7999", + last_seen_at: "2026-03-13T00:01:00.000Z", + sync_status: "idle", + }, + { + peer_id: "peer-2", + base_url: "http://127.0.0.1:8000", + last_seen_at: "2026-03-13T00:02:00.000Z", + sync_status: "ok", + }, + ], }); const debug = getMirrordaemonDebugState(daemon, { port: 7788, From 0316562e24a0d3b8d8dfe4cf6a37166dab01c14c Mon Sep 17 00:00:00 2001 From: Agent 0 Date: Mon, 23 Mar 2026 13:23:27 -0400 Subject: [PATCH 3/9] sync(canon): inject canon sync metric hooks --- src/mirror-sync/canon_sync.ts | 19 ++++++++++++------- src/mirror-sync/sync_manager.ts | 8 ++++++++ 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/src/mirror-sync/canon_sync.ts b/src/mirror-sync/canon_sync.ts index 31123009bb..3d23d5fdeb 100644 --- a/src/mirror-sync/canon_sync.ts +++ b/src/mirror-sync/canon_sync.ts @@ -1,7 +1,6 @@ import crypto from "node:crypto"; import fs from "node:fs/promises"; import path from "node:path"; -import { incrementMetric } from "../mirror-observability/index.js"; import { ensureScrollIndexUpToDate } from "../mirror/lore_sources/index.js"; import { validateLoreDraftInCorpusContext } from "../mirror/lore_validation/index.js"; import type { @@ -10,6 +9,11 @@ import type { MirrorSyncConflict, } from "./sync_types.js"; +type MirrorCanonSyncMetricHooks = { + onConflictWarning?: () => void; + onUpdatesPulled?: (count: number) => void; +}; + async function listCanonicalMarkdownFiles(loreDir: string): Promise { const files: string[] = []; @@ -131,6 +135,7 @@ export async function applyRemoteCanonUpdates(params: { local: MirrorCanonUpdatesSnapshot; remote: MirrorCanonUpdatesSnapshot; remoteContents: Record; + metrics?: MirrorCanonSyncMetricHooks; }): Promise<{ pulledFiles: string[]; skippedFiles: Array<{ path: string; reason: string }>; @@ -145,7 +150,7 @@ export async function applyRemoteCanonUpdates(params: { for (const remoteFile of remoteByPath.values()) { const safePath = resolveSafeCanonPath(params.loreDir, remoteFile.path); if (!safePath) { - incrementMetric("conflict_warnings"); + params.metrics?.onConflictWarning?.(); conflicts.push({ path: remoteFile.path, reason: "unsafe_path", @@ -161,7 +166,7 @@ export async function applyRemoteCanonUpdates(params: { } if (localFile && localFile.updated_at_ms > remoteFile.updated_at_ms) { - incrementMetric("conflict_warnings"); + params.metrics?.onConflictWarning?.(); conflicts.push({ path: remoteFile.path, reason: "local_newer", @@ -175,7 +180,7 @@ export async function applyRemoteCanonUpdates(params: { localFile.updated_at_ms === remoteFile.updated_at_ms && localFile.sha256 !== remoteFile.sha256 ) { - incrementMetric("conflict_warnings"); + params.metrics?.onConflictWarning?.(); conflicts.push({ path: remoteFile.path, reason: "same_timestamp_different_content", @@ -189,7 +194,7 @@ export async function applyRemoteCanonUpdates(params: { params.remote.index_version < params.local.index_version && remoteFile.updated_at_ms <= localFile.updated_at_ms ) { - incrementMetric("conflict_warnings"); + params.metrics?.onConflictWarning?.(); conflicts.push({ path: remoteFile.path, reason: "remote_older_index", @@ -210,7 +215,7 @@ export async function applyRemoteCanonUpdates(params: { draftContent: content, }); if (validation.warningCount > 0) { - incrementMetric("conflict_warnings"); + params.metrics?.onConflictWarning?.(); conflicts.push({ path: remoteFile.path, reason: "invalid_remote_canon", @@ -225,7 +230,7 @@ export async function applyRemoteCanonUpdates(params: { if (pulledFiles.length > 0) { await ensureScrollIndexUpToDate(params.loreDir); - incrementMetric("updates_pulled", pulledFiles.length); + params.metrics?.onUpdatesPulled?.(pulledFiles.length); } return { diff --git a/src/mirror-sync/sync_manager.ts b/src/mirror-sync/sync_manager.ts index 2d120216f3..1e9edc31e3 100644 --- a/src/mirror-sync/sync_manager.ts +++ b/src/mirror-sync/sync_manager.ts @@ -304,6 +304,14 @@ export function createMirrorSyncManager(options: MirrorSyncManagerOptions): Mirr local: localUpdates.canon, remote: remoteUpdates.canon, remoteContents, + metrics: { + onConflictWarning: () => { + incrementMetric("conflict_warnings"); + }, + onUpdatesPulled: (count) => { + incrementMetric("updates_pulled", count); + }, + }, }); const graphResult = await syncLocalGraphFromRemote({ From d9ac7c62eac9e07292076fbd57c757b850428ac4 Mon Sep 17 00:00:00 2001 From: Agent 0 Date: Mon, 23 Mar 2026 13:47:51 -0400 Subject: [PATCH 4/9] sync: inject sync manager observability hooks --- src/mirror-service/runtime_host.ts | 20 ++++++++++ src/mirror-sync/mirror_sync.test.ts | 59 ++++++++++++++++++++++++----- src/mirror-sync/sync_manager.ts | 26 +++++++++---- 3 files changed, 88 insertions(+), 17 deletions(-) diff --git a/src/mirror-service/runtime_host.ts b/src/mirror-service/runtime_host.ts index e4b95266f3..75ded5c368 100644 --- a/src/mirror-service/runtime_host.ts +++ b/src/mirror-service/runtime_host.ts @@ -123,6 +123,26 @@ export async function createMirrorRuntimeHost( baseUrl: config.baseUrl, fetchImpl: deps.fetchImpl, onRuntimeEvent: daemon.publishRuntimeEvent, + observability: { + onConflictWarning: () => { + daemon.getObservability().incrementMetric("conflict_warnings"); + }, + onUpdatesPulled: (count) => { + daemon.getObservability().incrementMetric("updates_pulled", count); + }, + onSyncFailure: () => { + daemon.getObservability().incrementMetric("sync_failures"); + }, + onPeerAnnounced: (payload) => { + daemon.getObservability().logEvent("sync.peer.announced", payload); + }, + onPullCompleted: (payload) => { + daemon.getObservability().logEvent("sync.pull.completed", payload); + }, + onPullFailed: (payload) => { + daemon.getObservability().logEvent("sync.pull.failed", payload); + }, + }, }); return { diff --git a/src/mirror-sync/mirror_sync.test.ts b/src/mirror-sync/mirror_sync.test.ts index 6104a797c4..b6afe40632 100644 --- a/src/mirror-sync/mirror_sync.test.ts +++ b/src/mirror-sync/mirror_sync.test.ts @@ -5,6 +5,8 @@ import { afterEach, describe, expect, it, vi } from "vitest"; import { getMirrorDiagnostics, getMirrorMetrics, + incrementMetric, + logMirrorEvent, resetMirrorDiagnostics, resetMirrorMetrics, } from "../mirror-observability/index.js"; @@ -122,6 +124,43 @@ function createMockResponse() { }; } +function createTestSyncObservabilityHooks() { + return { + onConflictWarning: () => { + incrementMetric("conflict_warnings"); + }, + onUpdatesPulled: (count: number) => { + incrementMetric("updates_pulled", count); + }, + onSyncFailure: () => { + incrementMetric("sync_failures"); + }, + onPeerAnnounced: (payload: { peer_id: string; base_url: string }) => { + logMirrorEvent("sync.peer.announced", payload); + }, + onPullCompleted: (payload: { + peer_id: string; + pulled_files: number; + conflicts: number; + graph_rebuilt: boolean; + }) => { + logMirrorEvent("sync.pull.completed", payload); + }, + onPullFailed: (payload: { peer_id: string; error: string }) => { + logMirrorEvent("sync.pull.failed", payload); + }, + }; +} + +function createObservedSyncManager( + options: Parameters[0], +): MirrorSyncManager { + return createMirrorSyncManager({ + ...options, + observability: createTestSyncObservabilityHooks(), + }); +} + describe("mirror sync", () => { it("executes shared sync actions for the supported routes", async () => { const peer = { @@ -381,7 +420,7 @@ describe("mirror sync", () => { ); await seedIndexFiles(loreDir, { stillness: ["TOBY_L0001_SeedOfStillness.md"] }); - const manager = createMirrorSyncManager({ + const manager = createObservedSyncManager({ nodeId: "node-a", loreDir, baseUrl: "http://127.0.0.1:7001", @@ -407,7 +446,7 @@ describe("mirror sync", () => { ); await seedIndexFiles(loreDir, { renewal: ["TOBY_L0001_SeedOfStillness.md"] }); - const manager = createMirrorSyncManager({ + const manager = createObservedSyncManager({ nodeId: "node-a", loreDir, baseUrl: "http://127.0.0.1:7001", @@ -450,12 +489,12 @@ describe("mirror sync", () => { pond: ["TOBY_L0002_PondMemory.md"], }); - const remoteManager = createMirrorSyncManager({ + const remoteManager = createObservedSyncManager({ nodeId: "node-remote", loreDir: remoteLoreDir, baseUrl: "http://127.0.0.1:7002", }); - const localManager = createMirrorSyncManager({ + const localManager = createObservedSyncManager({ nodeId: "node-local", loreDir: localLoreDir, baseUrl: "http://127.0.0.1:7001", @@ -499,12 +538,12 @@ describe("mirror sync", () => { await fs.utimes(path.join(localLoreDir, "TOBY_L0001_SeedOfStillness.md"), oldTime, oldTime); await fs.utimes(path.join(remoteLoreDir, "TOBY_L0001_SeedOfStillness.md"), newTime, newTime); - const remoteManager = createMirrorSyncManager({ + const remoteManager = createObservedSyncManager({ nodeId: "node-remote", loreDir: remoteLoreDir, baseUrl: "http://127.0.0.1:7002", }); - const localManager = createMirrorSyncManager({ + const localManager = createObservedSyncManager({ nodeId: "node-local", loreDir: localLoreDir, baseUrl: "http://127.0.0.1:7001", @@ -543,12 +582,12 @@ describe("mirror sync", () => { ); await seedIndexFiles(remoteLoreDir, { invalid: ["TOBY_L0002_InvalidRemote.md"] }); - const remoteManager = createMirrorSyncManager({ + const remoteManager = createObservedSyncManager({ nodeId: "node-remote", loreDir: remoteLoreDir, baseUrl: "http://127.0.0.1:7002", }); - const localManager = createMirrorSyncManager({ + const localManager = createObservedSyncManager({ nodeId: "node-local", loreDir: localLoreDir, baseUrl: "http://127.0.0.1:7001", @@ -599,12 +638,12 @@ describe("mirror sync", () => { pond: ["TOBY_L0002_PondMemory.md"], }); - const remoteManager = createMirrorSyncManager({ + const remoteManager = createObservedSyncManager({ nodeId: "node-remote", loreDir: remoteLoreDir, baseUrl: "http://127.0.0.1:7002", }); - const localManager = createMirrorSyncManager({ + const localManager = createObservedSyncManager({ nodeId: "node-local", loreDir: localLoreDir, baseUrl: "http://127.0.0.1:7001", diff --git a/src/mirror-sync/sync_manager.ts b/src/mirror-sync/sync_manager.ts index 1e9edc31e3..95cfe87b19 100644 --- a/src/mirror-sync/sync_manager.ts +++ b/src/mirror-sync/sync_manager.ts @@ -1,5 +1,4 @@ import express from "express"; -import { incrementMetric, logMirrorEvent } from "../mirror-observability/index.js"; import type { FetchLike } from "../mirror-provider/index.js"; import { applyRemoteCanonUpdates, @@ -47,6 +46,19 @@ type MirrorSyncManagerOptions = { fetchImpl?: FetchLike; registry?: MirrorPeerRegistry; onRuntimeEvent?: (type: string, payload?: Record) => void; + observability?: { + onConflictWarning?: () => void; + onUpdatesPulled?: (count: number) => void; + onSyncFailure?: () => void; + onPeerAnnounced?: (payload: { peer_id: string; base_url: string }) => void; + onPullCompleted?: (payload: { + peer_id: string; + pulled_files: number; + conflicts: number; + graph_rebuilt: boolean; + }) => void; + onPullFailed?: (payload: { peer_id: string; error: string }) => void; + }; }; async function parseJsonResponse(response: Response): Promise { @@ -241,7 +253,7 @@ export function createMirrorSyncManager(options: MirrorSyncManagerOptions): Mirr peer_id: peer.peer_id, base_url: peer.base_url, }); - logMirrorEvent("sync.peer.announced", { + options.observability?.onPeerAnnounced?.({ peer_id: peer.peer_id, base_url: peer.base_url, }); @@ -306,10 +318,10 @@ export function createMirrorSyncManager(options: MirrorSyncManagerOptions): Mirr remoteContents, metrics: { onConflictWarning: () => { - incrementMetric("conflict_warnings"); + options.observability?.onConflictWarning?.(); }, onUpdatesPulled: (count) => { - incrementMetric("updates_pulled", count); + options.observability?.onUpdatesPulled?.(count); }, }, }); @@ -326,7 +338,7 @@ export function createMirrorSyncManager(options: MirrorSyncManagerOptions): Mirr conflicts: canonResult.conflicts.length, graph_rebuilt: graphResult.rebuilt, }); - logMirrorEvent("sync.pull.completed", { + options.observability?.onPullCompleted?.({ peer_id: peer.peer_id, pulled_files: canonResult.pulledFiles.length, conflicts: canonResult.conflicts.length, @@ -340,13 +352,13 @@ export function createMirrorSyncManager(options: MirrorSyncManagerOptions): Mirr graphResult, }); } catch (error) { - incrementMetric("sync_failures"); + options.observability?.onSyncFailure?.(); registry.markStatus(peer.peer_id, "error", String(error)); options.onRuntimeEvent?.("sync.pull.failed", { peer_id: peer.peer_id, error: String(error), }); - logMirrorEvent("sync.pull.failed", { + options.observability?.onPullFailed?.({ peer_id: peer.peer_id, error: String(error), }); From c54166192c57caa5ce629d52393f6e39bfcc111b Mon Sep 17 00:00:00 2001 From: ToadAid Date: Mon, 23 Mar 2026 15:04:46 -0400 Subject: [PATCH 5/9] status: derive status fields from daemon summaries ## Summary - derive status fields from existing daemon-backed runtime and health summaries instead of reading the boot snapshot directly for those same values - keep provider list behavior unchanged - add focused status-test assertions to pin the summary-derived field relationships ## Why This tightens the Mirror-native status seam by reducing mixed truth sources and leaning on daemon-backed summaries already in use. ## Scope Changed: - src/mirror/status/status.ts - src/mirror/status/tests/status.test.ts Not changed: - src/mirror-service/mirror_service.ts - src/mirrordaemon/runtime_state.ts - src/mirror-sync/* - adapter-boundary files - compat runtime files - status formatting - provider list behavior ## Details This patch: - derives service.lore_dir from health.service.lore_dir - derives service.provider_url from health.service.provider_url - derives service.operator_auth_configured from health.service.operator_auth_configured - derives service.workspace_users_root from runtime.readiness.workspace.users_root - derives lore.ready / lore.discovered_files from runtime.readiness.lore - derives lore.dir from health.service.lore_dir - derives workspace.ready / workspace.users_root from runtime.readiness.workspace - derives sync.node_id from runtime.node_id ## Verification - pnpm vitest run src/mirror/status/tests/status.test.ts Co-authored-by: Agent 0 --- src/mirror/status/status.ts | 21 ++++++++++----------- src/mirror/status/tests/status.test.ts | 3 +++ 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/src/mirror/status/status.ts b/src/mirror/status/status.ts index 7487c58af0..fc2792df90 100644 --- a/src/mirror/status/status.ts +++ b/src/mirror/status/status.ts @@ -50,7 +50,6 @@ export type GetMirrorStatusOptions = { export async function getMirrorStatus(opts: GetMirrorStatusOptions): Promise { const daemon = opts.runtimeHost.daemon; - const boot = daemon.getBootSnapshot(); const peers = opts.runtimeHost.syncManager.listPeers(); const baseUrl = opts.runtimeHost.syncManager.getLocalBaseUrl(); const runtime = getMirrordaemonRuntimeState(daemon, { @@ -66,10 +65,10 @@ export async function getMirrorStatus(opts: GetMirrorStatusOptions): Promise { expect(status.runtime.node_id).toBe("status-node"); expect(status.runtime.sessions.total).toBe(0); expect(status.service.lore_dir).toBe(path.resolve(loreDir)); + expect(status.lore.dir).toBe(status.service.lore_dir); + expect(status.service.workspace_users_root).toBe(status.workspace.users_root); expect(status.provider.configured).toBe(false); expect(status.provider.active_provider_id).toBe("primary"); expect(status.provider.total).toBe(1); @@ -71,6 +73,7 @@ describe("mirror status", () => { expect(status.runtime.sessions.total).toBe(1); expect(status.runtime.sessions.open).toBe(1); + expect(status.sync.node_id).toBe(status.runtime.node_id); expect(status.provider.providers[0]?.provider_id).toBe("primary"); const json = JSON.stringify(status); From a3f8989179ef6234b2eb920f46ea7444762cdc22 Mon Sep 17 00:00:00 2001 From: ToadAid Date: Mon, 23 Mar 2026 15:35:43 -0400 Subject: [PATCH 6/9] status: derive provider list from daemon provider summary ## Summary - derive status.provider.providers from the daemon-backed provider summary instead of reading runtimeHost.providerPlane.listProviders() directly - extend the daemon provider summary to carry url and last_error - add focused daemon provider-summary coverage for the new fields ## Why This finishes the remaining provider truth gap in the status seam by making the status provider list come from the same daemon-backed summary path as the other provider aggregate fields. ## Scope Changed: - src/mirrordaemon/daemon_types.ts - src/mirrordaemon/runtime_state.ts - src/mirrordaemon/runtime_state.test.ts - src/mirror/status/status.ts Not changed: - src/mirror-service/* - src/mirror-sync/* - adapter-boundary files - compat runtime files - status formatting - the excluded #130 / #133 seams ## Details This patch: - adds url and last_error to MirrordaemonProvidersSummary.providers[] - populates those fields in buildProvidersSummary(...) - changes status.ts to source: - provider.active_provider_id - provider.total - provider.available - provider.fallback_available - provider.providers from getMirrordaemonProvidersState(...) instead of mixing in direct providerPlane.listProviders() reads ## Verification - pnpm vitest run src/mirrordaemon/runtime_state.test.ts - pnpm vitest run src/mirror/status/tests/status.test.ts --------- Co-authored-by: Agent 0 --- src/mirror/status/status.ts | 14 +++--- src/mirrordaemon/daemon_types.ts | 2 + src/mirrordaemon/runtime_state.test.ts | 62 ++++++++++++++++++++++++++ src/mirrordaemon/runtime_state.ts | 2 + 4 files changed, 75 insertions(+), 5 deletions(-) diff --git a/src/mirror/status/status.ts b/src/mirror/status/status.ts index fc2792df90..c5aabddfd1 100644 --- a/src/mirror/status/status.ts +++ b/src/mirror/status/status.ts @@ -1,6 +1,7 @@ import type { MirrorRuntimeHost } from "../../mirror-service/index.js"; import { getMirrordaemonHealthState, + getMirrordaemonProvidersState, getMirrordaemonRuntimeState, } from "../../mirrordaemon/index.js"; @@ -61,6 +62,9 @@ export async function getMirrorStatus(opts: GetMirrorStatusOptions): Promise ({ + active_provider_id: providers.active_provider_id, + total: providers.total, + available: providers.available, + fallback_available: providers.fallback_available, + providers: providers.providers.map((provider) => ({ provider_id: provider.provider_id, label: provider.label, url: provider.url, diff --git a/src/mirrordaemon/daemon_types.ts b/src/mirrordaemon/daemon_types.ts index d0e35c3130..9bf7c781f0 100644 --- a/src/mirrordaemon/daemon_types.ts +++ b/src/mirrordaemon/daemon_types.ts @@ -194,9 +194,11 @@ export type MirrordaemonProvidersSummary = { provider_id: string; label: string; kind: string; + url: string; ready: boolean; configured: boolean; selected: boolean; + last_error?: string; }>; }; diff --git a/src/mirrordaemon/runtime_state.test.ts b/src/mirrordaemon/runtime_state.test.ts index 197783eb98..16ea7aab51 100644 --- a/src/mirrordaemon/runtime_state.test.ts +++ b/src/mirrordaemon/runtime_state.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from "vitest"; import { + buildProvidersSummary, buildDebugSnapshot, buildHealthSummary, buildRuntimeSummary, @@ -227,4 +228,65 @@ describe("mirrordaemon runtime state", () => { expect(health.event_stream.recent_events).toBe(daemon.getRecentEvents().length); expect(health.sync.peers_known).toBe(2); }); + + it("includes provider url and last_error in the daemon provider summary", () => { + const daemon = createMirrordaemon({ + config: baseConfig, + lifecycle: { + discoveredLoreFiles: 2, + shutdown: async () => undefined, + }, + runtimeStartedAt: "2026-03-13T00:00:00.000Z", + }); + + const providers = buildProvidersSummary(daemon, { + providerPlane: { + listProviders: () => [ + { + provider_id: "primary", + label: "Primary", + kind: "openai-compatible", + url: "http://brain.local/v1/chat/completions", + ready: true, + configured: true, + selected: true, + last_error: undefined, + }, + { + provider_id: "fallback", + label: "Fallback", + kind: "openai-compatible", + url: "http://brain.local/v1/fallback", + ready: false, + configured: false, + selected: false, + last_error: "provider unavailable", + }, + ], + } as never, + }); + + expect(providers.providers).toEqual([ + { + provider_id: "primary", + label: "Primary", + kind: "openai-compatible", + url: "http://brain.local/v1/chat/completions", + ready: true, + configured: true, + selected: true, + last_error: undefined, + }, + { + provider_id: "fallback", + label: "Fallback", + kind: "openai-compatible", + url: "http://brain.local/v1/fallback", + ready: false, + configured: false, + selected: false, + last_error: "provider unavailable", + }, + ]); + }); }); diff --git a/src/mirrordaemon/runtime_state.ts b/src/mirrordaemon/runtime_state.ts index 260905be59..f4e0575dfb 100644 --- a/src/mirrordaemon/runtime_state.ts +++ b/src/mirrordaemon/runtime_state.ts @@ -95,9 +95,11 @@ export function buildProvidersSummary( provider_id: provider.provider_id, label: provider.label, kind: provider.kind, + url: provider.url, ready: provider.ready, configured: provider.configured, selected: provider.selected, + last_error: provider.last_error, })) ?? []; return { From 2d3776d3d1ec49daed0cff3921a328669d09fd28 Mon Sep 17 00:00:00 2001 From: ToadAid Date: Mon, 23 Mar 2026 19:22:36 -0400 Subject: [PATCH 7/9] runtime(ws): derive hello envelope from daemon runtime summary Description: - derive websocket hello envelope truth from daemon runtime summary - source node_id and runtime_started_at from getMirrordaemonRuntimeState(...) - preserve hello envelope shape, backlog replay, and live event flow - keep scope limited to runtime_events_ws only - no routing, sync, compat, daemon-internal, or protocol-shape changes After merge, run this on home dev: Co-authored-by: Agent 0 --- src/mirror-service/runtime_events_ws.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/mirror-service/runtime_events_ws.ts b/src/mirror-service/runtime_events_ws.ts index a91aab36a0..ba1b985ba1 100644 --- a/src/mirror-service/runtime_events_ws.ts +++ b/src/mirror-service/runtime_events_ws.ts @@ -3,7 +3,11 @@ import { randomUUID } from "node:crypto"; import type http from "node:http"; import type { Duplex } from "node:stream"; import { WebSocketServer, type WebSocket } from "ws"; -import type { Mirrordaemon, MirrordaemonRuntimeEvent } from "../mirrordaemon/index.js"; +import { + getMirrordaemonRuntimeState, + type Mirrordaemon, + type MirrordaemonRuntimeEvent, +} from "../mirrordaemon/index.js"; export const MIRROR_RUNTIME_WS_PROTOCOL = "mirror.runtime.ws.v1"; export const MIRROR_RUNTIME_WS_PATH = "/mirror/runtime/ws"; @@ -121,7 +125,7 @@ export function createMirrorRuntimeWebSocketServer(params: { wss.on("connection", (ws, req) => { sockets.add(ws); const connectionId = randomUUID(); - const boot = params.daemon.getBootSnapshot(); + const runtime = getMirrordaemonRuntimeState(params.daemon); const url = new URL(req.url ?? MIRROR_RUNTIME_WS_PATH, "http://127.0.0.1"); params.daemon.publishRuntimeEvent("runtime.ws.connected", { @@ -133,8 +137,8 @@ export function createMirrorRuntimeWebSocketServer(params: { protocol: MIRROR_RUNTIME_WS_PROTOCOL, type: "hello", connection_id: connectionId, - node_id: boot.config.node_id, - runtime_started_at: boot.runtime_started_at, + node_id: runtime.node_id, + runtime_started_at: runtime.runtime_started_at, stream: RUNTIME_EVENT_STREAM, }); From 79be884d9fd10ce1eb2bef3de42116793629dc8a Mon Sep 17 00:00:00 2001 From: Toadaid Date: Mon, 23 Mar 2026 19:29:21 -0400 Subject: [PATCH 8/9] sync: inject observability hooks through sync manager --- src/mirror-service/runtime_host.ts | 21 +------ src/mirror-sync/sync_manager.ts | 91 +++++++++++++++++++++++------- 2 files changed, 73 insertions(+), 39 deletions(-) diff --git a/src/mirror-service/runtime_host.ts b/src/mirror-service/runtime_host.ts index 75ded5c368..cefab49be7 100644 --- a/src/mirror-service/runtime_host.ts +++ b/src/mirror-service/runtime_host.ts @@ -123,26 +123,7 @@ export async function createMirrorRuntimeHost( baseUrl: config.baseUrl, fetchImpl: deps.fetchImpl, onRuntimeEvent: daemon.publishRuntimeEvent, - observability: { - onConflictWarning: () => { - daemon.getObservability().incrementMetric("conflict_warnings"); - }, - onUpdatesPulled: (count) => { - daemon.getObservability().incrementMetric("updates_pulled", count); - }, - onSyncFailure: () => { - daemon.getObservability().incrementMetric("sync_failures"); - }, - onPeerAnnounced: (payload) => { - daemon.getObservability().logEvent("sync.peer.announced", payload); - }, - onPullCompleted: (payload) => { - daemon.getObservability().logEvent("sync.pull.completed", payload); - }, - onPullFailed: (payload) => { - daemon.getObservability().logEvent("sync.pull.failed", payload); - }, - }, + observability: daemon.getObservability(), }); return { diff --git a/src/mirror-sync/sync_manager.ts b/src/mirror-sync/sync_manager.ts index 95cfe87b19..f1973787b6 100644 --- a/src/mirror-sync/sync_manager.ts +++ b/src/mirror-sync/sync_manager.ts @@ -1,4 +1,5 @@ import express from "express"; +import type { MirrorObservabilityContext } from "../mirror-observability/index.js"; import type { FetchLike } from "../mirror-provider/index.js"; import { applyRemoteCanonUpdates, @@ -39,6 +40,25 @@ export type MirrorSyncHandlers = { pull: (req: express.Request, res: express.Response) => Promise; }; +export type MirrorSyncObservabilityHooks = { + onConflictWarning?: () => void; + onUpdatesPulled?: (count: number) => void; + onSyncFailure?: () => void; + onPeerAnnounced?: (payload: { peer_id: string; base_url: string }) => void; + onPullCompleted?: (payload: { + peer_id: string; + pulled_files: number; + conflicts: number; + graph_rebuilt: boolean; + }) => void; + onPullFailed?: (payload: { peer_id: string; error: string }) => void; +}; + +type MirrorSyncObservabilityContextLike = Pick< + MirrorObservabilityContext, + "incrementMetric" | "logEvent" +>; + type MirrorSyncManagerOptions = { nodeId: string; loreDir: string; @@ -46,21 +66,53 @@ type MirrorSyncManagerOptions = { fetchImpl?: FetchLike; registry?: MirrorPeerRegistry; onRuntimeEvent?: (type: string, payload?: Record) => void; - observability?: { - onConflictWarning?: () => void; - onUpdatesPulled?: (count: number) => void; - onSyncFailure?: () => void; - onPeerAnnounced?: (payload: { peer_id: string; base_url: string }) => void; - onPullCompleted?: (payload: { - peer_id: string; - pulled_files: number; - conflicts: number; - graph_rebuilt: boolean; - }) => void; - onPullFailed?: (payload: { peer_id: string; error: string }) => void; - }; + observability?: MirrorSyncObservabilityHooks | MirrorSyncObservabilityContextLike; }; +function hasMirrorSyncObservabilityContext( + observability: MirrorSyncManagerOptions["observability"], +): observability is MirrorSyncObservabilityContextLike { + return ( + !!observability && + typeof observability === "object" && + "incrementMetric" in observability && + typeof observability.incrementMetric === "function" && + "logEvent" in observability && + typeof observability.logEvent === "function" + ); +} + +function normalizeMirrorSyncObservabilityHooks( + observability: MirrorSyncManagerOptions["observability"], +): MirrorSyncObservabilityHooks { + if (!observability) { + return {}; + } + if (!hasMirrorSyncObservabilityContext(observability)) { + return observability; + } + return { + onConflictWarning: () => { + observability.incrementMetric("conflict_warnings"); + }, + onUpdatesPulled: (count) => { + observability.incrementMetric("updates_pulled", count); + }, + onSyncFailure: () => { + observability.incrementMetric("sync_failures"); + }, + onPeerAnnounced: (payload) => { + observability.logEvent("sync.peer.announced", payload); + }, + onPullCompleted: (payload) => { + observability.logEvent("sync.pull.completed", payload); + }, + onPullFailed: (payload) => { + observability.logEvent("sync.pull.failed", payload); + }, + }; +} + async function parseJsonResponse(response: Response): Promise { if (!response.ok) { throw new Error(`sync request failed: ${response.status} ${await response.text()}`); @@ -237,6 +289,7 @@ function collectMirrorSyncPullNeededPaths( export function createMirrorSyncManager(options: MirrorSyncManagerOptions): MirrorSyncManager { const fetchImpl = options.fetchImpl ?? fetch; const registry = options.registry ?? createMirrorPeerRegistry(); + const observability = normalizeMirrorSyncObservabilityHooks(options.observability); let localBaseUrl = options.baseUrl ? normalizeMirrorPeerBaseUrl(options.baseUrl) : null; return { @@ -253,7 +306,7 @@ export function createMirrorSyncManager(options: MirrorSyncManagerOptions): Mirr peer_id: peer.peer_id, base_url: peer.base_url, }); - options.observability?.onPeerAnnounced?.({ + observability.onPeerAnnounced?.({ peer_id: peer.peer_id, base_url: peer.base_url, }); @@ -318,10 +371,10 @@ export function createMirrorSyncManager(options: MirrorSyncManagerOptions): Mirr remoteContents, metrics: { onConflictWarning: () => { - options.observability?.onConflictWarning?.(); + observability.onConflictWarning?.(); }, onUpdatesPulled: (count) => { - options.observability?.onUpdatesPulled?.(count); + observability.onUpdatesPulled?.(count); }, }, }); @@ -338,7 +391,7 @@ export function createMirrorSyncManager(options: MirrorSyncManagerOptions): Mirr conflicts: canonResult.conflicts.length, graph_rebuilt: graphResult.rebuilt, }); - options.observability?.onPullCompleted?.({ + observability.onPullCompleted?.({ peer_id: peer.peer_id, pulled_files: canonResult.pulledFiles.length, conflicts: canonResult.conflicts.length, @@ -352,13 +405,13 @@ export function createMirrorSyncManager(options: MirrorSyncManagerOptions): Mirr graphResult, }); } catch (error) { - options.observability?.onSyncFailure?.(); + observability.onSyncFailure?.(); registry.markStatus(peer.peer_id, "error", String(error)); options.onRuntimeEvent?.("sync.pull.failed", { peer_id: peer.peer_id, error: String(error), }); - options.observability?.onPullFailed?.({ + observability.onPullFailed?.({ peer_id: peer.peer_id, error: String(error), }); From ac234c2cd6773b863f1d7908d0b4ccba071e6b82 Mon Sep 17 00:00:00 2001 From: Toadaid Date: Mon, 23 Mar 2026 19:45:31 -0400 Subject: [PATCH 9/9] mirrordaemon: drop debug peersKnown override fallback --- src/mirror-service/mirror_service.ts | 1 - src/mirrordaemon/mirrordaemon.test.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/src/mirror-service/mirror_service.ts b/src/mirror-service/mirror_service.ts index 42aa02a879..8951968094 100644 --- a/src/mirror-service/mirror_service.ts +++ b/src/mirror-service/mirror_service.ts @@ -180,7 +180,6 @@ export async function startMirrorService( getMirrordaemonDebugState(daemon, { port: boundPort, baseUrl: syncManager.getLocalBaseUrl(), - peersKnown: observability.getMetrics().gauges.peers_known || syncManager.listPeers().length, }), ); }); diff --git a/src/mirrordaemon/mirrordaemon.test.ts b/src/mirrordaemon/mirrordaemon.test.ts index 4ab69a8e63..48dca71885 100644 --- a/src/mirrordaemon/mirrordaemon.test.ts +++ b/src/mirrordaemon/mirrordaemon.test.ts @@ -109,7 +109,6 @@ describe("mirrordaemon", () => { const debug = getMirrordaemonDebugState(daemon, { port: 7788, baseUrl: "http://127.0.0.1:7788", - peersKnown: 2, }); expect(runtime.node_id).toBe("daemon-node");