[codex] Prevent background Git fetches from leaking temp packs#3537

closed7
main
codex/fix-git-fetch-temp-pack-leaks
openedJun 23, 2026byJJulius Marminge
View on GitHubTwo-way sync enabledPR last imported 3d ago

Summary

  • remove only newly-created tmp_pack_* files after a failed background status fetch
  • fail closed when a root or linked-worktree FETCH_HEAD.lock indicates concurrent fetch activity
  • expose upstream refresh failures separately from stale status reads so the existing scheduler applies exponential backoff
  • extend the status-fetch timeout from 5 seconds to 1 minute and cache failures for 30 seconds

Root cause

The status refresher killed every fetch after 5 seconds. Repositories whose fetches took longer could leave .git/objects/pack/tmp_pack_* files behind on every attempt. The driver then swallowed the refresh failure while returning stale status, so the broadcaster observed a successful status read and never activated its failure backoff.

Impact

Long-running or failed background fetches no longer accumulate newly-created temporary packs. Existing temporary packs are deliberately preserved, and cleanup is skipped whenever concurrent Git fetch activity cannot be ruled out.

This PR is stacked on #2679.

Closes #3525.

Validation

  • vp check
  • vp run typecheck
  • vp test apps/server/src/vcs/GitVcsDriverCore.test.ts apps/server/src/vcs/VcsStatusBroadcaster.test.ts apps/server/src/git/GitWorkflowService.test.ts (47 tests)

Note

Remove temporary Git pack files left by failed background fetches in GitVcsDriverCore

  • In GitVcsDriverCore.ts, fetchRemoteForStatus now snapshots tmp_pack_* files before fetching and deletes newly created ones after a failed fetch, skipping cleanup when a FETCH_HEAD.lock is present to avoid interfering with concurrent worktree fetches.
  • Introduces a new BackgroundPolicy service that gates background work (VCS status refresh, provider health checks, etc.) based on client activity leases, host power state, and a configurable activity profile (balanced/performance/battery-saver).
  • Adds a full resource telemetry subsystem: a Rust native sidecar (native/resource-monitor), a NativeTelemetryClient and DesktopTelemetryPublisher to stream process metrics, and a ResourceTelemetry service that merges native and desktop telemetry into grouped snapshots with history.
  • Rewrites ProcessDiagnostics and ProcessResourceMonitor to source data from ResourceTelemetry rather than spawning ps/powershell; process signaling now requires a matching startTimeMs identity.
  • Adds background activity settings UI in SettingsPanels.tsx and wires the Git fetch interval through the new background activity override system.
  • Risk: ProcessDiagnostics.signal now rejects requests where startTimeMs does not match the live telemetry snapshot, and ProcessResourceMonitor no longer self-samplesβ€”both are breaking behavioral changes for callers relying on prior error shapes.
πŸ“Š Macroscope summarized 5834060. 71 files reviewed, 0 issues evaluated, 0 issues filtered, 0 comments posted

πŸ—‚οΈ Filtered Issues

No issues evaluated.


Note

Medium Risk Touches background git fetch and filesystem cleanup in .git/objects/pack; incorrect lock or cleanup logic could interfere with concurrent Git activity, though tests cover lock skip and pack removal.

Overview Background upstream fetches for VCS status no longer leave behind new tmp_pack_* files when they fail or time out. fetchRemoteForStatus snapshots pack dir contents before fetch, surfaces non-zero exits as GitCommandError (instead of swallowing them), and on failure removes only new tmp_pack_* filesβ€”skipping cleanup when FETCH_HEAD.lock is present on the common dir or linked worktrees.

Fetch tuning changes: status upstream timeout 5s β†’ 1 minute, failure cache cooldown 5s β†’ 30s.

A dedicated refreshStatusUpstream path is added on GitVcsDriver / GitWorkflowService. VcsStatusBroadcaster calls it before invalidating remote cache and reads remote status with refreshUpstream: false, so fetch failures can drive backoff without discarding cached snapshots.

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

Added 7 commits

  • Route desktop telemetry control and stale power handling041f875
  • Add background activity policy and host power monitoring1115b14
  • feat: add native resource telemetry1732212
  • Prevent leaked Git status fetch packs5834060
  • Reduce provider observability disk churn8e2d5d3
  • Buffer backend output before persisting failure logsa03b7bc
  • Remove legacy local backend and telemetry RPCb239fa9
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

Run ID: 464bbb3b-a772-4a68-9e19-6cc604e2b870

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 codex/fix-git-fetch-temp-pack-leaks

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

CcursorBotreviewed Β· 1 inline threadJun 23, 2026
Synced from GitHub

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

Fix All in Cursor

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

  • βœ… Fixed: Deletes unrelated fetch temp packs
    • Expanded the lock check to also detect gc.pid, preventing the cleanup from deleting tmp_pack_* files created by concurrent git gc or git repack operations that don't hold FETCH_HEAD.lock.

Create PR

Or push these changes by commenting:

1@cursor push dbbfa6798f
Preview (pingdotgg/t3code@dbbfa6798f)
diff
1diff --git a/apps/server/src/vcs/GitVcsDriverCore.ts b/apps/server/src/vcs/GitVcsDriverCore.ts
2--- a/apps/server/src/vcs/GitVcsDriverCore.ts
3+++ b/apps/server/src/vcs/GitVcsDriverCore.ts
4@@ -968,7 +968,7 @@
5 ];
6 });
7
8- const hasActiveFetchLock = Effect.fn("GitVcsDriver.hasActiveFetchLock")(function* (
9+ const hasActiveGitPackLock = Effect.fn("GitVcsDriver.hasActiveGitPackLock")(function* (
10 gitCommonDir: string,
11 ): Effect.fn.Return<boolean> {
12 const lockPaths = yield* listFetchHeadLockPaths(gitCommonDir);
13@@ -988,6 +988,14 @@
14 return true;
15 }
16 }
17+ const gcPidExists = yield* fileSystem.exists(path.join(gitCommonDir, "gc.pid")).pipe(
18+ Effect.catchTags({
19+ PlatformError: () => Effect.succeed(false),
20+ }),
21+ );
22+ if (gcPidExists) {
23+ return true;
24+ }
25 return false;
26 });
27
28@@ -1004,13 +1012,10 @@
29 if (newFiles.length === 0) {
30 return;
31 }
32- if (yield* hasActiveFetchLock(gitCommonDir)) {
33- yield* Effect.logWarning(
34- "Skipped temporary Git pack cleanup while a fetch lock is active",
35- {
36- temporaryPackCount: newFiles.length,
37- },
38- );
39+ if (yield* hasActiveGitPackLock(gitCommonDir)) {
40+ yield* Effect.logWarning("Skipped temporary Git pack cleanup while a git lock is active", {
41+ temporaryPackCount: newFiles.length,
42+ });
43 return;
44 }

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

Reviewed by Cursor Bugbot for commit 58340604f0b7967e7a5a3e532993f3443442d14e. Configure here.

……@@ -920,23 +921,149 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*
1024+ PlatformError: () => Effect.succeed(false),
1025+ }),
1026+ ),
1027+ { concurrency: 1 },
CcursorBoton line 1027Jun 23, 2026

Deletes unrelated fetch temp packs

Medium Severity

After a failed status fetchRemoteForStatus, cleanup removes every new tmp_pack_* name versus a pre-fetch snapshot, not packs created only by that fetch. Another git fetch (for example fetchRemote) can fail, release FETCH_HEAD.lock, and leave temp packs that this path then deletes.

Reviewed by Cursor Bugbot for commit 58340604f0b7967e7a5a3e532993f3443442d14e. Configure here.

Synced from GitHub

Approvability

Verdict: Needs human review

15 blocking correctness issues found. Diff is too large for automated approval analysis. A human reviewer should evaluate this PR.

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

……@@ -0,0 +1,264 @@
231+ }
232+ };
233+
234+ const snapshots = Stream.concat(
MmacroscopeappBoton line 234Jul 29, 2026

🟑 Medium telemetry/DesktopTelemetryPublisher.ts:234

A telemetry consumer connecting via the encoded stream can miss the current snapshot. snapshots first reads Ref.get(latest) into a one-element stream, then subscribes to Stream.fromPubSub(changes). A snapshot published between the Ref.get(latest) read and the Stream.fromPubSub(changes) subscription is absent from both sources, so the consumer stays stale until the next periodic sample (up to 30 seconds in background mode). Subscribe to changes before reading latest, or use a replay-capable source.

πŸš€ Reply "fix it for me" or copy this AI Prompt for your agent:
text
1In file @apps/desktop/src/telemetry/DesktopTelemetryPublisher.ts around line 234:
2
3A telemetry consumer connecting via the `encoded` stream can miss the current snapshot. `snapshots` first reads `Ref.get(latest)` into a one-element stream, then subscribes to `Stream.fromPubSub(changes)`. A snapshot published between the `Ref.get(latest)` read and the `Stream.fromPubSub(changes)` subscription is absent from both sources, so the consumer stays stale until the next periodic sample (up to 30 seconds in background mode). Subscribe to `changes` before reading `latest`, or use a replay-capable source.
……@@ -0,0 +1,922 @@
366+ status: "healthy" as const,
367+ lastSampleAt: Option.some(sampledAt),
368+ lastError: Option.none(),
369+ }));
MmacroscopeappBoton line 369Jul 29, 2026

🟑 Medium resourceTelemetry/NativeTelemetryClient.ts:369

Successful snapshot and historyChunk events update state.status to "healthy" and clear lastError but never call publishHealth, so the healthChanges stream is not notified. After a recoverable monitor error sets the status to "degraded" and publishes that transition, a later successful snapshot restores the internal state to "healthy" without emitting a health change β€” subscribers that listen on healthChanges (e.g. ResourceTelemetry) never see the recovery and can remain visibly degraded indefinitely. The hello and error branches both call publishHealth after their Ref.update, but the snapshot and historyChunk branches omit it. Add a publishHealth call after each of those state updates.

πŸš€ Reply "fix it for me" or copy this AI Prompt for your agent:
text
1In file @apps/server/src/resourceTelemetry/NativeTelemetryClient.ts around line 369:
2
3Successful snapshot and `historyChunk` events update `state.status` to `"healthy"` and clear `lastError` but never call `publishHealth`, so the `healthChanges` stream is not notified. After a recoverable monitor error sets the status to `"degraded"` and publishes that transition, a later successful snapshot restores the internal state to `"healthy"` without emitting a health change β€” subscribers that listen on `healthChanges` (e.g. `ResourceTelemetry`) never see the recovery and can remain visibly `degraded` indefinitely. The `hello` and `error` branches both call `publishHealth` after their `Ref.update`, but the `snapshot` and `historyChunk` branches omit it. Add a `publishHealth` call after each of those state updates.
……@@ -0,0 +1,274 @@
42+ "t3/background/BackgroundPolicy",
43+) {}
44+
45+const DEFAULT_LEASE_TTL_MS = 45_000;
MmacroscopeappBoton line 45Jul 29, 2026

🟑 Medium background/BackgroundPolicy.ts:45

reportClientActivity always uses DEFAULT_LEASE_TTL_MS (or the client-supplied ttlMs) and never reads ResolvedBackgroundActivitySettings.idleClientTtl, so changing the server's idleClientTtl override has no effect on lease expiry. Clients keep getting 45-second leases regardless of the configured value, making the idleClientTtl background-activity setting non-functional. Consider clamping the resolved idleClientTtl setting instead of the hard-coded constant when input.ttlMs is not provided.

πŸš€ Reply "fix it for me" or copy this AI Prompt for your agent:
text
1In file @apps/server/src/background/BackgroundPolicy.ts around line 45:
2
3`reportClientActivity` always uses `DEFAULT_LEASE_TTL_MS` (or the client-supplied `ttlMs`) and never reads `ResolvedBackgroundActivitySettings.idleClientTtl`, so changing the server's `idleClientTtl` override has no effect on lease expiry. Clients keep getting 45-second leases regardless of the configured value, making the `idleClientTtl` background-activity setting non-functional. Consider clamping the resolved `idleClientTtl` setting instead of the hard-coded constant when `input.ttlMs` is not provided.
……@@ -0,0 +1,922 @@
649+ Effect.forkScoped,
650+ );
651+
652+ const applyCollectionControl = Effect.fn(
MmacroscopeappBoton line 652Jul 29, 2026

🟑 Medium resourceTelemetry/NativeTelemetryClient.ts:652

setHostPowerState and changeLiveSubscriberCount both atomically update collectionControl and then send commands to the sidecar, but the read-modify-send sequence is not protected across the two operations. When they run concurrently, the one whose Ref.modify executes first can have its commands delivered second, so the sidecar ends up with stale settings. For example, if a subscriber arrives while a host-power update is in flight, the power update's stale sampleIntervalMs/setStreaming commands can overwrite the subscriber's newer commands, leaving the sidecar at the wrong interval or with streaming disabled despite an active subscriber. Consider serializing the full read-modify-send sequence (e.g., with a dedicated mutex around both the Ref.modify and the applyCollectionControl call) so command delivery order always matches control-update 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
3`setHostPowerState` and `changeLiveSubscriberCount` both atomically update `collectionControl` and then send commands to the sidecar, but the read-modify-send sequence is not protected across the two operations. When they run concurrently, the one whose `Ref.modify` executes first can have its commands delivered second, so the sidecar ends up with stale settings. For example, if a subscriber arrives while a host-power update is in flight, the power update's stale `sampleIntervalMs`/`setStreaming` commands can overwrite the subscriber's newer commands, leaving the sidecar at the wrong interval or with streaming disabled despite an active subscriber. Consider serializing the full read-modify-send sequence (e.g., with a dedicated mutex around both the `Ref.modify` and the `applyCollectionControl` call) so command delivery order always matches control-update order.
……@@ -62,225 +165,421 @@ function resolveThreadSegment(raw: string | null | undefined): string {
108-}): Effect.fn.Return<ThreadWriter | undefined> {
109- const sinkResult = yield* Effect.sync(() => {
225+ for (const entry of entries) {
226+ if (!entry.isFile() || !PROVIDER_LOG_FILE_PATTERN.test(entry.name)) continue;
MmacroscopeappBoton lines 226–226Jul 29, 2026

🟠 High Layers/EventNdjsonLogger.ts:226

enforceRetention deletes every file in the store's directory whose name matches /\.log(?:\.\d+)?$/u β€” including unrelated log files from other components. Since makeEventNdjsonLogStore accepts arbitrary paths (e.g. adapter-supplied nativeEventLogPath), pointing it at a shared log directory causes the initial and periodic retention sweeps to permanently delete other components' .log files once they exceed maxAgeMs or contribute to maxTotalBytes. The PROVIDER_LOG_FILE_PATTERN regex is too broad to scope deletion to this store's own thread-log files. Consider restricting candidates to the thread-segment naming scheme (e.g. ^[A-Za-z0-9_-]+\.log(?:\.\d+)?$ matching toSafeThreadAttachmentSegment output) or requiring a dedicated directory.

suggestion
1 if (!entry.isFile() || !PROVIDER_THREAD_LOG_PATTERN.test(entry.name)) continue;
πŸš€ Reply "fix it for me" or copy this AI Prompt for your agent:
text
1In file @apps/server/src/provider/Layers/EventNdjsonLogger.ts around line 226:
2
3`enforceRetention` deletes every file in the store's directory whose name matches `/\.log(?:\.\d+)?$/u` β€” including unrelated log files from other components. Since `makeEventNdjsonLogStore` accepts arbitrary paths (e.g. adapter-supplied `nativeEventLogPath`), pointing it at a shared log directory causes the initial and periodic retention sweeps to permanently delete other components' `.log` files once they exceed `maxAgeMs` or contribute to `maxTotalBytes`. The `PROVIDER_LOG_FILE_PATTERN` regex is too broad to scope deletion to this store's own thread-log files. Consider restricting candidates to the thread-segment naming scheme (e.g. `^[A-Za-z0-9_-]+\.log(?:\.\d+)?$` matching `toSafeThreadAttachmentSegment` output) or requiring a dedicated directory.
……@@ -0,0 +1,412 @@
353+ yield* Effect.forever(
354+ Effect.sleep(STALE_CHECK_INTERVAL).pipe(
355+ Effect.andThen(
356+ Effect.gen(function* () {
MmacroscopeappBoton line 356Jul 29, 2026

🟑 Medium resourceTelemetry/DesktopTelemetryReceiver.ts:356

The stale-check loop reads latest, marks the snapshot stale, then unconditionally calls Ref.set(latest, ...). If a fresh telemetry message arrives between the read and the set, the checker overwrites the newer snapshot with the older stale-marked one and publishes that regressed data on changes. Consider using Ref.modify with a timestamp comparison so a newer sample is never replaced by an older stale-marked snapshot.

πŸš€ Reply "fix it for me" or copy this AI Prompt for your agent:
text
1In file @apps/server/src/resourceTelemetry/DesktopTelemetryReceiver.ts around line 356:
2
3The stale-check loop reads `latest`, marks the snapshot stale, then unconditionally calls `Ref.set(latest, ...)`. If a fresh telemetry message arrives between the read and the set, the checker overwrites the newer snapshot with the older stale-marked one and publishes that regressed data on `changes`. Consider using `Ref.modify` with a timestamp comparison so a newer sample is never replaced by an older stale-marked snapshot.
……@@ -110,7 +141,128 @@ const TIMESTAMP_FORMAT_LABELS = {
220+ };
221+}
222+
223+function backgroundActivityOverrideSettings(
MmacroscopeappBoton line 223Jul 29, 2026

🟑 Medium settings/SettingsPanels.tsx:223

backgroundActivityOverrideSettings builds the custom configuration only from current.overrides, but resolveServerBackgroundActivitySettings can display values sourced from legacy flat fields (automaticGitFetchInterval, providerHealthRefreshInterval) when current.backgroundActivity is still default with empty overrides. Editing a single interval control then writes a custom backgroundActivity containing only that one override, so the other displayed legacy interval silently reverts to the base-profile preset β€” e.g. changing provider health loses a user's legacy custom Git-fetch interval. Build the patch from the effective resolved settings (or migrate legacy overrides before applying the patch) so all currently displayed values are preserved.

πŸš€ 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 223:
2
3`backgroundActivityOverrideSettings` builds the custom configuration only from `current.overrides`, but `resolveServerBackgroundActivitySettings` can display values sourced from legacy flat fields (`automaticGitFetchInterval`, `providerHealthRefreshInterval`) when `current.backgroundActivity` is still default with empty overrides. Editing a single interval control then writes a custom `backgroundActivity` containing only that one override, so the other displayed legacy interval silently reverts to the base-profile preset β€” e.g. changing provider health loses a user's legacy custom Git-fetch interval. Build the patch from the effective resolved settings (or migrate legacy overrides before applying the patch) so all currently displayed values are preserved.
……@@ -62,225 +165,421 @@ function resolveThreadSegment(raw: string | null | undefined): string {
368+ } catch (cause) {
369+ failedSegments.add(threadSegment);
370+ failures.push({ filePath, cause });
371+ }
MmacroscopeappBoton lines 368–371Jul 29, 2026

🟑 Medium Layers/EventNdjsonLogger.ts:368

When a sink.write call throws, drainPending adds the thread segment to failedSegments permanently. Every subsequent drain skips that segment at the if (failedSegments.has(threadSegment)) continue check, but still clears all pending records β€” so a transient write error (temporary disk pressure, short-lived FS error) permanently disables logging for that thread for the remaining store lifetime with no retry or recovery, and silently drops every future record for that thread. Previously, batch write failures did not mark the thread writer permanently failed. Consider removing the failedSegments add on write failure (or retrying on the next drain) so transient errors don't permanently block the segment.

diff
1 } catch (cause) {
2- failedSegments.add(threadSegment);
3 failures.push({ filePath, cause });
4 }
πŸš€ 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 368-371:
2
3When a `sink.write` call throws, `drainPending` adds the thread segment to `failedSegments` permanently. Every subsequent drain skips that segment at the `if (failedSegments.has(threadSegment)) continue` check, but still clears all pending records β€” so a transient write error (temporary disk pressure, short-lived FS error) permanently disables logging for that thread for the remaining store lifetime with no retry or recovery, and silently drops every future record for that thread. Previously, batch write failures did not mark the thread writer permanently failed. Consider removing the `failedSegments` add on write failure (or retrying on the next drain) so transient errors don't permanently block the segment.
……@@ -338,44 +405,78 @@ export const make = Effect.gen(function* () {
437+ persistFailure: Effect.fn("desktop.observability.backendOutput.persistFailure")(function* ({
438+ details,
439+ }) {
440+ const session = yield* Ref.modify(sessionRef, (current) => [
MmacroscopeappBoton line 440Jul 29, 2026

🟑 Medium app/DesktopBackendOutputLog.ts:440

persistFailure clears the buffered chunks from sessionRef before writing them to the log file. Because writeBackendChildLogRecord catches encoding and write failures and returns success, a transient filesystem error causes the buffered backend output to be permanently discarded β€” a retry cannot recover it, and the next beginSession overwrites the session. Clear the buffer only after the writes have succeeded, or propagate failures so the session data is retained on error.

πŸš€ Reply "fix it for me" or copy this AI Prompt for your agent:
text
1In file @apps/desktop/src/app/DesktopBackendOutputLog.ts around line 440:
2
3`persistFailure` clears the buffered `chunks` from `sessionRef` *before* writing them to the log file. Because `writeBackendChildLogRecord` catches encoding and write failures and returns success, a transient filesystem error causes the buffered backend output to be permanently discarded β€” a retry cannot recover it, and the next `beginSession` overwrites the session. Clear the buffer only after the writes have succeeded, or propagate failures so the session data is retained on error.
……@@ -0,0 +1,214 @@
149+ const snapshots = input.snapshots
150+ .filter((snapshot) => snapshot.sampledAtUnixMs >= windowStartMs)
151+ .toSorted((left, right) => left.sampledAtUnixMs - right.sampledAtUnixMs);
152+ const electronRootPids = Option.match(input.desktopSnapshot, {
MmacroscopeappBoton line 152Jul 29, 2026

🟑 Medium resourceTelemetry/ResourceTelemetryHistory.ts:152

buildResourceTelemetryHistory attributes every snapshot in the window using only the current desktopSnapshot.electronPid as electronRootPids. When Electron restarts mid-window, samples from the prior Electron PID and its descendants are mislabeled as server-child, and any unrelated process that happened to reuse the current PID is mislabeled as Electron. This corrupts the per-group buckets and topProcesses.category values for any history window spanning a restart. The fix requires per-snapshot Electron identity data or reliable per-sample inference rather than applying one PID set to all historical samples.

πŸš€ Reply "fix it for me" or copy this AI Prompt for your agent:
text
1In file @apps/server/src/resourceTelemetry/ResourceTelemetryHistory.ts around line 152:
2
3`buildResourceTelemetryHistory` attributes every snapshot in the window using only the *current* `desktopSnapshot.electronPid` as `electronRootPids`. When Electron restarts mid-window, samples from the prior Electron PID and its descendants are mislabeled as `server-child`, and any unrelated process that happened to reuse the current PID is mislabeled as Electron. This corrupts the per-group `buckets` and `topProcesses.category` values for any history window spanning a restart. The fix requires per-snapshot Electron identity data or reliable per-sample inference rather than applying one PID set to all historical samples.

🟑 Medium

https://github.com/pingdotgg/t3code/blob/58340604f0b7967e7a5a3e532993f3443442d14e/apps/server/src/diagnostics/ProcessDiagnostics.ts#L1

The read function filters only the server PID out of snapshot.processes, so independent Electron roots and the resource-monitor process are returned as server child processes. This inflates processCount, totalRssBytes, and totalCpuPercent, and their depth is clamped to 0 via Math.max(0, entry.depth - 1) even though they are not descendants of the server. The filter at line 78 should restrict the list to the server/backend descendant tree rather than all telemetry processes.

πŸš€ Reply "fix it for me" or copy this AI Prompt for your agent:
text
1In file @apps/server/src/diagnostics/ProcessDiagnostics.ts around line 1:
2
3The `read` function filters only the server PID out of `snapshot.processes`, so independent Electron roots and the resource-monitor process are returned as server child processes. This inflates `processCount`, `totalRssBytes`, and `totalCpuPercent`, and their `depth` is clamped to `0` via `Math.max(0, entry.depth - 1)` even though they are not descendants of the server. The filter at line 78 should restrict the list to the server/backend descendant tree rather than all telemetry processes.
……@@ -0,0 +1,214 @@
146+ const readAtMs = DateTime.toEpochMillis(input.readAt);
147+ const { windowMs, bucketMs } = normalizeResourceTelemetryHistoryInput(input);
148+ const windowStartMs = readAtMs - windowMs;
149+ const snapshots = input.snapshots
MmacroscopeappBoton line 149Jul 29, 2026

🟑 Medium resourceTelemetry/ResourceTelemetryHistory.ts:149

buildResourceTelemetryHistory filters snapshots only with a lower bound (>= windowStartMs), so snapshots with sampledAtUnixMs > readAtMs pass through. Those snapshots are excluded from every bucket by buildBuckets (which caps each bucket's end at nowMs), but they are still iterated into processSamples and counted in retainedSampleCount, so the returned history has buckets that omit the latest snapshots while topProcesses includes them β€” and the process "current" values can be later than readAt. Add an upper-bound filter (sampledAtUnixMs <= readAtMs) alongside the existing lower-bound filter.

πŸš€ Reply "fix it for me" or copy this AI Prompt for your agent:
text
1In file @apps/server/src/resourceTelemetry/ResourceTelemetryHistory.ts around line 149:
2
3`buildResourceTelemetryHistory` filters snapshots only with a lower bound (`>= windowStartMs`), so snapshots with `sampledAtUnixMs > readAtMs` pass through. Those snapshots are excluded from every bucket by `buildBuckets` (which caps each bucket's end at `nowMs`), but they are still iterated into `processSamples` and counted in `retainedSampleCount`, so the returned history has buckets that omit the latest snapshots while `topProcesses` includes them β€” and the process "current" values can be later than `readAt`. Add an upper-bound filter (`sampledAtUnixMs <= readAtMs`) alongside the existing lower-bound filter.
……@@ -0,0 +1,559 @@
393+ ) {
394+ metricsByPid.set(metric.pid, metric);
395+ }
396+ }
MmacroscopeappBoton line 396Jul 29, 2026

🟑 Medium resourceTelemetry/Model.ts:396

When an Electron metric arrives without a matching native sample, mergeProcesses inserts a synthetic process keyed by metric.creationTimeMs at line 387. A subsequent native sample for the same PID that passes the 2-second tolerance check (line 391-395) is then keyed by its own different startTimeMs. Because processIdentityKey (line 85-87) requires exact equality, the same live process is recorded as both an exit (old identity) and a start (new identity), which resets firstSeenAt/deltas and inflates the processStarts/processExits lifecycle counters. This happens whenever desktop telemetry arrives before the native monitor first observes the Electron process.

πŸš€ Reply "fix it for me" or copy this AI Prompt for your agent:
text
1In file @apps/server/src/resourceTelemetry/Model.ts around line 396:
2
3When an Electron metric arrives without a matching native sample, `mergeProcesses` inserts a synthetic process keyed by `metric.creationTimeMs` at line 387. A subsequent native sample for the same PID that passes the 2-second tolerance check (line 391-395) is then keyed by its own different `startTimeMs`. Because `processIdentityKey` (line 85-87) requires exact equality, the same live process is recorded as both an exit (old identity) and a start (new identity), which resets `firstSeenAt`/deltas and inflates the `processStarts`/`processExits` lifecycle counters. This happens whenever desktop telemetry arrives before the native monitor first observes the Electron process.
……@@ -0,0 +1,274 @@
238+ yield* Stream.runForEach(hostPowerMonitor.streamChanges, () => publishSnapshot).pipe(
239+ Effect.forkScoped,
240+ );
241+
MmacroscopeappBoton lines 238–241Jul 29, 2026

🟑 Medium background/BackgroundPolicy.ts:238

make never subscribes to serverSettings.streamChanges, so changing background-activity settings (e.g. enabling a battery pause while the host is already on battery) does not produce a new BackgroundPolicySnapshot on streamChanges until an unrelated host-power, lease, or 15-second cleanup event occurs. Stream consumers continue using the old policy for up to 15 seconds. Subscribe to settings changes and invoke publishSnapshot, as is done for host-power changes.

diff
1 yield* Stream.runForEach(hostPowerMonitor.streamChanges, () => publishSnapshot).pipe(
2 Effect.forkScoped,
3 );
4+
5+ yield* Stream.runForEach(serverSettings.streamChanges, () => publishSnapshot).pipe(
6+ Effect.forkScoped,
7 );
πŸš€ Reply "fix it for me" or copy this AI Prompt for your agent:
text
1In file @apps/server/src/background/BackgroundPolicy.ts around lines 238-241:
2
3`make` never subscribes to `serverSettings.streamChanges`, so changing background-activity settings (e.g. enabling a battery pause while the host is already on battery) does not produce a new `BackgroundPolicySnapshot` on `streamChanges` until an unrelated host-power, lease, or 15-second cleanup event occurs. Stream consumers continue using the old policy for up to 15 seconds. Subscribe to settings changes and invoke `publishSnapshot`, as is done for host-power changes.
……@@ -0,0 +1,559 @@
453+ ? "server"
454+ : Option.contains(input.sidecarPid, process.pid)
455+ ? "resource-monitor"
456+ : explicitElectronRootPids.has(process.pid)
MmacroscopeappBoton line 456Jul 29, 2026

🟑 Medium resourceTelemetry/Model.ts:456

mergeProcesses classifies a process as electron-main solely because its PID is in electronRootPids, without validating the process start time against the desktop metric's creationTimeMs. When a stale desktop snapshot is replayed and the PID has been reused by an unrelated process (e.g. a server child), that process and its descendants are misattributed to Electron. The same stale PID is correctly rejected by metricsByPid via the ELECTRON_IDENTITY_TOLERANCE_MS check, but the explicitElectronRootPids.has(process.pid) branch bypasses that validation. Consider keying electronRootPids by process identity (PID + start time) or validating the start time before classifying a root as electron-main.

πŸš€ Reply "fix it for me" or copy this AI Prompt for your agent:
text
1In file @apps/server/src/resourceTelemetry/Model.ts around line 456:
2
3`mergeProcesses` classifies a process as `electron-main` solely because its PID is in `electronRootPids`, without validating the process start time against the desktop metric's `creationTimeMs`. When a stale desktop snapshot is replayed and the PID has been reused by an unrelated process (e.g. a server child), that process and its descendants are misattributed to Electron. The same stale PID is correctly rejected by `metricsByPid` via the `ELECTRON_IDENTITY_TOLERANCE_MS` check, but the `explicitElectronRootPids.has(process.pid)` branch bypasses that validation. Consider keying `electronRootPids` by process identity (PID + start time) or validating the start time before classifying a root as `electron-main`.

Closing in favor of #2829 (orchestration V2).

#2829 deletes the V1 orchestration layer this PR builds on β€” apps/server/src/orchestration/**, provider/Layers/*Adapter.ts and provider/Services/** are removed and replaced by apps/server/src/orchestration-v2/**, with the IPC surface renamed to ORCHESTRATION_V2_WS_METHODS. The files this PR touches either no longer exist or are rewritten, so it can't be rebased β€” it would need reimplementing against the V2 adapters.

This is not a judgement on the change itself. Several of these are real gaps we still want fixed; the base just moved out from under them.

Once #2829 merges, please rebase onto main, port the change to the V2 equivalent, and reopen (or open a fresh PR). Ping me and I'll prioritise the review.