Reduce idle work and disk churn with native resource diagnostics#2679

merged15
main
t3code/a55b49df
openedMay 13, 2026byJJulius Marminge
View on GitHubTwo-way sync enabledPR last imported 4d ago
38 unresolved threads · Jump to file
Selected thread: apps/server/src/resourceTelemetry/NativeTelemetryClient.ts:652 — 🟡 **Medium** `resourceTelemetry/NativeTelemetryClient.ts:652` Concurrent calls to `setHostPowerState` or `changeLiveSubscriberCount` can overwrite the sidecar with stale values. Each call atomically commits the new control state to `collectionControl` via `Ref.modify`, then calls `applyCollectionControl` to send the corresponding commands — but these two steps are not serialized together. Two overlapping calls can commit in order A then B (so the ref holds B's state), yet have `applyCollectionControl` acquire `commandMutex` in order B then A, causing the older call A to send stale interval/streaming values to the sidecar last. The sidecar then runs at the wrong sampling interval or streaming mode until the next control change or restart, even though the in-memory ref reflects the newer state. The same problem affects `setExternalProcesses`: `Ref.set` and the subsequent `writeCommand` are independent, so concurrent callers can have their `commandMutex` acquisition order differ from their ref write order, leaving the sidecar configured with an older process list while `externalProcesses` holds the newest. Consider serializing the entire read-modify-send sequence (both the ref update and the command writes) under a shared mutex so that commits and their corresponding sidecar commands always apply in the same order. <details> <summary>🚀 Reply "<strong>fix it for me</strong>" or copy this <strong>AI Prompt</strong> for your agent:</summary> ```text In file @apps/server/src/resourceTelemetry/NativeTelemetryClient.ts around line 652: Concurrent calls to `setHostPowerState` or `changeLiveSubscriberCount` can overwrite the sidecar with stale values. Each call atomically commits the new control state to `collectionControl` via `Ref.modify`, then calls `applyCollectionControl` to send the corresponding commands — but these two steps are not serialized together. Two overlapping calls can commit in order A then B (so the ref holds B's state), yet have `applyCollectionControl` acquire `commandMutex` in order B then A, causing the older call A to send stale interval/streaming values to the sidecar last. The sidecar then runs at the wrong sampling interval or streaming mode until the next control change or restart, even though the in-memory ref reflects the newer state. The same problem affects `setExternalProcesses`: `Ref.set` and the subsequent `writeCommand` are independent, so concurrent callers can have their `commandMutex` acquisition order differ from their ref write order, leaving the sidecar configured with an older process list while `externalProcesses` holds the newest. Consider serializing the entire read-modify-send sequence (both the ref update and the command writes) under a shared mutex so that commits and their corresponding sidecar commands always apply in the same order. ``` </details>
0 / 100 viewed100 files changedLine totals unavailable

apps/web/src/hooks/serverSettingsWriteQueue.ts · Earlier revision 038e209e · right 9–30

Original diff
@@ -0,0 +1,31 @@
+import type { ServerSettings, ServerSettingsPatch } from "@t3tools/contracts";
+
+export interface ServerSettingsWriteQueue {
+  readonly enqueue: (patch: ServerSettingsPatch) => void;
+  readonly reset: () => void;
+  readonly drain: () => Promise<void>;
+}
+
+export function createServerSettingsWriteQueue(input: {
+  readonly updateSettings: (patch: ServerSettingsPatch) => Promise<ServerSettings>;
+  readonly applySettings: (settings: ServerSettings) => void;
+  readonly onError: (error: unknown) => void;
+}): ServerSettingsWriteQueue {
+  let queue: Promise<void> = Promise.resolve();
+
+  return {
+    enqueue: (patch) => {
+      queue = queue
+        .catch(() => undefined)
+        .then(async () => {
+          const settings = await input.updateSettings(patch);
+          input.applySettings(settings);
+        })
+        .catch(input.onError);
+    },
+    reset: () => {
+      queue = Promise.resolve();
+    },
+    drain: () => queue,
+  };

apps/web/src/hooks/serverSettingsWriteQueue.ts · right 9–30

M
macroscopeappBotMay 13, 2026

🟢 Low hooks/serverSettingsWriteQueue.ts:9

reset() breaks serialization by replacing queue with Promise.resolve() while prior operations are still in-flight. The old chain continues running and will invoke applySettings and onError, but new enqueue() calls start immediately from the resolved promise, so their operations run concurrently with the old ones. This allows applySettings callbacks to fire out-of-order, with stale settings potentially overwriting newer ones.

diff
1 return {
2 enqueue: (patch) => {
3 queue = queue
4 .catch(() => undefined)
5 .then(async () => {
6 const settings = await input.updateSettings(patch);
7 input.applySettings(settings);
8 })
9 .catch(input.onError);
10 },
11- reset: () => {
12- queue = Promise.resolve();
13- },
14+ reset: () => {
15+ const currentQueue = queue;
16+ queue = queue.then(() => {});
17+ return currentQueue;
18+ },
19 drain: () => queue,
20 };
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
text
1In file apps/web/src/hooks/serverSettingsWriteQueue.ts around lines 9-30:
2
3`reset()` breaks serialization by replacing `queue` with `Promise.resolve()` while prior operations are still in-flight. The old chain continues running and will invoke `applySettings` and `onError`, but new `enqueue()` calls start immediately from the resolved promise, so their operations run concurrently with the old ones. This allows `applySettings` callbacks to fire out-of-order, with stale settings potentially overwriting newer ones.
4
5Evidence trail:
6apps/web/src/hooks/serverSettingsWriteQueue.ts lines 16-32 (reset() implementation at lines 28-30, enqueue() at lines 18-25); apps/web/src/hooks/useSettings.ts line 237-244 (__resetClientSettingsPersistenceForTests calls serverSettingsWriteQueue.reset()); apps/web/src/localApi.ts lines 194-204 (__resetLocalApiForTests calls __resetClientSettingsPersistenceForTests); git_grep for 'serverSettingsWriteQueue\.reset' shows only one caller in test cleanup code.
J
Julius MarmingeJul 28, 2026

The obsolete write-queue implementation was removed by the settings persistence refactor in this PR, so reset can no longer detach an in-flight promise chain. Settings writes now flow through the current server-backed settings path.

packages/shared/src/serverSettings.ts · Earlier revision 038e209e · right 120

Original diff
@@ -76,14 +81,61 @@ export function applyServerSettingsPatch(
   patch: ServerSettingsPatch,
 ): ServerSettings {
   const selectionPatch = patch.textGenerationModelSelection;
-  const { automaticGitFetchInterval, ...patchForMerge } = patch;
+  const {
+    automaticGitFetchInterval,
+    providerHealthRefreshInterval,
+    backgroundActivityProfile,
+    backgroundActivity,
+    ...patchForMerge
+  } = patch;
+  const backgroundActivityPatch =
+    backgroundActivityProfile !== undefined
+      ? {
+          schemaVersion: 1 as const,
+          profile: backgroundActivityProfile,
+          overrides: {},
+        }
+      : automaticGitFetchInterval !== undefined || providerHealthRefreshInterval !== undefined
+        ? {
+            schemaVersion: 1 as const,
+            profile: "custom" as const,
+            baseProfile: getBackgroundActivityBaseProfile(current.backgroundActivity),
+            overrides: {
+              ...current.backgroundActivity.overrides,
+              ...(automaticGitFetchInterval !== undefined ? { automaticGitFetchInterval } : {}),
+              ...(providerHealthRefreshInterval !== undefined
+                ? { providerHealthRefreshInterval }
+                : {}),
+            },
+          }
+        : undefined;
   const next = deepMerge(current, patchForMerge);
-  const nextWithReplacements = {
+  const nextWithReplacementsBase = {
     ...next,
+    ...(backgroundActivity !== undefined
+      ? { backgroundActivity: deepMerge(current.backgroundActivity, backgroundActivity) }
+      : {}),
+    ...(backgroundActivityPatch !== undefined
+      ? { backgroundActivity: backgroundActivityPatch }
+      : {}),
C
cursorBotMay 13, 2026

Legacy patch fields silently override explicit backgroundActivity patch

Medium Severity

When a patch contains both an explicit backgroundActivity object AND a legacy field like automaticGitFetchInterval, the object spread ordering causes backgroundActivityPatch (derived from legacy fields) to silently overwrite the already-merged backgroundActivity value. The explicit new-style patch is lost because it's spread first and then immediately overwritten by the legacy-derived patch on lines 118-120.

Additional Locations (1)

Reviewed by Cursor Bugbot for commit 038e209ececaa278e51971a83a73cf11e7ecdeb8. Configure here.

apps/web/src/components/settings/SourceControlSettings.tsx · Earlier revision 038e209e · right 82

Original diff
@@ -70,6 +75,11 @@ const VCS_ICONS: Partial<Record<VcsDriverKind, Icon>> = {
 
 const SOURCE_CONTROL_SKELETON_ROWS = ["primary", "secondary"] as const;
 const GIT_FETCH_INTERVAL_STEP_SECONDS = 5;
+type BackgroundActivityOverridePatch = Partial<{
+  [K in keyof BackgroundActivitySettings["overrides"]]:
+    | BackgroundActivitySettings["overrides"][K]
+    | undefined;
+}>;
C
cursorBotMay 13, 2026

Duplicated utility functions across settings panel files

Low Severity

SourceControlSettings.tsx defines BackgroundActivityOverridePatch, durationToSeconds, normalizeFetchIntervalSeconds, backgroundActivityOverrideSettings, and BackgroundPolicyTooltip which are identical (or trivially renamed) copies of the same types, functions, and components already defined in SettingsPanels.tsx. If the override-building logic needs a bug fix, it must be applied in both places.

Additional Locations (1)

Reviewed by Cursor Bugbot for commit 038e209ececaa278e51971a83a73cf11e7ecdeb8. Configure here.

apps/web/src/components/settings/SettingsPanels.tsx · File comment

M
macroscopeappBotMay 20, 2026

🟡 Medium

https://github.com/pingdotgg/t3code/blob/8da08596ca6d3ee2b5e6ae44164ee396a48bcf3f/apps/web/src/components/settings/SettingsPanels.tsx#L181-L186

When a user clears the NumberField for host power monitor intervals, onValueChange receives null. normalizeIntervalSeconds(null) returns 0, bypassing the min={5} constraint and saving Duration.seconds(0) to settings. This violates the minimum interval requirement, which could cause excessive background polling.

Consider enforcing the minimum in normalizeIntervalSeconds or handling the null case explicitly to ensure the saved value respects the min constraint.

diff
1function normalizeIntervalSeconds(value: number | null): number {
2 if (value === null || !Number.isFinite(value)) {
3- return 0;
4+ return NaN;
5 }
6- return Math.max(0, Math.round(value));
7+ return Math.max(5, Math.round(value));
8 }
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
text
1In file apps/web/src/components/settings/SettingsPanels.tsx around lines 181-186:
2
3When a user clears the `NumberField` for host power monitor intervals, `onValueChange` receives `null`. `normalizeIntervalSeconds(null)` returns `0`, bypassing the `min={5}` constraint and saving `Duration.seconds(0)` to settings. This violates the minimum interval requirement, which could cause excessive background polling.
4
5Consider enforcing the minimum in `normalizeIntervalSeconds` or handling the `null` case explicitly to ensure the saved value respects the `min` constraint.
6
7Evidence trail:
8SettingsPanels.tsx lines 181-186: normalizeIntervalSeconds returns 0 for null. Lines 778-801: Host power monitor active interval NumberField with min={5} and onValueChange at line 784 that saves Duration.seconds(normalizeIntervalSeconds(value)). Lines 804-835: Idle host power monitor NumberField also min={5}. Lines 212-233: backgroundActivityOverrideSettings performs no validation on the duration value. apps/web/package.json line 16: @base-ui/react ^1.4.1. apps/web/src/components/ui/number-field.tsx: NumberField wraps @base-ui/react NumberFieldPrimitive.Root with props passthrough.
J
Julius MarmingeJul 28, 2026

Fixed in 24e6ccd404. Host power NumberFields now normalize null and non-finite values to their declared five-second minimum; the integrated UI check cleared the field, reopened the dialog, and verified it persisted as 5 rather than 0.

apps/web/src/components/settings/SettingsPanels.tsx · Earlier revision 79ad0711 · right 684

Original diff
@@ -479,10 +630,255 @@ export function useSettingsRestore(onRestored?: () => void) {
   };
 }
 
+function BackgroundActivityAdvancedDialog({
+  open,
+  onOpenChange,
+}: {
+  readonly open: boolean;
+  readonly onOpenChange: (open: boolean) => void;
+}) {
+  const settings = useSettings();
+  const { updateSettings } = useUpdateSettings();
+  const resolvedBackgroundActivity = resolveServerBackgroundActivitySettings(settings);
+  const activeProfile = getBackgroundActivityBaseProfile(settings.backgroundActivity);
+  const automaticGitFetchIntervalSeconds = durationToSeconds(
+    resolvedBackgroundActivity.automaticGitFetchInterval,
+  );
+  const providerHealthRefreshIntervalSeconds = durationToSeconds(
+    resolvedBackgroundActivity.providerHealthRefreshInterval,
+  );
+  const hostPowerMonitorActiveIntervalSeconds = durationToSeconds(
+    resolvedBackgroundActivity.hostPowerMonitorActiveInterval,
+  );
+  const hostPowerMonitorIdleIntervalSeconds = durationToSeconds(
+    resolvedBackgroundActivity.hostPowerMonitorIdleInterval,
+  );
+
+  return (
+    <Dialog open={open} onOpenChange={onOpenChange}>
+      <DialogPopup className="max-w-xl">
+        <DialogHeader>
+          <DialogTitle>Background Activity</DialogTitle>
+          <DialogDescription>
+            Tune the shared power policy and the background intervals that feed it.
+          </DialogDescription>
+        </DialogHeader>
+        <DialogPanel className="space-y-0 px-6 pb-5">
+          <div className="overflow-hidden rounded-xl border bg-card text-card-foreground">
+            <div className="flex flex-col gap-3 border-b px-4 py-3 sm:flex-row sm:items-center sm:justify-between">
+              <div className="min-w-0 space-y-1">
+                <div className="text-sm font-medium">Shared policy</div>
+                <p className="text-xs leading-relaxed text-muted-foreground">
+                  Controls whether background work may run after a subscribed interval fires.
+                </p>
+              </div>
+              <Select
+                value={activeProfile}
+                onValueChange={(value) => {
+                  if (
+                    value === "balanced" ||
+                    value === "performance" ||
+                    value === "battery-saver"
+                  ) {
+                    updateSettings(backgroundActivityProfileSettings(value));
+                  }
C
cursorBotJun 16, 2026

Advanced dialog clears overrides

Medium Severity

In the advanced Background Activity dialog, changing “Shared policy” calls backgroundActivityProfileSettings, which sets profile to a preset and overrides to {}. Custom interval and pause toggles are wiped instead of updating baseProfile while staying on a custom profile.

Reviewed by Cursor Bugbot for commit 79ad07118493b7dfdc96c9205a913b356547f5bb. Configure here.

apps/server/src/vcs/VcsStatusBroadcaster.ts · Earlier revision 79ad0711 · right 281

Original diff
@@ -270,6 +272,14 @@ export const layer = Layer.effect(
             return activeInterval;
           }
 
+          const shouldRun = yield* backgroundPolicy.shouldRunScopeWork({
+            type: "vcs-status",
+            cwd,
+          });
+          if (!shouldRun) {
+            return activeInterval;
+          }
C
cursorBotJun 16, 2026

VCS scope cwd mismatch

High Severity

Remote VCS polling now gates on shouldRunScopeWork using the broadcaster’s normalized cwd, while client activity leases retain vcs-status with the raw project path. When those strings differ (for example after realPath), scope keys never match, so automatic remote refreshes stay disabled even with an active subscription and foreground client.

Additional Locations (1)

Reviewed by Cursor Bugbot for commit 79ad07118493b7dfdc96c9205a913b356547f5bb. Configure here.

apps/server/src/ws.ts · File comment

M
macroscopeappBotJun 16, 2026

🔴 Critical

https://github.com/pingdotgg/t3code/blob/79ad07118493b7dfdc96c9205a913b356547f5bb/apps/server/src/ws.ts#L210

The new RPC handlers serverReportClientActivity, serverReportHostPowerState, serverGetBackgroundPolicy, and subscribeBackgroundPolicy are missing entries in RPC_REQUIRED_SCOPE, so requiredScopeForMethod() throws Error: RPC method X has no declared authorization scope. when any of these methods are invoked.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
text
1In file @apps/server/src/ws.ts around line 210:
2
3The new RPC handlers `serverReportClientActivity`, `serverReportHostPowerState`, `serverGetBackgroundPolicy`, and `subscribeBackgroundPolicy` are missing entries in `RPC_REQUIRED_SCOPE`, so `requiredScopeForMethod()` throws `Error: RPC method X has no declared authorization scope.` when any of these methods are invoked.
4
5Evidence trail:
6apps/server/src/ws.ts lines 143-212 (RPC_REQUIRED_SCOPE map — none of the four methods listed); apps/server/src/ws.ts lines 321-327 (requiredScopeForMethod throws if method not in map); apps/server/src/ws.ts lines 1112-1131 (serverReportClientActivity, serverReportHostPowerState, serverGetBackgroundPolicy handlers using observeRpcEffect); apps/server/src/ws.ts lines 1650-1658 (subscribeBackgroundPolicy handler using observeRpcStream); apps/server/src/ws.ts lines 333-345 (observeRpcEffect and observeRpcStream call requiredScopeForMethod).
J
Julius MarmingeJul 28, 2026

Fixed in the rebased stack. The activity and resource telemetry RPC methods now declare authorization scopes in RpcAuthorization.ts, with focused authorization coverage. The integrated diagnostics page no longer reports undeclared-scope errors.

apps/server/src/background/BackgroundPolicy.ts · Earlier revision 79ad0711 · right 219

Original diff
@@ -0,0 +1,276 @@
+import {
+  type AuthSessionId,
+  type BackgroundPolicySnapshot,
+  type BackgroundScope,
+  type ClientActivityLease,
+  type ClientActivityReportInput,
+  type HostPowerSnapshot,
+  type RpcClientId,
+} from "@t3tools/contracts";
+import {
+  getBackgroundActivityPresetSettings,
+  resolveServerBackgroundActivitySettings,
+  type ResolvedBackgroundActivitySettings,
+} from "@t3tools/shared/backgroundActivitySettings";
+import * as DateTime from "effect/DateTime";
+import * as Context from "effect/Context";
+import * as Effect from "effect/Effect";
+import * as Layer from "effect/Layer";
+import * as PubSub from "effect/PubSub";
+import * as Ref from "effect/Ref";
+import * as Stream from "effect/Stream";
+
+import { ServerSettingsService } from "../serverSettings.ts";
+import * as HostPowerMonitor from "./HostPowerMonitor.ts";
+
+export interface BackgroundPolicyShape {
+  readonly reportClientActivity: (
+    sessionId: AuthSessionId,
+    rpcClientId: RpcClientId,
+    input: ClientActivityReportInput,
+  ) => Effect.Effect<void>;
+  readonly removeRpcClient: (rpcClientId: RpcClientId) => Effect.Effect<void>;
+  readonly reportHostPowerState: (snapshot: HostPowerSnapshot) => Effect.Effect<void>;
+  readonly snapshot: Effect.Effect<BackgroundPolicySnapshot>;
+  readonly streamChanges: Stream.Stream<BackgroundPolicySnapshot>;
+  readonly hasDemand: (scope: BackgroundScope) => Effect.Effect<boolean>;
+  readonly shouldRunScopeWork: (scope: BackgroundScope) => Effect.Effect<boolean>;
+  readonly shouldRunOpportunisticWork: Effect.Effect<boolean>;
+}
+
+export class BackgroundPolicy extends Context.Service<BackgroundPolicy, BackgroundPolicyShape>()(
+  "t3/background/BackgroundPolicy",
+) {}
+
+const DEFAULT_LEASE_TTL_MS = 45_000;
+const MAX_LEASE_TTL_MS = 120_000;
+
+function scopeKey(scope: BackgroundScope): string {
+  switch (scope.type) {
+    case "server-config":
+    case "diagnostics":
+      return scope.type;
+    case "provider-status":
+      return scope.instanceId ? `${scope.type}:${scope.instanceId}` : scope.type;
+    case "vcs-status":
+    case "git-refs":
+      return `${scope.type}:${scope.cwd}`;
+    case "thread":
+      return `${scope.type}:${scope.threadId}`;
+  }
+}
+
+function isLeaseActive(lease: ClientActivityLease, now: DateTime.Utc): boolean {
+  return DateTime.isGreaterThan(lease.expiresAt, now);
+}
+
+function isForegroundLease(lease: ClientActivityLease, now: DateTime.Utc): boolean {
+  return isLeaseActive(lease, now) && lease.visible && lease.focused;
+}
+
+function leaseHasScope(lease: ClientActivityLease, scope: BackgroundScope): boolean {
+  const key = scopeKey(scope);
+  return lease.scopes.some((leaseScope) => scopeKey(leaseScope) === key);
+}
+
+function hasThermalPressure(hostPower: HostPowerSnapshot): boolean {
+  return hostPower.thermalState === "serious" || hostPower.thermalState === "critical";
+}
+
+function isHostConstrained(
+  hostPower: HostPowerSnapshot,
+  settings: ResolvedBackgroundActivitySettings,
+): boolean {
+  if (
+    (settings.pauseWhenHostLocked && hostPower.locked === "true") ||
+    hasThermalPressure(hostPower)
+  ) {
+    return true;
+  }
+  if (settings.pauseWhenHostLowPower && hostPower.lowPowerMode === "true") return true;
+  return settings.pauseWhenOnBattery && hostPower.onBattery === "true";
+}
+
+function isClientConstrained(
+  lease: ClientActivityLease,
+  settings: ResolvedBackgroundActivitySettings,
+): boolean {
+  if (settings.pauseWhenClientLowPower && lease.lowPowerMode === "true") return true;
+  return settings.pauseWhenOnBattery && lease.batteryState === "unplugged";
+}
+
+function leaseMayRunScopedWork(
+  lease: ClientActivityLease,
+  scope: BackgroundScope,
+  now: DateTime.Utc,
+  settings: ResolvedBackgroundActivitySettings,
+): boolean {
+  const activeWithScope = isLeaseActive(lease, now) && leaseHasScope(lease, scope);
+  if (!activeWithScope || isClientConstrained(lease, settings)) {
+    return false;
+  }
+  if (settings.profile === "performance") {
+    return true;
+  }
+  return isForegroundLease(lease, now);
+}
+
+function computeSnapshot(input: {
+  readonly hostPower: HostPowerSnapshot;
+  readonly leases: ReadonlyMap<string, ClientActivityLease>;
+  readonly now: DateTime.Utc;
+  readonly settings: ResolvedBackgroundActivitySettings;
+  readonly updatedAt: DateTime.Utc;
+}): BackgroundPolicySnapshot {
+  const activeLeases = [...input.leases.values()].filter((lease) =>
+    isLeaseActive(lease, input.now),
+  );
+  const foregroundLeases = activeLeases.filter((lease) => isForegroundLease(lease, input.now));
+  const activeScopeKeys = new Set<string>();
+  for (const lease of activeLeases) {
+    for (const scope of lease.scopes) {
+      activeScopeKeys.add(scopeKey(scope));
+    }
+  }
+
+  return {
+    hostPower: input.hostPower,
+    leases: activeLeases,
+    activeForegroundLeaseCount: foregroundLeases.length,
+    activeScopeKeys: [...activeScopeKeys].toSorted(),
+    shouldRunOpportunisticWork:
+      foregroundLeases.some((lease) => !isClientConstrained(lease, input.settings)) &&
+      !isHostConstrained(input.hostPower, input.settings),
+    updatedAt: input.updatedAt,
+  };
+}
+
+export const make = Effect.fn("background.policy.make")(function* () {
+  const hostPowerMonitor = yield* HostPowerMonitor.HostPowerMonitor;
+  const serverSettings = yield* ServerSettingsService;
+  const leasesRef = yield* Ref.make(new Map<string, ClientActivityLease>());
+  const changes = yield* PubSub.sliding<BackgroundPolicySnapshot>(1);
+
+  const backgroundActivitySettings = serverSettings.getSettings.pipe(
+    Effect.map(resolveServerBackgroundActivitySettings),
+    Effect.catch(() => Effect.succeed(getBackgroundActivityPresetSettings("balanced"))),
+  );
+
+  const snapshot = Effect.gen(function* () {
+    const [hostPower, leases, now, settings] = yield* Effect.all([
+      hostPowerMonitor.snapshot,
+      Ref.get(leasesRef),
+      DateTime.now,
+      backgroundActivitySettings,
+    ]);
+    return computeSnapshot({ hostPower, leases, now, settings, updatedAt: now });
+  });
+
+  const publishSnapshot = snapshot.pipe(
+    Effect.tap((next) => hostPowerMonitor.setDemandActive(next.activeForegroundLeaseCount > 0)),
+    Effect.flatMap((next) => PubSub.publish(changes, next)),
+  );
+
+  const reportClientActivity: BackgroundPolicyShape["reportClientActivity"] = (
+    sessionId,
+    rpcClientId,
+    input,
+  ) =>
+    Effect.gen(function* () {
+      const ttlMs = Math.min(
+        Math.max(input.ttlMs ?? DEFAULT_LEASE_TTL_MS, 1_000),
+        MAX_LEASE_TTL_MS,
+      );
+      const now = yield* DateTime.now;
+      const expiresAt = DateTime.add(now, { milliseconds: ttlMs });
+      const lease: ClientActivityLease = {
+        sessionId,
+        rpcClientId,
+        clientId: input.clientId,
+        clientKind: input.clientKind,
+        visible: input.visible,
+        focused: input.focused,
+        recentlyInteracted: input.recentlyInteracted,
+        ...(input.appState !== undefined ? { appState: input.appState } : {}),
+        ...(input.lowPowerMode !== undefined ? { lowPowerMode: input.lowPowerMode } : {}),
+        ...(input.batteryState !== undefined ? { batteryState: input.batteryState } : {}),
+        ...(input.networkType !== undefined ? { networkType: input.networkType } : {}),
+        scopes: input.scopes,
+        updatedAt: now,
+        expiresAt,
+      };
+      yield* Ref.update(leasesRef, (leases) => {
+        const next = new Map(leases);
+        next.set(`${rpcClientId}:${input.clientId}`, lease);
+        return next;
+      });
+      yield* publishSnapshot;
+    });
+
+  const removeRpcClient: BackgroundPolicyShape["removeRpcClient"] = (rpcClientId) =>
+    Ref.update(leasesRef, (leases) => {
+      const next = new Map(leases);
+      for (const key of next.keys()) {
+        if (key.startsWith(`${rpcClientId}:`)) {
+          next.delete(key);
+        }
+      }
+      return next;
+    }).pipe(Effect.andThen(publishSnapshot), Effect.asVoid);
C
cursorBotJun 16, 2026

Disconnect never clears leases

Medium Severity

removeRpcClient is implemented and tested but never invoked when a WebSocket RPC client disconnects. Activity leases keyed by rpcClientId remain until TTL expiry, so background Git and provider work can keep running briefly after the client is gone.

Additional Locations (1)

Reviewed by Cursor Bugbot for commit 79ad07118493b7dfdc96c9205a913b356547f5bb. Configure here.

apps/server/src/resourceTelemetry/NativeTelemetryClient.ts · Earlier revision 0ebbdad6 · right 677

Original diff
@@ -0,0 +1,922 @@
+import type {
+  HostPowerSnapshot,
+  ResourceMonitorCapabilities,
+  ResourceMonitorCommand,
+  ResourceMonitorEvent,
+  ResourceMonitorExternalProcess,
+  ResourceMonitorHelloEvent,
+  ResourceMonitorSnapshotEvent,
+  ResourceTelemetrySourceStatus,
+} from "@t3tools/contracts";
+import {
+  RESOURCE_MONITOR_PROTOCOL_VERSION,
+  ResourceMonitorCommand as ResourceMonitorCommandSchema,
+  ResourceMonitorEvent as ResourceMonitorEventSchema,
+} from "@t3tools/contracts";
+import * as Cause from "effect/Cause";
+import * as Context from "effect/Context";
+import * as Crypto from "effect/Crypto";
+import * as DateTime from "effect/DateTime";
+import * as Deferred from "effect/Deferred";
+import * as Duration from "effect/Duration";
+import * as Effect from "effect/Effect";
+import * as Fiber from "effect/Fiber";
+import * as Layer from "effect/Layer";
+import * as Option from "effect/Option";
+import * as PubSub from "effect/PubSub";
+import * as Queue from "effect/Queue";
+import * as Ref from "effect/Ref";
+import * as Result from "effect/Result";
+import * as Schema from "effect/Schema";
+import * as Semaphore from "effect/Semaphore";
+import * as Stream from "effect/Stream";
+import * as Ndjson from "effect/unstable/encoding/Ndjson";
+import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
+
+import * as ResourceMonitorBinary from "./ResourceMonitorBinary.ts";
+import { ServerConfig } from "../config.ts";
+
+const SAMPLE_INTERVAL_MS = 1_000;
+const UNKNOWN_BACKGROUND_SAMPLE_INTERVAL_MS = 5_000;
+const BATTERY_SAMPLE_INTERVAL_MS = 5_000;
+const CONSTRAINED_SAMPLE_INTERVAL_MS = 15_000;
+const HANDSHAKE_TIMEOUT = Duration.seconds(5);
+const SAMPLE_REQUEST_TIMEOUT = Duration.seconds(5);
+const HISTORY_REQUEST_TIMEOUT = Duration.seconds(15);
+const INITIAL_RESTART_DELAY = Duration.millis(500);
+const MAX_RESTART_DELAY = Duration.seconds(10);
+const FAILURE_WINDOW_MS = 60_000;
+const MAX_FAILURES_PER_WINDOW = 5;
+
+export class NativeTelemetrySpawnFailed extends Schema.TaggedErrorClass<NativeTelemetrySpawnFailed>()(
+  "NativeTelemetrySpawnFailed",
+  {
+    path: Schema.String,
+    cause: Schema.Defect(),
+  },
+) {
+  override get message(): string {
+    return `Failed to start resource monitor '${this.path}'.`;
+  }
+}
+
+export class NativeTelemetryHandshakeTimedOut extends Schema.TaggedErrorClass<NativeTelemetryHandshakeTimedOut>()(
+  "NativeTelemetryHandshakeTimedOut",
+  {
+    timeoutMs: Schema.Number,
+  },
+) {
+  override get message(): string {
+    return `Resource monitor handshake timed out after ${this.timeoutMs}ms.`;
+  }
+}
+
+export class NativeTelemetryProtocolMismatch extends Schema.TaggedErrorClass<NativeTelemetryProtocolMismatch>()(
+  "NativeTelemetryProtocolMismatch",
+  {
+    expectedVersion: Schema.Number,
+    receivedVersion: Schema.Number,
+  },
+) {
+  override get message(): string {
+    return `Resource monitor protocol ${this.receivedVersion} is incompatible with expected protocol ${this.expectedVersion}.`;
+  }
+}
+
+export class NativeTelemetryDecodeFailed extends Schema.TaggedErrorClass<NativeTelemetryDecodeFailed>()(
+  "NativeTelemetryDecodeFailed",
+  {
+    cause: Schema.Defect(),
+  },
+) {
+  override get message(): string {
+    return "Failed to decode resource monitor output.";
+  }
+}
+
+export class NativeTelemetryCommandFailed extends Schema.TaggedErrorClass<NativeTelemetryCommandFailed>()(
+  "NativeTelemetryCommandFailed",
+  {
+    operation: Schema.String,
+    cause: Schema.Defect(),
+  },
+) {
+  override get message(): string {
+    return `Resource monitor command '${this.operation}' failed.`;
+  }
+}
+
+export class NativeTelemetryExited extends Schema.TaggedErrorClass<NativeTelemetryExited>()(
+  "NativeTelemetryExited",
+  {
+    exitCode: Schema.Number,
+  },
+) {
+  override get message(): string {
+    return `Resource monitor exited with code ${this.exitCode}.`;
+  }
+}
+
+export class NativeTelemetryUnavailable extends Schema.TaggedErrorClass<NativeTelemetryUnavailable>()(
+  "NativeTelemetryUnavailable",
+  {
+    reason: Schema.String,
+  },
+) {
+  override get message(): string {
+    return `Resource monitor is unavailable: ${this.reason}`;
+  }
+}
+
+export type NativeTelemetryClientError =
+  | NativeTelemetrySpawnFailed
+  | NativeTelemetryHandshakeTimedOut
+  | NativeTelemetryProtocolMismatch
+  | NativeTelemetryDecodeFailed
+  | NativeTelemetryCommandFailed
+  | NativeTelemetryExited
+  | NativeTelemetryUnavailable;
+
+export interface NativeTelemetryClientHealth {
+  readonly status: ResourceTelemetrySourceStatus;
+  readonly hello: Option.Option<ResourceMonitorHelloEvent>;
+  readonly lastSampleAt: Option.Option<DateTime.Utc>;
+  readonly lastError: Option.Option<string>;
+  readonly restartCount: number;
+  readonly sampleIntervalMs: number;
+}
+
+export interface NativeTelemetryClientShape {
+  readonly capabilities: Effect.Effect<ResourceMonitorCapabilities, NativeTelemetryClientError>;
+  readonly snapshots: Stream.Stream<ResourceMonitorSnapshotEvent, NativeTelemetryClientError>;
+  readonly readHistory: (
+    windowMs: number,
+  ) => Effect.Effect<ReadonlyArray<ResourceMonitorSnapshotEvent>, NativeTelemetryClientError>;
+  readonly setExternalProcesses: (
+    processes: ReadonlyArray<ResourceMonitorExternalProcess>,
+  ) => Effect.Effect<void, NativeTelemetryClientError>;
+  readonly setHostPowerState: (
+    snapshot: HostPowerSnapshot,
+  ) => Effect.Effect<void, NativeTelemetryClientError>;
+  readonly sampleNow: Effect.Effect<ResourceMonitorSnapshotEvent, NativeTelemetryClientError>;
+  readonly retry: Effect.Effect<boolean>;
+  readonly health: Effect.Effect<NativeTelemetryClientHealth>;
+  readonly healthChanges: Stream.Stream<NativeTelemetryClientHealth>;
+}
+
+export class NativeTelemetryClient extends Context.Service<
+  NativeTelemetryClient,
+  NativeTelemetryClientShape
+>()("t3/resourceTelemetry/NativeTelemetryClient") {}
+
+interface ClientState {
+  readonly status: ResourceTelemetrySourceStatus;
+  readonly handle: Option.Option<ChildProcessSpawner.ChildProcessHandle>;
+  readonly hello: Option.Option<ResourceMonitorHelloEvent>;
+  readonly lastSampleAt: Option.Option<DateTime.Utc>;
+  readonly lastError: Option.Option<string>;
+  readonly restartCount: number;
+}
+
+interface CollectionControl {
+  readonly hostPower: HostPowerSnapshot;
+  readonly liveSubscriberCount: number;
+  readonly sampleIntervalMs: number;
+}
+
+interface PendingHistoryRequest {
+  readonly deferred: Deferred.Deferred<
+    ReadonlyArray<ResourceMonitorSnapshotEvent>,
+    NativeTelemetryClientError
+  >;
+  readonly snapshots: ReadonlyArray<ResourceMonitorSnapshotEvent>;
+}
+
+const initialState: ClientState = {
+  status: "starting",
+  handle: Option.none(),
+  hello: Option.none(),
+  lastSampleAt: Option.none(),
+  lastError: Option.none(),
+  restartCount: 0,
+};
+
+function toHealth(state: ClientState, sampleIntervalMs: number): NativeTelemetryClientHealth {
+  return {
+    status: state.status,
+    hello: state.hello,
+    lastSampleAt: state.lastSampleAt,
+    lastError: state.lastError,
+    restartCount: state.restartCount,
+    sampleIntervalMs,
+  };
+}
+
+function isThermallyConstrained(snapshot: HostPowerSnapshot): boolean {
+  return snapshot.thermalState === "serious" || snapshot.thermalState === "critical";
+}
+
+export function resolveNativeSampleIntervalMs(
+  snapshot: HostPowerSnapshot,
+  liveSubscriberCount: number,
+): number {
+  if (snapshot.stale || snapshot.source === "unknown") {
+    return liveSubscriberCount > 0 ? SAMPLE_INTERVAL_MS : UNKNOWN_BACKGROUND_SAMPLE_INTERVAL_MS;
+  }
+  if (snapshot.suspended) return 0;
+  if (
+    snapshot.locked === "true" ||
+    snapshot.lowPowerMode === "true" ||
+    isThermallyConstrained(snapshot)
+  ) {
+    return CONSTRAINED_SAMPLE_INTERVAL_MS;
+  }
+  if (snapshot.onBattery === "true") return BATTERY_SAMPLE_INTERVAL_MS;
+  return SAMPLE_INTERVAL_MS;
+}
+
+const decodeMonitorEvent: (
+  value: unknown,
+) => Effect.Effect<ResourceMonitorEvent, Schema.SchemaError> = Schema.decodeUnknownEffect(
+  ResourceMonitorEventSchema,
+);
+const encodeMonitorCommand = Schema.encodeEffect(
+  Schema.fromJsonString(ResourceMonitorCommandSchema),
+);
+const isProtocolMismatch = Schema.is(NativeTelemetryProtocolMismatch);
+const isDecodeFailed = Schema.is(NativeTelemetryDecodeFailed);
+const isCommandFailed = Schema.is(NativeTelemetryCommandFailed);
+
+function eventVersion(value: unknown): number | undefined {
+  if (typeof value !== "object" || value === null) return undefined;
+  const version = Reflect.get(value, "version");
+  return typeof version === "number" ? version : undefined;
+}
+
+function restartDelay(attempt: number): Duration.Duration {
+  return Duration.min(Duration.times(INITIAL_RESTART_DELAY, 2 ** attempt), MAX_RESTART_DELAY);
+}
+
+function errorMessage(error: NativeTelemetryClientError): string {
+  return error.message;
+}
+
+export const make = Effect.fn("resourceTelemetry.nativeTelemetryClient.make")(function* () {
+  const binary = yield* ResourceMonitorBinary.ResourceMonitorBinary;
+  const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
+  const crypto = yield* Crypto.Crypto;
+  const config = yield* ServerConfig;
+  const initializedAt = yield* DateTime.now;
+  const state = yield* Ref.make(initialState);
+  const collectionControl = yield* Ref.make<CollectionControl>({
+    hostPower: {
+      source: "unknown",
+      idle: "unknown",
+      idleSeconds: null,
+      locked: "unknown",
+      suspended: false,
+      onBattery: "unknown",
+      lowPowerMode: "unknown",
+      thermalState: "unknown",
+      stale: true,
+      updatedAt: initializedAt,
+    },
+    liveSubscriberCount: 0,
+    sampleIntervalMs: UNKNOWN_BACKGROUND_SAMPLE_INTERVAL_MS,
+  });
+  const externalProcesses = yield* Ref.make<ReadonlyArray<ResourceMonitorExternalProcess>>([]);
+  const pendingSamples = yield* Ref.make(
+    new Map<string, Deferred.Deferred<ResourceMonitorSnapshotEvent, NativeTelemetryClientError>>(),
+  );
+  const pendingHistories = yield* Ref.make(new Map<string, PendingHistoryRequest>());
+  const snapshots = yield* PubSub.sliding<ResourceMonitorSnapshotEvent>(8);
+  const healthChanges = yield* PubSub.sliding<NativeTelemetryClientHealth>(4);
+  const retryQueue = yield* Queue.sliding<void>(1);
+  const commandMutex = yield* Semaphore.make(1);
+  const currentHealth = Effect.all([Ref.get(state), Ref.get(collectionControl)]).pipe(
+    Effect.map(([current, control]) => toHealth(current, control.sampleIntervalMs)),
+  );
+  const publishHealth = currentHealth.pipe(
+    Effect.flatMap((health) => PubSub.publish(healthChanges, health)),
+    Effect.asVoid,
+  );
+
+  const failPending = (error: NativeTelemetryClientError) =>
+    Effect.gen(function* () {
+      const samples = yield* Ref.getAndSet(pendingSamples, new Map());
+      const histories = yield* Ref.getAndSet(pendingHistories, new Map());
+      yield* Effect.forEach(samples.values(), (deferred) => Deferred.fail(deferred, error), {
+        discard: true,
+      });
+      yield* Effect.forEach(
+        histories.values(),
+        (request) => Deferred.fail(request.deferred, error),
+        { discard: true },
+      );
+    });
+
+  const writeCommand = (
+    handle: ChildProcessSpawner.ChildProcessHandle,
+    command: ResourceMonitorCommand,
+  ): Effect.Effect<void, NativeTelemetryClientError> =>
+    commandMutex.withPermits(1)(
+      encodeMonitorCommand(command).pipe(
+        Effect.map((encoded) => `${encoded}\n`),
+        Effect.mapError(
+          (cause) =>
+            new NativeTelemetryCommandFailed({
+              operation: command.type,
+              cause,
+            }),
+        ),
+        Effect.flatMap((encoded) =>
+          Stream.run(Stream.encodeText(Stream.make(encoded)), handle.stdin),
+        ),
+        Effect.mapError(
+          (cause) =>
+            new NativeTelemetryCommandFailed({
+              operation: command.type,
+              cause,
+            }),
+        ),
+      ),
+    );
+
+  const processEvent = (
+    event: ResourceMonitorEvent,
+    helloDeferred: Deferred.Deferred<ResourceMonitorHelloEvent>,
+  ): Effect.Effect<void, NativeTelemetryClientError> => {
+    switch (event.type) {
+      case "hello":
+        return Ref.update(state, (current) => ({
+          ...current,
+          status: "starting" as const,
+          hello: Option.some(event),
+          lastError: Option.none(),
+        })).pipe(
+          Effect.andThen(publishHealth),
+          Effect.andThen(Deferred.succeed(helloDeferred, event)),
+          Effect.asVoid,
+        );
+      case "snapshot":
+        return Effect.gen(function* () {
+          const sampledAt = DateTime.makeUnsafe(event.sampledAtUnixMs);
+          yield* Ref.update(state, (current) => ({
+            ...current,
+            status: "healthy" as const,
+            lastSampleAt: Option.some(sampledAt),
+            lastError: Option.none(),
+          }));
+          yield* PubSub.publish(snapshots, event);
+          if (event.requestId) {
+            const deferred = yield* Ref.modify(pendingSamples, (pending) => {
+              const next = new Map(pending);
+              const current = next.get(event.requestId!);
+              next.delete(event.requestId!);
+              return [Option.fromUndefinedOr(current), next];
+            });
+            if (Option.isSome(deferred)) {
+              yield* Deferred.succeed(deferred.value, event);
+            }
+          }
+        });
+      case "historyChunk":
+        return Effect.gen(function* () {
+          const latestSnapshot = event.snapshots.at(-1);
+          if (latestSnapshot) {
+            yield* Ref.update(state, (current) => ({
+              ...current,
+              status: "healthy" as const,
+              lastSampleAt: Option.some(DateTime.makeUnsafe(latestSnapshot.sampledAtUnixMs)),
+              lastError: Option.none(),
+            }));
+          }
+          const completed = yield* Ref.modify(pendingHistories, (pending) => {
+            const request = pending.get(event.requestId);
+            if (!request) return [Option.none(), pending] as const;
+            const snapshots = [...request.snapshots, ...event.snapshots];
+            const next = new Map(pending);
+            if (event.done) {
+              next.delete(event.requestId);
+              return [Option.some({ deferred: request.deferred, snapshots }), next] as const;
+            }
+            next.set(event.requestId, { deferred: request.deferred, snapshots });
+            return [Option.none(), next] as const;
+          });
+          if (Option.isSome(completed)) {
+            yield* Deferred.succeed(completed.value.deferred, completed.value.snapshots);
+          }
+        });
+      case "error":
+        return Ref.update(state, (current) => ({
+          ...current,
+          status: "degraded" as const,
+          lastError: Option.some(event.message),
+        })).pipe(
+          Effect.andThen(publishHealth),
+          Effect.andThen(
+            event.recoverable
+              ? Effect.void
+              : Effect.fail(
+                  new NativeTelemetryCommandFailed({
+                    operation: event.code,
+                    cause: event.message,
+                  }),
+                ),
+          ),
+        );
+    }
+  };
+
+  const runAttempt: Effect.Effect<void, NativeTelemetryClientError> = Effect.scoped(
+    Effect.gen(function* () {
+      const executablePath = yield* binary.resolve.pipe(
+        Effect.mapError(
+          (error) =>
+            new NativeTelemetryUnavailable({
+              reason: error.message,
+            }),
+        ),
+      );
+      const command = ChildProcess.make(executablePath, [], {
+        cwd: config.cwd,
+        stdin: {
+          stream: "pipe",
+          endOnDone: false,
+        },
+        stdout: "pipe",
+        stderr: "pipe",
+        killSignal: "SIGTERM",
+        forceKillAfter: Duration.seconds(2),
+      });
+      const handle = yield* Effect.acquireRelease(
+        spawner
+          .spawn(command)
+          .pipe(
+            Effect.mapError(
+              (cause) => new NativeTelemetrySpawnFailed({ path: executablePath, cause }),
+            ),
+          ),
+        (child) => child.kill().pipe(Effect.ignore),
+      );
+      yield* Ref.update(state, (current) => ({
+        ...current,
+        status: "starting" as const,
+        handle: Option.some(handle),
+        hello: Option.none(),
+      }));
+      yield* publishHealth;
+
+      const helloDeferred = yield* Deferred.make<ResourceMonitorHelloEvent>();
+      const eventFiber = yield* handle.stdout.pipe(
+        Stream.pipeThroughChannel(Ndjson.decode({ ignoreEmptyLines: true })),
+        Stream.mapEffect(
+          (
+            value,
+          ): Effect.Effect<
+            ResourceMonitorEvent,
+            NativeTelemetryProtocolMismatch | NativeTelemetryDecodeFailed
+          > => {
+            const version = eventVersion(value);
+            if (version !== undefined && version !== RESOURCE_MONITOR_PROTOCOL_VERSION) {
+              return Effect.fail(
+                new NativeTelemetryProtocolMismatch({
+                  expectedVersion: RESOURCE_MONITOR_PROTOCOL_VERSION,
+                  receivedVersion: version,
+                }),
+              );
+            }
+            return decodeMonitorEvent(value).pipe(
+              Effect.mapError((cause) => new NativeTelemetryDecodeFailed({ cause })),
+            );
+          },
+        ),
+        Stream.runForEach((event) => processEvent(event, helloDeferred)),
+        Effect.mapError((cause) =>
+          isProtocolMismatch(cause) || isDecodeFailed(cause) || isCommandFailed(cause)
+            ? cause
+            : new NativeTelemetryDecodeFailed({ cause }),
+        ),
+        Effect.forkScoped,
+      );
+      yield* handle.stderr.pipe(Stream.runDrain, Effect.ignore, Effect.forkScoped);
+
+      const hello = yield* Deferred.await(helloDeferred).pipe(
+        Effect.timeoutOption(HANDSHAKE_TIMEOUT),
+        Effect.flatMap(
+          Option.match({
+            onNone: () =>
+              Effect.fail(
+                new NativeTelemetryHandshakeTimedOut({
+                  timeoutMs: Duration.toMillis(HANDSHAKE_TIMEOUT),
+                }),
+              ),
+            onSome: Effect.succeed,
+          }),
+        ),
+      );
+      const control = yield* Ref.get(collectionControl);
+      yield* writeCommand(handle, {
+        version: RESOURCE_MONITOR_PROTOCOL_VERSION,
+        type: "configure",
+        rootPid: process.pid,
+        sampleIntervalMs: control.sampleIntervalMs,
+        externalProcesses: [...(yield* Ref.get(externalProcesses))],
+      });
+      if (control.liveSubscriberCount > 0) {
+        yield* writeCommand(handle, {
+          version: RESOURCE_MONITOR_PROTOCOL_VERSION,
+          type: "setStreaming",
+          enabled: true,
+        });
+      }
+      yield* Ref.update(state, (current) => ({
+        ...current,
+        status: "healthy" as const,
+        hello: Option.some(hello),
+      }));
+      yield* publishHealth;
+
+      yield* writeCommand(handle, {
+        version: RESOURCE_MONITOR_PROTOCOL_VERSION,
+        type: "setExternalProcesses",
+        processes: [...(yield* Ref.get(externalProcesses))],
+      });
+      const latestControl = yield* Ref.get(collectionControl);
+      if (latestControl.sampleIntervalMs !== control.sampleIntervalMs) {
+        yield* writeCommand(handle, {
+          version: RESOURCE_MONITOR_PROTOCOL_VERSION,
+          type: "setSampleInterval",
+          sampleIntervalMs: latestControl.sampleIntervalMs,
+        });
+      }
+      if (latestControl.liveSubscriberCount > 0 !== control.liveSubscriberCount > 0) {
+        yield* writeCommand(handle, {
+          version: RESOURCE_MONITOR_PROTOCOL_VERSION,
+          type: "setStreaming",
+          enabled: latestControl.liveSubscriberCount > 0,
+        });
+      }
+
+      const exitEffect = handle.exitCode.pipe(
+        Effect.mapError(
+          (cause) =>
+            new NativeTelemetryCommandFailed({
+              operation: "waitForExit",
+              cause,
+            }),
+        ),
+        Effect.flatMap((exitCode) =>
+          Effect.fail(new NativeTelemetryExited({ exitCode: Number(exitCode) })),
+        ),
+      );
+      const decoderEffect = Fiber.join(eventFiber).pipe(
+        Effect.andThen(Effect.fail(new NativeTelemetryExited({ exitCode: -1 }))),
+      );
+      return yield* Effect.raceFirst(exitEffect, decoderEffect);
+    }),
+  ).pipe(
+    Effect.ensuring(
+      Ref.update(state, (current) => ({
+        ...current,
+        handle: Option.none(),
+      })),
+    ),
+  );
+
+  yield* Effect.gen(function* () {
+    let failures: ReadonlyArray<number> = [];
+    let restartAttempt = 0;
+
+    while (true) {
+      const result = yield* Effect.result(runAttempt);
+      if (Result.isSuccess(result)) {
+        return;
+      }
+
+      const error = result.failure;
+      const now = DateTime.toEpochMillis(yield* DateTime.now);
+      failures = [...failures.filter((failedAt) => now - failedAt <= FAILURE_WINDOW_MS), now];
+      const exhausted = failures.length >= MAX_FAILURES_PER_WINDOW;
+      yield* Ref.update(state, (current) => ({
+        ...current,
+        status: exhausted ? ("unavailable" as const) : ("degraded" as const),
+        hello: Option.none(),
+        lastError: Option.some(errorMessage(error)),
+        restartCount: current.restartCount + 1,
+      }));
+      yield* publishHealth;
+      yield* failPending(error);
+
+      if (exhausted) {
+        yield* Queue.take(retryQueue);
+        failures = [];
+        restartAttempt = 0;
+        yield* Ref.update(state, (current) => ({
+          ...current,
+          status: "starting" as const,
+          hello: Option.none(),
+          lastError: Option.none(),
+        }));
+        yield* publishHealth;
+        continue;
+      }
+
+      const manuallyRetried = yield* Effect.raceFirst(
+        Effect.sleep(restartDelay(restartAttempt)).pipe(Effect.as(false)),
+        Queue.take(retryQueue).pipe(Effect.as(true)),
+      );
+      restartAttempt = manuallyRetried ? 0 : restartAttempt + 1;
+    }
+  }).pipe(
+    Effect.catchCause((cause) =>
+      Cause.hasInterruptsOnly(cause)
+        ? Effect.void
+        : Ref.update(state, (current) => ({
+            ...current,
+            status: "unavailable" as const,
+            hello: Option.none(),
+            lastError: Option.some(Cause.pretty(cause)),
+          })).pipe(
+            Effect.andThen(publishHealth),
+            Effect.andThen(
+              Effect.logWarning("Resource monitor supervisor failed", {
+                cause: Cause.pretty(cause),
+              }),
+            ),
+          ),
+    ),
+    Effect.forkScoped,
+  );
+
+  const applyCollectionControl = Effect.fn(
+    "resourceTelemetry.nativeTelemetryClient.applyCollectionControl",
+  )(function* (previous: CollectionControl, next: CollectionControl) {
+    const current = yield* Ref.get(state);
+    if (Option.isSome(current.handle) && current.status === "healthy") {
+      if (previous.sampleIntervalMs !== next.sampleIntervalMs) {
+        yield* writeCommand(current.handle.value, {
+          version: RESOURCE_MONITOR_PROTOCOL_VERSION,
+          type: "setSampleInterval",
+          sampleIntervalMs: next.sampleIntervalMs,
+        });
+      }
+      const wasStreaming = previous.liveSubscriberCount > 0;
+      const isStreaming = next.liveSubscriberCount > 0;
+      if (wasStreaming !== isStreaming) {
+        yield* writeCommand(current.handle.value, {
+          version: RESOURCE_MONITOR_PROTOCOL_VERSION,
+          type: "setStreaming",
+          enabled: isStreaming,
+        });
+      }
+    }
+    if (previous.sampleIntervalMs !== next.sampleIntervalMs) {
+      yield* publishHealth;
+    }
+  });
C
cursorBotJun 17, 2026

Power updates ignored when degraded

Medium Severity

setHostPowerState and live-subscriber changes always update in-memory collectionControl, but applyCollectionControl only sends setSampleInterval / setStreaming to the sidecar when client status is exactly healthy. While the monitor is degraded yet still running, host suspend or constraint changes will not pause or slow native sampling until the client returns to healthy or reconnects.

Reviewed by Cursor Bugbot for commit 0ebbdad6c2e35404789d04c498adeb05858ad981. Configure here.

J
Julius MarmingeJul 28, 2026

Fixed in 24e6ccd404. Collection-control commands are sent while the native client is healthy or degraded, so a recoverable error cannot freeze power policy updates while the sidecar remains connected.

apps/desktop/src/backend/DesktopBackendManager.ts · Earlier revision 2b59d67c · right 332

Original diff
@@ -274,7 +325,12 @@ const runBackendProcess = Effect.fn("runBackendProcess")(function* (
     Effect.forkScoped,
   );
 
-  return describeProcessExit(yield* Effect.result(handle.exitCode));
+  const exit = describeProcessExit(yield* Effect.result(handle.exitCode));
+  yield* Effect.forEach(outputFibers, Fiber.await, {
+    concurrency: "unbounded",
+    discard: true,
+  }).pipe(Effect.timeout(DEFAULT_BACKEND_OUTPUT_DRAIN_TIMEOUT), Effect.ignore);
C
cursorBotJun 17, 2026

Output drain timeout drops logs

Medium Severity

After the backend child exits, runBackendProcess waits at most 250ms for stdout/stderr drain fibers, then finalizeRun calls persistFailure once. If draining or writeOutputChunk takes longer, or fibers still run after the timeout, failure logs can omit trailing output that never gets another persist pass.

Additional Locations (1)

Reviewed by Cursor Bugbot for commit 2b59d67c05c980eb48ed34ca04d599dc08f405e3. Configure here.

apps/desktop/src/backend/DesktopBackendManager.ts · Earlier revision 4a875dbe · right 547

Original diff
@@ -475,10 +536,16 @@ const makeDesktopBackendManager = Effect.fn("makeDesktopBackendManager")(functio
               ),
             );
           }),
-          onReadinessFailure: (error) =>
-            logBackendManagerWarning("backend readiness check failed during bootstrap", {
-              error: error.message,
-            }),
+          onReadinessFailure: Effect.fn("desktop.backendManager.onReadinessFailure")(
+            function* (error) {
+              yield* logBackendManagerWarning("backend readiness check failed during bootstrap", {
+                error: error.message,
+              });
+              yield* backendOutputLog.persistFailure({
+                details: error.message,
+              });
+            },
C
cursorBotJun 19, 2026

Readiness timeout writes failure logs

Medium Severity

The new onReadinessFailure handler calls persistFailure when the HTTP readiness wait times out, even though the backend child is still running and output buffering was meant for real process failures. That writes ERROR failure logs for a slow or stuck bootstrap, and because persistFailure only clears chunks while leaving the session open, a later unexpected exit can flush another failure block for the same run.

Additional Locations (1)

Reviewed by Cursor Bugbot for commit 4a875dbe519b7891388272f0c81e0498619cc4f4. Configure here.

apps/server/src/background/BackgroundPolicy.ts · Earlier revision 4a875dbe · right 93

Original diff
@@ -0,0 +1,274 @@
+import {
+  type AuthSessionId,
+  type BackgroundPolicySnapshot,
+  type BackgroundScope,
+  type ClientActivityLease,
+  type ClientActivityReportInput,
+  type HostPowerSnapshot,
+  type RpcClientId,
+} from "@t3tools/contracts";
+import {
+  getBackgroundActivityPresetSettings,
+  resolveServerBackgroundActivitySettings,
+  type ResolvedBackgroundActivitySettings,
+} from "@t3tools/shared/backgroundActivitySettings";
+import * as DateTime from "effect/DateTime";
+import * as Context from "effect/Context";
+import * as Effect from "effect/Effect";
+import * as Layer from "effect/Layer";
+import * as PubSub from "effect/PubSub";
+import * as Ref from "effect/Ref";
+import * as Stream from "effect/Stream";
+
+import { ServerSettingsService } from "../serverSettings.ts";
+import * as HostPowerMonitor from "./HostPowerMonitor.ts";
+
+export interface BackgroundPolicyShape {
+  readonly reportClientActivity: (
+    sessionId: AuthSessionId,
+    rpcClientId: RpcClientId,
+    input: ClientActivityReportInput,
+  ) => Effect.Effect<void>;
+  readonly removeRpcClient: (rpcClientId: RpcClientId) => Effect.Effect<void>;
+  readonly reportHostPowerState: (snapshot: HostPowerSnapshot) => Effect.Effect<void>;
+  readonly snapshot: Effect.Effect<BackgroundPolicySnapshot>;
+  readonly streamChanges: Stream.Stream<BackgroundPolicySnapshot>;
+  readonly hasDemand: (scope: BackgroundScope) => Effect.Effect<boolean>;
+  readonly shouldRunScopeWork: (scope: BackgroundScope) => Effect.Effect<boolean>;
+  readonly shouldRunOpportunisticWork: Effect.Effect<boolean>;
+}
+
+export class BackgroundPolicy extends Context.Service<BackgroundPolicy, BackgroundPolicyShape>()(
+  "t3/background/BackgroundPolicy",
+) {}
+
+const DEFAULT_LEASE_TTL_MS = 45_000;
+const MAX_LEASE_TTL_MS = 120_000;
+
+function scopeKey(scope: BackgroundScope): string {
+  switch (scope.type) {
+    case "server-config":
+    case "diagnostics":
+      return scope.type;
+    case "provider-status":
+      return scope.instanceId ? `${scope.type}:${scope.instanceId}` : scope.type;
+    case "vcs-status":
+    case "git-refs":
+      return `${scope.type}:${scope.cwd}`;
+    case "thread":
+      return `${scope.type}:${scope.threadId}`;
+  }
+}
+
+function isLeaseActive(lease: ClientActivityLease, now: DateTime.Utc): boolean {
+  return DateTime.isGreaterThan(lease.expiresAt, now);
+}
+
+function isForegroundLease(lease: ClientActivityLease, now: DateTime.Utc): boolean {
+  return isLeaseActive(lease, now) && lease.visible && lease.focused;
+}
+
+function leaseHasScope(lease: ClientActivityLease, scope: BackgroundScope): boolean {
+  const key = scopeKey(scope);
+  return lease.scopes.some((leaseScope) => scopeKey(leaseScope) === key);
+}
+
+function hasThermalPressure(hostPower: HostPowerSnapshot): boolean {
+  return hostPower.thermalState === "serious" || hostPower.thermalState === "critical";
+}
+
+function isHostConstrained(
+  hostPower: HostPowerSnapshot,
+  settings: ResolvedBackgroundActivitySettings,
+): boolean {
+  if (hostPower.stale) return false;
+  if (
+    (settings.pauseWhenHostLocked && hostPower.locked === "true") ||
+    hasThermalPressure(hostPower)
+  ) {
+    return true;
+  }
+  if (settings.pauseWhenHostLowPower && hostPower.lowPowerMode === "true") return true;
+  return settings.pauseWhenOnBattery && hostPower.onBattery === "true";
+}
C
cursorBotJun 19, 2026

Host suspend does not gate work

Medium Severity

isHostConstrained pauses background work for lock, thermal pressure, low-power mode, and battery, but ignores hostPower.suspended even when desktop telemetry reports suspend/resume. Scoped and opportunistic refreshes can keep running while the host is suspended.

Reviewed by Cursor Bugbot for commit 4a875dbe519b7891388272f0c81e0498619cc4f4. Configure here.

apps/server/src/provider/Layers/EventNdjsonLogger.ts · Earlier revision 2f8211fa · right 561

Original diff
@@ -61,225 +164,421 @@ function resolveThreadSegment(raw: string | null | undefined): string {
   return normalized ?? GLOBAL_THREAD_SEGMENT;
 }
 
-function formatLoggerMessage(message: unknown): string {
-  if (Array.isArray(message)) {
-    return message.map((part) => (typeof part === "string" ? part : String(part))).join(" ");
-  }
-  return typeof message === "string" ? message : String(message);
+function resolveStreamLabel(stream: EventNdjsonStream): string {
+  return stream === "native" ? "NTIVE" : "CANON";
 }
 
-function makeLineLogger(streamLabel: string): Logger.Logger<unknown, string> {
-  return Logger.make(
-    ({ date, message }) =>
-      `[${date.toISOString()}] ${streamLabel}: ${formatLoggerMessage(message)}\n`,
-  );
+function shouldPersist(stream: EventNdjsonStream, event: unknown): boolean {
+  if (stream !== "canonical" || typeof event !== "object" || event === null) {
+    return true;
+  }
+  const type = Reflect.get(event, "type");
+  return typeof type !== "string" || !transientCanonicalEventTypes.has(type);
 }
 
-function resolveStreamLabel(stream: EventNdjsonStream): string {
-  switch (stream) {
-    case "native":
-      return "NTIVE";
-    case "canonical":
-    case "orchestration":
-    default:
-      return "CANON";
+function writeBatchedMessages(
+  sink: RotatingFileSink,
+  records: ReadonlyArray<PendingRecord>,
+  maxBytes: number,
+): void {
+  let pendingLines: Array<string> = [];
+  let pendingBytes = 0;
+
+  const flush = () => {
+    if (pendingLines.length === 0) return;
+    sink.write(pendingLines.join(""));
+    pendingLines = [];
+    pendingBytes = 0;
+  };
+
+  for (const record of records) {
+    if (pendingBytes > 0 && pendingBytes + record.bytes > maxBytes) {
+      flush();
+    }
+    pendingLines.push(record.line);
+    pendingBytes += record.bytes;
+    if (pendingBytes >= maxBytes) {
+      flush();
+    }
   }
+  flush();
 }
 
-const toLogMessage = Effect.fn("toLogMessage")(function* (
-  event: unknown,
-): Effect.fn.Return<string | undefined> {
-  return yield* encodeUnknownJsonString(event).pipe(
-    Effect.catch((error) =>
-      logWarning("failed to serialize provider event log record", { error }).pipe(
-        Effect.as(undefined),
-      ),
-    ),
-  );
-});
+function enforceRetention(input: {
+  readonly directory: string;
+  readonly maxTotalBytes: number;
+  readonly maxAgeMs: number;
+  readonly activeFilePaths: ReadonlySet<string>;
+  readonly now: number;
+}): RetentionResult {
+  const failures: Array<FileOperationFailure> = [];
+  const files: Array<{ filePath: string; mtimeMs: number; size: number }> = [];
+
+  let entries: ReadonlyArray<fs.Dirent>;
+  try {
+    entries = fs.readdirSync(input.directory, { withFileTypes: true });
+  } catch (cause) {
+    return { failures: [{ filePath: input.directory, cause }] };
+  }
 
-const makeThreadWriter = Effect.fn("makeThreadWriter")(function* (input: {
-  readonly filePath: string;
-  readonly maxBytes: number;
-  readonly maxFiles: number;
-  readonly batchWindowMs: number;
-  readonly streamLabel: string;
-}): Effect.fn.Return<ThreadWriter | undefined> {
-  const sinkResult = yield* Effect.sync(() => {
+  for (const entry of entries) {
+    if (!entry.isFile() || !PROVIDER_LOG_FILE_PATTERN.test(entry.name)) continue;
+    const filePath = path.join(input.directory, entry.name);
     try {
-      return {
-        ok: true as const,
-        sink: new RotatingFileSink({
-          filePath: input.filePath,
-          maxBytes: input.maxBytes,
-          maxFiles: input.maxFiles,
-          throwOnError: true,
-        }),
-      };
-    } catch (error) {
-      return { ok: false as const, error };
+      const stat = fs.statSync(filePath);
+      files.push({ filePath, mtimeMs: stat.mtimeMs, size: stat.size });
+    } catch (cause) {
+      failures.push({ filePath, cause });
+    }
+  }
+
+  let totalBytes = files.reduce((total, file) => total + file.size, 0);
+  const remove = (file: (typeof files)[number]) => {
+    if (input.activeFilePaths.has(file.filePath)) return false;
+    try {
+      fs.rmSync(file.filePath, { force: true });
+      totalBytes -= file.size;
+      return true;
+    } catch (cause) {
+      failures.push({ filePath: file.filePath, cause });
+      return false;
     }
+  };
+
+  const retained = files.filter((file) => {
+    if (input.now - file.mtimeMs <= input.maxAgeMs) return true;
+    return !remove(file);
   });
 
-  if (!sinkResult.ok) {
-    yield* logWarning("failed to initialize provider thread log file", {
-      filePath: input.filePath,
-      error: sinkResult.error,
-    });
-    return undefined;
+  for (const file of retained.toSorted(
+    (left, right) => left.mtimeMs - right.mtimeMs || left.filePath.localeCompare(right.filePath),
+  )) {
+    if (totalBytes <= input.maxTotalBytes) break;
+    remove(file);
   }
 
-  const sink = sinkResult.sink;
-  const scope = yield* Scope.make();
-  const lineLogger = makeLineLogger(input.streamLabel);
-  const batchedLogger = yield* Logger.batched(lineLogger, {
-    window: input.batchWindowMs,
-    flush: Effect.fn("makeThreadWriter.flush")(function* (messages) {
-      const flushResult = yield* Effect.sync(() => {
-        try {
-          for (const message of messages) {
-            sink.write(message);
-          }
-          return { ok: true as const };
-        } catch (error) {
-          return { ok: false as const, error };
-        }
-      });
+  return { failures };
+}
+
+function validateOption(input: {
+  readonly filePath: string;
+  readonly option: string;
+  readonly value: number;
+  readonly minimum: number;
+}): EventNdjsonLogConfigurationError | undefined {
+  if (Number.isInteger(input.value) && input.value >= input.minimum) return undefined;
+  return new EventNdjsonLogConfigurationError(input);
+}
+
+function resolveOptions(
+  filePath: string,
+  options: EventNdjsonLogStoreOptions,
+): Effect.Effect<ResolvedOptions, EventNdjsonLogConfigurationError> {
+  const resolved = {
+    maxBytes: options.maxBytes ?? DEFAULT_MAX_BYTES,
+    maxFiles: options.maxFiles ?? DEFAULT_MAX_FILES,
+    batchWindowMs: options.batchWindowMs ?? DEFAULT_BATCH_WINDOW_MS,
+    maxTotalBytes: options.maxTotalBytes ?? DEFAULT_MAX_TOTAL_BYTES,
+    maxAgeMs: options.maxAgeMs ?? DEFAULT_MAX_AGE_MS,
+    retentionCheckIntervalMs:
+      options.retentionCheckIntervalMs ?? DEFAULT_RETENTION_CHECK_INTERVAL_MS,
+    maxBufferedBytes: options.maxBufferedBytes ?? DEFAULT_MAX_BUFFERED_BYTES,
+    maxBufferedRecords: options.maxBufferedRecords ?? DEFAULT_MAX_BUFFERED_RECORDS,
+    attribution: options.attribution,
+  } satisfies ResolvedOptions;
+
+  const validations = [
+    ["maxBytes", resolved.maxBytes, 1],
+    ["maxFiles", resolved.maxFiles, 1],
+    ["batchWindowMs", resolved.batchWindowMs, 0],
+    ["maxTotalBytes", resolved.maxTotalBytes, 1],
+    ["maxAgeMs", resolved.maxAgeMs, 1],
+    ["retentionCheckIntervalMs", resolved.retentionCheckIntervalMs, 1],
+    ["maxBufferedBytes", resolved.maxBufferedBytes, 1],
+    ["maxBufferedRecords", resolved.maxBufferedRecords, 1],
+  ] as const;
+
+  for (const [option, value, minimum] of validations) {
+    const error = validateOption({ filePath, option, value, minimum });
+    if (error) return Effect.fail(error);
+  }
+  return Effect.succeed(resolved);
+}
+
+function drainPending(input: {
+  readonly directory: string;
+  readonly options: ResolvedOptions;
+  readonly state: StoreState;
+  readonly now: number;
+  readonly timerFired: boolean;
+  readonly close: boolean;
+}): readonly [DrainResult, StoreState] {
+  if (input.state.closed) {
+    return [{ attributions: [], failures: [] }, input.state];
+  }
 
-      if (!flushResult.ok) {
-        yield* logWarning("provider event log batch flush failed", {
-          filePath: input.filePath,
-          error: flushResult.error,
+  const sinks = new Map(input.state.sinks);
+  const failedSegments = new Set(input.state.failedSegments);
+  const failures: Array<FileOperationFailure> = [];
+  const attributionByStream = new Map<
+    EventNdjsonStream,
+    { count: number; logicalWriteBytes: number }
+  >();
+  const recordsBySegment = new Map<string, Array<PendingRecord>>();
+
+  for (const record of input.state.pending) {
+    const records = recordsBySegment.get(record.threadSegment) ?? [];
+    records.push(record);
+    recordsBySegment.set(record.threadSegment, records);
+  }
+
+  for (const [threadSegment, records] of recordsBySegment) {
+    if (failedSegments.has(threadSegment)) continue;
+    const filePath = path.join(input.directory, `${threadSegment}.log`);
+    let sink = sinks.get(threadSegment);
+    if (!sink) {
+      try {
+        sink = new RotatingFileSink({
+          filePath,
+          maxBytes: input.options.maxBytes,
+          maxFiles: input.options.maxFiles,
+          throwOnError: true,
         });
+        sinks.set(threadSegment, sink);
+      } catch (cause) {
+        failedSegments.add(threadSegment);
+        failures.push({ filePath, cause });
+        continue;
       }
-    }),
-  }).pipe(Effect.provideService(Scope.Scope, scope));
+    }
 
-  const loggerLayer = Logger.layer([batchedLogger], { mergeWithExisting: false });
+    try {
+      writeBatchedMessages(sink, records, input.options.maxBytes);
+      for (const record of records) {
+        const current = attributionByStream.get(record.stream) ?? {
+          count: 0,
+          logicalWriteBytes: 0,
+        };
+        attributionByStream.set(record.stream, {
+          count: current.count + 1,
+          logicalWriteBytes: current.logicalWriteBytes + record.bytes,
+        });
+      }
+    } catch (cause) {
+      failedSegments.add(threadSegment);
+      failures.push({ filePath, cause });
+    }
+  }
 
-  return {
-    writeMessage(message: string) {
-      return Effect.log(message).pipe(Effect.provide(loggerLayer));
+  const retentionDue =
+    input.now - input.state.lastRetentionAt >= input.options.retentionCheckIntervalMs;
+  const retention = retentionDue
+    ? enforceRetention({
+        directory: input.directory,
+        maxTotalBytes: input.options.maxTotalBytes,
+        maxAgeMs: input.options.maxAgeMs,
+        activeFilePaths: new Set(
+          Array.from(sinks.keys(), (threadSegment) =>
+            path.join(input.directory, `${threadSegment}.log`),
+          ),
+        ),
+        now: input.now,
+      })
+    : { failures: [] };
+
+  return [
+    {
+      attributions: Array.from(attributionByStream, ([stream, value]) => ({
+        stream,
+        ...value,
+      })),
+      failures: [...failures, ...retention.failures],
     },
-    close() {
-      return Scope.close(scope, Exit.void);
+    {
+      pending: [],
+      pendingBytes: 0,
+      sinks,
+      failedSegments,
+      flushScheduled: input.timerFired ? false : input.state.flushScheduled,
+      closed: input.close,
+      lastRetentionAt: retentionDue ? input.now : input.state.lastRetentionAt,
     },
-  } satisfies ThreadWriter;
+  ];
+}
+
+const serializeEvent = Effect.fnUntraced(function* (event: unknown) {
+  return yield* encodeUnknownJsonString(event).pipe(
+    Effect.catch((error) =>
+      logWarning("failed to serialize provider event log record", { error }).pipe(
+        Effect.as(undefined),
+      ),
+    ),
+  );
 });
 
-export const makeEventNdjsonLogger = Effect.fn("makeEventNdjsonLogger")(function* (
+export const makeEventNdjsonLogStore = Effect.fnUntraced(function* (
   filePath: string,
-  options: EventNdjsonLoggerOptions,
-): Effect.fn.Return<EventNdjsonLogger | undefined> {
-  const maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;
-  const maxFiles = options.maxFiles ?? DEFAULT_MAX_FILES;
-  const batchWindowMs = options.batchWindowMs ?? DEFAULT_BATCH_WINDOW_MS;
-  const streamLabel = resolveStreamLabel(options.stream);
-
-  const directoryReady = yield* Effect.sync(() => {
-    try {
-      fs.mkdirSync(path.dirname(filePath), { recursive: true });
-      return true;
-    } catch (error) {
-      return { ok: false as const, error };
-    }
+  options: EventNdjsonLogStoreOptions = {},
+): Effect.fn.Return<EventNdjsonLogStore, EventNdjsonLogStoreError> {
+  const resolved = yield* resolveOptions(filePath, options);
+  const directory = path.dirname(filePath);
+
+  yield* Effect.try({
+    try: () => fs.mkdirSync(directory, { recursive: true }),
+    catch: (cause) => new EventNdjsonLogDirectoryError({ directory, cause }),
   });
-  if (directoryReady !== true) {
-    yield* logWarning("failed to create provider event log directory", {
-      filePath,
-      error: directoryReady.error,
+
+  const initializedAt = yield* Clock.currentTimeMillis;
+  const initialRetention = yield* Effect.sync(() =>
+    enforceRetention({
+      directory,
+      maxTotalBytes: resolved.maxTotalBytes,
+      maxAgeMs: resolved.maxAgeMs,
+      activeFilePaths: new Set(),
+      now: initializedAt,
+    }),
+  );
+  for (const failure of initialRetention.failures) {
+    yield* logWarning("provider event log retention failed", {
+      filePath: failure.filePath,
+      cause: failure.cause,
     });
-    return undefined;
   }
 
-  const stateRef = yield* SynchronizedRef.make<LoggerState>({
-    threadWriters: new Map(),
+  const stateRef = yield* SynchronizedRef.make<StoreState>({
+    pending: [],
+    pendingBytes: 0,
+    sinks: new Map(),
     failedSegments: new Set(),
+    flushScheduled: false,
+    closed: false,
+    lastRetentionAt: initializedAt,
   });
-
-  const resolveThreadWriter = Effect.fn("resolveThreadWriter")(function* (
-    threadSegment: string,
-  ): Effect.fn.Return<ThreadWriter | undefined> {
-    return yield* SynchronizedRef.modifyEffect(stateRef, (state) => {
-      if (state.failedSegments.has(threadSegment)) {
-        return Effect.succeed([undefined, state] as const);
-      }
-
-      const existing = state.threadWriters.get(threadSegment);
-      if (existing) {
-        return Effect.succeed([existing, state] as const);
-      }
-
-      return makeThreadWriter({
-        filePath: path.join(path.dirname(filePath), `${threadSegment}.log`),
-        maxBytes,
-        maxFiles,
-        batchWindowMs,
-        streamLabel,
-      }).pipe(
-        Effect.map((writer) => {
-          if (!writer) {
-            const nextFailedSegments = new Set(state.failedSegments);
-            nextFailedSegments.add(threadSegment);
-            return [
-              undefined,
-              {
-                ...state,
-                failedSegments: nextFailedSegments,
-              },
-            ] as const;
-          }
-
-          const nextThreadWriters = new Map(state.threadWriters);
-          nextThreadWriters.set(threadSegment, writer);
-          return [
-            writer,
-            {
-              ...state,
-              threadWriters: nextThreadWriters,
-            },
-          ] as const;
+  const timerScope = yield* Scope.make();
+
+  const flush = Effect.fnUntraced(function* (timerFired: boolean, close: boolean) {
+    const startedAt = yield* Clock.currentTimeMillis;
+    const result = yield* SynchronizedRef.modifyEffect(stateRef, (state) =>
+      Effect.sync(() =>
+        drainPending({
+          directory,
+          options: resolved,
+          state,
+          now: startedAt,
+          timerFired,
+          close,
         }),
-      );
-    });
-  });
+      ),
+    );
 
-  const write = Effect.fn("write")(function* (event: unknown, threadId: ThreadId | null) {
-    const threadSegment = resolveThreadSegment(threadId);
-    const message = yield* toLogMessage(event);
-    if (!message) {
-      return;
+    for (const failure of result.failures) {
+      yield* logWarning("provider event log write or retention failed", {
+        filePath: failure.filePath,
+        cause: failure.cause,
+      });
     }
 
-    const writer = yield* resolveThreadWriter(threadSegment);
-    if (!writer) {
-      return;
+    if (resolved.attribution && result.attributions.length > 0) {
+      const completedAt = yield* Clock.currentTimeMillis;
+      const durationMs = Math.max(0, completedAt - startedAt);
+      const totalBytes = result.attributions.reduce(
+        (total, entry) => total + entry.logicalWriteBytes,
+        0,
+      );
+      yield* Effect.forEach(
+        result.attributions,
+        (entry) =>
+          resolved.attribution?.record({
+            component: "provider-event-log",
+            operation: `${entry.stream}.append`,
+            logicalWriteBytes: entry.logicalWriteBytes,
+            count: entry.count,
+            durationMs:
+              totalBytes === 0
+                ? 0
+                : Math.round(durationMs * (entry.logicalWriteBytes / totalBytes)),
+          }) ?? Effect.void,
+        { discard: true },
+      );
     }
+  });
 
-    yield* writer.writeMessage(message);
+  const scheduleFlush = Effect.fnUntraced(function* () {
+    yield* Effect.forkIn(
+      Effect.sleep(resolved.batchWindowMs).pipe(Effect.andThen(flush(true, false))),
+      timerScope,
+      { startImmediately: true },
+    );
   });
 
-  const close = Effect.fn("close")(function* () {
-    yield* SynchronizedRef.modifyEffect(stateRef, (state) =>
-      Effect.gen(function* () {
-        for (const writer of state.threadWriters.values()) {
-          yield* writer.close();
-        }
+  const close = Effect.fnUntraced(function* () {
+    yield* flush(false, true);
+    yield* Scope.close(timerScope, Exit.void);
+  });
 
+  const loggerViews = new Map<EventNdjsonStream, EventNdjsonLogger>();
+  const logger = (stream: EventNdjsonStream): EventNdjsonLogger => {
+    const existing = loggerViews.get(stream);
+    if (existing) return existing;
+
+    const write = Effect.fnUntraced(function* (event: unknown, threadId: ThreadId | null) {
+      if (!shouldPersist(stream, event)) return;
+      const payload = yield* serializeEvent(event);
+      if (payload === undefined) return;
+
+      const observedAt = yield* DateTime.now.pipe(Effect.map(DateTime.formatIso));
+      const line = `[${observedAt}] ${resolveStreamLabel(stream)}: ${payload}\n`;
+      const bytes = Buffer.byteLength(line);
+      const action = yield* SynchronizedRef.modify(stateRef, (state) => {
+        if (state.closed) {
+          return [{ flush: false, schedule: false }, state] as const;
+        }
+        const pending = [
+          ...state.pending,
+          { stream, threadSegment: resolveThreadSegment(threadId), line, bytes },
+        ];
+        const pendingBytes = state.pendingBytes + bytes;
+        const flush =
+          resolved.batchWindowMs === 0 ||
+          pending.length >= resolved.maxBufferedRecords ||
+          pendingBytes >= resolved.maxBufferedBytes;
+        const schedule = !flush && !state.flushScheduled;
         return [
-          undefined,
+          { flush, schedule },
           {
-            threadWriters: new Map<string, ThreadWriter>(),
-            failedSegments: new Set<string>(),
+            ...state,
+            pending,
+            pendingBytes,
+            flushScheduled: state.flushScheduled || schedule,
           },
         ] as const;
-      }),
-    );
-  });
+      });
+
+      if (action.flush) {
+        yield* flush(false, false);
+      } else if (action.schedule) {
+        yield* scheduleFlush();
+      }
C
cursorBotJun 23, 2026

Stuck flush flag drops batched logs

High Severity

The flushScheduled flag can become out of sync with the actual timer, particularly if scheduling is interrupted or fails. This causes subsequent writes to incorrectly assume a flush is pending, leading to events remaining buffered in memory longer than the batch window, especially for low-volume streams.

Reviewed by Cursor Bugbot for commit 2f8211fa857c9fbe01a1e852a5f707efe98835fd. Configure here.

apps/server/src/provider/Layers/EventNdjsonLogger.ts · Earlier revision 8e2d5d32 · right 168–169

Original diff
@@ -62,225 +165,421 @@ function resolveThreadSegment(raw: string | null | undefined): string {
   return normalized ?? GLOBAL_THREAD_SEGMENT;
 }
 
-function formatLoggerMessage(message: unknown): string {
-  if (Array.isArray(message)) {
-    return message.map((part) => (typeof part === "string" ? part : String(part))).join(" ");
-  }
-  return typeof message === "string" ? message : String(message);
+function resolveStreamLabel(stream: EventNdjsonStream): string {
+  return stream === "native" ? "NTIVE" : "CANON";

apps/server/src/provider/Layers/EventNdjsonLogger.ts · right 168–169

M
macroscopeappBotJun 23, 2026

🟢 Low Layers/EventNdjsonLogger.ts:168

resolveStreamLabel defaults to "CANON" for every stream except "native", so "orchestration" events are mislabeled as "CANON" in the log output. Add an explicit branch for "orchestration" that returns a distinct label such as "ORCH".

diff
1 function resolveStreamLabel(stream: EventNdjsonStream): string {
2- return stream === "native" ? "NTIVE" : "CANON";
3+ return stream === "native" ? "NTIVE" : stream === "orchestration" ? "ORCH" : "CANON";
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
text
1In file @apps/server/src/provider/Layers/EventNdjsonLogger.ts around lines 168-169:
2
3`resolveStreamLabel` defaults to `"CANON"` for every stream except `"native"`, so `"orchestration"` events are mislabeled as `"CANON"` in the log output. Add an explicit branch for `"orchestration"` that returns a distinct label such as `"ORCH"`.
4
5Evidence trail:
6apps/server/src/provider/Layers/EventNdjsonLogger.ts line 50: `export type EventNdjsonStream = "native" | "canonical" | "orchestration";`
7apps/server/src/provider/Layers/EventNdjsonLogger.ts lines 168-170: `function resolveStreamLabel(stream: EventNdjsonStream): string { return stream === "native" ? "NTIVE" : "CANON"; }`
8apps/server/src/provider/Layers/EventNdjsonLogger.ts line 531: `const line = \`[${observedAt}] ${resolveStreamLabel(stream)}: ${payload}\n\`;`
J
Julius MarmingeJul 28, 2026

Fixed in 24e6ccd404. Orchestration records now use the distinct ORCH label, and the event logger test asserts that label for global orchestration events.

apps/server/src/provider/Layers/EventNdjsonLogger.ts · Earlier revision 2f8211fa · right 404

Original diff
@@ -61,225 +164,421 @@ function resolveThreadSegment(raw: string | null | undefined): string {
   return normalized ?? GLOBAL_THREAD_SEGMENT;
 }
 
-function formatLoggerMessage(message: unknown): string {
-  if (Array.isArray(message)) {
-    return message.map((part) => (typeof part === "string" ? part : String(part))).join(" ");
-  }
-  return typeof message === "string" ? message : String(message);
+function resolveStreamLabel(stream: EventNdjsonStream): string {
+  return stream === "native" ? "NTIVE" : "CANON";
 }
 
-function makeLineLogger(streamLabel: string): Logger.Logger<unknown, string> {
-  return Logger.make(
-    ({ date, message }) =>
-      `[${date.toISOString()}] ${streamLabel}: ${formatLoggerMessage(message)}\n`,
-  );
+function shouldPersist(stream: EventNdjsonStream, event: unknown): boolean {
+  if (stream !== "canonical" || typeof event !== "object" || event === null) {
+    return true;
+  }
+  const type = Reflect.get(event, "type");
+  return typeof type !== "string" || !transientCanonicalEventTypes.has(type);
 }
 
-function resolveStreamLabel(stream: EventNdjsonStream): string {
-  switch (stream) {
-    case "native":
-      return "NTIVE";
-    case "canonical":
-    case "orchestration":
-    default:
-      return "CANON";
+function writeBatchedMessages(
+  sink: RotatingFileSink,
+  records: ReadonlyArray<PendingRecord>,
+  maxBytes: number,
+): void {
+  let pendingLines: Array<string> = [];
+  let pendingBytes = 0;
+
+  const flush = () => {
+    if (pendingLines.length === 0) return;
+    sink.write(pendingLines.join(""));
+    pendingLines = [];
+    pendingBytes = 0;
+  };
+
+  for (const record of records) {
+    if (pendingBytes > 0 && pendingBytes + record.bytes > maxBytes) {
+      flush();
+    }
+    pendingLines.push(record.line);
+    pendingBytes += record.bytes;
+    if (pendingBytes >= maxBytes) {
+      flush();
+    }
   }
+  flush();
 }
 
-const toLogMessage = Effect.fn("toLogMessage")(function* (
-  event: unknown,
-): Effect.fn.Return<string | undefined> {
-  return yield* encodeUnknownJsonString(event).pipe(
-    Effect.catch((error) =>
-      logWarning("failed to serialize provider event log record", { error }).pipe(
-        Effect.as(undefined),
-      ),
-    ),
-  );
-});
+function enforceRetention(input: {
+  readonly directory: string;
+  readonly maxTotalBytes: number;
+  readonly maxAgeMs: number;
+  readonly activeFilePaths: ReadonlySet<string>;
+  readonly now: number;
+}): RetentionResult {
+  const failures: Array<FileOperationFailure> = [];
+  const files: Array<{ filePath: string; mtimeMs: number; size: number }> = [];
+
+  let entries: ReadonlyArray<fs.Dirent>;
+  try {
+    entries = fs.readdirSync(input.directory, { withFileTypes: true });
+  } catch (cause) {
+    return { failures: [{ filePath: input.directory, cause }] };
+  }
 
-const makeThreadWriter = Effect.fn("makeThreadWriter")(function* (input: {
-  readonly filePath: string;
-  readonly maxBytes: number;
-  readonly maxFiles: number;
-  readonly batchWindowMs: number;
-  readonly streamLabel: string;
-}): Effect.fn.Return<ThreadWriter | undefined> {
-  const sinkResult = yield* Effect.sync(() => {
+  for (const entry of entries) {
+    if (!entry.isFile() || !PROVIDER_LOG_FILE_PATTERN.test(entry.name)) continue;
+    const filePath = path.join(input.directory, entry.name);
     try {
-      return {
-        ok: true as const,
-        sink: new RotatingFileSink({
-          filePath: input.filePath,
-          maxBytes: input.maxBytes,
-          maxFiles: input.maxFiles,
-          throwOnError: true,
-        }),
-      };
-    } catch (error) {
-      return { ok: false as const, error };
+      const stat = fs.statSync(filePath);
+      files.push({ filePath, mtimeMs: stat.mtimeMs, size: stat.size });
+    } catch (cause) {
+      failures.push({ filePath, cause });
+    }
+  }
+
+  let totalBytes = files.reduce((total, file) => total + file.size, 0);
+  const remove = (file: (typeof files)[number]) => {
+    if (input.activeFilePaths.has(file.filePath)) return false;
+    try {
+      fs.rmSync(file.filePath, { force: true });
+      totalBytes -= file.size;
+      return true;
+    } catch (cause) {
+      failures.push({ filePath: file.filePath, cause });
+      return false;
     }
+  };
+
+  const retained = files.filter((file) => {
+    if (input.now - file.mtimeMs <= input.maxAgeMs) return true;
+    return !remove(file);
   });
 
-  if (!sinkResult.ok) {
-    yield* logWarning("failed to initialize provider thread log file", {
-      filePath: input.filePath,
-      error: sinkResult.error,
-    });
-    return undefined;
+  for (const file of retained.toSorted(
+    (left, right) => left.mtimeMs - right.mtimeMs || left.filePath.localeCompare(right.filePath),
+  )) {
+    if (totalBytes <= input.maxTotalBytes) break;
+    remove(file);
   }
 
-  const sink = sinkResult.sink;
-  const scope = yield* Scope.make();
-  const lineLogger = makeLineLogger(input.streamLabel);
-  const batchedLogger = yield* Logger.batched(lineLogger, {
-    window: input.batchWindowMs,
-    flush: Effect.fn("makeThreadWriter.flush")(function* (messages) {
-      const flushResult = yield* Effect.sync(() => {
-        try {
-          for (const message of messages) {
-            sink.write(message);
-          }
-          return { ok: true as const };
-        } catch (error) {
-          return { ok: false as const, error };
-        }
-      });
+  return { failures };
+}
+
+function validateOption(input: {
+  readonly filePath: string;
+  readonly option: string;
+  readonly value: number;
+  readonly minimum: number;
+}): EventNdjsonLogConfigurationError | undefined {
+  if (Number.isInteger(input.value) && input.value >= input.minimum) return undefined;
+  return new EventNdjsonLogConfigurationError(input);
+}
+
+function resolveOptions(
+  filePath: string,
+  options: EventNdjsonLogStoreOptions,
+): Effect.Effect<ResolvedOptions, EventNdjsonLogConfigurationError> {
+  const resolved = {
+    maxBytes: options.maxBytes ?? DEFAULT_MAX_BYTES,
+    maxFiles: options.maxFiles ?? DEFAULT_MAX_FILES,
+    batchWindowMs: options.batchWindowMs ?? DEFAULT_BATCH_WINDOW_MS,
+    maxTotalBytes: options.maxTotalBytes ?? DEFAULT_MAX_TOTAL_BYTES,
+    maxAgeMs: options.maxAgeMs ?? DEFAULT_MAX_AGE_MS,
+    retentionCheckIntervalMs:
+      options.retentionCheckIntervalMs ?? DEFAULT_RETENTION_CHECK_INTERVAL_MS,
+    maxBufferedBytes: options.maxBufferedBytes ?? DEFAULT_MAX_BUFFERED_BYTES,
+    maxBufferedRecords: options.maxBufferedRecords ?? DEFAULT_MAX_BUFFERED_RECORDS,
+    attribution: options.attribution,
+  } satisfies ResolvedOptions;
+
+  const validations = [
+    ["maxBytes", resolved.maxBytes, 1],
+    ["maxFiles", resolved.maxFiles, 1],
+    ["batchWindowMs", resolved.batchWindowMs, 0],
+    ["maxTotalBytes", resolved.maxTotalBytes, 1],
+    ["maxAgeMs", resolved.maxAgeMs, 1],
+    ["retentionCheckIntervalMs", resolved.retentionCheckIntervalMs, 1],
+    ["maxBufferedBytes", resolved.maxBufferedBytes, 1],
+    ["maxBufferedRecords", resolved.maxBufferedRecords, 1],
+  ] as const;
+
+  for (const [option, value, minimum] of validations) {
+    const error = validateOption({ filePath, option, value, minimum });
+    if (error) return Effect.fail(error);
+  }
+  return Effect.succeed(resolved);
+}
+
+function drainPending(input: {
+  readonly directory: string;
+  readonly options: ResolvedOptions;
+  readonly state: StoreState;
+  readonly now: number;
+  readonly timerFired: boolean;
+  readonly close: boolean;
+}): readonly [DrainResult, StoreState] {
+  if (input.state.closed) {
+    return [{ attributions: [], failures: [] }, input.state];
+  }
 
-      if (!flushResult.ok) {
-        yield* logWarning("provider event log batch flush failed", {
-          filePath: input.filePath,
-          error: flushResult.error,
+  const sinks = new Map(input.state.sinks);
+  const failedSegments = new Set(input.state.failedSegments);
+  const failures: Array<FileOperationFailure> = [];
+  const attributionByStream = new Map<
+    EventNdjsonStream,
+    { count: number; logicalWriteBytes: number }
+  >();
+  const recordsBySegment = new Map<string, Array<PendingRecord>>();
+
+  for (const record of input.state.pending) {
+    const records = recordsBySegment.get(record.threadSegment) ?? [];
+    records.push(record);
+    recordsBySegment.set(record.threadSegment, records);
+  }
+
+  for (const [threadSegment, records] of recordsBySegment) {
+    if (failedSegments.has(threadSegment)) continue;
+    const filePath = path.join(input.directory, `${threadSegment}.log`);
+    let sink = sinks.get(threadSegment);
+    if (!sink) {
+      try {
+        sink = new RotatingFileSink({
+          filePath,
+          maxBytes: input.options.maxBytes,
+          maxFiles: input.options.maxFiles,
+          throwOnError: true,
         });
+        sinks.set(threadSegment, sink);
+      } catch (cause) {
+        failedSegments.add(threadSegment);
+        failures.push({ filePath, cause });
+        continue;
       }
-    }),
-  }).pipe(Effect.provideService(Scope.Scope, scope));
+    }
 
-  const loggerLayer = Logger.layer([batchedLogger], { mergeWithExisting: false });
+    try {
+      writeBatchedMessages(sink, records, input.options.maxBytes);
+      for (const record of records) {
+        const current = attributionByStream.get(record.stream) ?? {
+          count: 0,
+          logicalWriteBytes: 0,
+        };
+        attributionByStream.set(record.stream, {
+          count: current.count + 1,
+          logicalWriteBytes: current.logicalWriteBytes + record.bytes,
+        });
+      }
+    } catch (cause) {
+      failedSegments.add(threadSegment);
+      failures.push({ filePath, cause });
+    }
+  }
 
-  return {
-    writeMessage(message: string) {
-      return Effect.log(message).pipe(Effect.provide(loggerLayer));
+  const retentionDue =
+    input.now - input.state.lastRetentionAt >= input.options.retentionCheckIntervalMs;
+  const retention = retentionDue
+    ? enforceRetention({
+        directory: input.directory,
+        maxTotalBytes: input.options.maxTotalBytes,
+        maxAgeMs: input.options.maxAgeMs,
+        activeFilePaths: new Set(
+          Array.from(sinks.keys(), (threadSegment) =>
+            path.join(input.directory, `${threadSegment}.log`),
+          ),
+        ),
+        now: input.now,
+      })
+    : { failures: [] };
+
+  return [
+    {
+      attributions: Array.from(attributionByStream, ([stream, value]) => ({
+        stream,
+        ...value,
+      })),
+      failures: [...failures, ...retention.failures],
     },
-    close() {
-      return Scope.close(scope, Exit.void);
+    {
+      pending: [],
+      pendingBytes: 0,
+      sinks,
+      failedSegments,
+      flushScheduled: input.timerFired ? false : input.state.flushScheduled,
+      closed: input.close,
+      lastRetentionAt: retentionDue ? input.now : input.state.lastRetentionAt,
C
cursorBotJun 23, 2026

Flush failure disables thread logging

Medium Severity

In drainPending, a single failed append adds the thread segment to failedSegments, clears the entire pending queue, and later flushes skip that segment permanently. A transient disk error therefore stops all native and canonical logging for that thread for the rest of the process, with no recovery path.

Reviewed by Cursor Bugbot for commit 2f8211fa857c9fbe01a1e852a5f707efe98835fd. Configure here.

apps/server/src/diagnostics/ProcessDiagnostics.ts · Earlier revision 8e2d5d32 · right 102

Original diff
@@ -111,465 +52,140 @@ class ProcessDiagnosticsSignalFailedError extends Schema.TaggedErrorClass<Proces
   }
 }
 
-const ProcessDiagnosticsError = Schema.Union([
-  ProcessDiagnosticsQueryTimeoutError,
-  ProcessDiagnosticsQueryFailedError,
-  ProcessDiagnosticsServerProcessSignalError,
-  ProcessDiagnosticsNotDescendantError,
-  ProcessDiagnosticsSignalFailedError,
-]);
-type ProcessDiagnosticsError = typeof ProcessDiagnosticsError.Type;
-const isProcessDiagnosticsError = Schema.is(ProcessDiagnosticsError);
-
-function parsePositiveInt(value: string): number | null {
-  const parsed = Number.parseInt(value, 10);
-  return Number.isInteger(parsed) && parsed > 0 ? parsed : null;
-}
-
-function parseNonNegativeInt(value: string): number | null {
-  const parsed = Number.parseInt(value, 10);
-  return Number.isInteger(parsed) && parsed >= 0 ? parsed : null;
-}
-
-function parseNumber(value: string): number | null {
-  const parsed = Number.parseFloat(value);
-  return Number.isFinite(parsed) ? parsed : null;
-}
-
-export function parsePosixProcessRows(output: string): ReadonlyArray<ProcessRow> {
-  const rows: ProcessRow[] = [];
-  const rowPattern =
-    /^\s*(\d+)\s+(\d+)\s+(-?\d+)\s+(\S+)\s+([+-]?(?:\d+\.?\d*|\.\d+))\s+(\d+)\s+(\S+)\s+(.+)$/;
-
-  for (const line of output.split(/\r?\n/)) {
-    if (line.trim().length === 0) continue;
-
-    const match = rowPattern.exec(line);
-    if (!match) continue;
-
-    const pidText = match[1];
-    const ppidText = match[2];
-    const pgidText = match[3];
-    const status = match[4];
-    const cpuText = match[5];
-    const rssText = match[6];
-    const elapsed = match[7];
-    const command = match[8];
-    if (
-      pidText === undefined ||
-      ppidText === undefined ||
-      pgidText === undefined ||
-      status === undefined ||
-      cpuText === undefined ||
-      rssText === undefined ||
-      elapsed === undefined ||
-      command === undefined
-    ) {
-      continue;
-    }
-
-    const pid = parsePositiveInt(pidText);
-    const ppid = parseNonNegativeInt(ppidText);
-    const pgid = Number.parseInt(pgidText, 10);
-    const cpuPercent = parseNumber(cpuText);
-    const rssKiB = parseNonNegativeInt(rssText);
-    if (
-      pid === null ||
-      ppid === null ||
-      !Number.isInteger(pgid) ||
-      cpuPercent === null ||
-      rssKiB === null ||
-      !status ||
-      !elapsed ||
-      !command
-    ) {
-      continue;
-    }
-
-    rows.push({
-      pid,
-      ppid,
-      pgid,
-      status,
-      cpuPercent,
-      rssBytes: rssKiB * 1024,
-      elapsed,
-      command,
-    });
-  }
-
-  return rows;
-}
-
-function normalizeWindowsProcessRow(value: unknown): ProcessRow | null {
-  if (typeof value !== "object" || value === null) return null;
-  const record = value as Record<string, unknown>;
-  const pid = typeof record.ProcessId === "number" ? record.ProcessId : null;
-  const ppid = typeof record.ParentProcessId === "number" ? record.ParentProcessId : null;
-  const commandLine =
-    typeof record.CommandLine === "string" && record.CommandLine.trim().length > 0
-      ? record.CommandLine
-      : typeof record.Name === "string"
-        ? record.Name
-        : null;
-  const workingSet =
-    typeof record.WorkingSetSize === "number" && Number.isFinite(record.WorkingSetSize)
-      ? Math.max(0, Math.round(record.WorkingSetSize))
-      : 0;
-  const cpuPercent =
-    typeof record.PercentProcessorTime === "number" && Number.isFinite(record.PercentProcessorTime)
-      ? Math.max(0, record.PercentProcessorTime)
-      : 0;
+export type ProcessDiagnosticsError = ProcessIdentityChanged | ProcessSignalFailed;
 
-  if (!pid || pid <= 0 || ppid === null || ppid < 0 || !commandLine) return null;
-  return {
-    pid,
-    ppid,
-    pgid: null,
-    status: typeof record.Status === "string" && record.Status.length > 0 ? record.Status : "Live",
-    cpuPercent,
-    rssBytes: workingSet,
-    elapsed: "",
-    command: commandLine,
-  };
-}
-
-function parseWindowsProcessRows(output: string): ReadonlyArray<ProcessRow> {
-  if (output.trim().length === 0) return [];
-  try {
-    const parsed = JSON.parse(output) as unknown;
-    const records = Array.isArray(parsed) ? parsed : [parsed];
-    return records.flatMap((record) => {
-      const row = normalizeWindowsProcessRow(record);
-      return row ? [row] : [];
-    });
-  } catch {
-    return [];
-  }
+function formatElapsed(runTimeMs: number): string {
+  const totalSeconds = Math.max(0, Math.floor(runTimeMs / 1_000));
+  const hours = Math.floor(totalSeconds / 3_600);
+  const minutes = Math.floor((totalSeconds % 3_600) / 60);
+  const seconds = totalSeconds % 60;
+  return hours > 0
+    ? `${hours}:${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}`
+    : `${minutes}:${String(seconds).padStart(2, "0")}`;
 }
 
-export function buildDescendantEntries(
-  rows: ReadonlyArray<ProcessRow>,
-  serverPid: number,
-): ReadonlyArray<ServerProcessDiagnosticsEntry> {
-  const childrenByParent = new Map<number, ProcessRow[]>();
-  for (const row of rows) {
-    const children = childrenByParent.get(row.ppid) ?? [];
-    children.push(row);
-    childrenByParent.set(row.ppid, children);
-  }
-
-  const entries: ServerProcessDiagnosticsEntry[] = [];
-  const visited = new Set<number>();
-  const stack = [...(childrenByParent.get(serverPid) ?? [])]
-    .toSorted((left, right) => left.pid - right.pid)
-    .map((row) => ({ row, depth: 0 }));
-
-  while (stack.length > 0) {
-    const item = stack.shift();
-    if (!item || visited.has(item.row.pid)) continue;
-    visited.add(item.row.pid);
-
-    const children = [...(childrenByParent.get(item.row.pid) ?? [])].toSorted(
-      (left, right) => left.pid - right.pid,
-    );
-    entries.push({
-      pid: item.row.pid,
-      ppid: item.row.ppid,
-      pgid: Option.fromNullishOr(item.row.pgid),
-      status: item.row.status,
-      cpuPercent: item.row.cpuPercent,
-      rssBytes: item.row.rssBytes,
-      elapsed: item.row.elapsed || "n/a",
-      command: item.row.command,
-      depth: item.depth,
-      childPids: children.map((child) => child.pid),
-    });
-
-    stack.unshift(...children.map((row) => ({ row, depth: item.depth + 1 })));
-  }
-
-  return entries;
-}
-
-export function isDiagnosticsQueryProcess(row: ProcessRow, serverPid: number): boolean {
-  if (row.ppid !== serverPid) return false;
-
-  const command = row.command.trim();
+function canSignalCategory(category: ResourceTelemetryProcessCategory): boolean {
   return (
-    /(?:^|[/\\])ps\s+-axo\s+pid=,ppid=,pgid=,stat=,pcpu=,rss=,etime=,command=/.test(command) ||
-    (/\bpowershell(?:\.exe)?\b/i.test(command) &&
-      /\bGet-CimInstance\s+Win32_Process\b/i.test(command))
+    category === "server-child" || category === "provider-root" || category === "terminal-root"
   );
 }
 
-function makeResult(input: {
-  readonly serverPid: number;
-  readonly rows: ReadonlyArray<ProcessRow>;
-  readonly readAt: DateTime.Utc;
-  readonly error?: string;
-}): ServerProcessDiagnosticsResult {
-  const readAt = input.readAt;
-  const rows = input.rows.filter((row) => !isDiagnosticsQueryProcess(row, input.serverPid));
-  const processes = buildDescendantEntries(rows, input.serverPid);
-  const totalRssBytes = processes.reduce((total, process) => total + process.rssBytes, 0);
-  const totalCpuPercent = processes.reduce((total, process) => total + process.cpuPercent, 0);
-
-  return {
-    serverPid: input.serverPid,
-    readAt,
-    processCount: processes.length,
-    totalRssBytes,
-    totalCpuPercent,
-    processes,
-    error: input.error ? Option.some({ message: input.error }) : Option.none(),
-  };
-}
-
-interface ProcessOutput {
-  readonly cwd: string;
-  readonly exitCode: number;
-  readonly stdout: string;
-  readonly stdoutBytes: number;
-  readonly stdoutTruncated: boolean;
-  readonly stderr: string;
-  readonly stderrBytes: number;
-  readonly stderrTruncated: boolean;
-}
-
-const runProcess = Effect.fn("runProcess")(function* (input: {
-  readonly command: string;
-  readonly args: ReadonlyArray<string>;
-}) {
-  const cwd = process.cwd();
-  return yield* Effect.gen(function* () {
-    const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
-    // `ps` and `powershell.exe` are real executables; spawning through cmd.exe
-    // shell mode would re-tokenize the PowerShell `-Command` payload (which
-    // contains pipes) before PowerShell ever sees it.
-    const child = yield* spawner.spawn(
-      ChildProcess.make(input.command, input.args, {
-        cwd,
-      }),
-    );
-    const [stdout, stderr, exitCode] = yield* Effect.all(
-      [
-        collectUint8StreamText({
-          stream: child.stdout,
-          maxBytes: PROCESS_QUERY_MAX_OUTPUT_BYTES,
-          truncatedMarker: "\n\n[truncated]",
-        }),
-        collectUint8StreamText({
-          stream: child.stderr,
-          maxBytes: PROCESS_QUERY_MAX_OUTPUT_BYTES,
-          truncatedMarker: "\n\n[truncated]",
-        }),
-        child.exitCode,
-      ],
-      { concurrency: "unbounded" },
-    );
-
-    return {
-      cwd,
-      exitCode,
-      stdout: stdout.text,
-      stdoutBytes: stdout.bytes,
-      stdoutTruncated: stdout.truncated,
-      stderr: stderr.text,
-      stderrBytes: stderr.bytes,
-      stderrTruncated: stderr.truncated,
-    } satisfies ProcessOutput;
-  }).pipe(
-    Effect.scoped,
-    Effect.timeoutOption(Duration.millis(PROCESS_QUERY_TIMEOUT_MS)),
-    Effect.flatMap((result) =>
-      Option.match(result, {
-        onNone: () =>
-          Effect.fail(
-            new ProcessDiagnosticsQueryTimeoutError({
-              command: input.command,
-              argCount: input.args.length,
-              cwd,
-              timeoutMillis: PROCESS_QUERY_TIMEOUT_MS,
-            }),
-          ),
-        onSome: Effect.succeed,
-      }),
-    ),
-    Effect.mapError((cause) =>
-      isProcessDiagnosticsError(cause)
-        ? cause
-        : new ProcessDiagnosticsQueryFailedError({
-            command: input.command,
-            argCount: input.args.length,
-            cwd,
-            cause,
+export const make = Effect.fn("makeProcessDiagnostics")(function* () {
+  const telemetry = yield* ResourceTelemetry.ResourceTelemetry;
+  const read: ProcessDiagnosticsShape["read"] = telemetry.latest.pipe(
+    Effect.map((snapshot) => {
+      const processes = snapshot.processes
+        .filter((entry) => entry.identity.pid !== process.pid)
+        .map(
+          (entry): ServerProcessDiagnosticsEntry => ({
+            pid: entry.identity.pid,
+            startTimeMs: entry.identity.startTimeMs,
+            ppid: entry.ppid,
+            pgid: Option.none(),
+            status: entry.status || "Unknown",
+            cpuPercent: entry.cpuPercent,
+            rssBytes: entry.residentBytes,
+            elapsed: formatElapsed(entry.runTimeMs),
+            command: entry.command || entry.name || "unknown",
+            depth: Math.max(0, entry.depth - 1),
+            childPids: entry.childPids,
           }),
-    ),
-  );
-});
-
-function readPosixProcessRows(): Effect.Effect<
-  ReadonlyArray<ProcessRow>,
-  ProcessDiagnosticsError,
-  ChildProcessSpawner.ChildProcessSpawner
-> {
-  return runProcess({
-    command: "ps",
-    args: ["-axo", POSIX_PROCESS_QUERY_COMMAND],
-  }).pipe(
-    Effect.flatMap((result) =>
-      result.exitCode !== 0
-        ? Effect.fail(
-            new ProcessDiagnosticsQueryFailedError({
-              command: "ps",
-              argCount: 2,
-              cwd: result.cwd,
-              exitCode: result.exitCode,
-              stdoutBytes: result.stdoutBytes,
-              stderrBytes: result.stderrBytes,
-              stdoutTruncated: result.stdoutTruncated,
-              stderrTruncated: result.stderrTruncated,
-            }),
-          )
-        : Effect.succeed(parsePosixProcessRows(result.stdout)),
-    ),
-  );
-}
-
-function readWindowsProcessRows(): Effect.Effect<
-  ReadonlyArray<ProcessRow>,
-  ProcessDiagnosticsError,
-  ChildProcessSpawner.ChildProcessSpawner
-> {
-  const command = [
-    "$processes = Get-CimInstance Win32_Process | ForEach-Object {",
-    '$perf = Get-CimInstance Win32_PerfFormattedData_PerfProc_Process -Filter "IDProcess = $($_.ProcessId)" -ErrorAction SilentlyContinue;',
-    "[pscustomobject]@{ ProcessId = $_.ProcessId; ParentProcessId = $_.ParentProcessId; Name = $_.Name; CommandLine = $_.CommandLine; Status = $_.Status; WorkingSetSize = $_.WorkingSetSize; PercentProcessorTime = if ($perf) { $perf.PercentProcessorTime } else { 0 } }",
-    "};",
-    "$processes | ConvertTo-Json -Compress -Depth 3",
-  ].join(" ");
-
-  return runProcess({
-    command: "powershell.exe",
-    args: ["-NoProfile", "-NonInteractive", "-Command", command],
-  }).pipe(
-    Effect.flatMap((result) =>
-      result.exitCode !== 0
-        ? Effect.fail(
-            new ProcessDiagnosticsQueryFailedError({
-              command: "powershell.exe",
-              argCount: 4,
-              cwd: result.cwd,
-              exitCode: result.exitCode,
-              stdoutBytes: result.stdoutBytes,
-              stderrBytes: result.stderrBytes,
-              stdoutTruncated: result.stdoutTruncated,
-              stderrTruncated: result.stderrTruncated,
-            }),
-          )
-        : Effect.succeed(parseWindowsProcessRows(result.stdout)),
-    ),
-  );
-}
-
-export const readProcessRows = Effect.gen(function* () {
-  const platform = yield* HostProcessPlatform;
-  return yield* platform === "win32" ? readWindowsProcessRows() : readPosixProcessRows();
-});
-
-export function aggregateProcessDiagnostics(input: {
-  readonly serverPid: number;
-  readonly rows: ReadonlyArray<ProcessRow>;
-  readonly readAt: DateTime.Utc;
-}): ServerProcessDiagnosticsResult {
-  return makeResult(input);
-}
-
-function assertDescendantPid(
-  pid: number,
-): Effect.Effect<void, ProcessDiagnosticsError, ChildProcessSpawner.ChildProcessSpawner> {
-  if (pid === process.pid) {
-    return Effect.fail(
-      new ProcessDiagnosticsServerProcessSignalError({
-        pid,
-      }),
-    );
-  }
-
-  return readProcessRows.pipe(
-    Effect.flatMap((rows) => {
-      const filteredRows = rows.filter((row) => !isDiagnosticsQueryProcess(row, process.pid));
-      const descendant = buildDescendantEntries(filteredRows, process.pid).some(
-        (entry) => entry.pid === pid,
-      );
-      return descendant
-        ? Effect.void
-        : Effect.fail(
-            new ProcessDiagnosticsNotDescendantError({
-              pid,
-              serverPid: process.pid,
-            }),
-          );
+        );
+      return {
+        serverPid: process.pid,
+        readAt: snapshot.readAt,
+        processCount: processes.length,
+        totalRssBytes: processes.reduce((total, entry) => total + entry.rssBytes, 0),
+        totalCpuPercent: processes.reduce((total, entry) => total + entry.cpuPercent, 0),
+        processes,
+        error: Option.map(snapshot.health.native.lastError, (message) => ({ message })),
+      };
C
cursorBotJun 23, 2026

Process diagnostics use stale cache

Medium Severity

ProcessDiagnostics.read now maps directly from ResourceTelemetry.latest without sampling or refresh. Legacy callers that only hit the process diagnostics RPC get a cached snapshot, which can lag or stay empty when live telemetry is not subscribed, unlike the old on-demand ps query.

Reviewed by Cursor Bugbot for commit 8e2d5d326a0532e87806c217f8108d6b981bd823. Configure here.

native/resource-monitor/src/main.rs · Earlier revision deb3d483 · right 592

Original diff
@@ -0,0 +1,766 @@
+use serde::{Deserialize, Serialize};
+use std::collections::{HashMap, HashSet, VecDeque};
+use std::io::{self, BufRead, BufWriter, Write};
+use std::sync::mpsc::{self, Receiver, RecvTimeoutError};
+use std::thread;
+use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
+use sysinfo::{Pid, ProcessRefreshKind, ProcessesToUpdate, System, UpdateKind};
+
+const PROTOCOL_VERSION: u32 = 2;
+const MIN_SAMPLE_INTERVAL_MS: u64 = 250;
+const MAX_SAMPLE_INTERVAL_MS: u64 = 60_000;
+const EXTERNAL_PROCESS_START_TOLERANCE_MS: u64 = 2_000;
+const HISTORY_RETENTION_MS: u64 = 60 * 60_000;
+const MAX_HISTORY_SNAPSHOTS: usize = 3_600;
+const MAX_HISTORY_PROCESS_SAMPLES: usize = 20_000;
+const HISTORY_CHUNK_SNAPSHOTS: usize = 32;
+
+#[derive(Debug, Clone, Deserialize)]
+#[serde(rename_all = "camelCase")]
+struct ExternalProcess {
+    pid: u32,
+    #[serde(default)]
+    start_time_ms: Option<u64>,
+}
+
+#[derive(Debug, Deserialize)]
+#[serde(
+    tag = "type",
+    rename_all = "camelCase",
+    rename_all_fields = "camelCase"
+)]
+enum Command {
+    Configure {
+        version: u32,
+        root_pid: u32,
+        sample_interval_ms: u64,
+        #[serde(default)]
+        external_processes: Vec<ExternalProcess>,
+    },
+    SetExternalProcesses {
+        version: u32,
+        processes: Vec<ExternalProcess>,
+    },
+    SetSampleInterval {
+        version: u32,
+        sample_interval_ms: u64,
+    },
+    SetStreaming {
+        version: u32,
+        enabled: bool,
+    },
+    SampleNow {
+        version: u32,
+        request_id: String,
+    },
+    ReadHistory {
+        version: u32,
+        request_id: String,
+        window_ms: u64,
+    },
+    Shutdown {
+        version: u32,
+    },
+}
+
+impl Command {
+    fn version(&self) -> u32 {
+        match self {
+            Self::Configure { version, .. }
+            | Self::SetExternalProcesses { version, .. }
+            | Self::SetSampleInterval { version, .. }
+            | Self::SetStreaming { version, .. }
+            | Self::SampleNow { version, .. }
+            | Self::ReadHistory { version, .. }
+            | Self::Shutdown { version } => *version,
+        }
+    }
+}
+
+enum Input {
+    Command(Command),
+    Invalid(String),
+}
+
+#[derive(Debug, Serialize)]
+#[serde(rename_all = "camelCase")]
+struct Capabilities {
+    cumulative_cpu_time: bool,
+    current_cpu_percent: bool,
+    resident_memory: bool,
+    virtual_memory: bool,
+    io_bytes: bool,
+    process_start_time: bool,
+    process_tree: bool,
+}
+
+#[derive(Debug, Serialize)]
+#[serde(rename_all = "camelCase")]
+struct HelloEvent {
+    version: u32,
+    #[serde(rename = "type")]
+    event_type: &'static str,
+    sidecar_version: &'static str,
+    sidecar_pid: u32,
+    platform: &'static str,
+    arch: &'static str,
+    capabilities: Capabilities,
+}
+
+#[derive(Debug, Clone, Copy, Serialize)]
+#[serde(rename_all = "kebab-case")]
+enum IoSemantics {
+    Storage,
+    AllIo,
+}
+
+#[derive(Debug, Clone, Serialize)]
+#[serde(rename_all = "camelCase")]
+struct ProcessSample {
+    pid: u32,
+    ppid: u32,
+    start_time_ms: u64,
+    run_time_ms: u64,
+    name: String,
+    command: String,
+    status: String,
+    cpu_percent: f32,
+    cpu_time_ms: u64,
+    resident_bytes: u64,
+    virtual_bytes: u64,
+    io_read_bytes: u64,
+    io_write_bytes: u64,
+    io_semantics: IoSemantics,
+}
+
+#[derive(Debug, Clone, Serialize)]
+#[serde(rename_all = "camelCase")]
+struct SnapshotEvent {
+    version: u32,
+    #[serde(rename = "type")]
+    event_type: &'static str,
+    sequence: u64,
+    sampled_at_unix_ms: u64,
+    collection_duration_micros: u64,
+    scanned_process_count: usize,
+    retained_process_count: usize,
+    inaccessible_process_count: usize,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    request_id: Option<String>,
+    processes: Vec<ProcessSample>,
+}
+
+#[derive(Debug, Serialize)]
+#[serde(rename_all = "camelCase")]
+struct HistoryChunkEvent<'a> {
+    version: u32,
+    #[serde(rename = "type")]
+    event_type: &'static str,
+    request_id: &'a str,
+    done: bool,
+    snapshots: &'a [SnapshotEvent],
+}
+
+#[derive(Debug, Serialize)]
+#[serde(rename_all = "camelCase")]
+struct ErrorEvent {
+    version: u32,
+    #[serde(rename = "type")]
+    event_type: &'static str,
+    code: &'static str,
+    message: String,
+    recoverable: bool,
+}
+
+#[derive(Debug, Clone)]
+struct CollectorConfig {
+    root_pid: u32,
+    sample_interval: Option<Duration>,
+    external_processes: HashMap<u32, Option<u64>>,
+}
+
+#[derive(Default)]
+struct HistoryRecorder {
+    snapshots: VecDeque<SnapshotEvent>,
+    process_sample_count: usize,
+}
+
+impl HistoryRecorder {
+    fn record(&mut self, snapshot: &SnapshotEvent) {
+        let mut retained = snapshot.clone();
+        retained.request_id = None;
+        self.process_sample_count = self
+            .process_sample_count
+            .saturating_add(retained.processes.len());
+        self.snapshots.push_back(retained);
+        self.trim(snapshot.sampled_at_unix_ms);
+    }
+
+    fn trim(&mut self, now_ms: u64) {
+        while self.snapshots.front().is_some_and(|snapshot| {
+            snapshot.sampled_at_unix_ms < now_ms.saturating_sub(HISTORY_RETENTION_MS)
+                || self.snapshots.len() > MAX_HISTORY_SNAPSHOTS
+                || self.process_sample_count > MAX_HISTORY_PROCESS_SAMPLES
+        }) {
+            if let Some(removed) = self.snapshots.pop_front() {
+                self.process_sample_count = self
+                    .process_sample_count
+                    .saturating_sub(removed.processes.len());
+            }
+        }
+    }
+
+    fn read(&self, window_ms: u64, now_ms: u64) -> Vec<SnapshotEvent> {
+        let started_at_ms = now_ms.saturating_sub(window_ms.min(HISTORY_RETENTION_MS));
+        self.snapshots
+            .iter()
+            .filter(|snapshot| snapshot.sampled_at_unix_ms >= started_at_ms)
+            .cloned()
+            .collect()
+    }
+}
+
+struct Collector {
+    system: System,
+    sequence: u64,
+}
+
+impl Collector {
+    fn new() -> Self {
+        Self {
+            system: System::new(),
+            sequence: 0,
+        }
+    }
+
+    fn sample(&mut self, config: &CollectorConfig, request_id: Option<String>) -> SnapshotEvent {
+        let collection_started = Instant::now();
+        self.system.refresh_processes_specifics(
+            ProcessesToUpdate::All,
+            true,
+            process_refresh_kind(),
+        );
+
+        let rows = self
+            .system
+            .processes()
+            .iter()
+            .map(|(pid, process)| {
+                let pid = pid.as_u32();
+                let ppid = process.parent().map(Pid::as_u32).unwrap_or(0);
+                (pid, ppid, process.start_time().saturating_mul(1_000))
+            })
+            .collect::<Vec<_>>();
+        let mut roots = config
+            .external_processes
+            .iter()
+            .filter_map(|(pid, expected_start_time_ms)| {
+                let (_, _, actual_start_time_ms) = rows
+                    .iter()
+                    .find(|(candidate_pid, _, _)| candidate_pid == pid)?;
+                matches_external_identity(*actual_start_time_ms, *expected_start_time_ms)
+                    .then_some(*pid)
+            })
+            .collect::<HashSet<_>>();
+        roots.insert(config.root_pid);
+        let tracked = select_tracked_pids(&rows, &roots);
+        let mut processes = tracked
+            .into_iter()
+            .filter_map(|pid| {
+                let process = self.system.process(Pid::from_u32(pid))?;
+                let disk_usage = process.disk_usage();
+                let command = if process.cmd().is_empty() {
+                    process.name().to_string_lossy().into_owned()
+                } else {
+                    process
+                        .cmd()
+                        .iter()
+                        .map(|part| part.to_string_lossy())
+                        .collect::<Vec<_>>()
+                        .join(" ")
+                };
+
+                Some(ProcessSample {
+                    pid,
+                    ppid: process.parent().map(Pid::as_u32).unwrap_or(0),
+                    start_time_ms: process.start_time().saturating_mul(1_000),
+                    run_time_ms: process.run_time().saturating_mul(1_000),
+                    name: process.name().to_string_lossy().into_owned(),
+                    command,
+                    status: format!("{:?}", process.status()),
+                    cpu_percent: process.cpu_usage(),
+                    cpu_time_ms: process.accumulated_cpu_time(),
+                    resident_bytes: process.memory(),
+                    virtual_bytes: process.virtual_memory(),
+                    io_read_bytes: disk_usage.total_read_bytes,
+                    io_write_bytes: disk_usage.total_written_bytes,
+                    io_semantics: io_semantics(),
+                })
+            })
+            .collect::<Vec<_>>();
+        processes.sort_by_key(|process| process.pid);
+        self.sequence = self.sequence.saturating_add(1);
+
+        SnapshotEvent {
+            version: PROTOCOL_VERSION,
+            event_type: "snapshot",
+            sequence: self.sequence,
+            sampled_at_unix_ms: unix_time_ms(),
+            collection_duration_micros: collection_started.elapsed().as_micros() as u64,
+            scanned_process_count: self.system.processes().len(),
+            retained_process_count: processes.len(),
+            inaccessible_process_count: 0,
+            request_id,
+            processes,
+        }
+    }
+}
+
+fn process_refresh_kind() -> ProcessRefreshKind {
+    ProcessRefreshKind::nothing()
+        .with_memory()
+        .with_cpu()
+        .with_disk_usage()
+        .with_cmd(UpdateKind::OnlyIfNotSet)
+        .without_tasks()
+}
+
+fn matches_external_identity(
+    actual_start_time_ms: u64,
+    expected_start_time_ms: Option<u64>,
+) -> bool {
+    expected_start_time_ms.is_none_or(|expected| {
+        actual_start_time_ms.abs_diff(expected) <= EXTERNAL_PROCESS_START_TOLERANCE_MS
+    })
+}
+
+fn select_tracked_pids(rows: &[(u32, u32, u64)], roots: &HashSet<u32>) -> HashSet<u32> {
+    let mut children_by_parent = HashMap::<u32, Vec<u32>>::new();
+    for (pid, ppid, _) in rows {
+        children_by_parent.entry(*ppid).or_default().push(*pid);
+    }
+
+    let known_pids = rows.iter().map(|(pid, _, _)| *pid).collect::<HashSet<_>>();
+    let mut tracked = HashSet::new();
+    let mut queue = roots
+        .iter()
+        .copied()
+        .filter(|pid| known_pids.contains(pid))
+        .collect::<VecDeque<_>>();
+
+    while let Some(pid) = queue.pop_front() {
+        if !tracked.insert(pid) {
+            continue;
+        }
+        if let Some(children) = children_by_parent.get(&pid) {
+            queue.extend(children.iter().copied());
+        }
+    }
+
+    tracked
+}
+
+fn io_semantics() -> IoSemantics {
+    if cfg!(target_os = "windows") {
+        IoSemantics::AllIo
+    } else {
+        IoSemantics::Storage
+    }
+}
+
+fn unix_time_ms() -> u64 {
+    SystemTime::now()
+        .duration_since(UNIX_EPOCH)
+        .unwrap_or_default()
+        .as_millis() as u64
+}
+
+fn clamp_sample_interval(sample_interval_ms: u64) -> Option<Duration> {
+    (sample_interval_ms > 0).then(|| {
+        Duration::from_millis(
+            sample_interval_ms.clamp(MIN_SAMPLE_INTERVAL_MS, MAX_SAMPLE_INTERVAL_MS),
+        )
+    })
+}
+
+fn spawn_input_reader() -> Receiver<Input> {
+    let (sender, receiver) = mpsc::channel();
+    thread::spawn(move || {
+        let stdin = io::stdin();
+        for line in stdin.lock().lines() {
+            let line = match line {
+                Ok(line) => line,
+                Err(error) => {
+                    let _ = sender.send(Input::Invalid(format!(
+                        "failed reading command stream: {error}"
+                    )));
+                    return;
+                }
+            };
+            if line.trim().is_empty() {
+                continue;
+            }
+            match serde_json::from_str::<Command>(&line) {
+                Ok(command) => {
+                    if sender.send(Input::Command(command)).is_err() {
+                        return;
+                    }
+                }
+                Err(error) => {
+                    if sender
+                        .send(Input::Invalid(format!("invalid command: {error}")))
+                        .is_err()
+                    {
+                        return;
+                    }
+                }
+            }
+        }
+    });
+    receiver
+}
+
+fn write_event<T: Serialize>(writer: &mut impl Write, event: &T) -> io::Result<()> {
+    serde_json::to_writer(&mut *writer, event)?;
+    writer.write_all(b"\n")?;
+    writer.flush()
+}
+
+fn write_error(
+    writer: &mut impl Write,
+    code: &'static str,
+    message: impl Into<String>,
+    recoverable: bool,
+) -> io::Result<()> {
+    write_event(
+        writer,
+        &ErrorEvent {
+            version: PROTOCOL_VERSION,
+            event_type: "error",
+            code,
+            message: message.into(),
+            recoverable,
+        },
+    )
+}
+
+fn write_history(
+    writer: &mut impl Write,
+    request_id: &str,
+    snapshots: &[SnapshotEvent],
+) -> io::Result<()> {
+    if snapshots.is_empty() {
+        return write_event(
+            writer,
+            &HistoryChunkEvent {
+                version: PROTOCOL_VERSION,
+                event_type: "historyChunk",
+                request_id,
+                done: true,
+                snapshots,
+            },
+        );
+    }
+
+    let chunk_count = snapshots.len().div_ceil(HISTORY_CHUNK_SNAPSHOTS);
+    for (index, chunk) in snapshots.chunks(HISTORY_CHUNK_SNAPSHOTS).enumerate() {
+        write_event(
+            writer,
+            &HistoryChunkEvent {
+                version: PROTOCOL_VERSION,
+                event_type: "historyChunk",
+                request_id,
+                done: index + 1 == chunk_count,
+                snapshots: chunk,
+            },
+        )?;
+    }
+    Ok(())
+}
+
+fn main() -> io::Result<()> {
+    let mut writer = BufWriter::new(io::stdout().lock());
+    write_event(
+        &mut writer,
+        &HelloEvent {
+            version: PROTOCOL_VERSION,
+            event_type: "hello",
+            sidecar_version: env!("CARGO_PKG_VERSION"),
+            sidecar_pid: std::process::id(),
+            platform: std::env::consts::OS,
+            arch: std::env::consts::ARCH,
+            capabilities: Capabilities {
+                cumulative_cpu_time: true,
+                current_cpu_percent: true,
+                resident_memory: true,
+                virtual_memory: true,
+                io_bytes: true,
+                process_start_time: true,
+                process_tree: true,
+            },
+        },
+    )?;
+
+    let receiver = spawn_input_reader();
+    let mut collector = Collector::new();
+    let mut history = HistoryRecorder::default();
+    let mut config: Option<CollectorConfig> = None;
+    let mut next_sample_at: Option<Instant> = None;
+    let mut streaming_enabled = false;
+
+    loop {
+        let timeout = next_sample_at
+            .map(|deadline| deadline.saturating_duration_since(Instant::now()))
+            .unwrap_or(Duration::from_secs(60));
+
+        match receiver.recv_timeout(timeout) {
+            Ok(Input::Invalid(message)) => {
+                write_error(&mut writer, "invalid-command", message, true)?;
+            }
+            Ok(Input::Command(command)) => {
+                if command.version() != PROTOCOL_VERSION {
+                    write_error(
+                        &mut writer,
+                        "protocol-mismatch",
+                        format!(
+                            "unsupported protocol version {}; expected {PROTOCOL_VERSION}",
+                            command.version()
+                        ),
+                        false,
+                    )?;
+                    continue;
+                }
+
+                match command {
+                    Command::Configure {
+                        root_pid,
+                        sample_interval_ms,
+                        external_processes,
+                        ..
+                    } => {
+                        let sample_interval = clamp_sample_interval(sample_interval_ms);
+                        config = Some(CollectorConfig {
+                            root_pid,
+                            sample_interval,
+                            external_processes: external_processes
+                                .into_iter()
+                                .map(|process| (process.pid, process.start_time_ms))
+                                .collect(),
+                        });
+                        next_sample_at = sample_interval.map(|_| Instant::now());
+                    }
+                    Command::SetExternalProcesses { processes, .. } => {
+                        if let Some(current) = config.as_mut() {
+                            current.external_processes = processes
+                                .into_iter()
+                                .map(|process| (process.pid, process.start_time_ms))
+                                .collect();
+                        } else {
+                            write_error(
+                                &mut writer,
+                                "not-configured",
+                                "configure must be sent before external processes",
+                                true,
+                            )?;
+                        }
+                    }
+                    Command::SetSampleInterval {
+                        sample_interval_ms, ..
+                    } => {
+                        if let Some(current) = config.as_mut() {
+                            current.sample_interval = clamp_sample_interval(sample_interval_ms);
+                            next_sample_at = current
+                                .sample_interval
+                                .map(|interval| Instant::now() + interval);
+                        } else {
+                            write_error(
+                                &mut writer,
+                                "not-configured",
+                                "configure must be sent before changing the sample interval",
+                                true,
+                            )?;
+                        }
+                    }
+                    Command::SetStreaming { enabled, .. } => {
+                        streaming_enabled = enabled;
+                    }
+                    Command::SampleNow { request_id, .. } => {
+                        if let Some(current) = config.as_ref() {
+                            let event = collector.sample(current, Some(request_id));
+                            history.record(&event);
+                            write_event(&mut writer, &event)?;
+                            next_sample_at = current
M
macroscopeappBotJul 28, 2026

🟡 Medium src/main.rs:592

When SampleNow is received, next_sample_at is reset to Instant::now() + interval, which restarts the periodic sampling timer from scratch. If on-demand SampleNow requests arrive more frequently than the configured sample_interval, the periodic deadline is continually postponed and periodic snapshots are never emitted, breaking the configured streaming/history cadence. An on-demand sample should not extend an existing periodic deadline — only set one if it doesn't already exist.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
text
1In file @native/resource-monitor/src/main.rs around line 592:
2
3When `SampleNow` is received, `next_sample_at` is reset to `Instant::now() + interval`, which restarts the periodic sampling timer from scratch. If on-demand `SampleNow` requests arrive more frequently than the configured `sample_interval`, the periodic deadline is continually postponed and periodic snapshots are never emitted, breaking the configured streaming/history cadence. An on-demand sample should not extend an existing periodic deadline — only set one if it doesn't already exist.
J
Julius MarmingeJul 28, 2026

Fixed in 24e6ccd404. SampleNow preserves an existing periodic deadline and only creates one when none exists. A native unit test verifies on-demand samples do not postpone the scheduled cadence.

apps/server/src/ws.ts · Earlier revision deb3d483 · right 1608

Original diff
@@ -1571,10 +1582,42 @@ const makeWsRpcLayer = (
               "rpc.aggregate": "server",
             },
           ),
+        [WS_METHODS.serverGetResourceTelemetryHistory]: (input) =>
+          observeRpcEffect(
+            WS_METHODS.serverGetResourceTelemetryHistory,
+            resourceTelemetry.readHistory(input),
+            {
+              "rpc.aggregate": "server",
+            },
+          ),
+        [WS_METHODS.serverRetryResourceTelemetry]: (_input) =>
+          observeRpcEffect(WS_METHODS.serverRetryResourceTelemetry, resourceTelemetry.retry, {
+            "rpc.aggregate": "server",
+          }),
         [WS_METHODS.serverSignalProcess]: (input) =>
           observeRpcEffect(WS_METHODS.serverSignalProcess, processDiagnostics.signal(input), {
             "rpc.aggregate": "server",
           }),
+        [WS_METHODS.serverReportClientActivity]: (input, metadata) =>
+          observeRpcEffect(
+            WS_METHODS.serverReportClientActivity,
+            backgroundPolicy.reportClientActivity(
+              currentSessionId,
+              RpcClientId.make(metadata.client.id),
+              input,
+            ),
C
cursorBotJul 28, 2026

Disconnect leaves activity leases

High Severity

serverReportClientActivity records leases keyed by RPC client id, and BackgroundPolicy.removeRpcClient exists to clear them, but the websocket layer never calls it on disconnect. Leases keep scoped demand alive until TTL expiry, so background VCS/provider work can continue after the client is gone.

Additional Locations (1)

Reviewed by Cursor Bugbot for commit deb3d48317e7f69bbc7c37fccda09d6e075e6218. Configure here.

J
Julius MarmingeJul 28, 2026

Fixed in 24e6ccd404. Each websocket layer tracks the RPC client IDs it observed and removes all corresponding BackgroundPolicy leases in the connection-scope finalizer.

apps/server/src/resourceTelemetry/NativeTelemetryClient.ts · Earlier revision deb3d483 · right 652

Original diff
@@ -0,0 +1,922 @@
+import type {
+  HostPowerSnapshot,
+  ResourceMonitorCapabilities,
+  ResourceMonitorCommand,
+  ResourceMonitorEvent,
+  ResourceMonitorExternalProcess,
+  ResourceMonitorHelloEvent,
+  ResourceMonitorSnapshotEvent,
+  ResourceTelemetrySourceStatus,
+} from "@t3tools/contracts";
+import {
+  RESOURCE_MONITOR_PROTOCOL_VERSION,
+  ResourceMonitorCommand as ResourceMonitorCommandSchema,
+  ResourceMonitorEvent as ResourceMonitorEventSchema,
+} from "@t3tools/contracts";
+import * as Cause from "effect/Cause";
+import * as Context from "effect/Context";
+import * as Crypto from "effect/Crypto";
+import * as DateTime from "effect/DateTime";
+import * as Deferred from "effect/Deferred";
+import * as Duration from "effect/Duration";
+import * as Effect from "effect/Effect";
+import * as Fiber from "effect/Fiber";
+import * as Layer from "effect/Layer";
+import * as Option from "effect/Option";
+import * as PubSub from "effect/PubSub";
+import * as Queue from "effect/Queue";
+import * as Ref from "effect/Ref";
+import * as Result from "effect/Result";
+import * as Schema from "effect/Schema";
+import * as Semaphore from "effect/Semaphore";
+import * as Stream from "effect/Stream";
+import * as Ndjson from "effect/unstable/encoding/Ndjson";
+import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
+
+import * as ResourceMonitorBinary from "./ResourceMonitorBinary.ts";
+import { ServerConfig } from "../config.ts";
+
+const SAMPLE_INTERVAL_MS = 1_000;
+const UNKNOWN_BACKGROUND_SAMPLE_INTERVAL_MS = 5_000;
+const BATTERY_SAMPLE_INTERVAL_MS = 5_000;
+const CONSTRAINED_SAMPLE_INTERVAL_MS = 15_000;
+const HANDSHAKE_TIMEOUT = Duration.seconds(5);
+const SAMPLE_REQUEST_TIMEOUT = Duration.seconds(5);
+const HISTORY_REQUEST_TIMEOUT = Duration.seconds(15);
+const INITIAL_RESTART_DELAY = Duration.millis(500);
+const MAX_RESTART_DELAY = Duration.seconds(10);
+const FAILURE_WINDOW_MS = 60_000;
+const MAX_FAILURES_PER_WINDOW = 5;
+
+export class NativeTelemetrySpawnFailed extends Schema.TaggedErrorClass<NativeTelemetrySpawnFailed>()(
+  "NativeTelemetrySpawnFailed",
+  {
+    path: Schema.String,
+    cause: Schema.Defect(),
+  },
+) {
+  override get message(): string {
+    return `Failed to start resource monitor '${this.path}'.`;
+  }
+}
+
+export class NativeTelemetryHandshakeTimedOut extends Schema.TaggedErrorClass<NativeTelemetryHandshakeTimedOut>()(
+  "NativeTelemetryHandshakeTimedOut",
+  {
+    timeoutMs: Schema.Number,
+  },
+) {
+  override get message(): string {
+    return `Resource monitor handshake timed out after ${this.timeoutMs}ms.`;
+  }
+}
+
+export class NativeTelemetryProtocolMismatch extends Schema.TaggedErrorClass<NativeTelemetryProtocolMismatch>()(
+  "NativeTelemetryProtocolMismatch",
+  {
+    expectedVersion: Schema.Number,
+    receivedVersion: Schema.Number,
+  },
+) {
+  override get message(): string {
+    return `Resource monitor protocol ${this.receivedVersion} is incompatible with expected protocol ${this.expectedVersion}.`;
+  }
+}
+
+export class NativeTelemetryDecodeFailed extends Schema.TaggedErrorClass<NativeTelemetryDecodeFailed>()(
+  "NativeTelemetryDecodeFailed",
+  {
+    cause: Schema.Defect(),
+  },
+) {
+  override get message(): string {
+    return "Failed to decode resource monitor output.";
+  }
+}
+
+export class NativeTelemetryCommandFailed extends Schema.TaggedErrorClass<NativeTelemetryCommandFailed>()(
+  "NativeTelemetryCommandFailed",
+  {
+    operation: Schema.String,
+    cause: Schema.Defect(),
+  },
+) {
+  override get message(): string {
+    return `Resource monitor command '${this.operation}' failed.`;
+  }
+}
+
+export class NativeTelemetryExited extends Schema.TaggedErrorClass<NativeTelemetryExited>()(
+  "NativeTelemetryExited",
+  {
+    exitCode: Schema.Number,
+  },
+) {
+  override get message(): string {
+    return `Resource monitor exited with code ${this.exitCode}.`;
+  }
+}
+
+export class NativeTelemetryUnavailable extends Schema.TaggedErrorClass<NativeTelemetryUnavailable>()(
+  "NativeTelemetryUnavailable",
+  {
+    reason: Schema.String,
+  },
+) {
+  override get message(): string {
+    return `Resource monitor is unavailable: ${this.reason}`;
+  }
+}
+
+export type NativeTelemetryClientError =
+  | NativeTelemetrySpawnFailed
+  | NativeTelemetryHandshakeTimedOut
+  | NativeTelemetryProtocolMismatch
+  | NativeTelemetryDecodeFailed
+  | NativeTelemetryCommandFailed
+  | NativeTelemetryExited
+  | NativeTelemetryUnavailable;
+
+export interface NativeTelemetryClientHealth {
+  readonly status: ResourceTelemetrySourceStatus;
+  readonly hello: Option.Option<ResourceMonitorHelloEvent>;
+  readonly lastSampleAt: Option.Option<DateTime.Utc>;
+  readonly lastError: Option.Option<string>;
+  readonly restartCount: number;
+  readonly sampleIntervalMs: number;
+}
+
+export interface NativeTelemetryClientShape {
+  readonly capabilities: Effect.Effect<ResourceMonitorCapabilities, NativeTelemetryClientError>;
+  readonly snapshots: Stream.Stream<ResourceMonitorSnapshotEvent, NativeTelemetryClientError>;
+  readonly readHistory: (
+    windowMs: number,
+  ) => Effect.Effect<ReadonlyArray<ResourceMonitorSnapshotEvent>, NativeTelemetryClientError>;
+  readonly setExternalProcesses: (
+    processes: ReadonlyArray<ResourceMonitorExternalProcess>,
+  ) => Effect.Effect<void, NativeTelemetryClientError>;
+  readonly setHostPowerState: (
+    snapshot: HostPowerSnapshot,
+  ) => Effect.Effect<void, NativeTelemetryClientError>;
+  readonly sampleNow: Effect.Effect<ResourceMonitorSnapshotEvent, NativeTelemetryClientError>;
+  readonly retry: Effect.Effect<boolean>;
+  readonly health: Effect.Effect<NativeTelemetryClientHealth>;
+  readonly healthChanges: Stream.Stream<NativeTelemetryClientHealth>;
+}
+
+export class NativeTelemetryClient extends Context.Service<
+  NativeTelemetryClient,
+  NativeTelemetryClientShape
+>()("t3/resourceTelemetry/NativeTelemetryClient") {}
+
+interface ClientState {
+  readonly status: ResourceTelemetrySourceStatus;
+  readonly handle: Option.Option<ChildProcessSpawner.ChildProcessHandle>;
+  readonly hello: Option.Option<ResourceMonitorHelloEvent>;
+  readonly lastSampleAt: Option.Option<DateTime.Utc>;
+  readonly lastError: Option.Option<string>;
+  readonly restartCount: number;
+}
+
+interface CollectionControl {
+  readonly hostPower: HostPowerSnapshot;
+  readonly liveSubscriberCount: number;
+  readonly sampleIntervalMs: number;
+}
+
+interface PendingHistoryRequest {
+  readonly deferred: Deferred.Deferred<
+    ReadonlyArray<ResourceMonitorSnapshotEvent>,
+    NativeTelemetryClientError
+  >;
+  readonly snapshots: ReadonlyArray<ResourceMonitorSnapshotEvent>;
+}
+
+const initialState: ClientState = {
+  status: "starting",
+  handle: Option.none(),
+  hello: Option.none(),
+  lastSampleAt: Option.none(),
+  lastError: Option.none(),
+  restartCount: 0,
+};
+
+function toHealth(state: ClientState, sampleIntervalMs: number): NativeTelemetryClientHealth {
+  return {
+    status: state.status,
+    hello: state.hello,
+    lastSampleAt: state.lastSampleAt,
+    lastError: state.lastError,
+    restartCount: state.restartCount,
+    sampleIntervalMs,
+  };
+}
+
+function isThermallyConstrained(snapshot: HostPowerSnapshot): boolean {
+  return snapshot.thermalState === "serious" || snapshot.thermalState === "critical";
+}
+
+export function resolveNativeSampleIntervalMs(
+  snapshot: HostPowerSnapshot,
+  liveSubscriberCount: number,
+): number {
+  if (snapshot.stale || snapshot.source === "unknown") {
+    return liveSubscriberCount > 0 ? SAMPLE_INTERVAL_MS : UNKNOWN_BACKGROUND_SAMPLE_INTERVAL_MS;
+  }
+  if (snapshot.suspended) return 0;
+  if (
+    snapshot.locked === "true" ||
+    snapshot.lowPowerMode === "true" ||
+    isThermallyConstrained(snapshot)
+  ) {
+    return CONSTRAINED_SAMPLE_INTERVAL_MS;
+  }
+  if (snapshot.onBattery === "true") return BATTERY_SAMPLE_INTERVAL_MS;
+  return SAMPLE_INTERVAL_MS;
+}
+
+const decodeMonitorEvent: (
+  value: unknown,
+) => Effect.Effect<ResourceMonitorEvent, Schema.SchemaError> = Schema.decodeUnknownEffect(
+  ResourceMonitorEventSchema,
+);
+const encodeMonitorCommand = Schema.encodeEffect(
+  Schema.fromJsonString(ResourceMonitorCommandSchema),
+);
+const isProtocolMismatch = Schema.is(NativeTelemetryProtocolMismatch);
+const isDecodeFailed = Schema.is(NativeTelemetryDecodeFailed);
+const isCommandFailed = Schema.is(NativeTelemetryCommandFailed);
+
+function eventVersion(value: unknown): number | undefined {
+  if (typeof value !== "object" || value === null) return undefined;
+  const version = Reflect.get(value, "version");
+  return typeof version === "number" ? version : undefined;
+}
+
+function restartDelay(attempt: number): Duration.Duration {
+  return Duration.min(Duration.times(INITIAL_RESTART_DELAY, 2 ** attempt), MAX_RESTART_DELAY);
+}
+
+function errorMessage(error: NativeTelemetryClientError): string {
+  return error.message;
+}
+
+export const make = Effect.fn("resourceTelemetry.nativeTelemetryClient.make")(function* () {
+  const binary = yield* ResourceMonitorBinary.ResourceMonitorBinary;
+  const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
+  const crypto = yield* Crypto.Crypto;
+  const config = yield* ServerConfig;
+  const initializedAt = yield* DateTime.now;
+  const state = yield* Ref.make(initialState);
+  const collectionControl = yield* Ref.make<CollectionControl>({
+    hostPower: {
+      source: "unknown",
+      idle: "unknown",
+      idleSeconds: null,
+      locked: "unknown",
+      suspended: false,
+      onBattery: "unknown",
+      lowPowerMode: "unknown",
+      thermalState: "unknown",
+      stale: true,
+      updatedAt: initializedAt,
+    },
+    liveSubscriberCount: 0,
+    sampleIntervalMs: UNKNOWN_BACKGROUND_SAMPLE_INTERVAL_MS,
+  });
+  const externalProcesses = yield* Ref.make<ReadonlyArray<ResourceMonitorExternalProcess>>([]);
+  const pendingSamples = yield* Ref.make(
+    new Map<string, Deferred.Deferred<ResourceMonitorSnapshotEvent, NativeTelemetryClientError>>(),
+  );
+  const pendingHistories = yield* Ref.make(new Map<string, PendingHistoryRequest>());
+  const snapshots = yield* PubSub.sliding<ResourceMonitorSnapshotEvent>(8);
+  const healthChanges = yield* PubSub.sliding<NativeTelemetryClientHealth>(4);
+  const retryQueue = yield* Queue.sliding<void>(1);
+  const commandMutex = yield* Semaphore.make(1);
+  const currentHealth = Effect.all([Ref.get(state), Ref.get(collectionControl)]).pipe(
+    Effect.map(([current, control]) => toHealth(current, control.sampleIntervalMs)),
+  );
+  const publishHealth = currentHealth.pipe(
+    Effect.flatMap((health) => PubSub.publish(healthChanges, health)),
+    Effect.asVoid,
+  );
+
+  const failPending = (error: NativeTelemetryClientError) =>
+    Effect.gen(function* () {
+      const samples = yield* Ref.getAndSet(pendingSamples, new Map());
+      const histories = yield* Ref.getAndSet(pendingHistories, new Map());
+      yield* Effect.forEach(samples.values(), (deferred) => Deferred.fail(deferred, error), {
+        discard: true,
+      });
+      yield* Effect.forEach(
+        histories.values(),
+        (request) => Deferred.fail(request.deferred, error),
+        { discard: true },
+      );
+    });
+
+  const writeCommand = (
+    handle: ChildProcessSpawner.ChildProcessHandle,
+    command: ResourceMonitorCommand,
+  ): Effect.Effect<void, NativeTelemetryClientError> =>
+    commandMutex.withPermits(1)(
+      encodeMonitorCommand(command).pipe(
+        Effect.map((encoded) => `${encoded}\n`),
+        Effect.mapError(
+          (cause) =>
+            new NativeTelemetryCommandFailed({
+              operation: command.type,
+              cause,
+            }),
+        ),
+        Effect.flatMap((encoded) =>
+          Stream.run(Stream.encodeText(Stream.make(encoded)), handle.stdin),
+        ),
+        Effect.mapError(
+          (cause) =>
+            new NativeTelemetryCommandFailed({
+              operation: command.type,
+              cause,
+            }),
+        ),
+      ),
+    );
+
+  const processEvent = (
+    event: ResourceMonitorEvent,
+    helloDeferred: Deferred.Deferred<ResourceMonitorHelloEvent>,
+  ): Effect.Effect<void, NativeTelemetryClientError> => {
+    switch (event.type) {
+      case "hello":
+        return Ref.update(state, (current) => ({
+          ...current,
+          status: "starting" as const,
+          hello: Option.some(event),
+          lastError: Option.none(),
+        })).pipe(
+          Effect.andThen(publishHealth),
+          Effect.andThen(Deferred.succeed(helloDeferred, event)),
+          Effect.asVoid,
+        );
+      case "snapshot":
+        return Effect.gen(function* () {
+          const sampledAt = DateTime.makeUnsafe(event.sampledAtUnixMs);
+          yield* Ref.update(state, (current) => ({
+            ...current,
+            status: "healthy" as const,
+            lastSampleAt: Option.some(sampledAt),
+            lastError: Option.none(),
+          }));
+          yield* PubSub.publish(snapshots, event);
+          if (event.requestId) {
+            const deferred = yield* Ref.modify(pendingSamples, (pending) => {
+              const next = new Map(pending);
+              const current = next.get(event.requestId!);
+              next.delete(event.requestId!);
+              return [Option.fromUndefinedOr(current), next];
+            });
+            if (Option.isSome(deferred)) {
+              yield* Deferred.succeed(deferred.value, event);
+            }
+          }
+        });
+      case "historyChunk":
+        return Effect.gen(function* () {
+          const latestSnapshot = event.snapshots.at(-1);
+          if (latestSnapshot) {
+            yield* Ref.update(state, (current) => ({
+              ...current,
+              status: "healthy" as const,
+              lastSampleAt: Option.some(DateTime.makeUnsafe(latestSnapshot.sampledAtUnixMs)),
+              lastError: Option.none(),
+            }));
+          }
+          const completed = yield* Ref.modify(pendingHistories, (pending) => {
+            const request = pending.get(event.requestId);
+            if (!request) return [Option.none(), pending] as const;
+            const snapshots = [...request.snapshots, ...event.snapshots];
+            const next = new Map(pending);
+            if (event.done) {
+              next.delete(event.requestId);
+              return [Option.some({ deferred: request.deferred, snapshots }), next] as const;
+            }
+            next.set(event.requestId, { deferred: request.deferred, snapshots });
+            return [Option.none(), next] as const;
+          });
+          if (Option.isSome(completed)) {
+            yield* Deferred.succeed(completed.value.deferred, completed.value.snapshots);
+          }
+        });
+      case "error":
+        return Ref.update(state, (current) => ({
+          ...current,
+          status: "degraded" as const,
+          lastError: Option.some(event.message),
+        })).pipe(
+          Effect.andThen(publishHealth),
+          Effect.andThen(
+            event.recoverable
+              ? Effect.void
+              : Effect.fail(
+                  new NativeTelemetryCommandFailed({
+                    operation: event.code,
+                    cause: event.message,
+                  }),
+                ),
+          ),
+        );
+    }
+  };
+
+  const runAttempt: Effect.Effect<void, NativeTelemetryClientError> = Effect.scoped(
+    Effect.gen(function* () {
+      const executablePath = yield* binary.resolve.pipe(
+        Effect.mapError(
+          (error) =>
+            new NativeTelemetryUnavailable({
+              reason: error.message,
+            }),
+        ),
+      );
+      const command = ChildProcess.make(executablePath, [], {
+        cwd: config.cwd,
+        stdin: {
+          stream: "pipe",
+          endOnDone: false,
+        },
+        stdout: "pipe",
+        stderr: "pipe",
+        killSignal: "SIGTERM",
+        forceKillAfter: Duration.seconds(2),
+      });
+      const handle = yield* Effect.acquireRelease(
+        spawner
+          .spawn(command)
+          .pipe(
+            Effect.mapError(
+              (cause) => new NativeTelemetrySpawnFailed({ path: executablePath, cause }),
+            ),
+          ),
+        (child) => child.kill().pipe(Effect.ignore),
+      );
+      yield* Ref.update(state, (current) => ({
+        ...current,
+        status: "starting" as const,
+        handle: Option.some(handle),
+        hello: Option.none(),
+      }));
+      yield* publishHealth;
+
+      const helloDeferred = yield* Deferred.make<ResourceMonitorHelloEvent>();
+      const eventFiber = yield* handle.stdout.pipe(
+        Stream.pipeThroughChannel(Ndjson.decode({ ignoreEmptyLines: true })),
+        Stream.mapEffect(
+          (
+            value,
+          ): Effect.Effect<
+            ResourceMonitorEvent,
+            NativeTelemetryProtocolMismatch | NativeTelemetryDecodeFailed
+          > => {
+            const version = eventVersion(value);
+            if (version !== undefined && version !== RESOURCE_MONITOR_PROTOCOL_VERSION) {
+              return Effect.fail(
+                new NativeTelemetryProtocolMismatch({
+                  expectedVersion: RESOURCE_MONITOR_PROTOCOL_VERSION,
+                  receivedVersion: version,
+                }),
+              );
+            }
+            return decodeMonitorEvent(value).pipe(
+              Effect.mapError((cause) => new NativeTelemetryDecodeFailed({ cause })),
+            );
+          },
+        ),
+        Stream.runForEach((event) => processEvent(event, helloDeferred)),
+        Effect.mapError((cause) =>
+          isProtocolMismatch(cause) || isDecodeFailed(cause) || isCommandFailed(cause)
+            ? cause
+            : new NativeTelemetryDecodeFailed({ cause }),
+        ),
+        Effect.forkScoped,
+      );
+      yield* handle.stderr.pipe(Stream.runDrain, Effect.ignore, Effect.forkScoped);
+
+      const hello = yield* Deferred.await(helloDeferred).pipe(
+        Effect.timeoutOption(HANDSHAKE_TIMEOUT),
+        Effect.flatMap(
+          Option.match({
+            onNone: () =>
+              Effect.fail(
+                new NativeTelemetryHandshakeTimedOut({
+                  timeoutMs: Duration.toMillis(HANDSHAKE_TIMEOUT),
+                }),
+              ),
+            onSome: Effect.succeed,
+          }),
+        ),
+      );
+      const control = yield* Ref.get(collectionControl);
+      yield* writeCommand(handle, {
+        version: RESOURCE_MONITOR_PROTOCOL_VERSION,
+        type: "configure",
+        rootPid: process.pid,
+        sampleIntervalMs: control.sampleIntervalMs,
+        externalProcesses: [...(yield* Ref.get(externalProcesses))],
+      });
+      if (control.liveSubscriberCount > 0) {
+        yield* writeCommand(handle, {
+          version: RESOURCE_MONITOR_PROTOCOL_VERSION,
+          type: "setStreaming",
+          enabled: true,
+        });
+      }
+      yield* Ref.update(state, (current) => ({
+        ...current,
+        status: "healthy" as const,
+        hello: Option.some(hello),
+      }));
+      yield* publishHealth;
+
+      yield* writeCommand(handle, {
+        version: RESOURCE_MONITOR_PROTOCOL_VERSION,
+        type: "setExternalProcesses",
+        processes: [...(yield* Ref.get(externalProcesses))],
+      });
+      const latestControl = yield* Ref.get(collectionControl);
+      if (latestControl.sampleIntervalMs !== control.sampleIntervalMs) {
+        yield* writeCommand(handle, {
+          version: RESOURCE_MONITOR_PROTOCOL_VERSION,
+          type: "setSampleInterval",
+          sampleIntervalMs: latestControl.sampleIntervalMs,
+        });
+      }
+      if (latestControl.liveSubscriberCount > 0 !== control.liveSubscriberCount > 0) {
+        yield* writeCommand(handle, {
+          version: RESOURCE_MONITOR_PROTOCOL_VERSION,
+          type: "setStreaming",
+          enabled: latestControl.liveSubscriberCount > 0,
+        });
+      }
+
+      const exitEffect = handle.exitCode.pipe(
+        Effect.mapError(
+          (cause) =>
+            new NativeTelemetryCommandFailed({
+              operation: "waitForExit",
+              cause,
+            }),
+        ),
+        Effect.flatMap((exitCode) =>
+          Effect.fail(new NativeTelemetryExited({ exitCode: Number(exitCode) })),
+        ),
+      );
+      const decoderEffect = Fiber.join(eventFiber).pipe(
+        Effect.andThen(Effect.fail(new NativeTelemetryExited({ exitCode: -1 }))),
+      );
+      return yield* Effect.raceFirst(exitEffect, decoderEffect);
+    }),
+  ).pipe(
+    Effect.ensuring(
+      Ref.update(state, (current) => ({
+        ...current,
+        handle: Option.none(),
+      })),
+    ),
+  );
+
+  yield* Effect.gen(function* () {
+    let failures: ReadonlyArray<number> = [];
+    let restartAttempt = 0;
+
+    while (true) {
+      const result = yield* Effect.result(runAttempt);
+      if (Result.isSuccess(result)) {
+        return;
+      }
+
+      const error = result.failure;
+      const now = DateTime.toEpochMillis(yield* DateTime.now);
+      failures = [...failures.filter((failedAt) => now - failedAt <= FAILURE_WINDOW_MS), now];
+      const exhausted = failures.length >= MAX_FAILURES_PER_WINDOW;
+      yield* Ref.update(state, (current) => ({
+        ...current,
+        status: exhausted ? ("unavailable" as const) : ("degraded" as const),
+        hello: Option.none(),
+        lastError: Option.some(errorMessage(error)),
+        restartCount: current.restartCount + 1,
+      }));
+      yield* publishHealth;
+      yield* failPending(error);
+
+      if (exhausted) {
+        yield* Queue.take(retryQueue);
+        failures = [];
+        restartAttempt = 0;
+        yield* Ref.update(state, (current) => ({
+          ...current,
+          status: "starting" as const,
+          hello: Option.none(),
+          lastError: Option.none(),
+        }));
+        yield* publishHealth;
+        continue;
+      }
+
+      const manuallyRetried = yield* Effect.raceFirst(
+        Effect.sleep(restartDelay(restartAttempt)).pipe(Effect.as(false)),
+        Queue.take(retryQueue).pipe(Effect.as(true)),
+      );
+      restartAttempt = manuallyRetried ? 0 : restartAttempt + 1;
+    }
+  }).pipe(
+    Effect.catchCause((cause) =>
+      Cause.hasInterruptsOnly(cause)
+        ? Effect.void
+        : Ref.update(state, (current) => ({
+            ...current,
+            status: "unavailable" as const,
+            hello: Option.none(),
+            lastError: Option.some(Cause.pretty(cause)),
+          })).pipe(
+            Effect.andThen(publishHealth),
+            Effect.andThen(
+              Effect.logWarning("Resource monitor supervisor failed", {
+                cause: Cause.pretty(cause),
+              }),
+            ),
+          ),
+    ),
+    Effect.forkScoped,
+  );
+
+  const applyCollectionControl = Effect.fn(
M
macroscopeappBotJul 28, 2026

🟡 Medium resourceTelemetry/NativeTelemetryClient.ts:652

Concurrent calls to setHostPowerState or changeLiveSubscriberCount can overwrite the sidecar with stale values. Each call atomically commits the new control state to collectionControl via Ref.modify, then calls applyCollectionControl to send the corresponding commands — but these two steps are not serialized together. Two overlapping calls can commit in order A then B (so the ref holds B's state), yet have applyCollectionControl acquire commandMutex in order B then A, causing the older call A to send stale interval/streaming values to the sidecar last. The sidecar then runs at the wrong sampling interval or streaming mode until the next control change or restart, even though the in-memory ref reflects the newer state.

The same problem affects setExternalProcesses: Ref.set and the subsequent writeCommand are independent, so concurrent callers can have their commandMutex acquisition order differ from their ref write order, leaving the sidecar configured with an older process list while externalProcesses holds the newest.

Consider serializing the entire read-modify-send sequence (both the ref update and the command writes) under a shared mutex so that commits and their corresponding sidecar commands always apply in the same order.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
text
1In file @apps/server/src/resourceTelemetry/NativeTelemetryClient.ts around line 652:
2
3Concurrent calls to `setHostPowerState` or `changeLiveSubscriberCount` can overwrite the sidecar with stale values. Each call atomically commits the new control state to `collectionControl` via `Ref.modify`, then calls `applyCollectionControl` to send the corresponding commands — but these two steps are not serialized together. Two overlapping calls can commit in order A then B (so the ref holds B's state), yet have `applyCollectionControl` acquire `commandMutex` in order B then A, causing the older call A to send stale interval/streaming values to the sidecar last. The sidecar then runs at the wrong sampling interval or streaming mode until the next control change or restart, even though the in-memory ref reflects the newer state.
4
5The same problem affects `setExternalProcesses`: `Ref.set` and the subsequent `writeCommand` are independent, so concurrent callers can have their `commandMutex` acquisition order differ from their ref write order, leaving the sidecar configured with an older process list while `externalProcesses` holds the newest.
6
7Consider serializing the entire read-modify-send sequence (both the ref update and the command writes) under a shared mutex so that commits and their corresponding sidecar commands always apply in the same order.
J
Julius MarmingeJul 28, 2026

Fixed in 24e6ccd404. A dedicated control semaphore serializes the state transition and sidecar command application as one unit, preventing older concurrent control calls from being written last.

M
macroscopeappBotJul 28, 2026

The controlMutex correctly fixes the race for setHostPowerState and changeLiveSubscriberCount. However, setExternalProcesses (lines 729–739) still has the same race: Ref.set(externalProcesses, ...) and the subsequent writeCommand are not serialized together, so concurrent calls can still commit in one order but send commands in another. Consider wrapping setExternalProcesses under the same mutex (or a dedicated one) to serialize both the ref update and the command write.

apps/web/src/components/settings/SettingsPanels.tsx · Earlier revision deb3d483 · right 249

Original diff
@@ -132,7 +163,128 @@ const TIMESTAMP_FORMAT_LABELS = {
   "24-hour": "24-hour",
 } as const;
 
+const BACKGROUND_ACTIVITY_PROFILE_LABELS: Record<BackgroundActivityProfile, string> = {
+  balanced: "Balanced",
+  performance: "Performance",
+  "battery-saver": "Battery saver",
+};
+
+type BackgroundActivityProfileOption = BackgroundActivityProfile | "advanced";
+type BackgroundActivityOverridePatch = Partial<{
+  [K in keyof BackgroundActivitySettings["overrides"]]:
+    | BackgroundActivitySettings["overrides"][K]
+    | undefined;
+}>;
+
+const BACKGROUND_ACTIVITY_PROFILE_OPTION_LABELS: Record<BackgroundActivityProfileOption, string> = {
+  ...BACKGROUND_ACTIVITY_PROFILE_LABELS,
+  advanced: "Advanced",
+};
+
+const BACKGROUND_ACTIVITY_PROFILE_DESCRIPTIONS: Record<BackgroundActivityProfile, string> = {
+  balanced:
+    "Pauses background probes when clients are idle, the host is locked, or low power mode is active.",
+  performance: "Allows scoped background probes while any subscribed client remains connected.",
+  "battery-saver": "Also pauses background probes when the host or client is on battery.",
+};
+
+const ADVANCED_BACKGROUND_ACTIVITY_DESCRIPTION =
+  "Uses custom background intervals with the selected shared power policy.";
+
+const PROVIDER_HEALTH_INTERVAL_STEP_SECONDS = 30;
 const DEFAULT_DRIVER_KIND = ProviderDriverKind.make("codex");
+const BACKGROUND_ACTIVITY_BOOLEAN_OVERRIDES: ReadonlyArray<{
+  readonly key:
+    | "pauseWhenHostLocked"
+    | "pauseWhenHostLowPower"
+    | "pauseWhenClientLowPower"
+    | "pauseWhenOnBattery";
+  readonly label: string;
+}> = [
+  { key: "pauseWhenHostLocked", label: "Pause when host is locked" },
+  { key: "pauseWhenHostLowPower", label: "Pause on host low power" },
+  { key: "pauseWhenClientLowPower", label: "Pause on client low power" },
+  { key: "pauseWhenOnBattery", label: "Pause on battery" },
+];
+
+function durationToSeconds(duration: Duration.Duration): number {
+  return Math.round(Duration.toMillis(duration) / 1_000);
+}
+
+function normalizeIntervalSeconds(value: number | null): number {
+  if (value === null || !Number.isFinite(value)) {
+    return 0;
+  }
+  return Math.max(0, Math.round(value));
+}
+
+function resolveBackgroundActivityProfileOption(settings: {
+  readonly backgroundActivity: BackgroundActivitySettings;
+}): BackgroundActivityProfileOption {
+  return settings.backgroundActivity.profile === "custom"
+    ? "advanced"
+    : settings.backgroundActivity.profile;
+}
+
+function resetBackgroundActivitySettings() {
+  return {
+    backgroundActivity: DEFAULT_UNIFIED_SETTINGS.backgroundActivity,
+  };
+}
+
+function backgroundActivityProfileSettings(profile: BackgroundActivityProfile) {
+  return {
+    backgroundActivity: {
+      schemaVersion: 1 as const,
+      profile,
+      overrides: {},
+    },
+  };
+}
+
+function backgroundActivityOverrideSettings(
+  current: BackgroundActivitySettings,
+  overrides: BackgroundActivityOverridePatch,
+) {
+  const nextOverrides: BackgroundActivityOverridePatch = {
M
macroscopeappBotJul 28, 2026

🟡 Medium settings/SettingsPanels.tsx:249

Deleting an override key from nextOverrides does not clear the setting — the server merges the returned patch with deepMerge(current.backgroundActivity, backgroundActivity), so a missing key is retained from the current overrides. As a result, when a caller passes undefined for an override (e.g. resetting an interval), the old custom value stays in place and the reset has no effect. The patch must explicitly replace the entire overrides record or use a deletion representation understood by the merge layer.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
text
1In file @apps/web/src/components/settings/SettingsPanels.tsx around line 249:
2
3Deleting an override key from `nextOverrides` does not clear the setting — the server merges the returned patch with `deepMerge(current.backgroundActivity, backgroundActivity)`, so a missing key is retained from the current overrides. As a result, when a caller passes `undefined` for an override (e.g. resetting an interval), the old custom value stays in place and the reset has no effect. The patch must explicitly replace the entire `overrides` record or use a deletion representation understood by the merge layer.

apps/web/src/components/settings/ResourceTelemetryDiagnostics.tsx · Earlier revision 3f9ae064 · right 962

Original diff
@@ -0,0 +1,1244 @@
+import {
+  ActivityIcon,
+  AlertTriangleIcon,
+  BatteryIcon,
+  ChevronDownIcon,
+  ChevronRightIcon,
+  CpuIcon,
+  DatabaseIcon,
+  GaugeIcon,
+  HardDriveIcon,
+  MemoryStickIcon,
+  RefreshCwIcon,
+  RotateCcwIcon,
+} from "lucide-react";
+import type {
+  BackgroundBooleanState,
+  ResourceAttributionEntry,
+  ResourceTelemetryAggregate,
+  ResourceTelemetryHistoryBucket,
+  ResourceTelemetryIoSemantics,
+  ResourceTelemetryProcess,
+  ResourceTelemetryProcessCategory,
+  ResourceTelemetryProcessSummary,
+  ResourceTelemetrySourceHealth,
+  ResourceTelemetrySourceStatus,
+  ServerProcessSignal,
+} from "@t3tools/contracts";
+import * as DateTime from "effect/DateTime";
+import * as Option from "effect/Option";
+import { useCallback, useMemo, useState, type ReactNode } from "react";
+import {
+  isAtomCommandInterrupted,
+  squashAtomCommandFailure,
+} from "@t3tools/client-runtime/state/runtime";
+
+import {
+  useResourceTelemetry,
+  useResourceTelemetryHistory,
+} from "../../lib/resourceTelemetryState";
+import { cn } from "../../lib/utils";
+import { usePrimaryEnvironment } from "../../state/environments";
+import { serverEnvironment } from "../../state/server";
+import { useAtomCommand } from "../../state/use-atom-command";
+import { formatRelativeTime } from "../../timestampFormat";
+import { Button } from "../ui/button";
+import { ScrollArea } from "../ui/scroll-area";
+import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";
+import { toastManager } from "../ui/toast";
+import { SettingsSection, useRelativeTimeTick } from "./settingsLayout";
+
+const HISTORY_WINDOWS = [
+  { label: "5m", windowMs: 5 * 60_000, bucketMs: 15_000 },
+  { label: "15m", windowMs: 15 * 60_000, bucketMs: 30_000 },
+  { label: "30m", windowMs: 30 * 60_000, bucketMs: 60_000 },
+  { label: "1h", windowMs: 60 * 60_000, bucketMs: 2 * 60_000 },
+] as const;
+
+function formatBytes(value: number): string {
+  if (value < 1_024) return `${Math.round(value)} B`;
+  const units = ["KB", "MB", "GB", "TB"] as const;
+  let next = value;
+  let unitIndex = -1;
+  do {
+    next /= 1_024;
+    unitIndex += 1;
+  } while (next >= 1_024 && unitIndex < units.length - 1);
+  return `${next.toFixed(next >= 100 ? 0 : next >= 10 ? 1 : 2)} ${units[unitIndex]}`;
+}
+
+function formatRate(value: number): string {
+  return `${formatBytes(value)}/s`;
+}
+
+function formatCpuTime(valueMs: number): string {
+  const seconds = valueMs / 1_000;
+  if (seconds < 60) return `${seconds.toFixed(seconds >= 10 ? 1 : 2)}s`;
+  const minutes = seconds / 60;
+  if (minutes < 60) return `${minutes.toFixed(minutes >= 10 ? 1 : 2)}m`;
+  return `${(minutes / 60).toFixed(2)}h`;
+}
+
+function formatDurationMicros(value: number): string {
+  if (value < 1_000) return `${Math.round(value)} µs`;
+  if (value < 1_000_000) return `${(value / 1_000).toFixed(2)} ms`;
+  return `${(value / 1_000_000).toFixed(2)} s`;
+}
+
+function processIdentityKey(process: ResourceTelemetryProcess): string {
+  return `${process.identity.pid}:${process.identity.startTimeMs}`;
+}
+
+function processSummaryIdentityKey(process: ResourceTelemetryProcessSummary): string {
+  return `${process.identity.pid}:${process.identity.startTimeMs}`;
+}
+
+function formatProcessName(process: Pick<ResourceTelemetryProcess, "command" | "name">): string {
+  if (process.name.trim()) return process.name;
+  const firstToken = process.command.trim().split(/\s+/)[0] ?? process.command;
+  const normalized = firstToken.replace(/^['"]|['"]$/g, "");
+  return normalized.split(/[\\/]/).findLast((segment) => segment.length > 0) ?? normalized;
+}
+
+function categoryLabel(category: ResourceTelemetryProcessCategory): string {
+  switch (category) {
+    case "server":
+      return "Server";
+    case "server-child":
+      return "Backend child";
+    case "provider-root":
+      return "Provider";
+    case "terminal-root":
+      return "Terminal";
+    case "electron-main":
+      return "Electron main";
+    case "electron-renderer":
+      return "Renderer";
+    case "electron-gpu":
+      return "GPU";
+    case "electron-utility":
+      return "Electron utility";
+    case "resource-monitor":
+      return "Monitor";
+    case "unknown-t3":
+      return "T3 process";
+  }
+}
+
+function categoryDotClass(category: ResourceTelemetryProcessCategory): string {
+  if (category === "resource-monitor") return "bg-amber-500";
+  if (category.startsWith("electron-")) return "bg-sky-500";
+  if (category === "server") return "bg-violet-500";
+  return "bg-emerald-500";
+}
+
+function ioSemanticsLabel(semantics: ResourceTelemetryIoSemantics): string {
+  switch (semantics) {
+    case "storage":
+      return "Storage bytes";
+    case "logical":
+      return "Logical bytes";
+    case "all-io":
+      return "All I/O bytes";
+    case "unavailable":
+      return "Unavailable";
+  }
+}
+
+function booleanStateLabel(
+  value: BackgroundBooleanState,
+  labels: { readonly true: string; readonly false: string },
+): string {
+  if (value === "true") return labels.true;
+  if (value === "false") return labels.false;
+  return "Unknown";
+}
+
+function sourceStatusTone(status: ResourceTelemetrySourceStatus): "default" | "warning" | "danger" {
+  if (status === "healthy") return "default";
+  if (status === "starting" || status === "degraded") return "warning";
+  return "danger";
+}
+
+function SourceStatusBadge({
+  label,
+  status,
+  presentation,
+}: {
+  label: string;
+  status: ResourceTelemetrySourceStatus;
+  presentation?:
+    | {
+        readonly label: string;
+        readonly tone: "neutral";
+      }
+    | undefined;
+}) {
+  const tone = presentation?.tone ?? sourceStatusTone(status);
+  return (
+    <span
+      className={cn(
+        "inline-flex items-center gap-1.5 rounded-full border px-2.5 py-1 text-[10px] font-semibold uppercase tracking-[0.08em]",
+        tone === "neutral" && "border-border/70 bg-muted/45 text-muted-foreground",
+        tone === "default" &&
+          "border-emerald-500/25 bg-emerald-500/10 text-emerald-700 dark:text-emerald-300",
+        tone === "warning" &&
+          "border-amber-500/30 bg-amber-500/10 text-amber-700 dark:text-amber-300",
+        tone === "danger" && "border-destructive/30 bg-destructive/10 text-destructive",
+      )}
+    >
+      <span
+        className={cn(
+          "size-1.5 rounded-full",
+          tone === "neutral" && "bg-muted-foreground/55",
+          tone === "default" && "bg-emerald-500",
+          tone === "warning" && "bg-amber-500",
+          tone === "danger" && "bg-destructive",
+        )}
+      />
+      {label} {presentation?.label ?? status}
+    </span>
+  );
+}
+
+function LastSampleLabel({ sampledAt }: { sampledAt: DateTime.Utc | null }) {
+  useRelativeTimeTick();
+  if (!sampledAt) {
+    return <span className="text-[11px] text-muted-foreground/55">Waiting for sample</span>;
+  }
+  const relative = formatRelativeTime(DateTime.formatIso(sampledAt));
+  if (!relative) {
+    return <span className="text-[11px] text-muted-foreground/55">Waiting for sample</span>;
+  }
+  return (
+    <span className="text-[11px] text-muted-foreground/60">
+      Updated <span className="font-mono tabular-nums">{relative.value}</span>
+      {relative.suffix ? ` ${relative.suffix}` : ""}
+    </span>
+  );
+}
+
+function IconStat({
+  icon,
+  label,
+  value,
+  detail,
+  tone = "default",
+}: {
+  icon: ReactNode;
+  label: string;
+  value: string;
+  detail?: string | undefined;
+  tone?: "default" | "warning" | "danger";
+}) {
+  return (
+    <div className="group min-w-0 px-4 py-4 sm:px-5">
+      <div className="flex items-center gap-2 text-[10px] font-semibold uppercase tracking-[0.11em] text-muted-foreground/70">
+        <span className="text-muted-foreground/55 transition-colors group-hover:text-foreground/65">
+          {icon}
+        </span>
+        <span className="truncate">{label}</span>
+      </div>
+      <div
+        className={cn(
+          "mt-2.5 truncate font-mono text-2xl font-semibold tracking-[-0.05em] tabular-nums text-foreground",
+          tone === "warning" && "text-amber-600 dark:text-amber-300",
+          tone === "danger" && "text-destructive",
+        )}
+      >
+        {value}
+      </div>
+      {detail ? (
+        <div className="mt-1.5 truncate text-[10px] text-muted-foreground/60">{detail}</div>
+      ) : null}
+    </div>
+  );
+}
+
+function AggregateCard({
+  label,
+  accentClass,
+  aggregate,
+}: {
+  label: string;
+  accentClass: string;
+  aggregate: ResourceTelemetryAggregate;
+}) {
+  return (
+    <div className="relative overflow-hidden border-t border-border/60 px-4 py-4 first:border-t-0 md:border-t-0 md:border-l md:first:border-l-0 sm:px-5">
+      <span className={cn("absolute inset-x-5 top-0 h-0.5 rounded-full opacity-75", accentClass)} />
+      <div className="flex items-center justify-between gap-3">
+        <div className="text-[10px] font-semibold uppercase tracking-[0.11em] text-muted-foreground/75">
+          {label}
+        </div>
+        <div className="rounded-md bg-muted/55 px-1.5 py-0.5 font-mono text-[9px] tabular-nums text-muted-foreground/70">
+          {aggregate.processCount} {aggregate.processCount === 1 ? "process" : "processes"}
+        </div>
+      </div>
+      <div className="mt-3.5 grid grid-cols-2 gap-x-4 gap-y-2.5">
+        <MetricPair label="CPU" value={`${aggregate.currentCpuPercent.toFixed(1)}%`} />
+        <MetricPair label="Memory" value={formatBytes(aggregate.currentRssBytes)} />
+        <MetricPair label="Read" value={formatRate(aggregate.ioReadBytesPerSecond)} />
+        <MetricPair label="Write" value={formatRate(aggregate.ioWriteBytesPerSecond)} />
+      </div>
+    </div>
+  );
+}
+
+function MetricPair({ label, value }: { label: string; value: string }) {
+  return (
+    <div className="min-w-0">
+      <div className="text-[9px] font-semibold uppercase tracking-[0.08em] text-muted-foreground/45">
+        {label}
+      </div>
+      <div className="truncate font-mono text-xs font-medium tabular-nums text-foreground/90">
+        {value}
+      </div>
+    </div>
+  );
+}
+
+function HealthSource({ label, health }: { label: string; health: ResourceTelemetrySourceHealth }) {
+  const expectedInBrowser =
+    health.status === "unavailable" &&
+    Option.exists(health.lastError, (error) => error.includes("'web' mode"));
+  return (
+    <div className="flex items-start justify-between gap-4 border-t border-border/50 py-3 first:border-t-0">
+      <div className="min-w-0">
+        <div className="text-[13px] font-medium text-foreground">{label}</div>
+        <div className="mt-1 text-[11px] leading-relaxed text-muted-foreground/65">
+          {expectedInBrowser
+            ? "Available when this page runs inside the desktop app."
+            : Option.match(health.lastError, {
+                onNone: () => "No reported errors",
+                onSome: (error) => error,
+              })}
+        </div>
+      </div>
+      <SourceStatusBadge
+        label=""
+        status={health.status}
+        presentation={
+          expectedInBrowser
+            ? {
+                label: "Desktop only",
+                tone: "neutral",
+              }
+            : undefined
+        }
+      />
+    </div>
+  );
+}
+
+function DetailRow({
+  label,
+  value,
+  valueClassName,
+}: {
+  label: string;
+  value: ReactNode;
+  valueClassName?: string | undefined;
+}) {
+  return (
+    <div className="flex items-center justify-between gap-4 border-t border-border/50 py-2.5 first:border-t-0">
+      <span className="text-[11px] text-muted-foreground/75">{label}</span>
+      <span
+        className={cn(
+          "min-w-0 truncate text-right font-mono text-[11px] tabular-nums text-foreground/85",
+          valueClassName,
+        )}
+      >
+        {value}
+      </span>
+    </div>
+  );
+}
+
+function HistoryWindowSelector({
+  selectedWindowMs,
+  onSelect,
+}: {
+  selectedWindowMs: number;
+  onSelect: (windowMs: number) => void;
+}) {
+  return (
+    <div className="flex items-center rounded-md border border-border/60 p-0.5">
+      {HISTORY_WINDOWS.map((option) => (
+        <button
+          key={option.windowMs}
+          type="button"
+          className={cn(
+            "h-6 rounded-sm px-2 text-[11px] font-medium text-muted-foreground hover:text-foreground",
+            selectedWindowMs === option.windowMs && "bg-muted text-foreground",
+          )}
+          onClick={() => onSelect(option.windowMs)}
+        >
+          {option.label}
+        </button>
+      ))}
+    </div>
+  );
+}
+
+function ResourceHistoryChart({
+  buckets,
+}: {
+  buckets: ReadonlyArray<ResourceTelemetryHistoryBucket>;
+}) {
+  const maxCpu = Math.max(1, ...buckets.map((bucket) => bucket.maxCpuPercent));
+  const maxIo = Math.max(1, ...buckets.map((bucket) => bucket.ioReadBytes + bucket.ioWriteBytes));
+
+  return (
+    <div className="border-t border-border/60 px-4 py-4 sm:px-5">
+      <div className="mb-3 flex flex-wrap items-center gap-x-4 gap-y-1 text-[10px] text-muted-foreground/65">
+        <span className="inline-flex items-center gap-1.5">
+          <span className="h-1.5 w-3 rounded-full bg-foreground/70" /> CPU average
+        </span>
+        <span className="inline-flex items-center gap-1.5">
+          <span className="h-1.5 w-3 rounded-full bg-sky-500/70" /> I/O reads
+        </span>
+        <span className="inline-flex items-center gap-1.5">
+          <span className="h-1.5 w-3 rounded-full bg-amber-500/80" /> I/O writes
+        </span>
+      </div>
+      <div className="flex h-32 items-end gap-1 overflow-hidden rounded-lg border border-border/40 bg-muted/8 px-2 pt-3 pb-2">
+        {buckets.map((bucket) => {
+          const cpuHeight = Math.max(2, (bucket.avgCpuPercent / maxCpu) * 100);
+          const readHeight = Math.max(1, (bucket.ioReadBytes / maxIo) * 100);
+          const writeHeight = Math.max(1, (bucket.ioWriteBytes / maxIo) * 100);
+          return (
+            <Tooltip key={DateTime.formatIso(bucket.startedAt)}>
+              <TooltipTrigger
+                render={
+                  <div className="grid h-full min-w-1 flex-1 grid-cols-3 items-end gap-px">
+                    <span
+                      className="block min-h-px rounded-t-sm bg-foreground/65"
+                      style={{ height: `${cpuHeight}%` }}
+                    />
+                    <span
+                      className="block min-h-px rounded-t-sm bg-sky-500/70"
+                      style={{ height: `${readHeight}%` }}
+                    />
+                    <span
+                      className="block min-h-px rounded-t-sm bg-amber-500/80"
+                      style={{ height: `${writeHeight}%` }}
+                    />
+                  </div>
+                }
+              />
+              <TooltipPopup side="top" className="space-y-0.5 text-left">
+                <div>CPU avg {bucket.avgCpuPercent.toFixed(1)}%</div>
+                <div>CPU peak {bucket.maxCpuPercent.toFixed(1)}%</div>
+                <div>Read {formatBytes(bucket.ioReadBytes)}</div>
+                <div>Write {formatBytes(bucket.ioWriteBytes)}</div>
+              </TooltipPopup>
+            </Tooltip>
+          );
+        })}
+      </div>
+    </div>
+  );
+}
+
+function ProcessTreeName({
+  process,
+  collapsed,
+  onToggle,
+}: {
+  process: ResourceTelemetryProcess;
+  collapsed: boolean;
+  onToggle: (process: ResourceTelemetryProcess) => void;
+}) {
+  const name = formatProcessName(process);
+  const hasChildren = process.childPids.length > 0;
+  const ChevronIcon = collapsed ? ChevronRightIcon : ChevronDownIcon;
+  return (
+    <div
+      className="grid min-w-0 grid-cols-[1.25rem_0.375rem_minmax(0,1fr)] items-center gap-2"
+      style={{ paddingLeft: `${Math.min(process.depth, 7) * 10}px` }}
+    >
+      {hasChildren ? (
+        <button
+          type="button"
+          className="inline-flex size-5 items-center justify-center rounded-sm text-muted-foreground hover:bg-accent hover:text-foreground"
+          onClick={() => onToggle(process)}
+          aria-label={collapsed ? `Expand ${name}` : `Collapse ${name}`}
+        >
+          <ChevronIcon className="size-3.5" />
+        </button>
+      ) : (
+        <span className="size-5" aria-hidden />
+      )}
+      <span className={cn("size-1.5 rounded-full", categoryDotClass(process.category))} />
+      <Tooltip>
+        <TooltipTrigger
+          render={<span className="min-w-0 truncate font-medium text-foreground">{name}</span>}
+        />
+        <TooltipPopup
+          side="top"
+          className="max-w-[min(520px,calc(100vw-2rem))] whitespace-normal break-words text-left font-mono text-[11px]"
+        >
+          {process.command || process.name}
+        </TooltipPopup>
+      </Tooltip>
+    </div>
+  );
+}
+
+function canSignalProcess(process: ResourceTelemetryProcess): boolean {
+  return (
+    process.category === "server-child" ||
+    process.category === "provider-root" ||
+    process.category === "terminal-root"
+  );
+}
+
+function ProcessActions({
+  process,
+  signalingKey,
+  onSignal,
+}: {
+  process: ResourceTelemetryProcess;
+  signalingKey: string | null;
+  onSignal: (process: ResourceTelemetryProcess, signal: ServerProcessSignal) => void;
+}) {
+  if (!canSignalProcess(process)) {
+    return <span className="text-[10px] text-muted-foreground/35">—</span>;
+  }
+  const isSignaling = signalingKey === processIdentityKey(process);
+  return (
+    <div className="flex items-center justify-end gap-1.5">
+      <button
+        type="button"
+        disabled={isSignaling}
+        className="text-[10px] font-semibold text-muted-foreground hover:text-foreground disabled:opacity-50"
+        onClick={() => onSignal(process, "SIGINT")}
+      >
+        INT
+      </button>
+      <button
+        type="button"
+        disabled={isSignaling}
+        className="text-[10px] font-semibold text-destructive hover:underline disabled:opacity-50"
+        onClick={() => onSignal(process, "SIGKILL")}
+      >
+        KILL
+      </button>
+    </div>
+  );
+}
+
+function ProcessTable({
+  processes,
+  signalingKey,
+  onSignal,
+}: {
+  processes: ReadonlyArray<ResourceTelemetryProcess>;
+  signalingKey: string | null;
+  onSignal: (process: ResourceTelemetryProcess, signal: ServerProcessSignal) => void;
+}) {
+  const [collapsed, setCollapsed] = useState<ReadonlySet<string>>(() => new Set());
+  const visible = useMemo(() => {
+    const result: ResourceTelemetryProcess[] = [];
+    let hiddenDepth: number | null = null;
+    for (const process of processes) {
+      if (hiddenDepth !== null) {
+        if (process.depth > hiddenDepth) continue;
+        hiddenDepth = null;
+      }
+      result.push(process);
+      if (collapsed.has(processIdentityKey(process))) {
+        hiddenDepth = process.depth;
+      }
+    }
+    return result;
+  }, [collapsed, processes]);
+  const toggle = useCallback((process: ResourceTelemetryProcess) => {
+    const identityKey = processIdentityKey(process);
+    setCollapsed((current) => {
+      const next = new Set(current);
+      if (next.has(identityKey)) {
+        next.delete(identityKey);
+      } else {
+        next.add(identityKey);
+      }
+      return next;
+    });
+  }, []);
+
+  return (
+    <ScrollArea
+      chainVerticalScroll
+      scrollFade
+      hideScrollbars
+      className="max-h-[min(68vh,48rem)] w-full max-w-full border-t border-border/60"
+    >
+      <table className="w-full min-w-[1320px] table-fixed text-left text-xs">
+        <colgroup>
+          <col className="w-[20%]" />
+          <col className="w-[10%]" />
+          <col className="w-[7%]" />
+          <col className="w-[8%]" />
+          <col className="w-[9%]" />
+          <col className="w-[9%]" />
+          <col className="w-[9%]" />
+          <col className="w-[10%]" />
+          <col className="w-[8%]" />
+          <col className="w-[6%]" />
+          <col className="w-[4%]" />
+        </colgroup>
+        <thead className="sticky top-0 z-10 border-b border-border/60 bg-card text-[10px] uppercase tracking-[0.08em] text-muted-foreground/65">
+          <tr>
+            <th className="px-4 py-2 font-semibold sm:pl-5">Process</th>
+            <th className="px-3 py-2 font-semibold">Category</th>
+            <th className="px-3 py-2 text-right font-semibold">CPU</th>
+            <th className="px-3 py-2 text-right font-semibold">CPU Time</th>
+            <th className="px-3 py-2 text-right font-semibold">Memory</th>
+            <th className="px-3 py-2 text-right font-semibold">Read/s</th>
+            <th className="px-3 py-2 text-right font-semibold">Write/s</th>
+            <th className="px-3 py-2 text-right font-semibold">Read Total</th>
+            <th className="px-3 py-2 text-right font-semibold">Write Total</th>
+            <th className="px-3 py-2 text-right font-semibold">PID</th>
+            <th className="px-2 py-2 text-right font-semibold sm:pr-4">Kill</th>
+          </tr>
+        </thead>
+        <tbody className="divide-y divide-border/50">
+          {visible.length === 0 ? (
+            <tr>
+              <td colSpan={11} className="px-4 py-5 text-xs text-muted-foreground sm:px-5">
+                Waiting for the native process monitor.
+              </td>
+            </tr>
+          ) : null}
+          {visible.map((process) => (
+            <tr key={processIdentityKey(process)} className="hover:bg-muted/20">
+              <td className="px-4 py-2 sm:pl-5">
+                <ProcessTreeName
+                  process={process}
+                  collapsed={collapsed.has(processIdentityKey(process))}
+                  onToggle={toggle}
+                />
+              </td>
+              <td className="truncate px-3 py-2 text-[11px] text-muted-foreground">
+                {categoryLabel(process.category)}
+              </td>
+              <td className="px-3 py-2 text-right font-mono tabular-nums">
+                {process.cpuPercent.toFixed(1)}%
+              </td>
+              <td className="px-3 py-2 text-right font-mono tabular-nums">
+                {formatCpuTime(process.cpuTimeMs)}
+              </td>
+              <td className="px-3 py-2 text-right font-mono tabular-nums">
+                {formatBytes(process.residentBytes)}
+              </td>
+              <td className="px-3 py-2 text-right font-mono tabular-nums text-sky-700 dark:text-sky-300">
+                {formatRate(process.ioReadBytesPerSecond)}
+              </td>
+              <td className="px-3 py-2 text-right font-mono tabular-nums text-amber-700 dark:text-amber-300">
+                {formatRate(process.ioWriteBytesPerSecond)}
+              </td>
+              <td className="px-3 py-2 text-right font-mono tabular-nums text-muted-foreground">
+                {formatBytes(process.ioReadBytes)}
+              </td>
+              <td className="px-3 py-2 text-right font-mono tabular-nums text-muted-foreground">
+                <Tooltip>
+                  <TooltipTrigger render={<span>{formatBytes(process.ioWriteBytes)}</span>} />
+                  <TooltipPopup side="top">{ioSemanticsLabel(process.ioSemantics)}</TooltipPopup>
+                </Tooltip>
+              </td>
+              <td className="px-3 py-2 text-right font-mono tabular-nums text-muted-foreground">
+                {process.identity.pid}
+              </td>
+              <td className="px-2 py-2 text-right sm:pr-4">
+                <ProcessActions process={process} signalingKey={signalingKey} onSignal={onSignal} />
+              </td>
+            </tr>
+          ))}
+        </tbody>
+      </table>
+    </ScrollArea>
+  );
+}
+
+function HistoryProcessTable({
+  processes,
+}: {
+  processes: ReadonlyArray<ResourceTelemetryProcessSummary>;
+}) {
+  return (
+    <ScrollArea
+      chainVerticalScroll
+      scrollFade
+      hideScrollbars
+      className="max-h-[28rem] w-full max-w-full border-t border-border/60"
+    >
+      <table className="w-full min-w-[1020px] table-fixed text-left text-xs">
+        <colgroup>
+          <col className="w-[24%]" />
+          <col className="w-[11%]" />
+          <col className="w-[10%]" />
+          <col className="w-[10%]" />
+          <col className="w-[11%]" />
+          <col className="w-[11%]" />
+          <col className="w-[11%]" />
+          <col className="w-[7%]" />
+          <col className="w-[5%]" />
+        </colgroup>
+        <thead className="sticky top-0 z-10 border-b border-border/60 bg-card text-[10px] uppercase tracking-[0.08em] text-muted-foreground/65">
+          <tr>
+            <th className="px-4 py-2 font-semibold sm:pl-5">Process</th>
+            <th className="px-3 py-2 font-semibold">Category</th>
+            <th className="px-3 py-2 text-right font-semibold">CPU Time</th>
+            <th className="px-3 py-2 text-right font-semibold">Peak CPU</th>
+            <th className="px-3 py-2 text-right font-semibold">Peak Mem</th>
+            <th className="px-3 py-2 text-right font-semibold">Read</th>
+            <th className="px-3 py-2 text-right font-semibold">Write</th>
+            <th className="px-3 py-2 text-right font-semibold">Samples</th>
+            <th className="px-3 py-2 text-right font-semibold sm:pr-5">PID</th>
+          </tr>
+        </thead>
+        <tbody className="divide-y divide-border/50">
+          {processes.length === 0 ? (
+            <tr>
+              <td colSpan={9} className="px-4 py-5 text-xs text-muted-foreground sm:px-5">
+                No retained process samples in this window.
+              </td>
+            </tr>
+          ) : null}
+          {processes.map((process) => (
+            <tr key={processSummaryIdentityKey(process)} className="hover:bg-muted/20">
+              <td className="px-4 py-2 sm:pl-5">
+                <Tooltip>
+                  <TooltipTrigger
+                    render={
+                      <span className="block truncate font-medium text-foreground">
+                        {process.name || process.command}
+                      </span>
+                    }
+                  />
+                  <TooltipPopup
+                    side="top"
+                    className="max-w-[min(520px,calc(100vw-2rem))] whitespace-normal break-words text-left font-mono text-[11px]"
+                  >
+                    {process.command || process.name}
+                  </TooltipPopup>
+                </Tooltip>
+              </td>
+              <td className="truncate px-3 py-2 text-[11px] text-muted-foreground">
+                {categoryLabel(process.category)}
+              </td>
+              <td className="px-3 py-2 text-right font-mono tabular-nums">
+                {formatCpuTime(process.cpuTimeMs)}
+              </td>
+              <td className="px-3 py-2 text-right font-mono tabular-nums">
+                {process.maxCpuPercent.toFixed(1)}%
+              </td>
+              <td className="px-3 py-2 text-right font-mono tabular-nums">
+                {formatBytes(process.peakRssBytes)}
+              </td>
+              <td className="px-3 py-2 text-right font-mono tabular-nums text-sky-700 dark:text-sky-300">
+                {formatBytes(process.ioReadBytes)}
+              </td>
+              <td className="px-3 py-2 text-right font-mono tabular-nums text-amber-700 dark:text-amber-300">
+                {formatBytes(process.ioWriteBytes)}
+              </td>
+              <td className="px-3 py-2 text-right font-mono tabular-nums text-muted-foreground">
+                {process.sampleCount}
+              </td>
+              <td className="px-3 py-2 text-right font-mono tabular-nums text-muted-foreground sm:pr-5">
+                {process.identity.pid}
+              </td>
+            </tr>
+          ))}
+        </tbody>
+      </table>
+    </ScrollArea>
+  );
+}
+
+function AttributionTable({ entries }: { entries: ReadonlyArray<ResourceAttributionEntry> }) {
+  return (
+    <div className="overflow-x-auto border-t border-border/60">
+      <table className="w-full min-w-[720px] table-fixed text-left text-xs">
+        <colgroup>
+          <col className="w-[22%]" />
+          <col className="w-[28%]" />
+          <col className="w-[14%]" />
+          <col className="w-[14%]" />
+          <col className="w-[10%]" />
+          <col className="w-[12%]" />
+        </colgroup>
+        <thead className="border-b border-border/60 text-[10px] uppercase tracking-[0.08em] text-muted-foreground/65">
+          <tr>
+            <th className="px-4 py-2 font-semibold sm:pl-5">Component</th>
+            <th className="px-3 py-2 font-semibold">Operation</th>
+            <th className="px-3 py-2 text-right font-semibold">Logical Read</th>
+            <th className="px-3 py-2 text-right font-semibold">Logical Write</th>
+            <th className="px-3 py-2 text-right font-semibold">Count</th>
+            <th className="px-3 py-2 text-right font-semibold sm:pr-5">Time</th>
+          </tr>
+        </thead>
+        <tbody className="divide-y divide-border/50">
+          {entries.length === 0 ? (
+            <tr>
+              <td colSpan={6} className="px-4 py-5 text-xs text-muted-foreground sm:px-5">
+                No instrumented application I/O has been recorded yet.
+              </td>
+            </tr>
+          ) : null}
+          {entries.map((entry) => (
+            <tr key={`${entry.component}:${entry.operation}`} className="hover:bg-muted/20">
+              <td className="truncate px-4 py-2 font-medium text-foreground sm:pl-5">
+                {entry.component}
+              </td>
+              <td className="truncate px-3 py-2 text-muted-foreground">{entry.operation}</td>
+              <td className="px-3 py-2 text-right font-mono tabular-nums text-sky-700 dark:text-sky-300">
+                {formatBytes(entry.logicalReadBytes)}
+              </td>
+              <td className="px-3 py-2 text-right font-mono tabular-nums text-amber-700 dark:text-amber-300">
+                {formatBytes(entry.logicalWriteBytes)}
+              </td>
+              <td className="px-3 py-2 text-right font-mono tabular-nums">{entry.count}</td>
+              <td className="px-3 py-2 text-right font-mono tabular-nums text-muted-foreground sm:pr-5">
+                {(entry.durationMs / 1_000).toFixed(2)}s
+              </td>
+            </tr>
+          ))}
+        </tbody>
+      </table>
+    </div>
+  );
+}
+
+export function ResourceTelemetryDiagnostics() {
+  const [windowMs, setWindowMs] = useState(15 * 60_000);
+  const selectedWindow =
+    HISTORY_WINDOWS.find((option) => option.windowMs === windowMs) ?? HISTORY_WINDOWS[1];
+  const telemetry = useResourceTelemetry();
+  const retryTelemetry = telemetry.retry;
+  const history = useResourceTelemetryHistory({
+    windowMs: selectedWindow.windowMs,
+    bucketMs: selectedWindow.bucketMs,
+  });
+  const primaryEnvironment = usePrimaryEnvironment();
+  const signalServerProcess = useAtomCommand(serverEnvironment.signalProcess, {
+    reportFailure: false,
+  });
+  const [signalingKey, setSignalingKey] = useState<string | null>(null);
+  const [isRetrying, setIsRetrying] = useState(false);
+  const snapshot = telemetry.data;
+  const allT3 = snapshot?.groups.allT3;
+
+  const signalProcess = useCallback(
+    (process: ResourceTelemetryProcess, signal: ServerProcessSignal) => {
+      if (
+        signal === "SIGKILL" &&
+        !window.confirm(
+          `Send SIGKILL to process ${process.identity.pid}? This cannot be handled by the process.`,
+        )
+      ) {
+        return;
+      }
+      const identityKey = processIdentityKey(process);
+      const environmentId = primaryEnvironment?.environmentId;
+      if (environmentId === undefined) {
+        return;
+      }
+      setSignalingKey(identityKey);
+      void signalServerProcess({
+        environmentId,
+        input: {
+          pid: process.identity.pid,
+          startTimeMs: process.identity.startTimeMs,
+          signal,
+        },
+      })
+        .then((result) => {
+          if (result._tag === "Failure") {
+            if (isAtomCommandInterrupted(result)) return;
+            throw squashAtomCommandFailure(result);
+          }
+          if (result.value.signaled) return;
+          toastManager.add({
+            type: "error",
+            title: `Could not send ${signal}`,
+            description: Option.getOrElse(
+              result.value.message,
+              () => `Failed to send ${signal} to process ${process.identity.pid}.`,
+            ),
+          });
+        })
+        .catch((error: unknown) => {
+          toastManager.add({
+            type: "error",
+            title: `Could not send ${signal}`,
+            description: error instanceof Error ? error.message : `Failed to send ${signal}.`,
+          });
+        })
+        .finally(() => {
+          setSignalingKey((current) => (current === identityKey ? null : current));
+        });
+    },
+    [primaryEnvironment?.environmentId, signalServerProcess],
+  );
+
+  const retryCollector = useCallback(() => {
+    setIsRetrying(true);
+    void retryTelemetry()
+      .catch((error: unknown) => {
+        toastManager.add({
+          type: "error",
+          title: "Could not restart resource monitor",
+          description:
+            error instanceof Error ? error.message : "The resource monitor retry failed.",
+        });
+      })
+      .finally(() => {
+        setIsRetrying(false);
+      });
+  }, [retryTelemetry]);
+
+  const speedLimit = snapshot ? Option.getOrNull(snapshot.speedLimitPercent) : null;
+  const collectorNeedsRetry =
+    snapshot?.health.native.status === "degraded" ||
+    snapshot?.health.native.status === "unavailable" ||
+    snapshot?.health.native.status === "stopped";
+  const hasHostPowerSignal =
+    snapshot !== null &&
+    (snapshot.power.onBattery !== "unknown" ||
+      snapshot.power.lowPowerMode !== "unknown" ||
+      snapshot.power.idle !== "unknown" ||
+      snapshot.power.locked !== "unknown" ||
+      snapshot.power.thermalState !== "unknown");
+
+  return (
+    <>
+      <SettingsSection
+        title="Resource monitor"
+        icon={<ActivityIcon className="size-4 text-muted-foreground" />}
+        headerAction={
+          <div className="flex items-center gap-2">
+            {snapshot ? (
+              <SourceStatusBadge label="Native" status={snapshot.health.native.status} />
+            ) : null}
+            <LastSampleLabel sampledAt={snapshot?.readAt ?? null} />
+            <Tooltip>
+              <TooltipTrigger
+                render={
+                  <Button
+                    size="icon-xs"
+                    variant="ghost"
+                    className="size-5 rounded-sm p-0"
+                    disabled={telemetry.isPending}
+                    onClick={telemetry.refresh}
+                    aria-label="Refresh resource telemetry"
+                  >
+                    <RefreshCwIcon
+                      className={cn("size-3", telemetry.isPending && "animate-spin")}
+                    />
+                  </Button>
+                }
+              />
+              <TooltipPopup side="top">Refresh telemetry snapshot</TooltipPopup>
+            </Tooltip>
+          </div>
+        }
+      >
+        <div className="overflow-hidden rounded-2xl border border-border/70 bg-card shadow-[0_1px_1px_rgb(0_0_0/0.03),0_8px_30px_rgb(0_0_0/0.035)]">
+          <div className="flex flex-col gap-3 border-b border-border/60 bg-linear-to-r from-muted/45 via-muted/20 to-transparent px-4 py-4 sm:flex-row sm:items-center sm:justify-between sm:px-5">
+            <div>
+              <div className="text-[10px] font-semibold uppercase tracking-[0.14em] text-muted-foreground/70">
+                T3 system footprint
+              </div>
+              <p className="mt-1 max-w-xl text-xs leading-relaxed text-muted-foreground">
+                Live native counters for the server, providers, terminals, desktop processes, and
+                the monitor itself.
+              </p>
+            </div>
+            <div className="flex items-center gap-2 text-[10px] text-muted-foreground/65">
+              <span className="size-1.5 rounded-full bg-emerald-500" />
+              Sampling every 5 seconds
+            </div>
C
cursorBotJul 28, 2026

Wrong hardcoded sampling interval label

Low Severity

The resource overview hardcodes Sampling every 5 seconds, but live diagnostics keep a subscriber so native sampling is typically 1s on AC (and sampleIntervalMs on the snapshot already carries the real interval). The label misreports collector cadence while users are watching the page.

Reviewed by Cursor Bugbot for commit 3f9ae0646c6f221e911a1ed4ede46a45cb361fc0. Configure here.

apps/desktop/src/telemetry/DesktopTelemetryPublisher.ts · Earlier revision 4a875dbe · right 234

Original diff
@@ -0,0 +1,264 @@
+import {
+  DesktopHostTelemetryMessage,
+  type DesktopHostTelemetrySnapshot,
+  type DesktopTelemetryControlMessage,
+  type HostPowerSnapshot,
+} from "@t3tools/contracts";
+import * as Context from "effect/Context";
+import * as DateTime from "effect/DateTime";
+import * as Duration from "effect/Duration";
+import * as Effect from "effect/Effect";
+import * as Layer from "effect/Layer";
+import * as Option from "effect/Option";
+import * as PubSub from "effect/PubSub";
+import * as Queue from "effect/Queue";
+import * as Ref from "effect/Ref";
+import * as Schema from "effect/Schema";
+import * as Stream from "effect/Stream";
+
+import * as ElectronApp from "../electron/ElectronApp.ts";
+import * as ElectronPowerMonitor from "../electron/ElectronPowerMonitor.ts";
+
+const LIVE_SAMPLE_INTERVAL = Duration.seconds(1);
+const BATTERY_SAMPLE_INTERVAL = Duration.seconds(5);
+const CONSTRAINED_SAMPLE_INTERVAL = Duration.seconds(15);
+const BACKGROUND_HEARTBEAT_INTERVAL = Duration.seconds(30);
+const IDLE_THRESHOLD_SECONDS = 60;
+const encodeMessage = Schema.encodeSync(Schema.fromJsonString(DesktopHostTelemetryMessage));
+const textEncoder = new TextEncoder();
+
+type PowerEvent =
+  | { readonly type: "locked"; readonly value: boolean }
+  | { readonly type: "suspended"; readonly value: boolean }
+  | { readonly type: "onBattery"; readonly value: boolean }
+  | { readonly type: "thermal"; readonly value: HostPowerSnapshot["thermalState"] }
+  | { readonly type: "speedLimit"; readonly value: number };
+
+interface PowerState {
+  readonly locked: HostPowerSnapshot["locked"];
+  readonly suspended: boolean;
+  readonly onBattery: HostPowerSnapshot["onBattery"];
+  readonly thermalState: HostPowerSnapshot["thermalState"];
+  readonly speedLimitPercent: Option.Option<number>;
+}
+
+export interface DesktopTelemetryPublisherShape {
+  readonly latest: Effect.Effect<Option.Option<DesktopHostTelemetrySnapshot>>;
+  readonly changes: Stream.Stream<DesktopHostTelemetrySnapshot>;
+  readonly encoded: Stream.Stream<Uint8Array>;
+  readonly handleControl: (message: DesktopTelemetryControlMessage) => Effect.Effect<void>;
+}
+
+export class DesktopTelemetryPublisher extends Context.Service<
+  DesktopTelemetryPublisher,
+  DesktopTelemetryPublisherShape
+>()("@t3tools/desktop/telemetry/DesktopTelemetryPublisher") {}
+
+function booleanState(value: boolean): HostPowerSnapshot["onBattery"] {
+  return value ? "true" : "false";
+}
+
+function idleState(value: ElectronPowerMonitor.ElectronIdleState): HostPowerSnapshot["idle"] {
+  switch (value) {
+    case "active":
+      return "false";
+    case "idle":
+    case "locked":
+      return "true";
+    case "unknown":
+      return "unknown";
+  }
+}
+
+function updatePowerState(state: PowerState, event: PowerEvent): PowerState {
+  switch (event.type) {
+    case "locked":
+      return { ...state, locked: booleanState(event.value) };
+    case "suspended":
+      return { ...state, suspended: event.value };
+    case "onBattery":
+      return { ...state, onBattery: booleanState(event.value) };
+    case "thermal":
+      return { ...state, thermalState: event.value };
+    case "speedLimit":
+      return { ...state, speedLimitPercent: Option.some(event.value) };
+  }
+}
+
+function sampleInterval(power: PowerState, diagnosticsDemand: boolean): Duration.Duration {
+  if (!diagnosticsDemand) return BACKGROUND_HEARTBEAT_INTERVAL;
+  if (
+    power.suspended ||
+    power.locked === "true" ||
+    power.thermalState === "serious" ||
+    power.thermalState === "critical"
+  ) {
+    return CONSTRAINED_SAMPLE_INTERVAL;
+  }
+  if (power.onBattery === "true") return BATTERY_SAMPLE_INTERVAL;
+  return LIVE_SAMPLE_INTERVAL;
+}
+
+export const make = Effect.fn("desktop.telemetryPublisher.make")(function* () {
+  const electronApp = yield* ElectronApp.ElectronApp;
+  const powerMonitor = yield* ElectronPowerMonitor.ElectronPowerMonitor;
+  yield* electronApp.whenReady;
+
+  const initialPowerState: PowerState = {
+    locked: "unknown",
+    suspended: false,
+    onBattery: booleanState(yield* powerMonitor.isOnBatteryPower),
+    thermalState: yield* powerMonitor.getCurrentThermalState,
+    speedLimitPercent: Option.none(),
+  };
+  const powerState = yield* Ref.make(initialPowerState);
+  const powerEvents = yield* Queue.unbounded<PowerEvent>();
+  const sampleTriggers = yield* Queue.sliding<void>(1);
+  const diagnosticsDemand = yield* Ref.make(false);
+  const latest = yield* Ref.make(Option.none<DesktopHostTelemetrySnapshot>());
+  const changes = yield* PubSub.sliding<DesktopHostTelemetrySnapshot>(8);
+  const sequence = yield* Ref.make(0);
+
+  const offer = (event: PowerEvent): void => {
+    Queue.offerUnsafe(powerEvents, event);
+  };
+  yield* Effect.all(
+    [
+      powerMonitor.onSimpleEvent("lock-screen", () => offer({ type: "locked", value: true })),
+      powerMonitor.onSimpleEvent("unlock-screen", () => offer({ type: "locked", value: false })),
+      powerMonitor.onSimpleEvent("suspend", () => offer({ type: "suspended", value: true })),
+      powerMonitor.onSimpleEvent("resume", () => offer({ type: "suspended", value: false })),
+      powerMonitor.onSimpleEvent("on-battery", () => offer({ type: "onBattery", value: true })),
+      powerMonitor.onSimpleEvent("on-ac", () => offer({ type: "onBattery", value: false })),
+      powerMonitor.onThermalStateChange((value) => offer({ type: "thermal", value })),
+      powerMonitor.onSpeedLimitChange((value) => offer({ type: "speedLimit", value })),
+    ],
+    { concurrency: "unbounded" },
+  );
+  yield* Effect.forever(
+    Queue.take(powerEvents).pipe(
+      Effect.flatMap((event) => Ref.update(powerState, (state) => updatePowerState(state, event))),
+      Effect.andThen(Queue.offer(sampleTriggers, undefined)),
+    ),
+  ).pipe(Effect.forkScoped);
+
+  const sampleOnce = Effect.gen(function* () {
+    const sampledAt = yield* DateTime.now;
+    const sampledAtUnixMs = DateTime.toEpochMillis(sampledAt);
+    const demand = yield* Ref.get(diagnosticsDemand);
+    const [currentPower, idleSeconds, systemIdleState, onBattery, metrics] = yield* Effect.all(
+      [
+        Ref.get(powerState),
+        powerMonitor.getSystemIdleTime,
+        powerMonitor.getSystemIdleState(IDLE_THRESHOLD_SECONDS),
+        powerMonitor.isOnBatteryPower,
+        demand ? electronApp.getAppMetrics : Effect.succeed([]),
+      ],
+      { concurrency: "unbounded" },
+    );
+    const nextSequence = yield* Ref.modify(sequence, (current) => [current + 1, current + 1]);
+    const locked = systemIdleState === "locked" ? "true" : currentPower.locked;
+    const snapshot: DesktopHostTelemetrySnapshot = {
+      version: 1,
+      type: "desktopTelemetry",
+      sequence: nextSequence,
+      sampledAtUnixMs,
+      electronPid: process.pid,
+      power: {
+        source: "electron-main",
+        idle: idleState(systemIdleState),
+        idleSeconds,
+        locked,
+        suspended: currentPower.suspended,
+        onBattery: booleanState(onBattery),
+        lowPowerMode: "unknown",
+        thermalState: currentPower.thermalState,
+        stale: false,
+        updatedAt: sampledAt,
+      },
+      speedLimitPercent: currentPower.speedLimitPercent,
+      electronProcesses: metrics.map((metric) => ({
+        pid: metric.pid,
+        creationTimeMs: metric.creationTime,
+        type: metric.type,
+        ...(metric.name === undefined ? {} : { name: metric.name }),
+        ...(metric.serviceName === undefined ? {} : { serviceName: metric.serviceName }),
+        cpuPercent: metric.cpu.percentCPUUsage,
+        ...(metric.cpu.cumulativeCPUUsage === undefined
+          ? {}
+          : { cumulativeCpuSeconds: metric.cpu.cumulativeCPUUsage }),
+        idleWakeupsPerSecond: metric.cpu.idleWakeupsPerSecond,
+        workingSetBytes: Math.max(0, Math.round(metric.memory.workingSetSize * 1024)),
+        peakWorkingSetBytes: Math.max(0, Math.round(metric.memory.peakWorkingSetSize * 1024)),
+      })),
+    };
+
+    yield* Ref.set(latest, Option.some(snapshot));
+    yield* PubSub.publish(changes, snapshot);
+  }).pipe(
+    Effect.catchCause((cause) =>
+      Effect.logWarning("Failed to sample Electron telemetry", {
+        cause: String(cause),
+      }),
+    ),
+  );
+
+  yield* Effect.gen(function* () {
+    yield* sampleOnce;
+    while (true) {
+      const [currentPower, demand] = yield* Effect.all([
+        Ref.get(powerState),
+        Ref.get(diagnosticsDemand),
+      ]);
+      yield* Effect.raceFirst(
+        Queue.take(sampleTriggers),
+        Effect.sleep(sampleInterval(currentPower, demand)),
+      );
+      yield* sampleOnce;
+    }
+  }).pipe(Effect.forkScoped);
+
+  const handleControl: DesktopTelemetryPublisherShape["handleControl"] = (message) => {
+    switch (message.type) {
+      case "setDiagnosticsDemand":
+        return Ref.getAndSet(diagnosticsDemand, message.enabled).pipe(
+          Effect.flatMap((previous) =>
+            previous === message.enabled
+              ? Effect.void
+              : Queue.offer(sampleTriggers, undefined).pipe(Effect.asVoid),
+          ),
+        );
+    }
+  };
+
+  const snapshots = Stream.concat(
M
macroscopeappBotJun 19, 2026

🟡 Medium telemetry/DesktopTelemetryPublisher.ts:234

There is a race between reading Ref.get(latest) (line 236) and Stream.fromPubSub(changes) subscribing to the PubSub (line 245). Because the forked sampleOnce loop runs concurrently, a snapshot can be published to changes after latest is read but before the PubSub subscription is established. That snapshot updates the Ref and is published, but no subscriber is listening yet, so the consumer never receives it — it sees the stale latest value and then only snapshots arriving after subscription.

To close the gap, subscribe to the changes PubSub first, then read latest, so any snapshot published during or after the read is captured by the already-active subscription.

No longer relevant as of 3f9ae0646c6f221e911a1ed4ede46a45cb361fc0

apps/server/src/resourceTelemetry/Model.ts · Earlier revision deb3d483 · right 287

Original diff
@@ -0,0 +1,559 @@
+import type {
+  DesktopElectronProcessMetric,
+  DesktopHostTelemetrySnapshot,
+  ResourceMonitorProcessSample,
+  ResourceMonitorSnapshotEvent,
+  ResourceTelemetryAggregate,
+  ResourceTelemetryProcess,
+  ResourceTelemetryProcessCategory,
+} from "@t3tools/contracts";
+import * as DateTime from "effect/DateTime";
+import * as Option from "effect/Option";
+
+const MAX_DELTA_INTERVAL_MS = 30_000;
+const ELECTRON_IDENTITY_TOLERANCE_MS = 2_000;
+
+export interface ProcessState {
+  readonly process: ResourceTelemetryProcess;
+  readonly sampledAtMs: number;
+}
+
+export interface GroupCounters {
+  readonly cpuTimeMs: number;
+  readonly ioReadBytes: number;
+  readonly ioWriteBytes: number;
+  readonly processStarts: number;
+  readonly processExits: number;
+}
+
+export interface TelemetryCounters {
+  readonly backend: GroupCounters;
+  readonly electron: GroupCounters;
+  readonly monitor: GroupCounters;
+  readonly allT3: GroupCounters;
+}
+
+export interface ProcessDelta {
+  readonly identityKey: string;
+  readonly category: ResourceTelemetryProcessCategory;
+  readonly cpuTimeMs: number;
+  readonly ioReadBytes: number;
+  readonly ioWriteBytes: number;
+}
+
+export interface MergeProcessesInput {
+  readonly serverPid: number;
+  readonly sidecarPid: Option.Option<number>;
+  readonly fallbackSampledAtMs: number;
+  readonly nativeSnapshot: Option.Option<ResourceMonitorSnapshotEvent>;
+  readonly desktopSnapshot: Option.Option<DesktopHostTelemetrySnapshot>;
+  readonly electronRootPids?: ReadonlySet<number>;
+  readonly previous: ReadonlyMap<string, ProcessState>;
+  readonly counters: TelemetryCounters;
+  readonly updatePrevious: boolean;
+}
+
+export interface MergeProcessesResult {
+  readonly sampledAtMs: number;
+  readonly processes: ReadonlyArray<ResourceTelemetryProcess>;
+  readonly previous: ReadonlyMap<string, ProcessState>;
+  readonly counters: TelemetryCounters;
+  readonly groups: {
+    readonly backend: ResourceTelemetryAggregate;
+    readonly electron: ResourceTelemetryAggregate;
+    readonly monitor: ResourceTelemetryAggregate;
+    readonly allT3: ResourceTelemetryAggregate;
+  };
+  readonly deltas: ReadonlyArray<ProcessDelta>;
+}
+
+export const emptyGroupCounters = (): GroupCounters => ({
+  cpuTimeMs: 0,
+  ioReadBytes: 0,
+  ioWriteBytes: 0,
+  processStarts: 0,
+  processExits: 0,
+});
+
+export const emptyTelemetryCounters = (): TelemetryCounters => ({
+  backend: emptyGroupCounters(),
+  electron: emptyGroupCounters(),
+  monitor: emptyGroupCounters(),
+  allT3: emptyGroupCounters(),
+});
+
+export function processIdentityKey(pid: number, startTimeMs: number): string {
+  return `${pid}:${startTimeMs}`;
+}
+
+function finiteNonNegative(value: number): number {
+  return Number.isFinite(value) ? Math.max(0, value) : 0;
+}
+
+function categoryGroup(
+  category: ResourceTelemetryProcessCategory,
+): "backend" | "electron" | "monitor" {
+  if (category === "resource-monitor") return "monitor";
+  if (category.startsWith("electron-")) return "electron";
+  return "backend";
+}
+
+function electronCategory(metric: DesktopElectronProcessMetric): ResourceTelemetryProcessCategory {
+  switch (metric.type) {
+    case "Browser":
+      return "electron-main";
+    case "Tab":
+      return "electron-renderer";
+    case "GPU":
+      return "electron-gpu";
+    default:
+      return "electron-utility";
+  }
+}
+
+function inferredElectronCategory(
+  process: ResourceMonitorProcessSample,
+): ResourceTelemetryProcessCategory {
+  const command = process.command.toLowerCase();
+  if (command.includes("--type=renderer")) return "electron-renderer";
+  if (command.includes("--type=gpu-process")) return "electron-gpu";
+  return "electron-utility";
+}
+
+function matchElectronMetric(
+  process: ResourceMonitorProcessSample,
+  metricsByPid: ReadonlyMap<number, DesktopElectronProcessMetric>,
+): DesktopElectronProcessMetric | undefined {
+  const metric = metricsByPid.get(process.pid);
+  if (!metric) return undefined;
+  return Math.abs(metric.creationTimeMs - process.startTimeMs) <= ELECTRON_IDENTITY_TOLERANCE_MS
+    ? metric
+    : undefined;
+}
+
+function syntheticNativeSample(
+  metric: DesktopElectronProcessMetric,
+  sampledAtMs: number,
+): ResourceMonitorProcessSample {
+  return {
+    pid: metric.pid,
+    ppid: 0,
+    startTimeMs: metric.creationTimeMs,
+    runTimeMs: Math.max(0, sampledAtMs - metric.creationTimeMs),
+    name: metric.name ?? metric.serviceName ?? metric.type,
+    command: metric.name ?? metric.serviceName ?? metric.type,
+    status: "Running",
+    cpuPercent: metric.cpuPercent,
+    cpuTimeMs: Math.max(0, Math.round((metric.cumulativeCpuSeconds ?? 0) * 1_000)),
+    residentBytes: metric.workingSetBytes,
+    virtualBytes: 0,
+    ioReadBytes: 0,
+    ioWriteBytes: 0,
+    ioSemantics: "storage",
+  };
+}
+
+function processDepths(
+  processes: ReadonlyArray<ResourceMonitorProcessSample>,
+  roots: ReadonlySet<number>,
+): ReadonlyMap<number, number> {
+  const childrenByParent = new Map<number, number[]>();
+  for (const process of processes) {
+    const children = childrenByParent.get(process.ppid) ?? [];
+    children.push(process.pid);
+    childrenByParent.set(process.ppid, children);
+  }
+
+  const depths = new Map<number, number>();
+  const queue = [...roots].map((pid) => ({ pid, depth: 0 }));
+  while (queue.length > 0) {
+    const current = queue.shift();
+    if (!current || depths.has(current.pid)) continue;
+    depths.set(current.pid, current.depth);
+    for (const childPid of childrenByParent.get(current.pid) ?? []) {
+      queue.push({ pid: childPid, depth: current.depth + 1 });
+    }
+  }
+  return depths;
+}
+
+function isElectronDescendant(
+  pid: number,
+  processesByPid: ReadonlyMap<number, ResourceMonitorProcessSample>,
+  electronPids: ReadonlySet<number>,
+): boolean {
+  const visited = new Set<number>();
+  let currentPid = pid;
+  while (!visited.has(currentPid)) {
+    visited.add(currentPid);
+    if (electronPids.has(currentPid)) return true;
+    const current = processesByPid.get(currentPid);
+    if (!current || current.ppid <= 0 || current.ppid === currentPid) return false;
+    currentPid = current.ppid;
+  }
+  return false;
+}
+
+function hasElectronAncestor(
+  process: ResourceMonitorProcessSample,
+  processesByPid: ReadonlyMap<number, ResourceMonitorProcessSample>,
+  electronPids: ReadonlySet<number>,
+): boolean {
+  const visited = new Set<number>();
+  let currentPid = process.ppid;
+  while (currentPid > 0 && !visited.has(currentPid)) {
+    visited.add(currentPid);
+    if (electronPids.has(currentPid)) return true;
+    const current = processesByPid.get(currentPid);
+    if (!current || current.ppid === currentPid) return false;
+    currentPid = current.ppid;
+  }
+  return false;
+}
+
+function orderProcessTree(
+  processes: ReadonlyArray<ResourceTelemetryProcess>,
+  rootPids: ReadonlyArray<number>,
+): ReadonlyArray<ResourceTelemetryProcess> {
+  const processesByPid = new Map(processes.map((process) => [process.identity.pid, process]));
+  const childrenByParent = new Map<number, ResourceTelemetryProcess[]>();
+  for (const process of processes) {
+    const children = childrenByParent.get(process.ppid) ?? [];
+    children.push(process);
+    childrenByParent.set(process.ppid, children);
+  }
+  for (const children of childrenByParent.values()) {
+    children.sort((left, right) => left.identity.pid - right.identity.pid);
+  }
+
+  const ordered: ResourceTelemetryProcess[] = [];
+  const visited = new Set<number>();
+  const visit = (process: ResourceTelemetryProcess): void => {
+    if (visited.has(process.identity.pid)) return;
+    visited.add(process.identity.pid);
+    ordered.push(process);
+    for (const child of childrenByParent.get(process.identity.pid) ?? []) {
+      visit(child);
+    }
+  };
+
+  for (const rootPid of rootPids) {
+    const root = processesByPid.get(rootPid);
+    if (root) visit(root);
+  }
+  for (const process of processes.toSorted(
+    (left, right) => left.depth - right.depth || left.identity.pid - right.identity.pid,
+  )) {
+    visit(process);
+  }
+  return ordered;
+}
+
+function delta(input: {
+  readonly current: number;
+  readonly previous: number;
+  readonly elapsedMs: number;
+}): number {
+  if (
+    input.elapsedMs <= 0 ||
+    input.elapsedMs > MAX_DELTA_INTERVAL_MS ||
+    input.current < input.previous
+  ) {
+    return 0;
+  }
+  return input.current - input.previous;
+}
+
+function incrementCounters(counters: GroupCounters, update: Partial<GroupCounters>): GroupCounters {
+  return {
+    cpuTimeMs: counters.cpuTimeMs + (update.cpuTimeMs ?? 0),
+    ioReadBytes: counters.ioReadBytes + (update.ioReadBytes ?? 0),
+    ioWriteBytes: counters.ioWriteBytes + (update.ioWriteBytes ?? 0),
+    processStarts: counters.processStarts + (update.processStarts ?? 0),
+    processExits: counters.processExits + (update.processExits ?? 0),
+  };
+}
+
+function applyLifecycleCounters(input: {
+  readonly counters: TelemetryCounters;
+  readonly deltas: ReadonlyArray<ProcessDelta>;
+  readonly current: ReadonlyMap<string, ProcessState>;
+  readonly previous: ReadonlyMap<string, ProcessState>;
+}): TelemetryCounters {
+  let backend = input.counters.backend;
+  let electron = input.counters.electron;
+  let monitor = input.counters.monitor;
+  let allT3 = input.counters.allT3;
+  for (const processDelta of input.deltas) {
M
macroscopeappBotJul 28, 2026

🟡 Medium resourceTelemetry/Model.ts:287

applyLifecycleCounters attributes each delta only by the process's current category. When a process's category changes between samples (e.g. first classified as server-child and later as electron-main after desktop metrics arrive), the CPU/I/O deltas already accumulated under backend stay there permanently while subsequent deltas go to electron. This permanently corrupts both group aggregates — the backend group retains CPU time and I/O bytes that belong to the electron group, and the electron group never recovers them. Consider detecting identity keys whose category differs from the previous sample and rebalancing the prior contributions out of the old group and into the new one.

No longer relevant as of 3f9ae0646c6f221e911a1ed4ede46a45cb361fc0

apps/server/src/background/BackgroundPolicy.ts · Earlier revision deb3d483 · right 200

Original diff
@@ -0,0 +1,274 @@
+import {
+  type AuthSessionId,
+  type BackgroundPolicySnapshot,
+  type BackgroundScope,
+  type ClientActivityLease,
+  type ClientActivityReportInput,
+  type HostPowerSnapshot,
+  type RpcClientId,
+} from "@t3tools/contracts";
+import {
+  getBackgroundActivityPresetSettings,
+  resolveServerBackgroundActivitySettings,
+  type ResolvedBackgroundActivitySettings,
+} from "@t3tools/shared/backgroundActivitySettings";
+import * as DateTime from "effect/DateTime";
+import * as Context from "effect/Context";
+import * as Effect from "effect/Effect";
+import * as Layer from "effect/Layer";
+import * as PubSub from "effect/PubSub";
+import * as Ref from "effect/Ref";
+import * as Stream from "effect/Stream";
+
+import { ServerSettingsService } from "../serverSettings.ts";
+import * as HostPowerMonitor from "./HostPowerMonitor.ts";
+
+export interface BackgroundPolicyShape {
+  readonly reportClientActivity: (
+    sessionId: AuthSessionId,
+    rpcClientId: RpcClientId,
+    input: ClientActivityReportInput,
+  ) => Effect.Effect<void>;
+  readonly removeRpcClient: (rpcClientId: RpcClientId) => Effect.Effect<void>;
+  readonly reportHostPowerState: (snapshot: HostPowerSnapshot) => Effect.Effect<void>;
+  readonly snapshot: Effect.Effect<BackgroundPolicySnapshot>;
+  readonly streamChanges: Stream.Stream<BackgroundPolicySnapshot>;
+  readonly hasDemand: (scope: BackgroundScope) => Effect.Effect<boolean>;
+  readonly shouldRunScopeWork: (scope: BackgroundScope) => Effect.Effect<boolean>;
+  readonly shouldRunOpportunisticWork: Effect.Effect<boolean>;
+}
+
+export class BackgroundPolicy extends Context.Service<BackgroundPolicy, BackgroundPolicyShape>()(
+  "t3/background/BackgroundPolicy",
+) {}
+
+const DEFAULT_LEASE_TTL_MS = 45_000;
+const MAX_LEASE_TTL_MS = 120_000;
+
+function scopeKey(scope: BackgroundScope): string {
+  switch (scope.type) {
+    case "server-config":
+    case "diagnostics":
+      return scope.type;
+    case "provider-status":
+      return scope.instanceId ? `${scope.type}:${scope.instanceId}` : scope.type;
+    case "vcs-status":
+    case "git-refs":
+      return `${scope.type}:${scope.cwd}`;
+    case "thread":
+      return `${scope.type}:${scope.threadId}`;
+  }
+}
+
+function isLeaseActive(lease: ClientActivityLease, now: DateTime.Utc): boolean {
+  return DateTime.isGreaterThan(lease.expiresAt, now);
+}
+
+function isForegroundLease(lease: ClientActivityLease, now: DateTime.Utc): boolean {
+  return isLeaseActive(lease, now) && lease.visible && lease.focused;
+}
+
+function leaseHasScope(lease: ClientActivityLease, scope: BackgroundScope): boolean {
+  const key = scopeKey(scope);
+  return lease.scopes.some((leaseScope) => scopeKey(leaseScope) === key);
+}
+
+function hasThermalPressure(hostPower: HostPowerSnapshot): boolean {
+  return hostPower.thermalState === "serious" || hostPower.thermalState === "critical";
+}
+
+function isHostConstrained(
+  hostPower: HostPowerSnapshot,
+  settings: ResolvedBackgroundActivitySettings,
+): boolean {
+  if (hostPower.stale) return false;
+  if (
+    (settings.pauseWhenHostLocked && hostPower.locked === "true") ||
+    hasThermalPressure(hostPower)
+  ) {
+    return true;
+  }
+  if (settings.pauseWhenHostLowPower && hostPower.lowPowerMode === "true") return true;
+  return settings.pauseWhenOnBattery && hostPower.onBattery === "true";
+}
+
+function isClientConstrained(
+  lease: ClientActivityLease,
+  settings: ResolvedBackgroundActivitySettings,
+): boolean {
+  if (settings.pauseWhenClientLowPower && lease.lowPowerMode === "true") return true;
+  return settings.pauseWhenOnBattery && lease.batteryState === "unplugged";
+}
+
+function leaseMayRunScopedWork(
+  lease: ClientActivityLease,
+  scope: BackgroundScope,
+  now: DateTime.Utc,
+  settings: ResolvedBackgroundActivitySettings,
+): boolean {
+  const activeWithScope = isLeaseActive(lease, now) && leaseHasScope(lease, scope);
+  if (!activeWithScope || isClientConstrained(lease, settings)) {
+    return false;
+  }
+  if (settings.profile === "performance") {
+    return true;
+  }
+  return isForegroundLease(lease, now);
+}
+
+function computeSnapshot(input: {
+  readonly hostPower: HostPowerSnapshot;
+  readonly leases: ReadonlyMap<string, ClientActivityLease>;
+  readonly now: DateTime.Utc;
+  readonly settings: ResolvedBackgroundActivitySettings;
+  readonly updatedAt: DateTime.Utc;
+}): BackgroundPolicySnapshot {
+  const activeLeases = [...input.leases.values()].filter((lease) =>
+    isLeaseActive(lease, input.now),
+  );
+  const foregroundLeases = activeLeases.filter((lease) => isForegroundLease(lease, input.now));
+  const activeScopeKeys = new Set<string>();
+  for (const lease of activeLeases) {
+    for (const scope of lease.scopes) {
+      activeScopeKeys.add(scopeKey(scope));
+    }
+  }
+
+  return {
+    hostPower: input.hostPower,
+    leases: activeLeases,
+    activeForegroundLeaseCount: foregroundLeases.length,
+    activeScopeKeys: [...activeScopeKeys].toSorted(),
+    shouldRunOpportunisticWork:
+      foregroundLeases.some((lease) => !isClientConstrained(lease, input.settings)) &&
+      !isHostConstrained(input.hostPower, input.settings),
+    updatedAt: input.updatedAt,
+  };
+}
+
+export const make = Effect.fn("background.policy.make")(function* () {
+  const hostPowerMonitor = yield* HostPowerMonitor.HostPowerMonitor;
+  const serverSettings = yield* ServerSettingsService;
+  const leasesRef = yield* Ref.make(new Map<string, ClientActivityLease>());
+  const changes = yield* PubSub.sliding<BackgroundPolicySnapshot>(1);
+
+  const backgroundActivitySettings = serverSettings.getSettings.pipe(
+    Effect.map(resolveServerBackgroundActivitySettings),
+    Effect.orElseSucceed(() => getBackgroundActivityPresetSettings("balanced")),
+  );
+
+  const snapshot = Effect.gen(function* () {
+    const [hostPower, leases, now, settings] = yield* Effect.all([
+      hostPowerMonitor.snapshot,
+      Ref.get(leasesRef),
+      DateTime.now,
+      backgroundActivitySettings,
+    ]);
+    return computeSnapshot({ hostPower, leases, now, settings, updatedAt: now });
+  });
+
+  const publishSnapshot = snapshot.pipe(Effect.flatMap((next) => PubSub.publish(changes, next)));
+
+  const reportClientActivity: BackgroundPolicyShape["reportClientActivity"] = (
+    sessionId,
+    rpcClientId,
+    input,
+  ) =>
+    Effect.gen(function* () {
+      const ttlMs = Math.min(
+        Math.max(input.ttlMs ?? DEFAULT_LEASE_TTL_MS, 1_000),
+        MAX_LEASE_TTL_MS,
+      );
+      const now = yield* DateTime.now;
+      const expiresAt = DateTime.add(now, { milliseconds: ttlMs });
+      const lease: ClientActivityLease = {
+        sessionId,
+        rpcClientId,
+        clientId: input.clientId,
+        clientKind: input.clientKind,
+        visible: input.visible,
+        focused: input.focused,
+        recentlyInteracted: input.recentlyInteracted,
+        ...(input.appState !== undefined ? { appState: input.appState } : {}),
+        ...(input.lowPowerMode !== undefined ? { lowPowerMode: input.lowPowerMode } : {}),
+        ...(input.batteryState !== undefined ? { batteryState: input.batteryState } : {}),
+        ...(input.networkType !== undefined ? { networkType: input.networkType } : {}),
+        scopes: input.scopes,
+        updatedAt: now,
+        expiresAt,
+      };
+      yield* Ref.update(leasesRef, (leases) => {
M
macroscopeappBotJul 28, 2026

🟡 Medium background/BackgroundPolicy.ts:200

reportClientActivity unconditionally overwrites the existing lease and ignores input.observedAt, so an older activity report processed out of order can replace a newer one's visibility, focus, and scopes while still getting a fresh server-side expiry. This causes background work to be incorrectly enabled or suppressed until the next heartbeat. Consider comparing input.observedAt against the existing lease's timestamp and discarding stale reports before overwriting.

No longer relevant as of 3f9ae0646c6f221e911a1ed4ede46a45cb361fc0

apps/server/src/resourceTelemetry/Model.ts · Earlier revision deb3d483 · right 365

Original diff
@@ -0,0 +1,559 @@
+import type {
+  DesktopElectronProcessMetric,
+  DesktopHostTelemetrySnapshot,
+  ResourceMonitorProcessSample,
+  ResourceMonitorSnapshotEvent,
+  ResourceTelemetryAggregate,
+  ResourceTelemetryProcess,
+  ResourceTelemetryProcessCategory,
+} from "@t3tools/contracts";
+import * as DateTime from "effect/DateTime";
+import * as Option from "effect/Option";
+
+const MAX_DELTA_INTERVAL_MS = 30_000;
+const ELECTRON_IDENTITY_TOLERANCE_MS = 2_000;
+
+export interface ProcessState {
+  readonly process: ResourceTelemetryProcess;
+  readonly sampledAtMs: number;
+}
+
+export interface GroupCounters {
+  readonly cpuTimeMs: number;
+  readonly ioReadBytes: number;
+  readonly ioWriteBytes: number;
+  readonly processStarts: number;
+  readonly processExits: number;
+}
+
+export interface TelemetryCounters {
+  readonly backend: GroupCounters;
+  readonly electron: GroupCounters;
+  readonly monitor: GroupCounters;
+  readonly allT3: GroupCounters;
+}
+
+export interface ProcessDelta {
+  readonly identityKey: string;
+  readonly category: ResourceTelemetryProcessCategory;
+  readonly cpuTimeMs: number;
+  readonly ioReadBytes: number;
+  readonly ioWriteBytes: number;
+}
+
+export interface MergeProcessesInput {
+  readonly serverPid: number;
+  readonly sidecarPid: Option.Option<number>;
+  readonly fallbackSampledAtMs: number;
+  readonly nativeSnapshot: Option.Option<ResourceMonitorSnapshotEvent>;
+  readonly desktopSnapshot: Option.Option<DesktopHostTelemetrySnapshot>;
+  readonly electronRootPids?: ReadonlySet<number>;
+  readonly previous: ReadonlyMap<string, ProcessState>;
+  readonly counters: TelemetryCounters;
+  readonly updatePrevious: boolean;
+}
+
+export interface MergeProcessesResult {
+  readonly sampledAtMs: number;
+  readonly processes: ReadonlyArray<ResourceTelemetryProcess>;
+  readonly previous: ReadonlyMap<string, ProcessState>;
+  readonly counters: TelemetryCounters;
+  readonly groups: {
+    readonly backend: ResourceTelemetryAggregate;
+    readonly electron: ResourceTelemetryAggregate;
+    readonly monitor: ResourceTelemetryAggregate;
+    readonly allT3: ResourceTelemetryAggregate;
+  };
+  readonly deltas: ReadonlyArray<ProcessDelta>;
+}
+
+export const emptyGroupCounters = (): GroupCounters => ({
+  cpuTimeMs: 0,
+  ioReadBytes: 0,
+  ioWriteBytes: 0,
+  processStarts: 0,
+  processExits: 0,
+});
+
+export const emptyTelemetryCounters = (): TelemetryCounters => ({
+  backend: emptyGroupCounters(),
+  electron: emptyGroupCounters(),
+  monitor: emptyGroupCounters(),
+  allT3: emptyGroupCounters(),
+});
+
+export function processIdentityKey(pid: number, startTimeMs: number): string {
+  return `${pid}:${startTimeMs}`;
+}
+
+function finiteNonNegative(value: number): number {
+  return Number.isFinite(value) ? Math.max(0, value) : 0;
+}
+
+function categoryGroup(
+  category: ResourceTelemetryProcessCategory,
+): "backend" | "electron" | "monitor" {
+  if (category === "resource-monitor") return "monitor";
+  if (category.startsWith("electron-")) return "electron";
+  return "backend";
+}
+
+function electronCategory(metric: DesktopElectronProcessMetric): ResourceTelemetryProcessCategory {
+  switch (metric.type) {
+    case "Browser":
+      return "electron-main";
+    case "Tab":
+      return "electron-renderer";
+    case "GPU":
+      return "electron-gpu";
+    default:
+      return "electron-utility";
+  }
+}
+
+function inferredElectronCategory(
+  process: ResourceMonitorProcessSample,
+): ResourceTelemetryProcessCategory {
+  const command = process.command.toLowerCase();
+  if (command.includes("--type=renderer")) return "electron-renderer";
+  if (command.includes("--type=gpu-process")) return "electron-gpu";
+  return "electron-utility";
+}
+
+function matchElectronMetric(
+  process: ResourceMonitorProcessSample,
+  metricsByPid: ReadonlyMap<number, DesktopElectronProcessMetric>,
+): DesktopElectronProcessMetric | undefined {
+  const metric = metricsByPid.get(process.pid);
+  if (!metric) return undefined;
+  return Math.abs(metric.creationTimeMs - process.startTimeMs) <= ELECTRON_IDENTITY_TOLERANCE_MS
+    ? metric
+    : undefined;
+}
+
+function syntheticNativeSample(
+  metric: DesktopElectronProcessMetric,
+  sampledAtMs: number,
+): ResourceMonitorProcessSample {
+  return {
+    pid: metric.pid,
+    ppid: 0,
+    startTimeMs: metric.creationTimeMs,
+    runTimeMs: Math.max(0, sampledAtMs - metric.creationTimeMs),
+    name: metric.name ?? metric.serviceName ?? metric.type,
+    command: metric.name ?? metric.serviceName ?? metric.type,
+    status: "Running",
+    cpuPercent: metric.cpuPercent,
+    cpuTimeMs: Math.max(0, Math.round((metric.cumulativeCpuSeconds ?? 0) * 1_000)),
+    residentBytes: metric.workingSetBytes,
+    virtualBytes: 0,
+    ioReadBytes: 0,
+    ioWriteBytes: 0,
+    ioSemantics: "storage",
+  };
+}
+
+function processDepths(
+  processes: ReadonlyArray<ResourceMonitorProcessSample>,
+  roots: ReadonlySet<number>,
+): ReadonlyMap<number, number> {
+  const childrenByParent = new Map<number, number[]>();
+  for (const process of processes) {
+    const children = childrenByParent.get(process.ppid) ?? [];
+    children.push(process.pid);
+    childrenByParent.set(process.ppid, children);
+  }
+
+  const depths = new Map<number, number>();
+  const queue = [...roots].map((pid) => ({ pid, depth: 0 }));
+  while (queue.length > 0) {
+    const current = queue.shift();
+    if (!current || depths.has(current.pid)) continue;
+    depths.set(current.pid, current.depth);
+    for (const childPid of childrenByParent.get(current.pid) ?? []) {
+      queue.push({ pid: childPid, depth: current.depth + 1 });
+    }
+  }
+  return depths;
+}
+
+function isElectronDescendant(
+  pid: number,
+  processesByPid: ReadonlyMap<number, ResourceMonitorProcessSample>,
+  electronPids: ReadonlySet<number>,
+): boolean {
+  const visited = new Set<number>();
+  let currentPid = pid;
+  while (!visited.has(currentPid)) {
+    visited.add(currentPid);
+    if (electronPids.has(currentPid)) return true;
+    const current = processesByPid.get(currentPid);
+    if (!current || current.ppid <= 0 || current.ppid === currentPid) return false;
+    currentPid = current.ppid;
+  }
+  return false;
+}
+
+function hasElectronAncestor(
+  process: ResourceMonitorProcessSample,
+  processesByPid: ReadonlyMap<number, ResourceMonitorProcessSample>,
+  electronPids: ReadonlySet<number>,
+): boolean {
+  const visited = new Set<number>();
+  let currentPid = process.ppid;
+  while (currentPid > 0 && !visited.has(currentPid)) {
+    visited.add(currentPid);
+    if (electronPids.has(currentPid)) return true;
+    const current = processesByPid.get(currentPid);
+    if (!current || current.ppid === currentPid) return false;
+    currentPid = current.ppid;
+  }
+  return false;
+}
+
+function orderProcessTree(
+  processes: ReadonlyArray<ResourceTelemetryProcess>,
+  rootPids: ReadonlyArray<number>,
+): ReadonlyArray<ResourceTelemetryProcess> {
+  const processesByPid = new Map(processes.map((process) => [process.identity.pid, process]));
+  const childrenByParent = new Map<number, ResourceTelemetryProcess[]>();
+  for (const process of processes) {
+    const children = childrenByParent.get(process.ppid) ?? [];
+    children.push(process);
+    childrenByParent.set(process.ppid, children);
+  }
+  for (const children of childrenByParent.values()) {
+    children.sort((left, right) => left.identity.pid - right.identity.pid);
+  }
+
+  const ordered: ResourceTelemetryProcess[] = [];
+  const visited = new Set<number>();
+  const visit = (process: ResourceTelemetryProcess): void => {
+    if (visited.has(process.identity.pid)) return;
+    visited.add(process.identity.pid);
+    ordered.push(process);
+    for (const child of childrenByParent.get(process.identity.pid) ?? []) {
+      visit(child);
+    }
+  };
+
+  for (const rootPid of rootPids) {
+    const root = processesByPid.get(rootPid);
+    if (root) visit(root);
+  }
+  for (const process of processes.toSorted(
+    (left, right) => left.depth - right.depth || left.identity.pid - right.identity.pid,
+  )) {
+    visit(process);
+  }
+  return ordered;
+}
+
+function delta(input: {
+  readonly current: number;
+  readonly previous: number;
+  readonly elapsedMs: number;
+}): number {
+  if (
+    input.elapsedMs <= 0 ||
+    input.elapsedMs > MAX_DELTA_INTERVAL_MS ||
+    input.current < input.previous
+  ) {
+    return 0;
+  }
+  return input.current - input.previous;
+}
+
+function incrementCounters(counters: GroupCounters, update: Partial<GroupCounters>): GroupCounters {
+  return {
+    cpuTimeMs: counters.cpuTimeMs + (update.cpuTimeMs ?? 0),
+    ioReadBytes: counters.ioReadBytes + (update.ioReadBytes ?? 0),
+    ioWriteBytes: counters.ioWriteBytes + (update.ioWriteBytes ?? 0),
+    processStarts: counters.processStarts + (update.processStarts ?? 0),
+    processExits: counters.processExits + (update.processExits ?? 0),
+  };
+}
+
+function applyLifecycleCounters(input: {
+  readonly counters: TelemetryCounters;
+  readonly deltas: ReadonlyArray<ProcessDelta>;
+  readonly current: ReadonlyMap<string, ProcessState>;
+  readonly previous: ReadonlyMap<string, ProcessState>;
+}): TelemetryCounters {
+  let backend = input.counters.backend;
+  let electron = input.counters.electron;
+  let monitor = input.counters.monitor;
+  let allT3 = input.counters.allT3;
+  for (const processDelta of input.deltas) {
+    const group = categoryGroup(processDelta.category);
+    switch (group) {
+      case "backend":
+        backend = incrementCounters(backend, processDelta);
+        break;
+      case "electron":
+        electron = incrementCounters(electron, processDelta);
+        break;
+      case "monitor":
+        monitor = incrementCounters(monitor, processDelta);
+        break;
+    }
+    allT3 = incrementCounters(allT3, processDelta);
+  }
+
+  for (const [identityKey, current] of input.current) {
+    if (input.previous.has(identityKey)) continue;
+    const group = categoryGroup(current.process.category);
+    switch (group) {
+      case "backend":
+        backend = incrementCounters(backend, { processStarts: 1 });
+        break;
+      case "electron":
+        electron = incrementCounters(electron, { processStarts: 1 });
+        break;
+      case "monitor":
+        monitor = incrementCounters(monitor, { processStarts: 1 });
+        break;
+    }
+    allT3 = incrementCounters(allT3, { processStarts: 1 });
+  }
+
+  for (const [identityKey, previous] of input.previous) {
+    if (input.current.has(identityKey)) continue;
+    const group = categoryGroup(previous.process.category);
+    switch (group) {
+      case "backend":
+        backend = incrementCounters(backend, { processExits: 1 });
+        break;
+      case "electron":
+        electron = incrementCounters(electron, { processExits: 1 });
+        break;
+      case "monitor":
+        monitor = incrementCounters(monitor, { processExits: 1 });
+        break;
+    }
+    allT3 = incrementCounters(allT3, { processExits: 1 });
+  }
+
+  return { backend, electron, monitor, allT3 };
+}
+
+function aggregate(
+  processes: ReadonlyArray<ResourceTelemetryProcess>,
+  counters: GroupCounters,
+): ResourceTelemetryAggregate {
+  return {
+    processCount: processes.length,
+    currentCpuPercent: processes.reduce((total, process) => total + process.cpuPercent, 0),
+    cpuTimeMs: counters.cpuTimeMs,
+    currentRssBytes: processes.reduce((total, process) => total + process.residentBytes, 0),
+    peakRssBytes: processes.reduce((total, process) => total + process.peakResidentBytes, 0),
+    ioReadBytes: counters.ioReadBytes,
+    ioWriteBytes: counters.ioWriteBytes,
+    ioReadBytesPerSecond: processes.reduce(
+      (total, process) => total + process.ioReadBytesPerSecond,
+      0,
+    ),
+    ioWriteBytesPerSecond: processes.reduce(
+      (total, process) => total + process.ioWriteBytesPerSecond,
+      0,
+    ),
+    processStarts: counters.processStarts,
+    processExits: counters.processExits,
+  };
+}
+
+export function mergeProcesses(input: MergeProcessesInput): MergeProcessesResult {
M
macroscopeappBotJul 28, 2026

🟡 Medium resourceTelemetry/Model.ts:365

When an Electron process is first observed only in desktop metrics, syntheticNativeSample creates a synthetic native sample using metric.creationTimeMs as startTimeMs. If the same PID later appears in the native monitor snapshot, its startTimeMs may differ by up to ELECTRON_IDENTITY_TOLERANCE_MS (2 seconds) — a difference the code explicitly tolerates for matching. But processIdentityKey is built from exact startTimeMs equality, so the later native sample gets a different identity key. This causes the process to be counted as an exit plus a new start, resets its CPU/IO rate history, and produces incorrect lifecycle telemetry for what is actually the same process. Consider deriving the synthetic sample's startTimeMs from the native sample when one later arrives, or using a fuzzy identity key that accounts for the tolerated time difference.

No longer relevant as of 3f9ae0646c6f221e911a1ed4ede46a45cb361fc0

apps/server/src/background/BackgroundPolicy.ts · Earlier revision deb3d483 · right 45

Original diff
@@ -0,0 +1,274 @@
+import {
+  type AuthSessionId,
+  type BackgroundPolicySnapshot,
+  type BackgroundScope,
+  type ClientActivityLease,
+  type ClientActivityReportInput,
+  type HostPowerSnapshot,
+  type RpcClientId,
+} from "@t3tools/contracts";
+import {
+  getBackgroundActivityPresetSettings,
+  resolveServerBackgroundActivitySettings,
+  type ResolvedBackgroundActivitySettings,
+} from "@t3tools/shared/backgroundActivitySettings";
+import * as DateTime from "effect/DateTime";
+import * as Context from "effect/Context";
+import * as Effect from "effect/Effect";
+import * as Layer from "effect/Layer";
+import * as PubSub from "effect/PubSub";
+import * as Ref from "effect/Ref";
+import * as Stream from "effect/Stream";
+
+import { ServerSettingsService } from "../serverSettings.ts";
+import * as HostPowerMonitor from "./HostPowerMonitor.ts";
+
+export interface BackgroundPolicyShape {
+  readonly reportClientActivity: (
+    sessionId: AuthSessionId,
+    rpcClientId: RpcClientId,
+    input: ClientActivityReportInput,
+  ) => Effect.Effect<void>;
+  readonly removeRpcClient: (rpcClientId: RpcClientId) => Effect.Effect<void>;
+  readonly reportHostPowerState: (snapshot: HostPowerSnapshot) => Effect.Effect<void>;
+  readonly snapshot: Effect.Effect<BackgroundPolicySnapshot>;
+  readonly streamChanges: Stream.Stream<BackgroundPolicySnapshot>;
+  readonly hasDemand: (scope: BackgroundScope) => Effect.Effect<boolean>;
+  readonly shouldRunScopeWork: (scope: BackgroundScope) => Effect.Effect<boolean>;
+  readonly shouldRunOpportunisticWork: Effect.Effect<boolean>;
+}
+
+export class BackgroundPolicy extends Context.Service<BackgroundPolicy, BackgroundPolicyShape>()(
+  "t3/background/BackgroundPolicy",
+) {}
+
+const DEFAULT_LEASE_TTL_MS = 45_000;
M
macroscopeappBotJul 28, 2026

🟡 Medium background/BackgroundPolicy.ts:45

reportClientActivity always uses the hard-coded DEFAULT_LEASE_TTL_MS (45s) as the lease TTL fallback and never reads ResolvedBackgroundActivitySettings.idleClientTtl. That setting is otherwise unused, so changing the configured idle-client TTL has no effect: leases remain active for 45 seconds regardless of the configured value, causing background work to run for the wrong duration after a client becomes idle. The fix is to source the default TTL from the resolved settings rather than the hard-coded constant.

No longer relevant as of 3f9ae0646c6f221e911a1ed4ede46a45cb361fc0

apps/server/src/resourceTelemetry/ResourceMonitorBinary.ts · Earlier revision deb3d483 · right 167

Original diff
@@ -0,0 +1,193 @@
+import {
+  HostProcessArchitecture,
+  HostProcessEnvironment,
+  HostProcessPlatform,
+} from "@t3tools/shared/hostProcess";
+import * as Context from "effect/Context";
+import * as Effect from "effect/Effect";
+import * as FileSystem from "effect/FileSystem";
+import * as Layer from "effect/Layer";
+import * as Option from "effect/Option";
+import * as Path from "effect/Path";
+import * as Schema from "effect/Schema";
+
+import { ServerConfig } from "../config.ts";
+
+export class ResourceMonitorBinaryUnsupported extends Schema.TaggedErrorClass<ResourceMonitorBinaryUnsupported>()(
+  "ResourceMonitorBinaryUnsupported",
+  {
+    platform: Schema.String,
+    architecture: Schema.String,
+  },
+) {
+  override get message(): string {
+    return `Resource monitoring is unsupported on ${this.platform}/${this.architecture}.`;
+  }
+}
+
+export class ResourceMonitorBinaryNotFound extends Schema.TaggedErrorClass<ResourceMonitorBinaryNotFound>()(
+  "ResourceMonitorBinaryNotFound",
+  {
+    platform: Schema.String,
+    architecture: Schema.String,
+    candidates: Schema.Array(Schema.String),
+  },
+) {
+  override get message(): string {
+    return `Resource monitor binary was not found for ${this.platform}/${this.architecture}.`;
+  }
+}
+
+export class ResourceMonitorBinaryNotExecutable extends Schema.TaggedErrorClass<ResourceMonitorBinaryNotExecutable>()(
+  "ResourceMonitorBinaryNotExecutable",
+  {
+    path: Schema.String,
+    mode: Schema.Number,
+  },
+) {
+  override get message(): string {
+    return `Resource monitor binary at '${this.path}' is not executable.`;
+  }
+}
+
+export type ResourceMonitorBinaryError =
+  | ResourceMonitorBinaryUnsupported
+  | ResourceMonitorBinaryNotFound
+  | ResourceMonitorBinaryNotExecutable;
+
+export interface ResourceMonitorBinaryShape {
+  readonly resolve: Effect.Effect<string, ResourceMonitorBinaryError>;
+}
+
+export class ResourceMonitorBinary extends Context.Service<
+  ResourceMonitorBinary,
+  ResourceMonitorBinaryShape
+>()("t3/resourceTelemetry/ResourceMonitorBinary") {}
+
+function binaryName(platform: NodeJS.Platform): string {
+  return platform === "win32" ? "t3-resource-monitor.exe" : "t3-resource-monitor";
+}
+
+export function resourceMonitorPlatformKey(
+  platform: NodeJS.Platform,
+  architecture: NodeJS.Architecture,
+): string | undefined {
+  if (
+    (platform !== "darwin" && platform !== "linux" && platform !== "win32") ||
+    (architecture !== "arm64" && architecture !== "x64")
+  ) {
+    return undefined;
+  }
+  return `${platform}-${architecture}`;
+}
+
+function resourceMonitorRustTarget(
+  platform: NodeJS.Platform,
+  architecture: NodeJS.Architecture,
+): string | undefined {
+  if (platform === "darwin") {
+    return architecture === "arm64"
+      ? "aarch64-apple-darwin"
+      : architecture === "x64"
+        ? "x86_64-apple-darwin"
+        : undefined;
+  }
+  if (platform === "linux") {
+    return architecture === "arm64"
+      ? "aarch64-unknown-linux-gnu"
+      : architecture === "x64"
+        ? "x86_64-unknown-linux-gnu"
+        : undefined;
+  }
+  if (platform === "win32") {
+    return architecture === "arm64"
+      ? "aarch64-pc-windows-msvc"
+      : architecture === "x64"
+        ? "x86_64-pc-windows-msvc"
+        : undefined;
+  }
+  return undefined;
+}
+
+export const make = Effect.fn("resourceTelemetry.resourceMonitorBinary.make")(function* () {
+  const config = yield* ServerConfig;
+  const fileSystem = yield* FileSystem.FileSystem;
+  const path = yield* Path.Path;
+  const platform = yield* HostProcessPlatform;
+  const architecture = yield* HostProcessArchitecture;
+  const environment = yield* HostProcessEnvironment;
+  const executableName = binaryName(platform);
+  const platformKey = resourceMonitorPlatformKey(platform, architecture);
+  const rustTarget = resourceMonitorRustTarget(platform, architecture);
+  if (platformKey === undefined || rustTarget === undefined) {
+    return ResourceMonitorBinary.of({
+      resolve: Effect.fail(
+        new ResourceMonitorBinaryUnsupported({
+          platform,
+          architecture,
+        }),
+      ),
+    });
+  }
+
+  const candidates = [
+    environment.T3CODE_RESOURCE_MONITOR_PATH,
+    config.resourceMonitorPath,
+    path.resolve(import.meta.dirname, "resource-monitor", platformKey, executableName),
+    path.resolve(import.meta.dirname, "resource-monitor", executableName),
+    path.resolve(import.meta.dirname, "../resource-monitor", executableName),
+    path.resolve(
+      import.meta.dirname,
+      "../../../../native/resource-monitor/target",
+      rustTarget,
+      "release",
+      executableName,
+    ),
+    path.resolve(
+      import.meta.dirname,
+      "../../../native/resource-monitor/target",
+      rustTarget,
+      "release",
+      executableName,
+    ),
+    path.resolve(
+      import.meta.dirname,
+      "../../../../native/resource-monitor/target/release",
+      executableName,
+    ),
+    path.resolve(
+      import.meta.dirname,
+      "../../../../native/resource-monitor/target/debug",
+      executableName,
+    ),
+  ].filter((candidate): candidate is string => Boolean(candidate));
+
+  const resolve: ResourceMonitorBinaryShape["resolve"] = Effect.gen(function* () {
+    for (const candidate of candidates) {
+      const exists = yield* fileSystem.exists(candidate).pipe(Effect.orElseSucceed(() => false));
M
macroscopeappBotJul 28, 2026

🟡 Medium resourceTelemetry/ResourceMonitorBinary.ts:167

resolve checks only that a candidate path exists and (on Unix) that it has an execute bit — it never verifies the path is a regular file. A T3CODE_RESOURCE_MONITOR_PATH or resourceMonitorPath pointing at a directory is returned as the monitor executable, so the caller later tries to spawn a directory and telemetry silently fails instead of getting a resolution error. Consider checking stat.isFile() (or the equivalent on the FileSystem API) before accepting a candidate.

No longer relevant as of 3f9ae0646c6f221e911a1ed4ede46a45cb361fc0

apps/server/src/resourceTelemetry/ResourceMonitorBinary.ts · Earlier revision 4a875dbe · right 172–177

Original diff
@@ -0,0 +1,193 @@
+import {
+  HostProcessArchitecture,
+  HostProcessEnvironment,
+  HostProcessPlatform,
+} from "@t3tools/shared/hostProcess";
+import * as Context from "effect/Context";
+import * as Effect from "effect/Effect";
+import * as FileSystem from "effect/FileSystem";
+import * as Layer from "effect/Layer";
+import * as Option from "effect/Option";
+import * as Path from "effect/Path";
+import * as Schema from "effect/Schema";
+
+import { ServerConfig } from "../config.ts";
+
+export class ResourceMonitorBinaryUnsupported extends Schema.TaggedErrorClass<ResourceMonitorBinaryUnsupported>()(
+  "ResourceMonitorBinaryUnsupported",
+  {
+    platform: Schema.String,
+    architecture: Schema.String,
+  },
+) {
+  override get message(): string {
+    return `Resource monitoring is unsupported on ${this.platform}/${this.architecture}.`;
+  }
+}
+
+export class ResourceMonitorBinaryNotFound extends Schema.TaggedErrorClass<ResourceMonitorBinaryNotFound>()(
+  "ResourceMonitorBinaryNotFound",
+  {
+    platform: Schema.String,
+    architecture: Schema.String,
+    candidates: Schema.Array(Schema.String),
+  },
+) {
+  override get message(): string {
+    return `Resource monitor binary was not found for ${this.platform}/${this.architecture}.`;
+  }
+}
+
+export class ResourceMonitorBinaryNotExecutable extends Schema.TaggedErrorClass<ResourceMonitorBinaryNotExecutable>()(
+  "ResourceMonitorBinaryNotExecutable",
+  {
+    path: Schema.String,
+    mode: Schema.Number,
+  },
+) {
+  override get message(): string {
+    return `Resource monitor binary at '${this.path}' is not executable.`;
+  }
+}
+
+export type ResourceMonitorBinaryError =
+  | ResourceMonitorBinaryUnsupported
+  | ResourceMonitorBinaryNotFound
+  | ResourceMonitorBinaryNotExecutable;
+
+export interface ResourceMonitorBinaryShape {
+  readonly resolve: Effect.Effect<string, ResourceMonitorBinaryError>;
+}
+
+export class ResourceMonitorBinary extends Context.Service<
+  ResourceMonitorBinary,
+  ResourceMonitorBinaryShape
+>()("t3/resourceTelemetry/ResourceMonitorBinary") {}
+
+function binaryName(platform: NodeJS.Platform): string {
+  return platform === "win32" ? "t3-resource-monitor.exe" : "t3-resource-monitor";
+}
+
+export function resourceMonitorPlatformKey(
+  platform: NodeJS.Platform,
+  architecture: NodeJS.Architecture,
+): string | undefined {
+  if (
+    (platform !== "darwin" && platform !== "linux" && platform !== "win32") ||
+    (architecture !== "arm64" && architecture !== "x64")
+  ) {
+    return undefined;
+  }
+  return `${platform}-${architecture}`;
+}
+
+function resourceMonitorRustTarget(
+  platform: NodeJS.Platform,
+  architecture: NodeJS.Architecture,
+): string | undefined {
+  if (platform === "darwin") {
+    return architecture === "arm64"
+      ? "aarch64-apple-darwin"
+      : architecture === "x64"
+        ? "x86_64-apple-darwin"
+        : undefined;
+  }
+  if (platform === "linux") {
+    return architecture === "arm64"
+      ? "aarch64-unknown-linux-gnu"
+      : architecture === "x64"
+        ? "x86_64-unknown-linux-gnu"
+        : undefined;
+  }
+  if (platform === "win32") {
+    return architecture === "arm64"
+      ? "aarch64-pc-windows-msvc"
+      : architecture === "x64"
+        ? "x86_64-pc-windows-msvc"
+        : undefined;
+  }
+  return undefined;
+}
+
+export const make = Effect.fn("resourceTelemetry.resourceMonitorBinary.make")(function* () {
+  const config = yield* ServerConfig;
+  const fileSystem = yield* FileSystem.FileSystem;
+  const path = yield* Path.Path;
+  const platform = yield* HostProcessPlatform;
+  const architecture = yield* HostProcessArchitecture;
+  const environment = yield* HostProcessEnvironment;
+  const executableName = binaryName(platform);
+  const platformKey = resourceMonitorPlatformKey(platform, architecture);
+  const rustTarget = resourceMonitorRustTarget(platform, architecture);
+  if (platformKey === undefined || rustTarget === undefined) {
+    return ResourceMonitorBinary.of({
+      resolve: Effect.fail(
+        new ResourceMonitorBinaryUnsupported({
+          platform,
+          architecture,
+        }),
+      ),
+    });
+  }
+
+  const candidates = [
+    environment.T3CODE_RESOURCE_MONITOR_PATH,
+    config.resourceMonitorPath,
+    path.resolve(import.meta.dirname, "resource-monitor", platformKey, executableName),
+    path.resolve(import.meta.dirname, "resource-monitor", executableName),
+    path.resolve(import.meta.dirname, "../resource-monitor", executableName),
+    path.resolve(
+      import.meta.dirname,
+      "../../../../native/resource-monitor/target",
+      rustTarget,
+      "release",
+      executableName,
+    ),
+    path.resolve(
+      import.meta.dirname,
+      "../../../native/resource-monitor/target",
+      rustTarget,
+      "release",
+      executableName,
+    ),
+    path.resolve(
+      import.meta.dirname,
+      "../../../../native/resource-monitor/target/release",
+      executableName,
+    ),
+    path.resolve(
+      import.meta.dirname,
+      "../../../../native/resource-monitor/target/debug",
+      executableName,
+    ),
+  ].filter((candidate): candidate is string => Boolean(candidate));
+
+  const resolve: ResourceMonitorBinaryShape["resolve"] = Effect.gen(function* () {
+    for (const candidate of candidates) {
+      const exists = yield* fileSystem.exists(candidate).pipe(Effect.orElseSucceed(() => false));
+      if (!exists) continue;
+
+      if (platform !== "win32") {
+        const stat = yield* fileSystem.stat(candidate).pipe(Effect.option);
+        if (Option.isSome(stat) && (stat.value.mode & 0o111) === 0) {
+          return yield* new ResourceMonitorBinaryNotExecutable({
+            path: candidate,
+            mode: stat.value.mode,
+          });
+        }

apps/server/src/resourceTelemetry/ResourceMonitorBinary.ts · right 172–177

M
macroscopeappBotJun 19, 2026

🟢 Low resourceTelemetry/ResourceMonitorBinary.ts:172

When a candidate exists but is not executable, the code immediately returns ResourceMonitorBinaryNotExecutable, aborting the loop and preventing later fallback candidates from being checked. If a valid executable exists further down the list, it is never reached. Consider using continue to keep checking the remaining candidates.

No longer relevant as of 3f9ae0646c6f221e911a1ed4ede46a45cb361fc0

apps/server/src/resourceTelemetry/NativeTelemetryClient.ts · Earlier revision deb3d483 · right 504

Original diff
@@ -0,0 +1,922 @@
+import type {
+  HostPowerSnapshot,
+  ResourceMonitorCapabilities,
+  ResourceMonitorCommand,
+  ResourceMonitorEvent,
+  ResourceMonitorExternalProcess,
+  ResourceMonitorHelloEvent,
+  ResourceMonitorSnapshotEvent,
+  ResourceTelemetrySourceStatus,
+} from "@t3tools/contracts";
+import {
+  RESOURCE_MONITOR_PROTOCOL_VERSION,
+  ResourceMonitorCommand as ResourceMonitorCommandSchema,
+  ResourceMonitorEvent as ResourceMonitorEventSchema,
+} from "@t3tools/contracts";
+import * as Cause from "effect/Cause";
+import * as Context from "effect/Context";
+import * as Crypto from "effect/Crypto";
+import * as DateTime from "effect/DateTime";
+import * as Deferred from "effect/Deferred";
+import * as Duration from "effect/Duration";
+import * as Effect from "effect/Effect";
+import * as Fiber from "effect/Fiber";
+import * as Layer from "effect/Layer";
+import * as Option from "effect/Option";
+import * as PubSub from "effect/PubSub";
+import * as Queue from "effect/Queue";
+import * as Ref from "effect/Ref";
+import * as Result from "effect/Result";
+import * as Schema from "effect/Schema";
+import * as Semaphore from "effect/Semaphore";
+import * as Stream from "effect/Stream";
+import * as Ndjson from "effect/unstable/encoding/Ndjson";
+import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
+
+import * as ResourceMonitorBinary from "./ResourceMonitorBinary.ts";
+import { ServerConfig } from "../config.ts";
+
+const SAMPLE_INTERVAL_MS = 1_000;
+const UNKNOWN_BACKGROUND_SAMPLE_INTERVAL_MS = 5_000;
+const BATTERY_SAMPLE_INTERVAL_MS = 5_000;
+const CONSTRAINED_SAMPLE_INTERVAL_MS = 15_000;
+const HANDSHAKE_TIMEOUT = Duration.seconds(5);
+const SAMPLE_REQUEST_TIMEOUT = Duration.seconds(5);
+const HISTORY_REQUEST_TIMEOUT = Duration.seconds(15);
+const INITIAL_RESTART_DELAY = Duration.millis(500);
+const MAX_RESTART_DELAY = Duration.seconds(10);
+const FAILURE_WINDOW_MS = 60_000;
+const MAX_FAILURES_PER_WINDOW = 5;
+
+export class NativeTelemetrySpawnFailed extends Schema.TaggedErrorClass<NativeTelemetrySpawnFailed>()(
+  "NativeTelemetrySpawnFailed",
+  {
+    path: Schema.String,
+    cause: Schema.Defect(),
+  },
+) {
+  override get message(): string {
+    return `Failed to start resource monitor '${this.path}'.`;
+  }
+}
+
+export class NativeTelemetryHandshakeTimedOut extends Schema.TaggedErrorClass<NativeTelemetryHandshakeTimedOut>()(
+  "NativeTelemetryHandshakeTimedOut",
+  {
+    timeoutMs: Schema.Number,
+  },
+) {
+  override get message(): string {
+    return `Resource monitor handshake timed out after ${this.timeoutMs}ms.`;
+  }
+}
+
+export class NativeTelemetryProtocolMismatch extends Schema.TaggedErrorClass<NativeTelemetryProtocolMismatch>()(
+  "NativeTelemetryProtocolMismatch",
+  {
+    expectedVersion: Schema.Number,
+    receivedVersion: Schema.Number,
+  },
+) {
+  override get message(): string {
+    return `Resource monitor protocol ${this.receivedVersion} is incompatible with expected protocol ${this.expectedVersion}.`;
+  }
+}
+
+export class NativeTelemetryDecodeFailed extends Schema.TaggedErrorClass<NativeTelemetryDecodeFailed>()(
+  "NativeTelemetryDecodeFailed",
+  {
+    cause: Schema.Defect(),
+  },
+) {
+  override get message(): string {
+    return "Failed to decode resource monitor output.";
+  }
+}
+
+export class NativeTelemetryCommandFailed extends Schema.TaggedErrorClass<NativeTelemetryCommandFailed>()(
+  "NativeTelemetryCommandFailed",
+  {
+    operation: Schema.String,
+    cause: Schema.Defect(),
+  },
+) {
+  override get message(): string {
+    return `Resource monitor command '${this.operation}' failed.`;
+  }
+}
+
+export class NativeTelemetryExited extends Schema.TaggedErrorClass<NativeTelemetryExited>()(
+  "NativeTelemetryExited",
+  {
+    exitCode: Schema.Number,
+  },
+) {
+  override get message(): string {
+    return `Resource monitor exited with code ${this.exitCode}.`;
+  }
+}
+
+export class NativeTelemetryUnavailable extends Schema.TaggedErrorClass<NativeTelemetryUnavailable>()(
+  "NativeTelemetryUnavailable",
+  {
+    reason: Schema.String,
+  },
+) {
+  override get message(): string {
+    return `Resource monitor is unavailable: ${this.reason}`;
+  }
+}
+
+export type NativeTelemetryClientError =
+  | NativeTelemetrySpawnFailed
+  | NativeTelemetryHandshakeTimedOut
+  | NativeTelemetryProtocolMismatch
+  | NativeTelemetryDecodeFailed
+  | NativeTelemetryCommandFailed
+  | NativeTelemetryExited
+  | NativeTelemetryUnavailable;
+
+export interface NativeTelemetryClientHealth {
+  readonly status: ResourceTelemetrySourceStatus;
+  readonly hello: Option.Option<ResourceMonitorHelloEvent>;
+  readonly lastSampleAt: Option.Option<DateTime.Utc>;
+  readonly lastError: Option.Option<string>;
+  readonly restartCount: number;
+  readonly sampleIntervalMs: number;
+}
+
+export interface NativeTelemetryClientShape {
+  readonly capabilities: Effect.Effect<ResourceMonitorCapabilities, NativeTelemetryClientError>;
+  readonly snapshots: Stream.Stream<ResourceMonitorSnapshotEvent, NativeTelemetryClientError>;
+  readonly readHistory: (
+    windowMs: number,
+  ) => Effect.Effect<ReadonlyArray<ResourceMonitorSnapshotEvent>, NativeTelemetryClientError>;
+  readonly setExternalProcesses: (
+    processes: ReadonlyArray<ResourceMonitorExternalProcess>,
+  ) => Effect.Effect<void, NativeTelemetryClientError>;
+  readonly setHostPowerState: (
+    snapshot: HostPowerSnapshot,
+  ) => Effect.Effect<void, NativeTelemetryClientError>;
+  readonly sampleNow: Effect.Effect<ResourceMonitorSnapshotEvent, NativeTelemetryClientError>;
+  readonly retry: Effect.Effect<boolean>;
+  readonly health: Effect.Effect<NativeTelemetryClientHealth>;
+  readonly healthChanges: Stream.Stream<NativeTelemetryClientHealth>;
+}
+
+export class NativeTelemetryClient extends Context.Service<
+  NativeTelemetryClient,
+  NativeTelemetryClientShape
+>()("t3/resourceTelemetry/NativeTelemetryClient") {}
+
+interface ClientState {
+  readonly status: ResourceTelemetrySourceStatus;
+  readonly handle: Option.Option<ChildProcessSpawner.ChildProcessHandle>;
+  readonly hello: Option.Option<ResourceMonitorHelloEvent>;
+  readonly lastSampleAt: Option.Option<DateTime.Utc>;
+  readonly lastError: Option.Option<string>;
+  readonly restartCount: number;
+}
+
+interface CollectionControl {
+  readonly hostPower: HostPowerSnapshot;
+  readonly liveSubscriberCount: number;
+  readonly sampleIntervalMs: number;
+}
+
+interface PendingHistoryRequest {
+  readonly deferred: Deferred.Deferred<
+    ReadonlyArray<ResourceMonitorSnapshotEvent>,
+    NativeTelemetryClientError
+  >;
+  readonly snapshots: ReadonlyArray<ResourceMonitorSnapshotEvent>;
+}
+
+const initialState: ClientState = {
+  status: "starting",
+  handle: Option.none(),
+  hello: Option.none(),
+  lastSampleAt: Option.none(),
+  lastError: Option.none(),
+  restartCount: 0,
+};
+
+function toHealth(state: ClientState, sampleIntervalMs: number): NativeTelemetryClientHealth {
+  return {
+    status: state.status,
+    hello: state.hello,
+    lastSampleAt: state.lastSampleAt,
+    lastError: state.lastError,
+    restartCount: state.restartCount,
+    sampleIntervalMs,
+  };
+}
+
+function isThermallyConstrained(snapshot: HostPowerSnapshot): boolean {
+  return snapshot.thermalState === "serious" || snapshot.thermalState === "critical";
+}
+
+export function resolveNativeSampleIntervalMs(
+  snapshot: HostPowerSnapshot,
+  liveSubscriberCount: number,
+): number {
+  if (snapshot.stale || snapshot.source === "unknown") {
+    return liveSubscriberCount > 0 ? SAMPLE_INTERVAL_MS : UNKNOWN_BACKGROUND_SAMPLE_INTERVAL_MS;
+  }
+  if (snapshot.suspended) return 0;
+  if (
+    snapshot.locked === "true" ||
+    snapshot.lowPowerMode === "true" ||
+    isThermallyConstrained(snapshot)
+  ) {
+    return CONSTRAINED_SAMPLE_INTERVAL_MS;
+  }
+  if (snapshot.onBattery === "true") return BATTERY_SAMPLE_INTERVAL_MS;
+  return SAMPLE_INTERVAL_MS;
+}
+
+const decodeMonitorEvent: (
+  value: unknown,
+) => Effect.Effect<ResourceMonitorEvent, Schema.SchemaError> = Schema.decodeUnknownEffect(
+  ResourceMonitorEventSchema,
+);
+const encodeMonitorCommand = Schema.encodeEffect(
+  Schema.fromJsonString(ResourceMonitorCommandSchema),
+);
+const isProtocolMismatch = Schema.is(NativeTelemetryProtocolMismatch);
+const isDecodeFailed = Schema.is(NativeTelemetryDecodeFailed);
+const isCommandFailed = Schema.is(NativeTelemetryCommandFailed);
+
+function eventVersion(value: unknown): number | undefined {
+  if (typeof value !== "object" || value === null) return undefined;
+  const version = Reflect.get(value, "version");
+  return typeof version === "number" ? version : undefined;
+}
+
+function restartDelay(attempt: number): Duration.Duration {
+  return Duration.min(Duration.times(INITIAL_RESTART_DELAY, 2 ** attempt), MAX_RESTART_DELAY);
+}
+
+function errorMessage(error: NativeTelemetryClientError): string {
+  return error.message;
+}
+
+export const make = Effect.fn("resourceTelemetry.nativeTelemetryClient.make")(function* () {
+  const binary = yield* ResourceMonitorBinary.ResourceMonitorBinary;
+  const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
+  const crypto = yield* Crypto.Crypto;
+  const config = yield* ServerConfig;
+  const initializedAt = yield* DateTime.now;
+  const state = yield* Ref.make(initialState);
+  const collectionControl = yield* Ref.make<CollectionControl>({
+    hostPower: {
+      source: "unknown",
+      idle: "unknown",
+      idleSeconds: null,
+      locked: "unknown",
+      suspended: false,
+      onBattery: "unknown",
+      lowPowerMode: "unknown",
+      thermalState: "unknown",
+      stale: true,
+      updatedAt: initializedAt,
+    },
+    liveSubscriberCount: 0,
+    sampleIntervalMs: UNKNOWN_BACKGROUND_SAMPLE_INTERVAL_MS,
+  });
+  const externalProcesses = yield* Ref.make<ReadonlyArray<ResourceMonitorExternalProcess>>([]);
+  const pendingSamples = yield* Ref.make(
+    new Map<string, Deferred.Deferred<ResourceMonitorSnapshotEvent, NativeTelemetryClientError>>(),
+  );
+  const pendingHistories = yield* Ref.make(new Map<string, PendingHistoryRequest>());
+  const snapshots = yield* PubSub.sliding<ResourceMonitorSnapshotEvent>(8);
+  const healthChanges = yield* PubSub.sliding<NativeTelemetryClientHealth>(4);
+  const retryQueue = yield* Queue.sliding<void>(1);
+  const commandMutex = yield* Semaphore.make(1);
+  const currentHealth = Effect.all([Ref.get(state), Ref.get(collectionControl)]).pipe(
+    Effect.map(([current, control]) => toHealth(current, control.sampleIntervalMs)),
+  );
+  const publishHealth = currentHealth.pipe(
+    Effect.flatMap((health) => PubSub.publish(healthChanges, health)),
+    Effect.asVoid,
+  );
+
+  const failPending = (error: NativeTelemetryClientError) =>
+    Effect.gen(function* () {
+      const samples = yield* Ref.getAndSet(pendingSamples, new Map());
+      const histories = yield* Ref.getAndSet(pendingHistories, new Map());
+      yield* Effect.forEach(samples.values(), (deferred) => Deferred.fail(deferred, error), {
+        discard: true,
+      });
+      yield* Effect.forEach(
+        histories.values(),
+        (request) => Deferred.fail(request.deferred, error),
+        { discard: true },
+      );
+    });
+
+  const writeCommand = (
+    handle: ChildProcessSpawner.ChildProcessHandle,
+    command: ResourceMonitorCommand,
+  ): Effect.Effect<void, NativeTelemetryClientError> =>
+    commandMutex.withPermits(1)(
+      encodeMonitorCommand(command).pipe(
+        Effect.map((encoded) => `${encoded}\n`),
+        Effect.mapError(
+          (cause) =>
+            new NativeTelemetryCommandFailed({
+              operation: command.type,
+              cause,
+            }),
+        ),
+        Effect.flatMap((encoded) =>
+          Stream.run(Stream.encodeText(Stream.make(encoded)), handle.stdin),
+        ),
+        Effect.mapError(
+          (cause) =>
+            new NativeTelemetryCommandFailed({
+              operation: command.type,
+              cause,
+            }),
+        ),
+      ),
+    );
+
+  const processEvent = (
+    event: ResourceMonitorEvent,
+    helloDeferred: Deferred.Deferred<ResourceMonitorHelloEvent>,
+  ): Effect.Effect<void, NativeTelemetryClientError> => {
+    switch (event.type) {
+      case "hello":
+        return Ref.update(state, (current) => ({
+          ...current,
+          status: "starting" as const,
+          hello: Option.some(event),
+          lastError: Option.none(),
+        })).pipe(
+          Effect.andThen(publishHealth),
+          Effect.andThen(Deferred.succeed(helloDeferred, event)),
+          Effect.asVoid,
+        );
+      case "snapshot":
+        return Effect.gen(function* () {
+          const sampledAt = DateTime.makeUnsafe(event.sampledAtUnixMs);
+          yield* Ref.update(state, (current) => ({
+            ...current,
+            status: "healthy" as const,
+            lastSampleAt: Option.some(sampledAt),
+            lastError: Option.none(),
+          }));
+          yield* PubSub.publish(snapshots, event);
+          if (event.requestId) {
+            const deferred = yield* Ref.modify(pendingSamples, (pending) => {
+              const next = new Map(pending);
+              const current = next.get(event.requestId!);
+              next.delete(event.requestId!);
+              return [Option.fromUndefinedOr(current), next];
+            });
+            if (Option.isSome(deferred)) {
+              yield* Deferred.succeed(deferred.value, event);
+            }
+          }
+        });
+      case "historyChunk":
+        return Effect.gen(function* () {
+          const latestSnapshot = event.snapshots.at(-1);
+          if (latestSnapshot) {
+            yield* Ref.update(state, (current) => ({
+              ...current,
+              status: "healthy" as const,
+              lastSampleAt: Option.some(DateTime.makeUnsafe(latestSnapshot.sampledAtUnixMs)),
+              lastError: Option.none(),
+            }));
+          }
+          const completed = yield* Ref.modify(pendingHistories, (pending) => {
+            const request = pending.get(event.requestId);
+            if (!request) return [Option.none(), pending] as const;
+            const snapshots = [...request.snapshots, ...event.snapshots];
+            const next = new Map(pending);
+            if (event.done) {
+              next.delete(event.requestId);
+              return [Option.some({ deferred: request.deferred, snapshots }), next] as const;
+            }
+            next.set(event.requestId, { deferred: request.deferred, snapshots });
+            return [Option.none(), next] as const;
+          });
+          if (Option.isSome(completed)) {
+            yield* Deferred.succeed(completed.value.deferred, completed.value.snapshots);
+          }
+        });
+      case "error":
+        return Ref.update(state, (current) => ({
+          ...current,
+          status: "degraded" as const,
+          lastError: Option.some(event.message),
+        })).pipe(
+          Effect.andThen(publishHealth),
+          Effect.andThen(
+            event.recoverable
+              ? Effect.void
+              : Effect.fail(
+                  new NativeTelemetryCommandFailed({
+                    operation: event.code,
+                    cause: event.message,
+                  }),
+                ),
+          ),
+        );
+    }
+  };
+
+  const runAttempt: Effect.Effect<void, NativeTelemetryClientError> = Effect.scoped(
+    Effect.gen(function* () {
+      const executablePath = yield* binary.resolve.pipe(
+        Effect.mapError(
+          (error) =>
+            new NativeTelemetryUnavailable({
+              reason: error.message,
+            }),
+        ),
+      );
+      const command = ChildProcess.make(executablePath, [], {
+        cwd: config.cwd,
+        stdin: {
+          stream: "pipe",
+          endOnDone: false,
+        },
+        stdout: "pipe",
+        stderr: "pipe",
+        killSignal: "SIGTERM",
+        forceKillAfter: Duration.seconds(2),
+      });
+      const handle = yield* Effect.acquireRelease(
+        spawner
+          .spawn(command)
+          .pipe(
+            Effect.mapError(
+              (cause) => new NativeTelemetrySpawnFailed({ path: executablePath, cause }),
+            ),
+          ),
+        (child) => child.kill().pipe(Effect.ignore),
+      );
+      yield* Ref.update(state, (current) => ({
+        ...current,
+        status: "starting" as const,
+        handle: Option.some(handle),
+        hello: Option.none(),
+      }));
+      yield* publishHealth;
+
+      const helloDeferred = yield* Deferred.make<ResourceMonitorHelloEvent>();
+      const eventFiber = yield* handle.stdout.pipe(
+        Stream.pipeThroughChannel(Ndjson.decode({ ignoreEmptyLines: true })),
+        Stream.mapEffect(
+          (
+            value,
+          ): Effect.Effect<
+            ResourceMonitorEvent,
+            NativeTelemetryProtocolMismatch | NativeTelemetryDecodeFailed
+          > => {
+            const version = eventVersion(value);
+            if (version !== undefined && version !== RESOURCE_MONITOR_PROTOCOL_VERSION) {
+              return Effect.fail(
+                new NativeTelemetryProtocolMismatch({
+                  expectedVersion: RESOURCE_MONITOR_PROTOCOL_VERSION,
+                  receivedVersion: version,
+                }),
+              );
+            }
+            return decodeMonitorEvent(value).pipe(
+              Effect.mapError((cause) => new NativeTelemetryDecodeFailed({ cause })),
+            );
+          },
+        ),
+        Stream.runForEach((event) => processEvent(event, helloDeferred)),
+        Effect.mapError((cause) =>
+          isProtocolMismatch(cause) || isDecodeFailed(cause) || isCommandFailed(cause)
+            ? cause
+            : new NativeTelemetryDecodeFailed({ cause }),
+        ),
+        Effect.forkScoped,
+      );
+      yield* handle.stderr.pipe(Stream.runDrain, Effect.ignore, Effect.forkScoped);
+
+      const hello = yield* Deferred.await(helloDeferred).pipe(
M
macroscopeappBotJul 28, 2026

🟡 Medium resourceTelemetry/NativeTelemetryClient.ts:504

runAttempt waits for the handshake using only Deferred.await(helloDeferred) with a 5-second timeout, without racing it against eventFiber. If the child process exits, stdout closes, or a protocol/decode error occurs before the hello event arrives, eventFiber has already failed but the attempt stays blocked for the full timeout and then reports NativeTelemetryHandshakeTimedOut, hiding the real failure and delaying every restart by 5 seconds. Consider racing Deferred.await(helloDeferred) against Fiber.join(eventFiber) so the actual error propagates immediately.

No longer relevant as of 3f9ae0646c6f221e911a1ed4ede46a45cb361fc0

apps/desktop/src/telemetry/DesktopTelemetryPublisher.ts · Earlier revision 38797e97 · right 246

Original diff
@@ -0,0 +1,264 @@
+import {
+  DesktopHostTelemetryMessage,
+  type DesktopHostTelemetrySnapshot,
+  type DesktopTelemetryControlMessage,
+  type HostPowerSnapshot,
+} from "@t3tools/contracts";
+import * as Context from "effect/Context";
+import * as DateTime from "effect/DateTime";
+import * as Duration from "effect/Duration";
+import * as Effect from "effect/Effect";
+import * as Layer from "effect/Layer";
+import * as Option from "effect/Option";
+import * as PubSub from "effect/PubSub";
+import * as Queue from "effect/Queue";
+import * as Ref from "effect/Ref";
+import * as Schema from "effect/Schema";
+import * as Stream from "effect/Stream";
+
+import * as ElectronApp from "../electron/ElectronApp.ts";
+import * as ElectronPowerMonitor from "../electron/ElectronPowerMonitor.ts";
+
+const LIVE_SAMPLE_INTERVAL = Duration.seconds(1);
+const BATTERY_SAMPLE_INTERVAL = Duration.seconds(5);
+const CONSTRAINED_SAMPLE_INTERVAL = Duration.seconds(15);
+const BACKGROUND_HEARTBEAT_INTERVAL = Duration.seconds(30);
+const IDLE_THRESHOLD_SECONDS = 60;
+const encodeMessage = Schema.encodeSync(Schema.fromJsonString(DesktopHostTelemetryMessage));
+const textEncoder = new TextEncoder();
+
+type PowerEvent =
+  | { readonly type: "locked"; readonly value: boolean }
+  | { readonly type: "suspended"; readonly value: boolean }
+  | { readonly type: "onBattery"; readonly value: boolean }
+  | { readonly type: "thermal"; readonly value: HostPowerSnapshot["thermalState"] }
+  | { readonly type: "speedLimit"; readonly value: number };
+
+interface PowerState {
+  readonly locked: HostPowerSnapshot["locked"];
+  readonly suspended: boolean;
+  readonly onBattery: HostPowerSnapshot["onBattery"];
+  readonly thermalState: HostPowerSnapshot["thermalState"];
+  readonly speedLimitPercent: Option.Option<number>;
+}
+
+export interface DesktopTelemetryPublisherShape {
+  readonly latest: Effect.Effect<Option.Option<DesktopHostTelemetrySnapshot>>;
+  readonly changes: Stream.Stream<DesktopHostTelemetrySnapshot>;
+  readonly encoded: Stream.Stream<Uint8Array>;
+  readonly handleControl: (message: DesktopTelemetryControlMessage) => Effect.Effect<void>;
+}
+
+export class DesktopTelemetryPublisher extends Context.Service<
+  DesktopTelemetryPublisher,
+  DesktopTelemetryPublisherShape
+>()("@t3tools/desktop/telemetry/DesktopTelemetryPublisher") {}
+
+function booleanState(value: boolean): HostPowerSnapshot["onBattery"] {
+  return value ? "true" : "false";
+}
+
+function idleState(value: ElectronPowerMonitor.ElectronIdleState): HostPowerSnapshot["idle"] {
+  switch (value) {
+    case "active":
+      return "false";
+    case "idle":
+    case "locked":
+      return "true";
+    case "unknown":
+      return "unknown";
+  }
+}
+
+function updatePowerState(state: PowerState, event: PowerEvent): PowerState {
+  switch (event.type) {
+    case "locked":
+      return { ...state, locked: booleanState(event.value) };
+    case "suspended":
+      return { ...state, suspended: event.value };
+    case "onBattery":
+      return { ...state, onBattery: booleanState(event.value) };
+    case "thermal":
+      return { ...state, thermalState: event.value };
+    case "speedLimit":
+      return { ...state, speedLimitPercent: Option.some(event.value) };
+  }
+}
+
+function sampleInterval(power: PowerState, diagnosticsDemand: boolean): Duration.Duration {
+  if (!diagnosticsDemand) return BACKGROUND_HEARTBEAT_INTERVAL;
+  if (
+    power.suspended ||
+    power.locked === "true" ||
+    power.thermalState === "serious" ||
+    power.thermalState === "critical"
+  ) {
+    return CONSTRAINED_SAMPLE_INTERVAL;
+  }
+  if (power.onBattery === "true") return BATTERY_SAMPLE_INTERVAL;
+  return LIVE_SAMPLE_INTERVAL;
+}
+
+export const make = Effect.fn("desktop.telemetryPublisher.make")(function* () {
+  const electronApp = yield* ElectronApp.ElectronApp;
+  const powerMonitor = yield* ElectronPowerMonitor.ElectronPowerMonitor;
+  yield* electronApp.whenReady;
+
+  const initialPowerState: PowerState = {
+    locked: "unknown",
+    suspended: false,
+    onBattery: booleanState(yield* powerMonitor.isOnBatteryPower),
+    thermalState: yield* powerMonitor.getCurrentThermalState,
+    speedLimitPercent: Option.none(),
+  };
+  const powerState = yield* Ref.make(initialPowerState);
+  const powerEvents = yield* Queue.unbounded<PowerEvent>();
+  const sampleTriggers = yield* Queue.sliding<void>(1);
+  const diagnosticsDemand = yield* Ref.make(false);
+  const latest = yield* Ref.make(Option.none<DesktopHostTelemetrySnapshot>());
+  const changes = yield* PubSub.sliding<DesktopHostTelemetrySnapshot>(8);
+  const sequence = yield* Ref.make(0);
+
+  const offer = (event: PowerEvent): void => {
+    Queue.offerUnsafe(powerEvents, event);
+  };
+  yield* Effect.all(
+    [
+      powerMonitor.onSimpleEvent("lock-screen", () => offer({ type: "locked", value: true })),
+      powerMonitor.onSimpleEvent("unlock-screen", () => offer({ type: "locked", value: false })),
+      powerMonitor.onSimpleEvent("suspend", () => offer({ type: "suspended", value: true })),
+      powerMonitor.onSimpleEvent("resume", () => offer({ type: "suspended", value: false })),
+      powerMonitor.onSimpleEvent("on-battery", () => offer({ type: "onBattery", value: true })),
+      powerMonitor.onSimpleEvent("on-ac", () => offer({ type: "onBattery", value: false })),
+      powerMonitor.onThermalStateChange((value) => offer({ type: "thermal", value })),
+      powerMonitor.onSpeedLimitChange((value) => offer({ type: "speedLimit", value })),
+    ],
+    { concurrency: "unbounded" },
+  );
+  yield* Effect.forever(
+    Queue.take(powerEvents).pipe(
+      Effect.flatMap((event) => Ref.update(powerState, (state) => updatePowerState(state, event))),
+      Effect.andThen(Queue.offer(sampleTriggers, undefined)),
+    ),
+  ).pipe(Effect.forkScoped);
+
+  const sampleOnce = Effect.gen(function* () {
+    const sampledAt = yield* DateTime.now;
+    const sampledAtUnixMs = DateTime.toEpochMillis(sampledAt);
+    const demand = yield* Ref.get(diagnosticsDemand);
+    const [currentPower, idleSeconds, systemIdleState, onBattery, metrics] = yield* Effect.all(
+      [
+        Ref.get(powerState),
+        powerMonitor.getSystemIdleTime,
+        powerMonitor.getSystemIdleState(IDLE_THRESHOLD_SECONDS),
+        powerMonitor.isOnBatteryPower,
+        demand ? electronApp.getAppMetrics : Effect.succeed([]),
+      ],
+      { concurrency: "unbounded" },
+    );
+    const nextSequence = yield* Ref.modify(sequence, (current) => [current + 1, current + 1]);
+    const locked = systemIdleState === "locked" ? "true" : currentPower.locked;
+    const snapshot: DesktopHostTelemetrySnapshot = {
+      version: 1,
+      type: "desktopTelemetry",
+      sequence: nextSequence,
+      sampledAtUnixMs,
+      electronPid: process.pid,
+      power: {
+        source: "electron-main",
+        idle: idleState(systemIdleState),
+        idleSeconds,
+        locked,
+        suspended: currentPower.suspended,
+        onBattery: booleanState(onBattery),
+        lowPowerMode: "unknown",
+        thermalState: currentPower.thermalState,
+        stale: false,
+        updatedAt: sampledAt,
+      },
+      speedLimitPercent: currentPower.speedLimitPercent,
+      electronProcesses: metrics.map((metric) => ({
+        pid: metric.pid,
+        creationTimeMs: metric.creationTime,
+        type: metric.type,
+        ...(metric.name === undefined ? {} : { name: metric.name }),
+        ...(metric.serviceName === undefined ? {} : { serviceName: metric.serviceName }),
+        cpuPercent: metric.cpu.percentCPUUsage,
+        ...(metric.cpu.cumulativeCPUUsage === undefined
+          ? {}
+          : { cumulativeCpuSeconds: metric.cpu.cumulativeCPUUsage }),
+        idleWakeupsPerSecond: metric.cpu.idleWakeupsPerSecond,
+        workingSetBytes: Math.max(0, Math.round(metric.memory.workingSetSize * 1024)),
+        peakWorkingSetBytes: Math.max(0, Math.round(metric.memory.peakWorkingSetSize * 1024)),
+      })),
+    };
+
+    yield* Ref.set(latest, Option.some(snapshot));
+    yield* PubSub.publish(changes, snapshot);
+  }).pipe(
+    Effect.catchCause((cause) =>
+      Effect.logWarning("Failed to sample Electron telemetry", {
+        cause: String(cause),
+      }),
+    ),
+  );
+
+  yield* Effect.gen(function* () {
+    yield* sampleOnce;
+    while (true) {
+      const [currentPower, demand] = yield* Effect.all([
+        Ref.get(powerState),
+        Ref.get(diagnosticsDemand),
+      ]);
+      yield* Effect.raceFirst(
+        Queue.take(sampleTriggers),
+        Effect.sleep(sampleInterval(currentPower, demand)),
+      );
+      yield* sampleOnce;
+    }
+  }).pipe(Effect.forkScoped);
+
+  const handleControl: DesktopTelemetryPublisherShape["handleControl"] = (message) => {
+    switch (message.type) {
+      case "setDiagnosticsDemand":
+        return Ref.getAndSet(diagnosticsDemand, message.enabled).pipe(
+          Effect.flatMap((previous) =>
+            previous === message.enabled
+              ? Effect.void
+              : Queue.offer(sampleTriggers, undefined).pipe(Effect.asVoid),
+          ),
+        );
+    }
+  };
+
+  const snapshots = Stream.concat(
+    Stream.unwrap(
+      Ref.get(latest).pipe(
+        Effect.map(
+          Option.match({
+            onNone: () => Stream.empty,
+            onSome: Stream.make,
+          }),
+        ),
+      ),
+    ),
+    Stream.fromPubSub(changes),
+  );
C
cursorBotJul 28, 2026

Telemetry stream drops PubSub updates

Medium Severity

encoded builds its live stream by reading latest first and only then subscribing via Stream.fromPubSub(changes). Samples published in that gap are dropped, so a newly spawned backend can miss power or Electron metric updates until the next sample.

Reviewed by Cursor Bugbot for commit 38797e97c7798797cab825fca8004f5362bba3b4. Configure here.

apps/server/src/provider/Layers/EventNdjsonLogger.ts · Earlier revision deb3d483 · right 37

Original diff
@@ -1,56 +1,159 @@
 // @effect-diagnostics nodeBuiltinImport:off
 /**
- * Provider event logger helper.
+ * Best-effort provider event logging with one shared writer per thread.
  *
- * Best-effort writer for observability logs. Each record is formatted as a
- * single effect-style text line in a thread-scoped file. Failures are
- * downgraded to warnings so provider runtime behavior is unaffected.
+ * Native and canonical views share batching, rotation, and retention state so
+ * they cannot race while appending to the same thread-scoped file.
  */
 import * as NodeFS from "node:fs";
 import * as NodePath from "node:path";
 
 import type { ThreadId } from "@t3tools/contracts";
 import { RotatingFileSink } from "@t3tools/shared/logging";
 import { errorTag } from "@t3tools/shared/observability";
+import * as Clock from "effect/Clock";
+import * as DateTime from "effect/DateTime";
 import * as Effect from "effect/Effect";
 import * as Exit from "effect/Exit";
-import * as Logger from "effect/Logger";
 import * as Schema from "effect/Schema";
 import * as Scope from "effect/Scope";
 import * as SynchronizedRef from "effect/SynchronizedRef";
 
 import { toSafeThreadAttachmentSegment } from "../../attachmentStore.ts";
+import type { ResourceAttributionShape } from "../../resourceTelemetry/ResourceAttribution.ts";
 
-const DEFAULT_MAX_BYTES = 10 * 1024 * 1024;
+const MEBIBYTE = 1024 * 1024;
+const DAY_MS = 24 * 60 * 60 * 1_000;
+const DEFAULT_MAX_BYTES = 10 * MEBIBYTE;
 const DEFAULT_MAX_FILES = 10;
-const DEFAULT_BATCH_WINDOW_MS = 200;
+const DEFAULT_BATCH_WINDOW_MS = 1_000;
+const DEFAULT_MAX_TOTAL_BYTES = 512 * MEBIBYTE;
+const DEFAULT_MAX_AGE_MS = 14 * DAY_MS;
+const DEFAULT_RETENTION_CHECK_INTERVAL_MS = 5 * 60 * 1_000;
+const DEFAULT_MAX_BUFFERED_BYTES = MEBIBYTE;
+const DEFAULT_MAX_BUFFERED_RECORDS = 512;
 const GLOBAL_THREAD_SEGMENT = "_global";
 const LOG_SCOPE = "provider-observability";
+const PROVIDER_LOG_FILE_PATTERN = /\.log(?:\.\d+)?$/u;
M
macroscopeappBotJul 28, 2026

🟠 High Layers/EventNdjsonLogger.ts:37

enforceRetention deletes any file matching /\.log(?:\.\d+)?$/u in the log directory, including at startup when activeFilePaths is empty. If makeEventNdjsonLogStore is pointed at a shared log directory, retention deletes unrelated *.log files from other components. Consider scoping the pattern to a provider-specific prefix so only files owned by this store are eligible for deletion.

No longer relevant as of 56b763edb5f03ea538e7b72ee9699b19e44424cb

apps/server/src/background/BackgroundPolicy.ts · Earlier revision deb3d483 · right 238

Original diff
@@ -0,0 +1,274 @@
+import {
+  type AuthSessionId,
+  type BackgroundPolicySnapshot,
+  type BackgroundScope,
+  type ClientActivityLease,
+  type ClientActivityReportInput,
+  type HostPowerSnapshot,
+  type RpcClientId,
+} from "@t3tools/contracts";
+import {
+  getBackgroundActivityPresetSettings,
+  resolveServerBackgroundActivitySettings,
+  type ResolvedBackgroundActivitySettings,
+} from "@t3tools/shared/backgroundActivitySettings";
+import * as DateTime from "effect/DateTime";
+import * as Context from "effect/Context";
+import * as Effect from "effect/Effect";
+import * as Layer from "effect/Layer";
+import * as PubSub from "effect/PubSub";
+import * as Ref from "effect/Ref";
+import * as Stream from "effect/Stream";
+
+import { ServerSettingsService } from "../serverSettings.ts";
+import * as HostPowerMonitor from "./HostPowerMonitor.ts";
+
+export interface BackgroundPolicyShape {
+  readonly reportClientActivity: (
+    sessionId: AuthSessionId,
+    rpcClientId: RpcClientId,
+    input: ClientActivityReportInput,
+  ) => Effect.Effect<void>;
+  readonly removeRpcClient: (rpcClientId: RpcClientId) => Effect.Effect<void>;
+  readonly reportHostPowerState: (snapshot: HostPowerSnapshot) => Effect.Effect<void>;
+  readonly snapshot: Effect.Effect<BackgroundPolicySnapshot>;
+  readonly streamChanges: Stream.Stream<BackgroundPolicySnapshot>;
+  readonly hasDemand: (scope: BackgroundScope) => Effect.Effect<boolean>;
+  readonly shouldRunScopeWork: (scope: BackgroundScope) => Effect.Effect<boolean>;
+  readonly shouldRunOpportunisticWork: Effect.Effect<boolean>;
+}
+
+export class BackgroundPolicy extends Context.Service<BackgroundPolicy, BackgroundPolicyShape>()(
+  "t3/background/BackgroundPolicy",
+) {}
+
+const DEFAULT_LEASE_TTL_MS = 45_000;
+const MAX_LEASE_TTL_MS = 120_000;
+
+function scopeKey(scope: BackgroundScope): string {
+  switch (scope.type) {
+    case "server-config":
+    case "diagnostics":
+      return scope.type;
+    case "provider-status":
+      return scope.instanceId ? `${scope.type}:${scope.instanceId}` : scope.type;
+    case "vcs-status":
+    case "git-refs":
+      return `${scope.type}:${scope.cwd}`;
+    case "thread":
+      return `${scope.type}:${scope.threadId}`;
+  }
+}
+
+function isLeaseActive(lease: ClientActivityLease, now: DateTime.Utc): boolean {
+  return DateTime.isGreaterThan(lease.expiresAt, now);
+}
+
+function isForegroundLease(lease: ClientActivityLease, now: DateTime.Utc): boolean {
+  return isLeaseActive(lease, now) && lease.visible && lease.focused;
+}
+
+function leaseHasScope(lease: ClientActivityLease, scope: BackgroundScope): boolean {
+  const key = scopeKey(scope);
+  return lease.scopes.some((leaseScope) => scopeKey(leaseScope) === key);
+}
+
+function hasThermalPressure(hostPower: HostPowerSnapshot): boolean {
+  return hostPower.thermalState === "serious" || hostPower.thermalState === "critical";
+}
+
+function isHostConstrained(
+  hostPower: HostPowerSnapshot,
+  settings: ResolvedBackgroundActivitySettings,
+): boolean {
+  if (hostPower.stale) return false;
+  if (
+    (settings.pauseWhenHostLocked && hostPower.locked === "true") ||
+    hasThermalPressure(hostPower)
+  ) {
+    return true;
+  }
+  if (settings.pauseWhenHostLowPower && hostPower.lowPowerMode === "true") return true;
+  return settings.pauseWhenOnBattery && hostPower.onBattery === "true";
+}
+
+function isClientConstrained(
+  lease: ClientActivityLease,
+  settings: ResolvedBackgroundActivitySettings,
+): boolean {
+  if (settings.pauseWhenClientLowPower && lease.lowPowerMode === "true") return true;
+  return settings.pauseWhenOnBattery && lease.batteryState === "unplugged";
+}
+
+function leaseMayRunScopedWork(
+  lease: ClientActivityLease,
+  scope: BackgroundScope,
+  now: DateTime.Utc,
+  settings: ResolvedBackgroundActivitySettings,
+): boolean {
+  const activeWithScope = isLeaseActive(lease, now) && leaseHasScope(lease, scope);
+  if (!activeWithScope || isClientConstrained(lease, settings)) {
+    return false;
+  }
+  if (settings.profile === "performance") {
+    return true;
+  }
+  return isForegroundLease(lease, now);
+}
+
+function computeSnapshot(input: {
+  readonly hostPower: HostPowerSnapshot;
+  readonly leases: ReadonlyMap<string, ClientActivityLease>;
+  readonly now: DateTime.Utc;
+  readonly settings: ResolvedBackgroundActivitySettings;
+  readonly updatedAt: DateTime.Utc;
+}): BackgroundPolicySnapshot {
+  const activeLeases = [...input.leases.values()].filter((lease) =>
+    isLeaseActive(lease, input.now),
+  );
+  const foregroundLeases = activeLeases.filter((lease) => isForegroundLease(lease, input.now));
+  const activeScopeKeys = new Set<string>();
+  for (const lease of activeLeases) {
+    for (const scope of lease.scopes) {
+      activeScopeKeys.add(scopeKey(scope));
+    }
+  }
+
+  return {
+    hostPower: input.hostPower,
+    leases: activeLeases,
+    activeForegroundLeaseCount: foregroundLeases.length,
+    activeScopeKeys: [...activeScopeKeys].toSorted(),
+    shouldRunOpportunisticWork:
+      foregroundLeases.some((lease) => !isClientConstrained(lease, input.settings)) &&
+      !isHostConstrained(input.hostPower, input.settings),
+    updatedAt: input.updatedAt,
+  };
+}
+
+export const make = Effect.fn("background.policy.make")(function* () {
+  const hostPowerMonitor = yield* HostPowerMonitor.HostPowerMonitor;
+  const serverSettings = yield* ServerSettingsService;
+  const leasesRef = yield* Ref.make(new Map<string, ClientActivityLease>());
+  const changes = yield* PubSub.sliding<BackgroundPolicySnapshot>(1);
+
+  const backgroundActivitySettings = serverSettings.getSettings.pipe(
+    Effect.map(resolveServerBackgroundActivitySettings),
+    Effect.orElseSucceed(() => getBackgroundActivityPresetSettings("balanced")),
+  );
+
+  const snapshot = Effect.gen(function* () {
+    const [hostPower, leases, now, settings] = yield* Effect.all([
+      hostPowerMonitor.snapshot,
+      Ref.get(leasesRef),
+      DateTime.now,
+      backgroundActivitySettings,
+    ]);
+    return computeSnapshot({ hostPower, leases, now, settings, updatedAt: now });
+  });
+
+  const publishSnapshot = snapshot.pipe(Effect.flatMap((next) => PubSub.publish(changes, next)));
+
+  const reportClientActivity: BackgroundPolicyShape["reportClientActivity"] = (
+    sessionId,
+    rpcClientId,
+    input,
+  ) =>
+    Effect.gen(function* () {
+      const ttlMs = Math.min(
+        Math.max(input.ttlMs ?? DEFAULT_LEASE_TTL_MS, 1_000),
+        MAX_LEASE_TTL_MS,
+      );
+      const now = yield* DateTime.now;
+      const expiresAt = DateTime.add(now, { milliseconds: ttlMs });
+      const lease: ClientActivityLease = {
+        sessionId,
+        rpcClientId,
+        clientId: input.clientId,
+        clientKind: input.clientKind,
+        visible: input.visible,
+        focused: input.focused,
+        recentlyInteracted: input.recentlyInteracted,
+        ...(input.appState !== undefined ? { appState: input.appState } : {}),
+        ...(input.lowPowerMode !== undefined ? { lowPowerMode: input.lowPowerMode } : {}),
+        ...(input.batteryState !== undefined ? { batteryState: input.batteryState } : {}),
+        ...(input.networkType !== undefined ? { networkType: input.networkType } : {}),
+        scopes: input.scopes,
+        updatedAt: now,
+        expiresAt,
+      };
+      yield* Ref.update(leasesRef, (leases) => {
+        const next = new Map(leases);
+        next.set(`${rpcClientId}:${input.clientId}`, lease);
+        return next;
+      });
+      yield* publishSnapshot;
+    });
+
+  const removeRpcClient: BackgroundPolicyShape["removeRpcClient"] = (rpcClientId) =>
+    Ref.update(leasesRef, (leases) => {
+      const next = new Map(leases);
+      for (const key of next.keys()) {
+        if (key.startsWith(`${rpcClientId}:`)) {
+          next.delete(key);
+        }
+      }
+      return next;
+    }).pipe(Effect.andThen(publishSnapshot), Effect.asVoid);
+
+  const hasDemand: BackgroundPolicyShape["hasDemand"] = (scope) =>
+    Effect.map(snapshot, (current) => current.activeScopeKeys.includes(scopeKey(scope)));
+
+  const shouldRunScopeWork: BackgroundPolicyShape["shouldRunScopeWork"] = (scope) =>
+    Effect.gen(function* () {
+      const [current, settings] = yield* Effect.all([snapshot, backgroundActivitySettings]);
+      if (isHostConstrained(current.hostPower, settings)) {
+        return false;
+      }
+      return current.leases.some((lease) =>
+        leaseMayRunScopedWork(lease, scope, current.updatedAt, settings),
+      );
+    });
+
+  const shouldRunOpportunisticWork = Effect.map(
+    snapshot,
+    (current) => current.shouldRunOpportunisticWork,
+  );
+
+  yield* Stream.runForEach(hostPowerMonitor.streamChanges, () => publishSnapshot).pipe(
M
macroscopeappBotJul 28, 2026

🟡 Medium background/BackgroundPolicy.ts:238

BackgroundPolicy.make never subscribes to serverSettings.streamChanges, so changing the background profile or pause flags does not update the policy's streamChanges output until an unrelated lease/host-power event or the 15-second cleanup timer triggers publishSnapshot. Callers observing streamChanges receive stale policy state — and may act on it — after a settings update. Consider adding a Stream.runForEach(serverSettings.streamChanges, () => publishSnapshot) subscription alongside the existing host-power watcher.

✅ Resolved in 56b763edb5f03ea538e7b72ee9699b19e44424cb

apps/desktop/src/backend/DesktopBackendManager.ts · File comment

M
macroscopeappBotJul 28, 2026

🟡 Medium

https://github.com/pingdotgg/t3code/blob/8f3849bcf0d1db5b439017f24840a6131f66e486/apps/desktop/src/backend/DesktopBackendManager.ts#L506

Every backend instance routes its telemetry control stream into the same desktopTelemetryPublisher.handleControl, and each stream's Effect.ensuring unconditionally sends enabled: false when it closes. When two instances are running, stopping or restarting either one sends enabled: false to the shared publisher and clears diagnostics demand for the other live backend, so Electron telemetry sampling stops for a backend that is still running. Demand needs to be tracked per instance or aggregated across subscribers rather than funneled into one global boolean.

✅ Resolved in 56b763edb5f03ea538e7b72ee9699b19e44424cb

apps/server/src/background/BackgroundPolicy.ts · Earlier revision 38797e97 · right 80–93

Original diff
@@ -0,0 +1,274 @@
+import {
+  type AuthSessionId,
+  type BackgroundPolicySnapshot,
+  type BackgroundScope,
+  type ClientActivityLease,
+  type ClientActivityReportInput,
+  type HostPowerSnapshot,
+  type RpcClientId,
+} from "@t3tools/contracts";
+import {
+  getBackgroundActivityPresetSettings,
+  resolveServerBackgroundActivitySettings,
+  type ResolvedBackgroundActivitySettings,
+} from "@t3tools/shared/backgroundActivitySettings";
+import * as DateTime from "effect/DateTime";
+import * as Context from "effect/Context";
+import * as Effect from "effect/Effect";
+import * as Layer from "effect/Layer";
+import * as PubSub from "effect/PubSub";
+import * as Ref from "effect/Ref";
+import * as Stream from "effect/Stream";
+
+import { ServerSettingsService } from "../serverSettings.ts";
+import * as HostPowerMonitor from "./HostPowerMonitor.ts";
+
+export interface BackgroundPolicyShape {
+  readonly reportClientActivity: (
+    sessionId: AuthSessionId,
+    rpcClientId: RpcClientId,
+    input: ClientActivityReportInput,
+  ) => Effect.Effect<void>;
+  readonly removeRpcClient: (rpcClientId: RpcClientId) => Effect.Effect<void>;
+  readonly reportHostPowerState: (snapshot: HostPowerSnapshot) => Effect.Effect<void>;
+  readonly snapshot: Effect.Effect<BackgroundPolicySnapshot>;
+  readonly streamChanges: Stream.Stream<BackgroundPolicySnapshot>;
+  readonly hasDemand: (scope: BackgroundScope) => Effect.Effect<boolean>;
+  readonly shouldRunScopeWork: (scope: BackgroundScope) => Effect.Effect<boolean>;
+  readonly shouldRunOpportunisticWork: Effect.Effect<boolean>;
+}
+
+export class BackgroundPolicy extends Context.Service<BackgroundPolicy, BackgroundPolicyShape>()(
+  "t3/background/BackgroundPolicy",
+) {}
+
+const DEFAULT_LEASE_TTL_MS = 45_000;
+const MAX_LEASE_TTL_MS = 120_000;
+
+function scopeKey(scope: BackgroundScope): string {
+  switch (scope.type) {
+    case "server-config":
+    case "diagnostics":
+      return scope.type;
+    case "provider-status":
+      return scope.instanceId ? `${scope.type}:${scope.instanceId}` : scope.type;
+    case "vcs-status":
+    case "git-refs":
+      return `${scope.type}:${scope.cwd}`;
+    case "thread":
+      return `${scope.type}:${scope.threadId}`;
+  }
+}
+
+function isLeaseActive(lease: ClientActivityLease, now: DateTime.Utc): boolean {
+  return DateTime.isGreaterThan(lease.expiresAt, now);
+}
+
+function isForegroundLease(lease: ClientActivityLease, now: DateTime.Utc): boolean {
+  return isLeaseActive(lease, now) && lease.visible && lease.focused;
+}
+
+function leaseHasScope(lease: ClientActivityLease, scope: BackgroundScope): boolean {
+  const key = scopeKey(scope);
+  return lease.scopes.some((leaseScope) => scopeKey(leaseScope) === key);
+}
+
+function hasThermalPressure(hostPower: HostPowerSnapshot): boolean {
+  return hostPower.thermalState === "serious" || hostPower.thermalState === "critical";
+}
+
+function isHostConstrained(
+  hostPower: HostPowerSnapshot,
+  settings: ResolvedBackgroundActivitySettings,
+): boolean {
+  if (hostPower.stale) return false;
+  if (
+    (settings.pauseWhenHostLocked && hostPower.locked === "true") ||
+    hasThermalPressure(hostPower)
+  ) {
+    return true;
+  }
+  if (settings.pauseWhenHostLowPower && hostPower.lowPowerMode === "true") return true;
+  return settings.pauseWhenOnBattery && hostPower.onBattery === "true";
+}

apps/server/src/background/BackgroundPolicy.ts · right 80–93

M
macroscopeappBotJul 28, 2026

🟡 Medium background/BackgroundPolicy.ts:80

isHostConstrained never checks hostPower.suspended, so when the host is suspended but has no lock, low-power, battery, or thermal flags set, the function returns false and background work is allowed to run instead of being paused. Consider adding a check for hostPower.suspended === "true" alongside the existing host constraint conditions.

✅ Resolved in 56b763edb5f03ea538e7b72ee9699b19e44424cb

apps/desktop/src/app/DesktopObservability.ts · Earlier revision deb3d483 · right 447–450

Original diff
@@ -345,47 +412,84 @@ const makeBackendOutputLogShape = (
   environment: DesktopEnvironment.DesktopEnvironment["Service"],
   id: string,
   sink: Option.Option<RotatingLogFileWriter>,
-): DesktopBackendOutputLogShape =>
+): Effect.Effect<DesktopBackendOutputLogShape> =>
   Option.match(sink, {
-    onNone: () => DesktopBackendOutputLogNoop,
+    onNone: () => Effect.succeed(DesktopBackendOutputLogNoop),
     onSome: (logFile) =>
-      ({
-        writeSessionBoundary: Effect.fn("desktop.observability.backendOutput.writeSessionBoundary")(
-          function* ({ phase, details }) {
+      Effect.gen(function* () {
+        const sessionRef = yield* Ref.make(Option.none<BackendOutputSession>());
+        return {
+          beginSession: Effect.fn("desktop.observability.backendOutput.beginSession")(function* ({
+            details,
+          }) {
             const runId = yield* currentDesktopRunId;
-            yield* writeBackendChildLogRecord(logFile, {
-              message: `backend child process session ${phase.toLowerCase()}`,
-              level: "INFO",
-              annotations: {
-                component: "desktop-backend-child",
+            yield* Ref.set(
+              sessionRef,
+              Option.some({
                 runId,
-                instanceId: id,
-                phase,
-                details: sanitizeLogValue(details),
-              },
-            });
-          },
-        ),
-        writeOutputChunk: Effect.fn("desktop.observability.backendOutput.writeOutputChunk")(
-          function* (streamName, chunk) {
+                startDetails: sanitizeLogValue(details),
+                chunks: [],
+                byteLength: 0,
+              }),
+            );
+          }),
+          writeOutputChunk: Effect.fnUntraced(function* (streamName, chunk) {
             if (environment.isDevelopment) {
               yield* writeDevelopmentConsoleOutput(streamName, chunk);
             }
-            const runId = yield* currentDesktopRunId;
-            yield* writeBackendChildLogRecord(logFile, {
-              message: "backend child process output",
-              level: streamName === "stderr" ? "ERROR" : "INFO",
-              annotations: {
-                component: "desktop-backend-child",
-                runId,
-                instanceId: id,
-                stream: streamName,
-                text: textDecoder.decode(chunk),
-              },
-            });
-          },
-        ),
-      }) satisfies DesktopBackendOutputLogShape,
+            yield* Ref.update(
+              sessionRef,
+              Option.map((session) => appendBoundedOutputChunk(session, streamName, chunk)),
+            );
+          }),
+          persistFailure: Effect.fn("desktop.observability.backendOutput.persistFailure")(
+            function* ({ details }) {
+              const session = yield* Ref.modify(sessionRef, (current) => [
+                current,
+                Option.map(current, (value) => ({ ...value, chunks: [], byteLength: 0 })),
+              ]);

apps/desktop/src/app/DesktopObservability.ts · right 447–450

M
macroscopeappBotJul 28, 2026

🟡 Medium app/DesktopObservability.ts:447

persistFailure clears the buffered chunks but leaves sessionRef as Some, so a second call in the same run (e.g. finalizeRun after a readiness failure) writes a duplicate START/END failure record. The first call should reset sessionRef to Option.none() so the session is consumed and cannot be persisted twice.

✅ Resolved in 56b763edb5f03ea538e7b72ee9699b19e44424cb

apps/server/src/provider/Layers/EventNdjsonLogger.ts · Earlier revision deb3d483 · right 381

Original diff
@@ -62,225 +165,421 @@ function resolveThreadSegment(raw: string | null | undefined): string {
   return normalized ?? GLOBAL_THREAD_SEGMENT;
 }
 
-function formatLoggerMessage(message: unknown): string {
-  if (Array.isArray(message)) {
-    return message.map((part) => (typeof part === "string" ? part : String(part))).join(" ");
-  }
-  return typeof message === "string" ? message : String(message);
+function resolveStreamLabel(stream: EventNdjsonStream): string {
+  return stream === "native" ? "NTIVE" : "CANON";
 }
 
-function makeLineLogger(streamLabel: string): Logger.Logger<unknown, string> {
-  return Logger.make(
-    ({ date, message }) =>
-      `[${date.toISOString()}] ${streamLabel}: ${formatLoggerMessage(message)}\n`,
-  );
+function shouldPersist(stream: EventNdjsonStream, event: unknown): boolean {
+  if (stream !== "canonical" || typeof event !== "object" || event === null) {
+    return true;
+  }
+  const type = Reflect.get(event, "type");
+  return typeof type !== "string" || !transientCanonicalEventTypes.has(type);
 }
 
-function resolveStreamLabel(stream: EventNdjsonStream): string {
-  switch (stream) {
-    case "native":
-      return "NTIVE";
-    case "canonical":
-    case "orchestration":
-    default:
-      return "CANON";
+function writeBatchedMessages(
+  sink: RotatingFileSink,
+  records: ReadonlyArray<PendingRecord>,
+  maxBytes: number,
+): void {
+  let pendingLines: Array<string> = [];
+  let pendingBytes = 0;
+
+  const flush = () => {
+    if (pendingLines.length === 0) return;
+    sink.write(pendingLines.join(""));
+    pendingLines = [];
+    pendingBytes = 0;
+  };
+
+  for (const record of records) {
+    if (pendingBytes > 0 && pendingBytes + record.bytes > maxBytes) {
+      flush();
+    }
+    pendingLines.push(record.line);
+    pendingBytes += record.bytes;
+    if (pendingBytes >= maxBytes) {
+      flush();
+    }
   }
+  flush();
 }
 
-const toLogMessage = Effect.fn("toLogMessage")(function* (
-  event: unknown,
-): Effect.fn.Return<string | undefined> {
-  return yield* encodeUnknownJsonString(event).pipe(
-    Effect.catch((error) =>
-      logWarning("failed to serialize provider event log record", {
-        errorTag: errorTag(error),
-      }).pipe(Effect.as(undefined)),
-    ),
-  );
-});
+function enforceRetention(input: {
+  readonly directory: string;
+  readonly maxTotalBytes: number;
+  readonly maxAgeMs: number;
+  readonly activeFilePaths: ReadonlySet<string>;
+  readonly now: number;
+}): RetentionResult {
+  const failures: Array<FileOperationFailure> = [];
+  const files: Array<{ filePath: string; mtimeMs: number; size: number }> = [];
+
+  let entries: ReadonlyArray<NodeFS.Dirent>;
+  try {
+    entries = NodeFS.readdirSync(input.directory, { withFileTypes: true });
+  } catch (cause) {
+    return { failures: [{ filePath: input.directory, cause }] };
+  }
 
-const makeThreadWriter = Effect.fn("makeThreadWriter")(function* (input: {
-  readonly filePath: string;
-  readonly maxBytes: number;
-  readonly maxFiles: number;
-  readonly batchWindowMs: number;
-  readonly streamLabel: string;
-}): Effect.fn.Return<ThreadWriter | undefined> {
-  const sinkResult = yield* Effect.sync(() => {
+  for (const entry of entries) {
+    if (!entry.isFile() || !PROVIDER_LOG_FILE_PATTERN.test(entry.name)) continue;
+    const filePath = NodePath.join(input.directory, entry.name);
     try {
-      return {
-        ok: true as const,
-        sink: new RotatingFileSink({
-          filePath: input.filePath,
-          maxBytes: input.maxBytes,
-          maxFiles: input.maxFiles,
-          throwOnError: true,
-        }),
-      };
-    } catch (error) {
-      return { ok: false as const, error };
+      const stat = NodeFS.statSync(filePath);
+      files.push({ filePath, mtimeMs: stat.mtimeMs, size: stat.size });
+    } catch (cause) {
+      failures.push({ filePath, cause });
+    }
+  }
+
+  let totalBytes = files.reduce((total, file) => total + file.size, 0);
+  const remove = (file: (typeof files)[number]) => {
+    if (input.activeFilePaths.has(file.filePath)) return false;
+    try {
+      NodeFS.rmSync(file.filePath, { force: true });
+      totalBytes -= file.size;
+      return true;
+    } catch (cause) {
+      failures.push({ filePath: file.filePath, cause });
+      return false;
     }
+  };
+
+  const retained = files.filter((file) => {
+    if (input.now - file.mtimeMs <= input.maxAgeMs) return true;
+    return !remove(file);
   });
 
-  if (!sinkResult.ok) {
-    yield* logWarning("failed to initialize provider thread log file", {
-      filePath: input.filePath,
-      errorTag: errorTag(sinkResult.error),
-    });
-    return undefined;
+  for (const file of retained.toSorted(
+    (left, right) => left.mtimeMs - right.mtimeMs || left.filePath.localeCompare(right.filePath),
+  )) {
+    if (totalBytes <= input.maxTotalBytes) break;
+    remove(file);
   }
 
-  const sink = sinkResult.sink;
-  const scope = yield* Scope.make();
-  const lineLogger = makeLineLogger(input.streamLabel);
-  const batchedLogger = yield* Logger.batched(lineLogger, {
-    window: input.batchWindowMs,
-    flush: Effect.fn("makeThreadWriter.flush")(function* (messages) {
-      const flushResult = yield* Effect.sync(() => {
-        try {
-          for (const message of messages) {
-            sink.write(message);
-          }
-          return { ok: true as const };
-        } catch (error) {
-          return { ok: false as const, error };
-        }
-      });
+  return { failures };
+}
+
+function validateOption(input: {
+  readonly filePath: string;
+  readonly option: string;
+  readonly value: number;
+  readonly minimum: number;
+}): EventNdjsonLogConfigurationError | undefined {
+  if (Number.isInteger(input.value) && input.value >= input.minimum) return undefined;
+  return new EventNdjsonLogConfigurationError(input);
+}
 
-      if (!flushResult.ok) {
-        yield* logWarning("provider event log batch flush failed", {
-          filePath: input.filePath,
-          errorTag: errorTag(flushResult.error),
+function resolveOptions(
+  filePath: string,
+  options: EventNdjsonLogStoreOptions,
+): Effect.Effect<ResolvedOptions, EventNdjsonLogConfigurationError> {
+  const resolved = {
+    maxBytes: options.maxBytes ?? DEFAULT_MAX_BYTES,
+    maxFiles: options.maxFiles ?? DEFAULT_MAX_FILES,
+    batchWindowMs: options.batchWindowMs ?? DEFAULT_BATCH_WINDOW_MS,
+    maxTotalBytes: options.maxTotalBytes ?? DEFAULT_MAX_TOTAL_BYTES,
+    maxAgeMs: options.maxAgeMs ?? DEFAULT_MAX_AGE_MS,
+    retentionCheckIntervalMs:
+      options.retentionCheckIntervalMs ?? DEFAULT_RETENTION_CHECK_INTERVAL_MS,
+    maxBufferedBytes: options.maxBufferedBytes ?? DEFAULT_MAX_BUFFERED_BYTES,
+    maxBufferedRecords: options.maxBufferedRecords ?? DEFAULT_MAX_BUFFERED_RECORDS,
+    attribution: options.attribution,
+  } satisfies ResolvedOptions;
+
+  const validations = [
+    ["maxBytes", resolved.maxBytes, 1],
+    ["maxFiles", resolved.maxFiles, 1],
+    ["batchWindowMs", resolved.batchWindowMs, 0],
+    ["maxTotalBytes", resolved.maxTotalBytes, 1],
+    ["maxAgeMs", resolved.maxAgeMs, 1],
+    ["retentionCheckIntervalMs", resolved.retentionCheckIntervalMs, 1],
+    ["maxBufferedBytes", resolved.maxBufferedBytes, 1],
+    ["maxBufferedRecords", resolved.maxBufferedRecords, 1],
+  ] as const;
+
+  for (const [option, value, minimum] of validations) {
+    const error = validateOption({ filePath, option, value, minimum });
+    if (error) return Effect.fail(error);
+  }
+  return Effect.succeed(resolved);
+}
+
+function drainPending(input: {
+  readonly directory: string;
+  readonly options: ResolvedOptions;
+  readonly state: StoreState;
+  readonly now: number;
+  readonly timerFired: boolean;
+  readonly close: boolean;
+}): readonly [DrainResult, StoreState] {
+  if (input.state.closed) {
+    return [{ attributions: [], failures: [] }, input.state];
+  }
+
+  const sinks = new Map(input.state.sinks);
+  const failedSegments = new Set(input.state.failedSegments);
+  const failures: Array<FileOperationFailure> = [];
+  const attributionByStream = new Map<
+    EventNdjsonStream,
+    { count: number; logicalWriteBytes: number }
+  >();
+  const recordsBySegment = new Map<string, Array<PendingRecord>>();
+
+  for (const record of input.state.pending) {
+    const records = recordsBySegment.get(record.threadSegment) ?? [];
+    records.push(record);
+    recordsBySegment.set(record.threadSegment, records);
+  }
+
+  for (const [threadSegment, records] of recordsBySegment) {
+    if (failedSegments.has(threadSegment)) continue;
+    const filePath = NodePath.join(input.directory, `${threadSegment}.log`);
+    let sink = sinks.get(threadSegment);
+    if (!sink) {
+      try {
+        sink = new RotatingFileSink({
+          filePath,
+          maxBytes: input.options.maxBytes,
+          maxFiles: input.options.maxFiles,
+          throwOnError: true,
         });
+        sinks.set(threadSegment, sink);
+      } catch (cause) {
+        failedSegments.add(threadSegment);
+        failures.push({ filePath, cause });
+        continue;
       }
-    }),
-  }).pipe(Effect.provideService(Scope.Scope, scope));
+    }
 
-  const loggerLayer = Logger.layer([batchedLogger], { mergeWithExisting: false });
+    try {
+      writeBatchedMessages(sink, records, input.options.maxBytes);
+      for (const record of records) {
+        const current = attributionByStream.get(record.stream) ?? {
+          count: 0,
+          logicalWriteBytes: 0,
+        };
+        attributionByStream.set(record.stream, {
+          count: current.count + 1,
+          logicalWriteBytes: current.logicalWriteBytes + record.bytes,
+        });
+      }
+    } catch (cause) {
+      failedSegments.add(threadSegment);
+      failures.push({ filePath, cause });
+    }
+  }
 
-  return {
-    writeMessage(message: string) {
-      return Effect.log(message).pipe(Effect.provide(loggerLayer));
+  const retentionDue =
+    input.now - input.state.lastRetentionAt >= input.options.retentionCheckIntervalMs;
+  const retention = retentionDue
+    ? enforceRetention({
+        directory: input.directory,
+        maxTotalBytes: input.options.maxTotalBytes,
+        maxAgeMs: input.options.maxAgeMs,
+        activeFilePaths: new Set(
M
macroscopeappBotJul 28, 2026

🟡 Medium Layers/EventNdjsonLogger.ts:381

enforceRetention can never delete a log file once its sink has been opened, because drainPending adds every entry from the sinks map to activeFilePaths and sinks are never removed from that map for the lifetime of the store. This means maxTotalBytes is not enforced for any file that has been written to: once opened sinks' combined size exceeds the budget (e.g. ~52 thread logs at the default 10 MiB each exceed the 512 MiB limit), those files are permanently protected and total disk usage grows without bound as new threads create new logs.

The activeFilePaths set should protect only the file currently being written in this drain cycle, not every sink the store has ever opened. Consider building it from the segments that have pending records in this drain, or from the specific RotatingFileSink currently being written to, rather than from all entries in the sinks map.

✅ Resolved in 56b763edb5f03ea538e7b72ee9699b19e44424cb

apps/server/src/provider/Layers/EventNdjsonLogger.ts · Earlier revision deb3d483 · right 129

Original diff
@@ -1,56 +1,159 @@
 // @effect-diagnostics nodeBuiltinImport:off
 /**
- * Provider event logger helper.
+ * Best-effort provider event logging with one shared writer per thread.
  *
- * Best-effort writer for observability logs. Each record is formatted as a
- * single effect-style text line in a thread-scoped file. Failures are
- * downgraded to warnings so provider runtime behavior is unaffected.
+ * Native and canonical views share batching, rotation, and retention state so
+ * they cannot race while appending to the same thread-scoped file.
  */
 import * as NodeFS from "node:fs";
 import * as NodePath from "node:path";
 
 import type { ThreadId } from "@t3tools/contracts";
 import { RotatingFileSink } from "@t3tools/shared/logging";
 import { errorTag } from "@t3tools/shared/observability";
+import * as Clock from "effect/Clock";
+import * as DateTime from "effect/DateTime";
 import * as Effect from "effect/Effect";
 import * as Exit from "effect/Exit";
-import * as Logger from "effect/Logger";
 import * as Schema from "effect/Schema";
 import * as Scope from "effect/Scope";
 import * as SynchronizedRef from "effect/SynchronizedRef";
 
 import { toSafeThreadAttachmentSegment } from "../../attachmentStore.ts";
+import type { ResourceAttributionShape } from "../../resourceTelemetry/ResourceAttribution.ts";
 
-const DEFAULT_MAX_BYTES = 10 * 1024 * 1024;
+const MEBIBYTE = 1024 * 1024;
+const DAY_MS = 24 * 60 * 60 * 1_000;
+const DEFAULT_MAX_BYTES = 10 * MEBIBYTE;
 const DEFAULT_MAX_FILES = 10;
-const DEFAULT_BATCH_WINDOW_MS = 200;
+const DEFAULT_BATCH_WINDOW_MS = 1_000;
+const DEFAULT_MAX_TOTAL_BYTES = 512 * MEBIBYTE;
+const DEFAULT_MAX_AGE_MS = 14 * DAY_MS;
+const DEFAULT_RETENTION_CHECK_INTERVAL_MS = 5 * 60 * 1_000;
+const DEFAULT_MAX_BUFFERED_BYTES = MEBIBYTE;
+const DEFAULT_MAX_BUFFERED_RECORDS = 512;
 const GLOBAL_THREAD_SEGMENT = "_global";
 const LOG_SCOPE = "provider-observability";
+const PROVIDER_LOG_FILE_PATTERN = /\.log(?:\.\d+)?$/u;
 const encodeUnknownJsonString = Schema.encodeUnknownEffect(Schema.UnknownFromJsonString);
 
+const transientCanonicalEventTypes = new Set([
+  "content.delta",
+  "hook.progress",
+  "item.updated",
+  "task.progress",
+  "thread.realtime.audio.delta",
+  "tool.progress",
+  "turn.proposed.delta",
+]);
+
 export type EventNdjsonStream = "native" | "canonical" | "orchestration";
 
 export interface EventNdjsonLogger {
   readonly filePath: string;
-  write: (event: unknown, threadId: ThreadId | null) => Effect.Effect<void, never, never>;
-  close: () => Effect.Effect<void, never, never>;
+  readonly write: (event: unknown, threadId: ThreadId | null) => Effect.Effect<void>;
+  readonly close: () => Effect.Effect<void>;
 }
 
-export interface EventNdjsonLoggerOptions {
-  readonly stream: EventNdjsonStream;
+export interface EventNdjsonLogStore {
+  readonly filePath: string;
+  readonly logger: (stream: EventNdjsonStream) => EventNdjsonLogger;
+  readonly close: () => Effect.Effect<void>;
+}
+
+export interface EventNdjsonLogStoreOptions {
   readonly maxBytes?: number;
   readonly maxFiles?: number;
   readonly batchWindowMs?: number;
+  readonly maxTotalBytes?: number;
+  readonly maxAgeMs?: number;
+  readonly retentionCheckIntervalMs?: number;
+  readonly maxBufferedBytes?: number;
+  readonly maxBufferedRecords?: number;
+  readonly attribution?: ResourceAttributionShape;
 }
 
-interface ThreadWriter {
-  writeMessage: (message: string) => Effect.Effect<void>;
-  close: () => Effect.Effect<void>;
+export interface EventNdjsonLoggerOptions extends EventNdjsonLogStoreOptions {
+  readonly stream: EventNdjsonStream;
 }
 
-interface LoggerState {
-  readonly threadWriters: Map<string, ThreadWriter>;
-  readonly failedSegments: Set<string>;
+export class EventNdjsonLogConfigurationError extends Schema.TaggedErrorClass<EventNdjsonLogConfigurationError>()(
+  "EventNdjsonLogConfigurationError",
+  {
+    filePath: Schema.String,
+    option: Schema.String,
+    value: Schema.Number,
+    minimum: Schema.Number,
+  },
+) {
+  override get message(): string {
+    return `Provider event log option '${this.option}' must be an integer >= ${this.minimum}; received ${this.value} for '${this.filePath}'`;
+  }
+}
+
+export class EventNdjsonLogDirectoryError extends Schema.TaggedErrorClass<EventNdjsonLogDirectoryError>()(
+  "EventNdjsonLogDirectoryError",
+  {
+    directory: Schema.String,
+    cause: Schema.Defect(),
+  },
+) {
+  override get message(): string {
+    return `Failed to create provider event log directory '${this.directory}'`;
+  }
+}
+
+export type EventNdjsonLogStoreError =
+  | EventNdjsonLogConfigurationError
+  | EventNdjsonLogDirectoryError;
+
+interface ResolvedOptions {
+  readonly maxBytes: number;
+  readonly maxFiles: number;
+  readonly batchWindowMs: number;
+  readonly maxTotalBytes: number;
+  readonly maxAgeMs: number;
+  readonly retentionCheckIntervalMs: number;
+  readonly maxBufferedBytes: number;
+  readonly maxBufferedRecords: number;
+  readonly attribution: ResourceAttributionShape | undefined;
+}
+
+interface PendingRecord {
+  readonly stream: EventNdjsonStream;
+  readonly threadSegment: string;
+  readonly line: string;
+  readonly bytes: number;
+}
+
+interface StoreState {
M
macroscopeappBotJul 28, 2026

🟡 Medium Layers/EventNdjsonLogger.ts:129

failedSegments permanently blacklists a thread segment after a single sink construction or write failure. drainPending skips every subsequent record for that segment and clears the pending queue, so a transient condition such as a temporary disk-full or permission error silently drops all future provider event log records for that thread for the remainder of the store lifetime — even after the filesystem recovers. Consider retrying the sink on a later flush or expiring entries from failedSegments instead of retaining them indefinitely.

✅ Resolved in 56b763edb5f03ea538e7b72ee9699b19e44424cb

apps/server/src/resourceTelemetry/DesktopTelemetryReceiver.ts · Earlier revision deb3d483 · right 362–366

Original diff
@@ -0,0 +1,412 @@
+// @effect-diagnostics nodeBuiltinImport:off
+import * as NodeFS from "node:fs";
+
+import * as NodeStream from "@effect/platform-node/NodeStream";
+import {
+  DesktopHostTelemetryMessage,
+  type DesktopHostTelemetryMessage as DesktopHostTelemetryMessageValue,
+  type DesktopHostTelemetrySnapshot,
+  DesktopTelemetryControlMessage,
+  type ResourceTelemetrySourceStatus,
+} from "@t3tools/contracts";
+import * as Context from "effect/Context";
+import * as DateTime from "effect/DateTime";
+import * as Duration from "effect/Duration";
+import * as Effect from "effect/Effect";
+import * as Layer from "effect/Layer";
+import * as Option from "effect/Option";
+import * as PubSub from "effect/PubSub";
+import * as Ref from "effect/Ref";
+import * as Schema from "effect/Schema";
+import * as Semaphore from "effect/Semaphore";
+import * as Stream from "effect/Stream";
+import * as Ndjson from "effect/unstable/encoding/Ndjson";
+
+import { ServerConfig } from "../config.ts";
+
+const STALE_AFTER_MS = 90_000;
+const STALE_CHECK_INTERVAL = Duration.seconds(30);
+
+export class DesktopTelemetryDescriptorUnavailable extends Schema.TaggedErrorClass<DesktopTelemetryDescriptorUnavailable>()(
+  "DesktopTelemetryDescriptorUnavailable",
+  {
+    mode: Schema.String,
+  },
+) {
+  override get message(): string {
+    return `Desktop telemetry descriptor is unavailable in '${this.mode}' mode.`;
+  }
+}
+
+export class DesktopTelemetryProtocolMismatch extends Schema.TaggedErrorClass<DesktopTelemetryProtocolMismatch>()(
+  "DesktopTelemetryProtocolMismatch",
+  {
+    expectedVersion: Schema.Number,
+    receivedVersion: Schema.Number,
+  },
+) {
+  override get message(): string {
+    return `Desktop telemetry protocol ${this.receivedVersion} is incompatible with expected protocol ${this.expectedVersion}.`;
+  }
+}
+
+export class DesktopTelemetryDecodeFailed extends Schema.TaggedErrorClass<DesktopTelemetryDecodeFailed>()(
+  "DesktopTelemetryDecodeFailed",
+  {
+    cause: Schema.Defect(),
+  },
+) {
+  override get message(): string {
+    return "Failed to decode desktop telemetry.";
+  }
+}
+
+export class DesktopTelemetryStreamFailed extends Schema.TaggedErrorClass<DesktopTelemetryStreamFailed>()(
+  "DesktopTelemetryStreamFailed",
+  {
+    fd: Schema.Number,
+    cause: Schema.Defect(),
+  },
+) {
+  override get message(): string {
+    return `Desktop telemetry stream on fd ${this.fd} failed.`;
+  }
+}
+
+export class DesktopTelemetryStreamClosed extends Schema.TaggedErrorClass<DesktopTelemetryStreamClosed>()(
+  "DesktopTelemetryStreamClosed",
+  {
+    fd: Schema.Number,
+  },
+) {
+  override get message(): string {
+    return `Desktop telemetry stream on fd ${this.fd} closed.`;
+  }
+}
+
+export class DesktopTelemetryStale extends Schema.TaggedErrorClass<DesktopTelemetryStale>()(
+  "DesktopTelemetryStale",
+  {
+    fd: Schema.Number,
+    staleAfterMs: Schema.Number,
+  },
+) {
+  override get message(): string {
+    return `Desktop telemetry on fd ${this.fd} has not updated for ${this.staleAfterMs}ms.`;
+  }
+}
+
+export type DesktopTelemetryReceiverError =
+  | DesktopTelemetryDescriptorUnavailable
+  | DesktopTelemetryProtocolMismatch
+  | DesktopTelemetryDecodeFailed
+  | DesktopTelemetryStreamFailed
+  | DesktopTelemetryStreamClosed;
+
+export class DesktopTelemetryControlFailed extends Schema.TaggedErrorClass<DesktopTelemetryControlFailed>()(
+  "DesktopTelemetryControlFailed",
+  {
+    fd: Schema.Number,
+    operation: Schema.String,
+    cause: Schema.Defect(),
+  },
+) {
+  override get message(): string {
+    return `Desktop telemetry control '${this.operation}' failed on fd ${this.fd}.`;
+  }
+}
+
+export interface DesktopTelemetryReceiverHealth {
+  readonly status: ResourceTelemetrySourceStatus;
+  readonly lastSampleAt: Option.Option<DateTime.Utc>;
+  readonly lastError: Option.Option<string>;
+}
+
+export interface DesktopTelemetryReceiverShape {
+  readonly latest: Effect.Effect<Option.Option<DesktopHostTelemetrySnapshot>>;
+  readonly changes: Stream.Stream<DesktopHostTelemetrySnapshot>;
+  readonly health: Effect.Effect<DesktopTelemetryReceiverHealth>;
+  readonly healthChanges: Stream.Stream<DesktopTelemetryReceiverHealth>;
+  readonly setDiagnosticsDemand: (
+    enabled: boolean,
+  ) => Effect.Effect<void, DesktopTelemetryControlFailed>;
+}
+
+export class DesktopTelemetryReceiver extends Context.Service<
+  DesktopTelemetryReceiver,
+  DesktopTelemetryReceiverShape
+>()("t3/resourceTelemetry/DesktopTelemetryReceiver") {}
+
+const decodeMessage = Schema.decodeUnknownEffect(DesktopHostTelemetryMessage);
+const encodeControlMessage = Schema.encodeEffect(
+  Schema.fromJsonString(DesktopTelemetryControlMessage),
+);
+const isDescriptorUnavailable = Schema.is(DesktopTelemetryDescriptorUnavailable);
+const isProtocolMismatch = Schema.is(DesktopTelemetryProtocolMismatch);
+const isDecodeFailed = Schema.is(DesktopTelemetryDecodeFailed);
+const isStreamFailed = Schema.is(DesktopTelemetryStreamFailed);
+
+function normalizeReceiverError(error: unknown): DesktopTelemetryReceiverError {
+  if (
+    isDescriptorUnavailable(error) ||
+    isProtocolMismatch(error) ||
+    isDecodeFailed(error) ||
+    isStreamFailed(error)
+  ) {
+    return error;
+  }
+  return new DesktopTelemetryDecodeFailed({ cause: error });
+}
+
+function messageVersion(value: unknown): number | undefined {
+  if (typeof value !== "object" || value === null) return undefined;
+  const version = Reflect.get(value, "version");
+  return typeof version === "number" ? version : undefined;
+}
+
+export const make = Effect.fn("resourceTelemetry.desktopTelemetryReceiver.make")(function* () {
+  const config = yield* ServerConfig;
+  const latest = yield* Ref.make(Option.none<DesktopHostTelemetrySnapshot>());
+  const changes = yield* PubSub.sliding<DesktopHostTelemetrySnapshot>(8);
+  const healthChanges = yield* PubSub.sliding<DesktopTelemetryReceiverHealth>(4);
+  const controlMutex = yield* Semaphore.make(1);
+  const health = yield* Ref.make<DesktopTelemetryReceiverHealth>({
+    status: config.desktopTelemetryFd === undefined ? "unavailable" : "starting",
+    lastSampleAt: Option.none(),
+    lastError:
+      config.desktopTelemetryFd === undefined
+        ? Option.some(
+            new DesktopTelemetryDescriptorUnavailable({
+              mode: config.mode,
+            }).message,
+          )
+        : Option.none(),
+  });
+  const updateHealth = (
+    update: (current: DesktopTelemetryReceiverHealth) => DesktopTelemetryReceiverHealth,
+  ) =>
+    Ref.modify(health, (current) => {
+      const next = update(current);
+      return [next, next];
+    }).pipe(
+      Effect.flatMap((next) => PubSub.publish(healthChanges, next)),
+      Effect.asVoid,
+    );
+  const updateSampleHealth = (sampledAt: DateTime.Utc) =>
+    Ref.modify(health, (current) => {
+      const next: DesktopTelemetryReceiverHealth = {
+        status: "healthy",
+        lastSampleAt: Option.some(sampledAt),
+        lastError: Option.none(),
+      };
+      return [
+        current.status !== "healthy" || Option.isSome(current.lastError)
+          ? Option.some(next)
+          : Option.none(),
+        next,
+      ] as const;
+    }).pipe(
+      Effect.flatMap(
+        Option.match({
+          onNone: () => Effect.void,
+          onSome: (next) => PubSub.publish(healthChanges, next),
+        }),
+      ),
+      Effect.asVoid,
+    );
+
+  const setDiagnosticsDemand: DesktopTelemetryReceiverShape["setDiagnosticsDemand"] = (enabled) =>
+    controlMutex.withPermits(1)(
+      Effect.gen(function* () {
+        const fd = config.desktopTelemetryControlFd;
+        if (fd === undefined) return;
+        const encoded = yield* encodeControlMessage({
+          version: 1,
+          type: "setDiagnosticsDemand",
+          enabled,
+        }).pipe(
+          Effect.mapError(
+            (cause) =>
+              new DesktopTelemetryControlFailed({
+                fd,
+                operation: "encode",
+                cause,
+              }),
+          ),
+        );
+        yield* Effect.try({
+          try: () => {
+            const payload = Buffer.from(`${encoded}\n`);
+            let offset = 0;
+            while (offset < payload.byteLength) {
+              const written = NodeFS.writeSync(fd, payload, offset);
+              if (written <= 0) throw new Error("desktop telemetry control pipe accepted no bytes");
+              offset += written;
+            }
+          },
+          catch: (cause) =>
+            new DesktopTelemetryControlFailed({
+              fd,
+              operation: "write",
+              cause,
+            }),
+        }).pipe(
+          Effect.tapError((error) =>
+            updateHealth((current) => ({
+              ...current,
+              status: "degraded",
+              lastError: Option.some(error.message),
+            })),
+          ),
+        );
+      }),
+    );
+
+  if (config.desktopTelemetryFd !== undefined) {
+    const fd = config.desktopTelemetryFd;
+    const readable = yield* Effect.acquireRelease(
+      Effect.try({
+        try: () =>
+          NodeFS.createReadStream("", {
+            fd,
+            autoClose: true,
+          }),
+        catch: (cause) => new DesktopTelemetryStreamFailed({ fd, cause }),
+      }),
+      (stream) =>
+        Effect.sync(() => {
+          stream.destroy();
+        }),
+    );
+
+    const messages: Stream.Stream<DesktopHostTelemetryMessageValue, DesktopTelemetryReceiverError> =
+      NodeStream.fromReadable<Uint8Array, DesktopTelemetryStreamFailed>({
+        evaluate: () => readable,
+        closeOnDone: true,
+        onError: (cause) => new DesktopTelemetryStreamFailed({ fd, cause }),
+      }).pipe(
+        Stream.pipeThroughChannel(Ndjson.decode({ ignoreEmptyLines: true })),
+        Stream.mapEffect(
+          (
+            value,
+          ): Effect.Effect<
+            DesktopHostTelemetryMessageValue,
+            DesktopTelemetryProtocolMismatch | DesktopTelemetryDecodeFailed
+          > => {
+            const version = messageVersion(value);
+            if (version !== undefined && version !== 1) {
+              return Effect.fail(
+                new DesktopTelemetryProtocolMismatch({
+                  expectedVersion: 1,
+                  receivedVersion: version,
+                }),
+              );
+            }
+            return decodeMessage(value).pipe(
+              Effect.mapError((cause) => new DesktopTelemetryDecodeFailed({ cause })),
+            );
+          },
+        ),
+        Stream.mapError(normalizeReceiverError),
+      );
+
+    yield* messages.pipe(
+      Stream.runForEach((message) => {
+        if (message.type === "desktopTelemetryHello") {
+          return updateHealth(
+            (current): DesktopTelemetryReceiverHealth => ({
+              ...current,
+              status: "healthy",
+              lastError: Option.none(),
+            }),
+          );
+        }
+
+        const sampledAt = DateTime.makeUnsafe(message.sampledAtUnixMs);
+        return Ref.set(latest, Option.some(message)).pipe(
+          Effect.andThen(updateSampleHealth(sampledAt)),
+          Effect.andThen(PubSub.publish(changes, message)),
+          Effect.asVoid,
+        );
+      }),
+      Effect.andThen(
+        updateHealth(
+          (current): DesktopTelemetryReceiverHealth => ({
+            ...current,
+            status: "stopped",
+            lastError: Option.some(new DesktopTelemetryStreamClosed({ fd }).message),
+          }),
+        ),
+      ),
+      Effect.catch((error) =>
+        updateHealth(
+          (current): DesktopTelemetryReceiverHealth => ({
+            ...current,
+            status: "degraded",
+            lastError: Option.some(error.message),
+          }),
+        ),
+      ),
+      Effect.forkScoped,
+    );
+
+    yield* Effect.forever(
+      Effect.sleep(STALE_CHECK_INTERVAL).pipe(
+        Effect.andThen(
+          Effect.gen(function* () {
+            const current = yield* Ref.get(latest);
+            if (Option.isNone(current) || current.value.power.stale) return;
+            const now = yield* DateTime.now;
+            if (DateTime.toEpochMillis(now) - current.value.sampledAtUnixMs < STALE_AFTER_MS)
+              return;
+            const staleSnapshot: DesktopHostTelemetrySnapshot = {
+              ...current.value,
+              power: { ...current.value.power, stale: true },
+            };
+            yield* Ref.set(latest, Option.some(staleSnapshot));

apps/server/src/resourceTelemetry/DesktopTelemetryReceiver.ts · right 362–366

M
macroscopeappBotJul 28, 2026

🟡 Medium resourceTelemetry/DesktopTelemetryReceiver.ts:362

The stale checker overwrites newer telemetry with an older stale copy. A fresh message can update latest after the checker's Ref.get(latest) on line 357 but before its Ref.set(latest, ...) on line 366, so the checker replaces that newer snapshot with the stale one derived from the stale value it read earlier. Consumers then receive regressed telemetry incorrectly marked stale until the next sample arrives.

This happens because the read, staleness check, and write are split across multiple effects with a yield point on DateTime.now. Use an atomic Ref.modify that rechecks the current snapshot's sampledAtUnixMs before replacing it, so a newer sample is never clobbered.

✅ Resolved in 56b763edb5f03ea538e7b72ee9699b19e44424cb