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

Intent

Reduce T3 Code's steady-state resource usage, bound local observability disk churn, and make CPU, memory, power-state, and I/O regressions observable enough to debug.

Before this PR:

  • provider status refreshes, VCS remote refreshes, and other background work followed fixed schedules even when nobody needed the result;
  • process diagnostics repeatedly spawned ps or PowerShell, adding process churn while still missing short-lived work between samples;
  • macOS power state came from recurring shell probes;
  • the diagnostics page could show sampled CPU and memory, but had no process I/O counters, collector health, Electron overhead, or application-level write attribution;
  • native and canonical provider events used independent rotators against the same thread files, retained duplicate streaming deltas, and had no aggregate age or byte budget;
  • high-frequency provider-log and preview debugger operations generated their own Effect trace records.

This PR replaces those paths with a demand-aware background policy plus a persistent, cross-platform telemetry pipeline.

Runtime architecture

text
1Electron main
2 ├─ powerMonitor events and state
3 ├─ app.getAppMetrics()
4 └─ NDJSON over inherited fd 4
5 ┐
6 ▼
7Node server ── NDJSON stdio ── persistent Rust resource monitor
8 │
9 ├─ ResourceTelemetry Effect service
10 ├─ bounded in-memory history and I/O attribution
11 ├─ HostPowerMonitor → BackgroundPolicy
12 └─ typed RPC/subscription → diagnostics UI

The native monitor is intentionally a standalone executable, not an N-API or FFI module. One persistent child process gives the server a supervised crash boundary, avoids Node/Electron ABI coupling, and eliminates recurring process-table subprocesses. The same protocol and binary layout work for desktop and the published CLI.

What changed

Demand-aware background work

  • Adds an Effect BackgroundPolicy service that combines:
    • active client leases and requested scopes;
    • visibility, focus, recent interaction, app state, battery, and low-power hints;
    • host lock, suspend, battery, low-power, and thermal constraints;
    • balanced, performance, and battery-saver profiles with interval overrides.
  • Clients retain scopes for the data they currently need, such as a repository's VCS status or a provider instance's status.
  • Provider refreshes and VCS remote polling consult the policy instead of blindly running on fixed cadences.
  • Settings and reporting flows expose the policy without coupling individual background services to renderer state.

Native and Electron telemetry

  • Adds native/resource-monitor, a Rust sidecar backed by OS APIs through sysinfo.
  • Samples the server, all descendants, provider-spawned grandchildren such as node, tsgo, language servers and shells, Electron roots, Electron descendants, and the monitor itself.
  • Reports process identity (pid, startTimeMs), parent/child relationships, command line, CPU, cumulative CPU time, resident/virtual memory, cumulative I/O counters, and collection cost.
  • Disables Linux task/thread enumeration and avoids repeatedly reloading command lines.
  • Uses Electron powerMonitor and app.getAppMetrics() for battery/AC state, lock/idle, suspend/resume, thermal state, CPU speed limits, Electron CPU/memory, and idle wakeups.
  • Sends Electron-main data directly to the server over fd 4. It does not route telemetry through the renderer WebSocket.

Diagnostics, history, and safety

  • Adds a schema-backed ResourceTelemetry service that merges native and Electron samples, computes rates from cumulative counters, classifies process groups, and publishes live snapshots.
  • Adds bounded, in-memory history for CPU, memory, I/O, process lifecycle, and collector health. Telemetry is not persisted to disk.
  • Expands diagnostics with:
    • backend, Electron, monitor, and total resource groups;
    • live process tree and process lifecycle counters;
    • CPU, memory, and I/O history;
    • power and thermal state;
    • collector status, restart count, scan duration, and explicit retry;
    • logical write attribution for provider event logs and local trace output.
  • Reuses this service for the legacy process diagnostics RPCs, removing the old ps/PowerShell implementations entirely.
  • Validates process signals against both PID and start time and only permits signalable backend descendants; Electron and monitor processes remain visible but protected.

Bounded local observability

  • Replaces the competing native and canonical provider rotators with one shared, thread-aware log store.
  • Keeps full provider-native diagnostics while dropping duplicate transient canonical events such as content, item, task, hook, tool, proposal, and realtime audio deltas before serialization. Final states, warnings, and errors remain available.
  • Flushes provider records in demand-driven 1-second batches with byte and record thresholds; there is no permanent polling timer.
  • Enforces a 512 MiB aggregate provider-log budget and 14-day age limit, pruning oldest files at startup and during sustained writes.
  • Removes trace spans from provider-log writes and per-message Electron preview debugger handlers, which were dominant trace hot paths.
  • Closes adapter-owned log stores consistently, including the Claude adapter path.

Reliability and packaging

  • Models monitor resolution, protocol, transport, and refresh failures as schema-backed tagged error unions with descriptive messages.
  • Supervises the sidecar with version negotiation, bounded restart backoff, a failure circuit, health transitions, and manual retry.
  • Handles sidecar sequence resets across restarts and preserves native rates during Electron-only updates.
  • Builds and packages platform executables in desktop resources and CLI releases for supported macOS, Linux, and Windows targets.
  • Adds CI coverage for Rust formatting/tests and release wiring for per-platform artifacts.

Metric semantics and limits

  • Sampling is once per second, not syscall or process-lifecycle tracing. A process that starts and exits entirely between samples can still be missed.
  • Unix-like and Windows process I/O counters have different OS semantics, and filesystem caches mean they are not guaranteed to equal physical SSD traffic. The UI therefore labels them as I/O reads/writes, not disk reads/writes.
  • Application attribution reports known logical writes from instrumented T3 Code components; it complements rather than replaces process-level OS counters.
  • Headless/web servers still receive native process telemetry. Electron-specific power fields degrade to unknown instead of falling back to shell probes.
  • Collector failures degrade telemetry without taking down the server.

Validation

  • vp check
  • vp run typecheck
  • full server suite: 1,243 passed, 7 skipped
  • focused provider-log regression suite: 9 passed
  • 11 VCS/background-policy merge-integration tests
  • cargo fmt --manifest-path native/resource-monitor/Cargo.toml -- --check
  • cargo test --locked --manifest-path native/resource-monitor/Cargo.toml (6 tests)
  • cargo clippy --manifest-path native/resource-monitor/Cargo.toml -- -D warnings
  • vp run build:desktop
  • Live sidecar smoke test against the local process table, including full command-line and I/O output

Note

High Risk Large cross-cutting change to desktop backend spawn/teardown, release packaging, and new native binaries; incorrect fd/protocol handling or restart races could break backend startup or diagnostics, though coverage is extensive in tests.

Overview Adds a Rust t3-resource-monitor sidecar with CI formatting/tests and release artifacts per platform (mac/linux/win), bundled into the CLI and resolved from packaged resources or dev target/release for the desktop primary backend. WSL backends intentionally omit the monitor path.

Electron main now publishes NDJSON host telemetry (DesktopTelemetryPublisher + ElectronPowerMonitor) over inherited fds 4/5 alongside the fd 3 bootstrap; the backend manager wires control messages, demand-gated app.getAppMetrics(), and cleans up telemetry sources on stop/restart.

Backend child logging no longer writes stdout/stderr continuously: output is buffered (1 MiB / chunk limits) and flushed to server-child.log only on failure or readiness-timeout snapshots; normal shutdown discards the session. Trace batching widens to 1 s and per-chunk output tracing is removed.

Desktop backend process management gains structured tagged errors, post-exit output draining, readiness-timeout context, and safer stop/start/restart behavior (including restart during teardown and timed-out stop).

Mobile reports client activity leases and retains background scopes (e.g. VCS, diagnostics) while subscriptions are active.

Server adds a compile-time RPC_REQUIRED_SCOPES map so every WebSocket RPC (including background policy and resource telemetry) has an explicit auth scope.

Preview forwards debugger screencast frames to recording listeners only while recording is active.

Reviewed by Cursor Bugbot for commit 6b8a5163be9bac7c3b8b73d8b59a0d7f4dc6ad48. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Add native resource monitor sidecar and background activity policy to reduce idle work and disk churn

  • Introduces a Rust native sidecar (main.rs) that samples system/process metrics via a JSON protocol; the server spawns and communicates with it through NativeTelemetryClient and ResourceMonitorBinary.
  • Adds a BackgroundPolicy service that tracks client activity leases, host power state, and per-scope demand to gate whether periodic background work (provider health refreshes, VCS remote polling) actually runs.
  • Adds ResourceTelemetry pipeline (NativeTelemetryClient → DesktopTelemetryReceiver → ResourceAttribution → ResourceTelemetry) and exposes history, retry, and live-subscription RPCs to clients.
  • Desktop backend now wires ElectronPowerMonitor and DesktopTelemetryPublisher to stream Electron process metrics and power state to the server over dedicated FDs (4/5).
  • Web and mobile clients gain background activity reporters that retain scope leases while subscriptions are active and periodically report visibility/focus/app-state to the server.
  • Provider health refresh loop and VCS remote refresh loop skip work when BackgroundPolicy reports no foreground demand, and the refresh interval is now driven by ServerSettingsService with a new structured backgroundActivity settings model.
  • EventNdjsonLogger is refactored into a shared log store with batched writes, size-bounded rotation, transient-event filtering, and periodic retention enforcement to reduce disk churn.
  • RotatingFileSink no longer triggers an immediate rotation after a write; rotation is deferred to the next write cycle.
  • Risk: LocalApi browser facade no longer exposes server.* methods or shell.openInEditor; callers that previously received a rejected Promise will now see undefined properties.

Macroscope summarized 6b8a516.

Synced from GitHub

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: db92ffbb-f25e-4d12-94a9-61f4c23ece27

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch t3code/a55b49df

Comment @coderabbitai help to get the list of available commands.

apps/web/src/hooks/serverSettingsWriteQueue.tslines 9–30Outdated · 038e209Open thread
……@@ -0,0 +1,31 @@
27+ queue = Promise.resolve();
28+ },
29+ drain: () => queue,
30+ };
MmacroscopeappBoton lines 9–30May 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.

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.

CcursorBotreviewed · 2 inline threadsMay 13, 2026
Synced from GitHub

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Autofix Details

Bugbot Autofix prepared fixes for both issues found in the latest run.

  • ✅ Fixed: Legacy patch fields silently override explicit backgroundActivity patch
    • Added a guard to skip computing backgroundActivityPatch when the explicit backgroundActivity field is present in the patch, ensuring new-style patches take precedence over legacy fields.
  • ✅ Fixed: Duplicated utility functions across settings panel files
    • Extracted BackgroundActivityOverridePatch, durationToSeconds, normalizeIntervalSeconds, backgroundActivityOverrideSettings, and BackgroundPolicyTooltip into a shared backgroundActivityUtils.tsx module imported by both SettingsPanels.tsx and SourceControlSettings.tsx.

Create PR

Or push these changes by commenting:

1@cursor push 6d184f0660
Preview (pingdotgg/t3code@6d184f0660)
diff
1diff --git a/apps/web/src/components/settings/SettingsPanels.tsx b/apps/web/src/components/settings/SettingsPanels.tsx
2--- a/apps/web/src/components/settings/SettingsPanels.tsx
3+++ b/apps/web/src/components/settings/SettingsPanels.tsx
4@@ -1,7 +1,6 @@
5 import {
6 ArchiveIcon,
7 ArchiveX,
8- InfoIcon,
9 LoaderIcon,
10 PlusIcon,
11 RefreshCwIcon,
12@@ -106,6 +105,12 @@
13 SettingsSection,
14 useRelativeTimeTick,
15 } from "./settingsLayout";
16+import {
17+ BackgroundPolicyTooltip as PolicyTooltip,
18+ backgroundActivityOverrideSettings,
19+ durationToSeconds,
20+ normalizeIntervalSeconds,
21+} from "./backgroundActivityUtils";
22 import { ProjectFavicon } from "../ProjectFavicon";
23 import { useServerObservability, useServerProviders } from "../../rpc/serverState";
24
25@@ -137,11 +142,6 @@
26 };
27
28 type BackgroundActivityProfileOption = BackgroundActivityProfile | "advanced";
29-type BackgroundActivityOverridePatch = Partial<{
30- [K in keyof BackgroundActivitySettings["overrides"]]:
31- | BackgroundActivitySettings["overrides"][K]
32- | undefined;
33-}>;
34
35 const BACKGROUND_ACTIVITY_PROFILE_OPTION_LABELS: Record<BackgroundActivityProfileOption, string> = {
36 ...BACKGROUND_ACTIVITY_PROFILE_LABELS,
37@@ -174,17 +174,6 @@
38 { key: "pauseWhenOnBattery", label: "Pause on battery" },
39 ];
40
41-function durationToSeconds(duration: Duration.Duration): number {
42- return Math.round(Duration.toMillis(duration) / 1_000);
43-}
44-
45-function normalizeIntervalSeconds(value: number | null): number {
46- if (value === null || !Number.isFinite(value)) {
47- return 0;
48- }
49- return Math.max(0, Math.round(value));
50-}
51-
52 function resolveBackgroundActivityProfileOption(settings: {
53 readonly backgroundActivity: BackgroundActivitySettings;
54 }): BackgroundActivityProfileOption {
55@@ -209,50 +198,6 @@
56 };
57 }
58
59-function backgroundActivityOverrideSettings(
60- current: BackgroundActivitySettings,
61- overrides: BackgroundActivityOverridePatch,
62-) {
63- const nextOverrides: BackgroundActivityOverridePatch = {
64- ...current.overrides,
65- ...overrides,
66- };
67- for (const [key, value] of Object.entries(nextOverrides)) {
68- if (value === undefined) {
69- delete nextOverrides[key as keyof typeof nextOverrides];
70- }
71- }
72- return {
73- backgroundActivity: {
74- schemaVersion: 1 as const,
75- profile: "custom" as const,
76- baseProfile: getBackgroundActivityBaseProfile(current),
77- overrides: nextOverrides as BackgroundActivitySettings["overrides"],
78- },
79- };
80-}
81-
82-function PolicyTooltip({ children }: { readonly children: string }) {
83- return (
84- <Tooltip>
85- <TooltipTrigger
86- render={
87- <button
88- type="button"
89- className="inline-flex size-5 items-center justify-center rounded-sm text-muted-foreground hover:text-foreground"
90- aria-label="Background policy details"
91- >
92- <InfoIcon className="size-3.5" />
93- </button>
94- }
95- />
96- <TooltipPopup side="top" className="max-w-72">
97- {children}
98- </TooltipPopup>
99- </Tooltip>
100- );
101-}
102-
103 function withoutProviderInstanceKey<V>(
104 record: Readonly<Record<ProviderInstanceId, V>> | undefined,
105 key: ProviderInstanceId,
106
107diff --git a/apps/web/src/components/settings/SourceControlSettings.tsx b/apps/web/src/components/settings/SourceControlSettings.tsx
108--- a/apps/web/src/components/settings/SourceControlSettings.tsx
109+++ b/apps/web/src/components/settings/SourceControlSettings.tsx
110@@ -1,9 +1,8 @@
111-import { ChevronDownIcon, GitPullRequestIcon, InfoIcon, RefreshCwIcon } from "lucide-react";
112+import { ChevronDownIcon, GitPullRequestIcon, RefreshCwIcon } from "lucide-react";
113 import * as Duration from "effect/Duration";
114 import * as Option from "effect/Option";
115 import { useState, type ReactNode } from "react";
116 import type {
117- BackgroundActivitySettings,
118 SourceControlProviderKind,
119 SourceControlDiscoveryResult,
120 SourceControlProviderAuth,
121@@ -55,6 +54,12 @@
122 } from "../Icons";
123 import { RedactedSensitiveText } from "./RedactedSensitiveText";
124 import { SettingResetButton, SettingsPageContainer, SettingsSection } from "./settingsLayout";
125+import {
126+ BackgroundPolicyTooltip,
127+ backgroundActivityOverrideSettings,
128+ durationToSeconds,
129+ normalizeIntervalSeconds as normalizeFetchIntervalSeconds,
130+} from "./backgroundActivityUtils";
131
132 const EMPTY_DISCOVERY_RESULT: SourceControlDiscoveryResult = {
133 versionControlSystems: [],
134@@ -75,44 +80,7 @@
135
136 const SOURCE_CONTROL_SKELETON_ROWS = ["primary", "secondary"] as const;
137 const GIT_FETCH_INTERVAL_STEP_SECONDS = 5;
138-type BackgroundActivityOverridePatch = Partial<{
139- [K in keyof BackgroundActivitySettings["overrides"]]:
140- | BackgroundActivitySettings["overrides"][K]
141- | undefined;
142-}>;
143
144-function durationToSeconds(duration: Duration.Duration): number {
145- return Math.round(Duration.toMillis(duration) / 1_000);
146-}
147-
148-function normalizeFetchIntervalSeconds(value: number | null): number {
149- if (value === null || !Number.isFinite(value)) {
150- return 0;
151- }
152- return Math.max(0, Math.round(value));
153-}
154-
155-function BackgroundPolicyTooltip({ children }: { readonly children: string }) {
156- return (
157- <Tooltip>
158- <TooltipTrigger
159- render={
160- <button
161- type="button"
162- className="inline-flex size-5 items-center justify-center rounded-sm text-muted-foreground hover:text-foreground"
163- aria-label="Background policy details"
164- >
165- <InfoIcon className="size-3.5" />
166- </button>
167- }
168- />
169- <TooltipPopup side="top" className="max-w-72">
170- {children}
171- </TooltipPopup>
172- </Tooltip>
173- );
174-}
175-
176 function optionLabel(value: Option.Option<string>): string | null {
177 return Option.getOrNull(value);
178 }
179@@ -335,28 +303,6 @@
180 );
181 const canResetFetchInterval =
182 automaticGitFetchIntervalSeconds !== defaultAutomaticGitFetchIntervalSeconds;
183- const backgroundActivityOverrideSettings = (
184- current: BackgroundActivitySettings,
185- overrides: BackgroundActivityOverridePatch,
186- ) => {
187- const nextOverrides: BackgroundActivityOverridePatch = {
188- ...current.overrides,
189- ...overrides,
190- };
191- for (const [key, value] of Object.entries(nextOverrides)) {
192- if (value === undefined) {
193- delete nextOverrides[key as keyof typeof nextOverrides];
194- }
195- }
196- return {
197- backgroundActivity: {
198- schemaVersion: 1 as const,
199- profile: "custom" as const,
200- baseProfile: getBackgroundActivityBaseProfile(current),
201- overrides: nextOverrides as BackgroundActivitySettings["overrides"],
202- },
203- };
204- };
205
206 return (
207 <div className="grid gap-3">
208
209diff --git a/apps/web/src/components/settings/backgroundActivityUtils.tsx b/apps/web/src/components/settings/backgroundActivityUtils.tsx
210new file mode 100644
211--- /dev/null
212+++ b/apps/web/src/components/settings/backgroundActivityUtils.tsx
213@@ -1,0 +1,67 @@
214+import { InfoIcon } from "lucide-react";
215+import * as Duration from "effect/Duration";
216+import type { BackgroundActivitySettings } from "@t3tools/contracts";
217+import { getBackgroundActivityBaseProfile } from "@t3tools/shared/backgroundActivitySettings";
218+
219+import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";
220+
221+export type BackgroundActivityOverridePatch = Partial<{
222+ [K in keyof BackgroundActivitySettings["overrides"]]:
223+ | BackgroundActivitySettings["overrides"][K]
224+ | undefined;
225+}>;
226+
227+export function durationToSeconds(duration: Duration.Duration): number {
228+ return Math.round(Duration.toMillis(duration) / 1_000);
229+}
230+
231+export function normalizeIntervalSeconds(value: number | null): number {
232+ if (value === null || !Number.isFinite(value)) {
233+ return 0;
234+ }
235+ return Math.max(0, Math.round(value));
236+}
237+
238+export function backgroundActivityOverrideSettings(
239+ current: BackgroundActivitySettings,
240+ overrides: BackgroundActivityOverridePatch,
241+) {
242+ const nextOverrides: BackgroundActivityOverridePatch = {
243+ ...current.overrides,
244+ ...overrides,
245+ };
246+ for (const [key, value] of Object.entries(nextOverrides)) {
247+ if (value === undefined) {
248+ delete nextOverrides[key as keyof typeof nextOverrides];
249+ }
250+ }
251+ return {
252+ backgroundActivity: {
253+ schemaVersion: 1 as const,
254+ profile: "custom" as const,
255+ baseProfile: getBackgroundActivityBaseProfile(current),
256+ overrides: nextOverrides as BackgroundActivitySettings["overrides"],
257+ },
258+ };
259+}
260+
261+export function BackgroundPolicyTooltip({ children }: { readonly children: string }) {
262+ return (
263+ <Tooltip>
264+ <TooltipTrigger
265+ render={
266+ <button
267+ type="button"
268+ className="inline-flex size-5 items-center justify-center rounded-sm text-muted-foreground hover:text-foreground"
269+ aria-label="Background policy details"
270+ >
271+ <InfoIcon className="size-3.5" />
272+ </button>
273+ }
274+ />
275+ <TooltipPopup side="top" className="max-w-72">
276+ {children}
277+ </TooltipPopup>
278+ </Tooltip>
279+ );
280+}
281
282diff --git a/packages/shared/src/serverSettings.ts b/packages/shared/src/serverSettings.ts
283--- a/packages/shared/src/serverSettings.ts
284+++ b/packages/shared/src/serverSettings.ts
285@@ -89,26 +89,28 @@
286 ...patchForMerge
287 } = patch;
288 const backgroundActivityPatch =
289- backgroundActivityProfile !== undefined
290- ? {
291- schemaVersion: 1 as const,
292- profile: backgroundActivityProfile,
293- overrides: {},
294- }
295- : automaticGitFetchInterval !== undefined || providerHealthRefreshInterval !== undefined
296+ backgroundActivity !== undefined
297+ ? undefined
298+ : backgroundActivityProfile !== undefined
299 ? {
300 schemaVersion: 1 as const,
301- profile: "custom" as const,
302- baseProfile: getBackgroundActivityBaseProfile(current.backgroundActivity),
303- overrides: {
304- ...current.backgroundActivity.overrides,
305- ...(automaticGitFetchInterval !== undefined ? { automaticGitFetchInterval } : {}),
306- ...(providerHealthRefreshInterval !== undefined
307- ? { providerHealthRefreshInterval }
308- : {}),
309- },
310+ profile: backgroundActivityProfile,
311+ overrides: {},
312 }
313- : undefined;
314+ : automaticGitFetchInterval !== undefined || providerHealthRefreshInterval !== undefined
315+ ? {
316+ schemaVersion: 1 as const,
317+ profile: "custom" as const,
318+ baseProfile: getBackgroundActivityBaseProfile(current.backgroundActivity),
319+ overrides: {
320+ ...current.backgroundActivity.overrides,
321+ ...(automaticGitFetchInterval !== undefined ? { automaticGitFetchInterval } : {}),
322+ ...(providerHealthRefreshInterval !== undefined
323+ ? { providerHealthRefreshInterval }
324+ : {}),
325+ },
326+ }
327+ : undefined;
328 const next = deepMerge(current, patchForMerge);
329 const nextWithReplacementsBase = {
330 ...next,

You can send follow-ups to the cloud agent here.

packages/shared/src/serverSettings.tsline 120Outdated · 038e209Open thread
……@@ -76,14 +81,61 @@ export function applyServerSettingsPatch(
117+ : {}),
118+ ...(backgroundActivityPatch !== undefined
119+ ? { backgroundActivity: backgroundActivityPatch }
120+ : {}),
CcursorBoton line 120May 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.

……@@ -70,6 +75,11 @@ const VCS_ICONS: Partial<Record<VcsDriverKind, Icon>> = {
79+ [K in keyof BackgroundActivitySettings["overrides"]]:
80+ | BackgroundActivitySettings["overrides"][K]
81+ | undefined;
82+}>;
CcursorBoton line 82May 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.

Synced from GitHub

Approvability

Verdict: Needs human review

Diff is too large for automated approval analysis. A human reviewer should evaluate this PR.

You can customize Macroscope's approvability policy. Learn more.

apps/web/src/components/settings/SettingsPanels.tsxfileOutdated · 8da0859Open thread

🟡 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.

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.

CcursorBotreviewed · 3 inline threadsJun 16, 2026
Synced from GitHub

Cursor Bugbot has reviewed your changes and found 3 potential issues.

Autofix Details

Bugbot Autofix prepared fixes for all 3 issues found in the latest run.

  • ✅ Fixed: VCS scope cwd mismatch
    • Added a scopeCwd parameter to makeRemoteRefreshLoop and retainRemotePoller that passes the raw (un-normalized) input.cwd to shouldRunScopeWork, ensuring scope keys match the client's lease scopes.
  • ✅ Fixed: Disconnect never clears leases
    • Added removeSession method to BackgroundPolicy that removes leases by sessionId, and wired it into the WebSocket disconnect handler's acquireUseRelease release function.
  • ✅ Fixed: Advanced dialog clears overrides
    • Changed the Advanced dialog's shared policy Select to update baseProfile while keeping profile as 'custom' and preserving existing overrides, instead of calling backgroundActivityProfileSettings which resets everything.

Create PR

Or push these changes by commenting:

1@cursor push 44ee7a6c3a
Preview (pingdotgg/t3code@44ee7a6c3a)
diff
1diff --git a/apps/server/src/background/BackgroundPolicy.ts b/apps/server/src/background/BackgroundPolicy.ts
2--- a/apps/server/src/background/BackgroundPolicy.ts
3+++ b/apps/server/src/background/BackgroundPolicy.ts
4@@ -30,6 +30,7 @@
5 input: ClientActivityReportInput,
6 ) => Effect.Effect<void>;
7 readonly removeRpcClient: (rpcClientId: RpcClientId) => Effect.Effect<void>;
8+ readonly removeSession: (sessionId: AuthSessionId) => Effect.Effect<void>;
9 readonly reportHostPowerState: (snapshot: HostPowerSnapshot) => Effect.Effect<void>;
10 readonly snapshot: Effect.Effect<BackgroundPolicySnapshot>;
11 readonly streamChanges: Stream.Stream<BackgroundPolicySnapshot>;
12@@ -218,6 +219,17 @@
13 return next;
14 }).pipe(Effect.andThen(publishSnapshot), Effect.asVoid);
15
16+ const removeSession: BackgroundPolicyShape["removeSession"] = (sessionId) =>
17+ Ref.update(leasesRef, (leases) => {
18+ const next = new Map(leases);
19+ for (const [key, lease] of next) {
20+ if (lease.sessionId === sessionId) {
21+ next.delete(key);
22+ }
23+ }
24+ return next;
25+ }).pipe(Effect.andThen(publishSnapshot), Effect.asVoid);
26+
27 const hasDemand: BackgroundPolicyShape["hasDemand"] = (scope) =>
28 Effect.map(snapshot, (current) => current.activeScopeKeys.includes(scopeKey(scope)));
29
30@@ -264,6 +276,7 @@
31 return BackgroundPolicy.of({
32 reportClientActivity,
33 removeRpcClient,
34+ removeSession,
35 reportHostPowerState: hostPowerMonitor.report,
36 snapshot,
37 streamChanges: Stream.fromPubSub(changes),
38
39diff --git a/apps/server/src/provider/Layers/ProviderInstanceRegistryLive.test.ts b/apps/server/src/provider/Layers/ProviderInstanceRegistryLive.test.ts
40--- a/apps/server/src/provider/Layers/ProviderInstanceRegistryLive.test.ts
41+++ b/apps/server/src/provider/Layers/ProviderInstanceRegistryLive.test.ts
42@@ -64,6 +64,7 @@
43 const BackgroundPolicyAlwaysRunLayer = Layer.mock(BackgroundPolicy.BackgroundPolicy)({
44 reportClientActivity: () => Effect.void,
45 removeRpcClient: () => Effect.void,
46+ removeSession: () => Effect.void,
47 reportHostPowerState: () => Effect.void,
48 snapshot: Effect.succeed({
49 hostPower: {
50
51diff --git a/apps/server/src/provider/Layers/ProviderRegistry.test.ts b/apps/server/src/provider/Layers/ProviderRegistry.test.ts
52--- a/apps/server/src/provider/Layers/ProviderRegistry.test.ts
53+++ b/apps/server/src/provider/Layers/ProviderRegistry.test.ts
54@@ -78,6 +78,7 @@
55 const BackgroundPolicyAlwaysRunLayer = Layer.mock(BackgroundPolicy.BackgroundPolicy)({
56 reportClientActivity: () => Effect.void,
57 removeRpcClient: () => Effect.void,
58+ removeSession: () => Effect.void,
59 reportHostPowerState: () => Effect.void,
60 snapshot: Effect.succeed({
61 hostPower: {
62
63diff --git a/apps/server/src/provider/makeManagedServerProvider.test.ts b/apps/server/src/provider/makeManagedServerProvider.test.ts
64--- a/apps/server/src/provider/makeManagedServerProvider.test.ts
65+++ b/apps/server/src/provider/makeManagedServerProvider.test.ts
66@@ -97,6 +97,7 @@
67 return Layer.mock(BackgroundPolicy.BackgroundPolicy)({
68 reportClientActivity: () => Effect.void,
69 removeRpcClient: () => Effect.void,
70+ removeSession: () => Effect.void,
71 reportHostPowerState: () => Effect.void,
72 snapshot: Effect.succeed({
73 hostPower: {
74
75diff --git a/apps/server/src/server.test.ts b/apps/server/src/server.test.ts
76--- a/apps/server/src/server.test.ts
77+++ b/apps/server/src/server.test.ts
78@@ -777,6 +777,7 @@
79 Layer.mock(BackgroundPolicy.BackgroundPolicy)({
80 reportClientActivity: () => Effect.void,
81 removeRpcClient: () => Effect.void,
82+ removeSession: () => Effect.void,
83 reportHostPowerState: () => Effect.void,
84 snapshot: Effect.succeed({
85 hostPower: {
86
87diff --git a/apps/server/src/vcs/VcsStatusBroadcaster.test.ts b/apps/server/src/vcs/VcsStatusBroadcaster.test.ts
88--- a/apps/server/src/vcs/VcsStatusBroadcaster.test.ts
89+++ b/apps/server/src/vcs/VcsStatusBroadcaster.test.ts
90@@ -92,6 +92,7 @@
91 return Layer.mock(BackgroundPolicy.BackgroundPolicy)({
92 reportClientActivity: () => Effect.void,
93 removeRpcClient: () => Effect.void,
94+ removeSession: () => Effect.void,
95 reportHostPowerState: () => Effect.void,
96 snapshot: Effect.succeed({
97 hostPower: {
98
99diff --git a/apps/server/src/vcs/VcsStatusBroadcaster.ts b/apps/server/src/vcs/VcsStatusBroadcaster.ts
100--- a/apps/server/src/vcs/VcsStatusBroadcaster.ts
101+++ b/apps/server/src/vcs/VcsStatusBroadcaster.ts
102@@ -258,6 +258,7 @@
103
104 const makeRemoteRefreshLoop = (
105 cwd: string,
106+ scopeCwd: string,
107 automaticRemoteRefreshInterval: Effect.Effect<Duration.Duration, never>,
108 refreshImmediately: boolean,
109 ) => {
110@@ -274,7 +275,7 @@
111
112 const shouldRun = yield* backgroundPolicy.shouldRunScopeWork({
113 type: "vcs-status",
114- cwd,
115+ cwd: scopeCwd,
116 });
117 if (!shouldRun) {
118 return activeInterval;
119@@ -322,6 +323,7 @@
120
121 const retainRemotePoller = Effect.fn("VcsStatusBroadcaster.retainRemotePoller")(function* (
122 cwd: string,
123+ scopeCwd: string,
124 automaticRemoteRefreshInterval: Effect.Effect<Duration.Duration, never>,
125 refreshImmediately: boolean,
126 ) {
127@@ -336,7 +338,12 @@
128 return Effect.succeed([undefined, nextPollers] as const);
129 }
130
131- return makeRemoteRefreshLoop(cwd, automaticRemoteRefreshInterval, refreshImmediately).pipe(
132+ return makeRemoteRefreshLoop(
133+ cwd,
134+ scopeCwd,
135+ automaticRemoteRefreshInterval,
136+ refreshImmediately,
137+ ).pipe(
138 Effect.forkIn(broadcasterScope),
139 Effect.map((fiber) => {
140 const nextPollers = new Map(activePollers);
141@@ -388,6 +395,7 @@
142 const initialRemote = cachedStatus?.remote?.value ?? null;
143 yield* retainRemotePoller(
144 cwd,
145+ input.cwd,
146 options?.automaticRemoteRefreshInterval ??
147 Effect.succeed(DEFAULT_VCS_STATUS_REFRESH_INTERVAL),
148 cachedStatus?.remote === null || cachedStatus?.remote === undefined,
149
150diff --git a/apps/server/src/ws.ts b/apps/server/src/ws.ts
151--- a/apps/server/src/ws.ts
152+++ b/apps/server/src/ws.ts
153@@ -1669,6 +1669,7 @@
154 const request = yield* HttpServerRequest.HttpServerRequest;
155 const serverAuth = yield* EnvironmentAuth.EnvironmentAuth;
156 const sessions = yield* SessionStore.SessionStore;
157+ const backgroundPolicy = yield* BackgroundPolicy.BackgroundPolicy;
158 const session = yield* serverAuth.authenticateWebSocketUpgrade(request).pipe(
159 Effect.catchTags({
160 ServerAuthInvalidCredentialError: (error) => failEnvironmentAuthInvalid(error.reason),
161@@ -1710,7 +1711,11 @@
162 return yield* Effect.acquireUseRelease(
163 sessions.markConnected(session.sessionId),
164 () => rpcWebSocketHttpEffect,
165- () => sessions.markDisconnected(session.sessionId),
166+ () =>
167+ Effect.all([
168+ sessions.markDisconnected(session.sessionId),
169+ backgroundPolicy.removeSession(session.sessionId),
170+ ]).pipe(Effect.asVoid),
171 );
172 }).pipe(
173 Effect.catchTags({
174
175diff --git a/apps/web/src/components/settings/SettingsPanels.tsx b/apps/web/src/components/settings/SettingsPanels.tsx
176--- a/apps/web/src/components/settings/SettingsPanels.tsx
177+++ b/apps/web/src/components/settings/SettingsPanels.tsx
178@@ -680,7 +680,14 @@
179 value === "performance" ||
180 value === "battery-saver"
181 ) {
182- updateSettings(backgroundActivityProfileSettings(value));
183+ updateSettings({
184+ backgroundActivity: {
185+ schemaVersion: 1 as const,
186+ profile: "custom" as const,
187+ baseProfile: value,
188+ overrides: settings.backgroundActivity.overrides,
189+ },
190+ });
191 }
192 }}
193 >

You can send follow-ups to the cloud agent here.

apps/web/src/components/settings/SettingsPanels.tsxline 684Outdated · 79ad071Open thread
……@@ -479,10 +630,255 @@ export function useSettingsRestore(onRestored?: () => void) {
681+ value === "battery-saver"
682+ ) {
683+ updateSettings(backgroundActivityProfileSettings(value));
684+ }
CcursorBoton line 684Jun 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.tsline 281Outdated · 79ad071Open thread
……@@ -270,6 +272,14 @@ export const layer = Layer.effect(
278+ });
279+ if (!shouldRun) {
280+ return activeInterval;
281+ }
CcursorBoton line 281Jun 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/background/BackgroundPolicy.tsline 219Outdated · 79ad071Open thread
……@@ -0,0 +1,276 @@
216+ }
217+ }
218+ return next;
219+ }).pipe(Effect.andThen(publishSnapshot), Effect.asVoid);
CcursorBoton line 219Jun 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.

Synced from GitHub

🚀 Expo continuous deployment is ready!

  • Project → t3-code
  • Platforms → android, ios
  • Scheme → t3code-preview
 🤖 Android🍎 iOS
Fingerprint27e228668e373d1941548dc669856ecf3fd6261ffa880122b8876ecaafbb21281d21e292d7b09484
Build DetailsBuild Permalink
DetailsDistribution: INTERNAL
Build profile: preview:dev
Runtime version: 27e228668e373d1941548dc669856ecf3fd6261f
App version: 0.1.0
Git commit: 47234257cf10fea3f507f57916355a6ae1cde29c
Build Permalink
DetailsDistribution: INTERNAL
Build profile: preview:dev
Runtime version: fa880122b8876ecaafbb21281d21e292d7b09484
App version: 0.1.0
Git commit: 8ef95c479ae1c01cbd33365fe792b762398ffd5d
Update DetailsUpdate Permalink
DetailsBranch: pr-2679
Runtime version: 27e228668e373d1941548dc669856ecf3fd6261f
Git commit: 47234257cf10fea3f507f57916355a6ae1cde29c
Update Permalink
DetailsBranch: pr-2679
Runtime version: fa880122b8876ecaafbb21281d21e292d7b09484
Git commit: 47234257cf10fea3f507f57916355a6ae1cde29c
Update QRExternal imageExternal image
apps/server/src/ws.tsfileOutdated · 79ad071Open thread

🔴 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).

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.

……@@ -0,0 +1,922 @@
674+ if (previous.sampleIntervalMs !== next.sampleIntervalMs) {
675+ yield* publishHealth;
676+ }
677+ });
CcursorBoton line 677Jun 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.

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.

CcursorBotreviewed · 1 inline threadJun 17, 2026
Synced from GitHub

Cursor Bugbot has reviewed your changes and found 1 potential issue.

There are 2 total unresolved issues (including 1 from previous review).

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Output drain timeout drops logs
    • Added Fiber.interrupt for all output fibers after the 250ms drain timeout so that still-running fibers are stopped before persistFailure snapshots and clears the session buffer, preventing late chunks from being lost.

Create PR

Or push these changes by commenting:

1@cursor push 3c6ed7cfa6
Preview (pingdotgg/t3code@3c6ed7cfa6)
diff
1diff --git a/apps/desktop/src/backend/DesktopBackendManager.ts b/apps/desktop/src/backend/DesktopBackendManager.ts
2--- a/apps/desktop/src/backend/DesktopBackendManager.ts
3+++ b/apps/desktop/src/backend/DesktopBackendManager.ts
4@@ -330,6 +330,10 @@
5 concurrency: "unbounded",
6 discard: true,
7 }).pipe(Effect.timeout(DEFAULT_BACKEND_OUTPUT_DRAIN_TIMEOUT), Effect.ignore);
8+ yield* Effect.forEach(outputFibers, Fiber.interrupt, {
9+ concurrency: "unbounded",
10+ discard: true,
11+ }).pipe(Effect.ignore);
12 return exit;
13 });

You can send follow-ups to the cloud agent here.

apps/desktop/src/backend/DesktopBackendManager.tsline 332Outdated · 2b59d67Open thread
……@@ -274,7 +325,12 @@ const runBackendProcess = Effect.fn("runBackendProcess")(function* (
329+ yield* Effect.forEach(outputFibers, Fiber.await, {
330+ concurrency: "unbounded",
331+ discard: true,
332+ }).pipe(Effect.timeout(DEFAULT_BACKEND_OUTPUT_DRAIN_TIMEOUT), Effect.ignore);
CcursorBoton line 332Jun 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.

CcursorBotreviewed · 2 inline threadsJun 19, 2026
Synced from GitHub

Cursor Bugbot has reviewed your changes using default effort and found 2 potential issues.

There are 3 total unresolved issues (including 1 from previous review).

Autofix Details

Bugbot Autofix prepared fixes for both issues found in the latest run.

  • ✅ Fixed: Readiness timeout writes failure logs
    • Removed the persistFailure call from onReadinessFailure since the backend process is still running during a readiness timeout and failure logs should only be persisted on actual process exit via finalizeRun.
  • ✅ Fixed: Host suspend does not gate work
    • Added an early return of true in isHostConstrained when hostPower.suspended is true, so background work is paused during host suspend.

Create PR

Or push these changes by commenting:

1@cursor push 54bbd4877a
Preview (pingdotgg/t3code@54bbd4877a)
diff
1diff --git a/apps/desktop/src/backend/DesktopBackendManager.ts b/apps/desktop/src/backend/DesktopBackendManager.ts
2--- a/apps/desktop/src/backend/DesktopBackendManager.ts
3+++ b/apps/desktop/src/backend/DesktopBackendManager.ts
4@@ -541,9 +541,6 @@
5 yield* logBackendManagerWarning("backend readiness check failed during bootstrap", {
6 error: error.message,
7 });
8- yield* backendOutputLog.persistFailure({
9- details: error.message,
10- });
11 },
12 ),
13 onOutput: (streamName, chunk) => backendOutputLog.writeOutputChunk(streamName, chunk),
14
15diff --git a/apps/server/src/background/BackgroundPolicy.ts b/apps/server/src/background/BackgroundPolicy.ts
16--- a/apps/server/src/background/BackgroundPolicy.ts
17+++ b/apps/server/src/background/BackgroundPolicy.ts
18@@ -82,6 +82,7 @@
19 settings: ResolvedBackgroundActivitySettings,
20 ): boolean {
21 if (hostPower.stale) return false;
22+ if (hostPower.suspended) return true;
23 if (
24 (settings.pauseWhenHostLocked && hostPower.locked === "true") ||
25 hasThermalPressure(hostPower)

You can send follow-ups to the cloud agent here.

apps/desktop/src/backend/DesktopBackendManager.tsline 547Outdated · 4a875dbOpen thread
……@@ -475,10 +536,16 @@ const makeDesktopBackendManager = Effect.fn("makeDesktopBackendManager")(functio
544+ yield* backendOutputLog.persistFailure({
545+ details: error.message,
546+ });
547+ },
CcursorBoton line 547Jun 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.tsline 93Outdated · 4a875dbOpen thread
……@@ -0,0 +1,274 @@
90+ }
91+ if (settings.pauseWhenHostLowPower && hostPower.lowPowerMode === "true") return true;
92+ return settings.pauseWhenOnBattery && hostPower.onBattery === "true";
93+}
CcursorBoton line 93Jun 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/desktop/src/telemetry/DesktopTelemetryPublisher.tsline 234Outdated · 4a875dbOpen thread
……@@ -0,0 +1,264 @@
231+ }
232+ };
233+
234+ const snapshots = Stream.concat(
MmacroscopeappBoton line 234Jun 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/ResourceMonitorBinary.tslines 172–177Outdated · 4a875dbOpen thread
……@@ -0,0 +1,193 @@
174+ path: candidate,
175+ mode: stat.value.mode,
176+ });
177+ }
MmacroscopeappBoton lines 172–177Jun 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

CcursorBotreviewed · 2 inline threadsJun 23, 2026
Synced from GitHub

Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.

There are 5 total unresolved issues (including 3 from previous reviews).

Autofix Details

Bugbot Autofix prepared fixes for both issues found in the latest run.

  • ✅ Fixed: Stuck flush flag drops batched logs
    • Changed drainPending to always clear flushScheduled to false after any flush (not just timer-fired), and added catchCause on scheduleFlush to reset the flag if the fork fails, preventing the flag from getting stuck.
  • ✅ Fixed: Flush failure disables thread logging
    • Changed drainPending to initialize failedSegments as a fresh empty set each drain instead of carrying over from state, so transient I/O errors no longer permanently block logging for a thread segment.

Create PR

Or push these changes by commenting:

1@cursor push b5ebfe1577
Preview (pingdotgg/t3code@b5ebfe1577)
diff
1diff --git a/apps/server/src/provider/Layers/EventNdjsonLogger.ts b/apps/server/src/provider/Layers/EventNdjsonLogger.ts
2--- a/apps/server/src/provider/Layers/EventNdjsonLogger.ts
3+++ b/apps/server/src/provider/Layers/EventNdjsonLogger.ts
4@@ -318,7 +318,7 @@
5 }
6
7 const sinks = new Map(input.state.sinks);
8- const failedSegments = new Set(input.state.failedSegments);
9+ const failedSegments = new Set<string>();
10 const failures: Array<FileOperationFailure> = [];
11 const attributionByStream = new Map<
12 EventNdjsonStream,
13@@ -399,7 +399,7 @@
14 pendingBytes: 0,
15 sinks,
16 failedSegments,
17- flushScheduled: input.timerFired ? false : input.state.flushScheduled,
18+ flushScheduled: false,
19 closed: input.close,
20 lastRetentionAt: retentionDue ? input.now : input.state.lastRetentionAt,
21 },
22@@ -557,7 +557,14 @@
23 if (action.flush) {
24 yield* flush(false, false);
25 } else if (action.schedule) {
26- yield* scheduleFlush();
27+ yield* scheduleFlush().pipe(
28+ Effect.catchCause(() =>
29+ SynchronizedRef.update(stateRef, (state) => ({
30+ ...state,
31+ flushScheduled: false,
32+ })),
33+ ),
34+ );
35 }
36 });

You can send follow-ups to the cloud agent here.

apps/server/src/provider/Layers/EventNdjsonLogger.tsline 561Outdated · 2f8211fOpen thread
……@@ -61,225 +164,421 @@ function resolveThreadSegment(raw: string | null | undefined): string {
558+ yield* flush(false, false);
559+ } else if (action.schedule) {
560+ yield* scheduleFlush();
561+ }
CcursorBoton line 561Jun 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.tsline 404Outdated · 2f8211fOpen thread
……@@ -61,225 +164,421 @@ function resolveThreadSegment(raw: string | null | undefined): string {
401+ failedSegments,
402+ flushScheduled: input.timerFired ? false : input.state.flushScheduled,
403+ closed: input.close,
404+ lastRetentionAt: retentionDue ? input.now : input.state.lastRetentionAt,
CcursorBoton line 404Jun 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.

CcursorBotreviewed · 1 inline threadJun 23, 2026
Synced from GitHub

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

There are 3 total unresolved issues (including 2 from previous reviews).

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Process diagnostics use stale cache
    • ProcessDiagnostics.read now attempts telemetry.refresh before mapping the snapshot, falling back to the cached latest on failure, ensuring on-demand reads always trigger a fresh sample from the native sidecar.

Create PR

Or push these changes by commenting:

1@cursor push 8e66c61cc5
Preview (pingdotgg/t3code@8e66c61cc5)
diff
1diff --git a/apps/server/src/diagnostics/ProcessDiagnostics.test.ts b/apps/server/src/diagnostics/ProcessDiagnostics.test.ts
2--- a/apps/server/src/diagnostics/ProcessDiagnostics.test.ts
3+++ b/apps/server/src/diagnostics/ProcessDiagnostics.test.ts
4@@ -101,9 +101,7 @@
5 const layer = ProcessDiagnostics.layer.pipe(Layer.provideMerge(telemetryLayer));
6
7 const diagnostics = yield* Effect.gen(function* () {
8- const telemetry = yield* ResourceTelemetry.ResourceTelemetry;
9 const processDiagnostics = yield* ProcessDiagnostics.ProcessDiagnostics;
10- yield* telemetry.refresh;
11 return yield* processDiagnostics.read;
12 }).pipe(Effect.provide(layer));
13
14
15diff --git a/apps/server/src/diagnostics/ProcessDiagnostics.ts b/apps/server/src/diagnostics/ProcessDiagnostics.ts
16--- a/apps/server/src/diagnostics/ProcessDiagnostics.ts
17+++ b/apps/server/src/diagnostics/ProcessDiagnostics.ts
18@@ -72,7 +72,8 @@
19
20 export const make = Effect.fn("makeProcessDiagnostics")(function* () {
21 const telemetry = yield* ResourceTelemetry.ResourceTelemetry;
22- const read: ProcessDiagnosticsShape["read"] = telemetry.latest.pipe(
23+ const read: ProcessDiagnosticsShape["read"] = telemetry.refresh.pipe(
24+ Effect.catch(() => telemetry.latest),
25 Effect.map((snapshot) => {
26 const processes = snapshot.processes
27 .filter((entry) => entry.identity.pid !== process.pid)

You can send follow-ups to the cloud agent here.

apps/server/src/diagnostics/ProcessDiagnostics.tsline 102Outdated · 8e2d5d3Open thread
……@@ -111,465 +52,140 @@ class ProcessDiagnosticsSignalFailedError extends Schema.TaggedErrorClass<Proces
99+ totalCpuPercent: processes.reduce((total, entry) => total + entry.cpuPercent, 0),
100+ processes,
101+ error: Option.map(snapshot.health.native.lastError, (message) => ({ message })),
102+ };
CcursorBoton line 102Jun 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.

apps/server/src/provider/Layers/EventNdjsonLogger.tslines 168–169Outdated · 8e2d5d3Open thread
……@@ -62,225 +165,421 @@ function resolveThreadSegment(raw: string | null | undefined): string {
68- }
69- return typeof message === "string" ? message : String(message);
168+function resolveStreamLabel(stream: EventNdjsonStream): string {
169+ return stream === "native" ? "NTIVE" : "CANON";
MmacroscopeappBoton lines 168–169Jun 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\`;`

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/ws.tsline 1608Outdated · deb3d48Open thread
……@@ -1571,10 +1582,42 @@ const makeWsRpcLayer = (
1605+ currentSessionId,
1606+ RpcClientId.make(metadata.client.id),
1607+ input,
1608+ ),
CcursorBoton line 1608Jul 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.

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

native/resource-monitor/src/main.rsline 592Outdated · deb3d48Open thread
……@@ -0,0 +1,766 @@
589+ let event = collector.sample(current, Some(request_id));
590+ history.record(&event);
591+ write_event(&mut writer, &event)?;
592+ next_sample_at = current
MmacroscopeappBoton line 592Jul 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.

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/web/src/components/settings/SettingsPanels.tsxline 249Outdated · deb3d48Open thread
……@@ -132,7 +163,128 @@ const TIMESTAMP_FORMAT_LABELS = {
246+ current: BackgroundActivitySettings,
247+ overrides: BackgroundActivityOverridePatch,
248+) {
249+ const nextOverrides: BackgroundActivityOverridePatch = {
MmacroscopeappBoton line 249Jul 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/server/src/resourceTelemetry/Model.tsline 287Outdated · deb3d48Open thread
……@@ -0,0 +1,559 @@
284+ let electron = input.counters.electron;
285+ let monitor = input.counters.monitor;
286+ let allT3 = input.counters.allT3;
287+ for (const processDelta of input.deltas) {
MmacroscopeappBoton line 287Jul 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.tsline 200Outdated · deb3d48Open thread
……@@ -0,0 +1,274 @@
197+ updatedAt: now,
198+ expiresAt,
199+ };
200+ yield* Ref.update(leasesRef, (leases) => {
MmacroscopeappBoton line 200Jul 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.tsline 365Outdated · deb3d48Open thread
……@@ -0,0 +1,559 @@
362+ };
363+}
364+
365+export function mergeProcesses(input: MergeProcessesInput): MergeProcessesResult {
MmacroscopeappBoton line 365Jul 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.tsline 45Outdated · deb3d48Open thread
……@@ -0,0 +1,274 @@
42+ "t3/background/BackgroundPolicy",
43+) {}
44+
45+const DEFAULT_LEASE_TTL_MS = 45_000;

🟡 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

……@@ -0,0 +1,193 @@
164+
165+ const resolve: ResourceMonitorBinaryShape["resolve"] = Effect.gen(function* () {
166+ for (const candidate of candidates) {
167+ const exists = yield* fileSystem.exists(candidate).pipe(Effect.orElseSucceed(() => false));
MmacroscopeappBoton line 167Jul 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

……@@ -0,0 +1,922 @@
501+ );
502+ yield* handle.stderr.pipe(Stream.runDrain, Effect.ignore, Effect.forkScoped);
503+
504+ const hello = yield* Deferred.await(helloDeferred).pipe(
MmacroscopeappBoton line 504Jul 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/backend/DesktopBackendManager.tsfileOutdated · deb3d48Open thread

🟡 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/provider/Layers/EventNdjsonLogger.tsline 381Outdated · deb3d48Open thread
……@@ -62,225 +165,421 @@ function resolveThreadSegment(raw: string | null | undefined): string {
378+ directory: input.directory,
379+ maxTotalBytes: input.options.maxTotalBytes,
380+ maxAgeMs: input.options.maxAgeMs,
381+ activeFilePaths: new Set(
MmacroscopeappBoton line 381Jul 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

……@@ -0,0 +1,922 @@
649+ Effect.forkScoped,
650+ );
651+
652+ const applyCollectionControl = Effect.fn(
MmacroscopeappBoton line 652Jul 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.

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.

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/server/src/provider/Layers/EventNdjsonLogger.tsline 37Outdated · deb3d48Open thread
……@@ -1,56 +1,159 @@
34+const DEFAULT_MAX_BUFFERED_RECORDS = 512;
2735const GLOBAL_THREAD_SEGMENT = "_global";
2836const LOG_SCOPE = "provider-observability";
37+const PROVIDER_LOG_FILE_PATTERN = /\.log(?:\.\d+)?$/u;

🟠 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.tsline 238Outdated · deb3d48Open thread
……@@ -0,0 +1,274 @@
235+ (current) => current.shouldRunOpportunisticWork,
236+ );
237+
238+ yield* Stream.runForEach(hostPowerMonitor.streamChanges, () => publishSnapshot).pipe(
MmacroscopeappBoton line 238Jul 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/app/DesktopObservability.tslines 447–450Outdated · deb3d48Open thread
……@@ -345,47 +412,84 @@ const makeBackendOutputLogShape = (
447+ const session = yield* Ref.modify(sessionRef, (current) => [
448+ current,
449+ Option.map(current, (value) => ({ ...value, chunks: [], byteLength: 0 })),
450+ ]);
MmacroscopeappBoton lines 447–450Jul 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.tsline 129Outdated · deb3d48Open thread
……@@ -1,56 +1,159 @@
126+ readonly bytes: number;
127+}
128+
129+interface StoreState {
MmacroscopeappBoton line 129Jul 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.tslines 362–366Outdated · deb3d48Open thread
……@@ -0,0 +1,412 @@
363+ ...current.value,
364+ power: { ...current.value.power, stale: true },
365+ };
366+ yield* Ref.set(latest, Option.some(staleSnapshot));
MmacroscopeappBoton lines 362–366Jul 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

……@@ -0,0 +1,1244 @@
959+ <div className="flex items-center gap-2 text-[10px] text-muted-foreground/65">
960+ <span className="size-1.5 rounded-full bg-emerald-500" />
961+ Sampling every 5 seconds
962+ </div>
CcursorBoton line 962Jul 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.tsline 246Outdated · 38797e9Open thread
……@@ -0,0 +1,264 @@
243+ ),
244+ ),
245+ Stream.fromPubSub(changes),
246+ );
CcursorBoton line 246Jul 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/background/BackgroundPolicy.tslines 80–93Outdated · 38797e9Open thread
……@@ -0,0 +1,274 @@
90+ }
91+ if (settings.pauseWhenHostLowPower && hostPower.lowPowerMode === "true") return true;
92+ return settings.pauseWhenOnBattery && hostPower.onBattery === "true";
93+}
MmacroscopeappBoton lines 80–93Jul 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

CcursorBotreviewedJul 29, 2026
Synced from GitHub

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

There are 19 total unresolved issues (including 18 from previous reviews).

Fix All in Cursor

Bugbot Autofix is ON, but a cloud agent failed to start.

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

Synced from GitHub

Effect service review

Reviewed the new/changed Effect services in this PR (resourceTelemetry/*, background/*, diagnostics/*, desktop/telemetry/*, provider/Layers/EventNdjsonLogger.ts, ProviderEventLoggers). Module layout, namespace imports, Context.Service + inline interface, make/layer naming, Foo["Service"] usage, dependency acquisition from the environment, and the test-only layerTest/Layer.mock seams all follow the conventions. No ManagedRuntime/runPromise leaked into domain services, and Layer.succeed is only used for test layers and the Electron boundary adapter (consistent with the other apps/desktop/src/electron/* modules).

Three error-modeling issues in the changed scope are noted inline.

Posted via Macroscope — Effect Service Conventions

Added 12 commits

  • Route desktop telemetry control and stale power handling9d15607
  • Buffer backend output before persisting failure logsa34671f
  • feat: add native resource telemetrya807364
  • Add background activity policy and host power monitoringd7f9ef7
  • fix(desktop): decode packaged host telemetry51ca44e
  • Polish resource diagnostics and harden RPC scopes53f2a8f
  • Remove legacy local backend and telemetry RPC7e86327
  • fix(desktop): honor backend stop deadlinesa2c20a4
  • Reduce provider observability disk churnb2c8abc
  • Address remaining observability review findingsc6f7b41
  • fix(server): preserve telemetry error structurec779e8b
  • Address observability review findingse0c0618
Synced from GitHub

Reviewed the new Effect services in this PR against the service conventions. Service definition, make/layer shape, namespace imports, dependency acquisition, and the ProviderEventLoggers/ProcessDiagnostics/ProcessResourceMonitor restructuring all look consistent, and the previously flagged binary.resolve collapse, fabricated timeout cause, and unused ProcessIdentityChanged union are resolved. Two remaining error-modelling issues in NativeTelemetryClient.ts.

Posted via Macroscope — Effect Service Conventions

Added 1 commit

  • fix(server): bound telemetry supervisor failuresf09c4be
Synced from GitHub

One finding on the new resource-telemetry service code. Previously flagged items (binary.resolve error collapsing, fabricated timeout cause, Cause.pretty in caller-visible lastError, unused ProcessIdentityChanged) are resolved in this revision.

Posted via Macroscope — Effect Service Conventions

Added 2 commits

  • fix(settings): preserve advanced activity overrides1d7c529
  • fix(observability): close lifecycle resource gaps6b8a516