Skip to content

fix(server): stop idle private-memory growth from immortal parent spans - #5411

Open
Benchance wants to merge 3 commits into
pingdotgg:mainfrom
Benchance:fix/idle-server-memory-span-leak
Open

fix(server): stop idle private-memory growth from immortal parent spans#5411
Benchance wants to merge 3 commits into
pingdotgg:mainfrom
Benchance:fix/idle-server-memory-span-leak

Conversation

@Benchance

@Benchance Benchance commented Aug 5, 2026

Copy link
Copy Markdown

Summary

Test plan

  • vp test run src/serverActivation.test.ts src/preview/PortScanner.test.ts (apps/server)
  • Restart desktop build with this change; leave idle/unfocused for 2–3 hours
  • Confirm server Private bytes stay roughly flat (ignore Working Set trims)
  • Confirm server.trace.ndjson no longer floods with PortDiscovery.pollTick / ServerSecretStore.get under startup.phase=reactors.start
  • Spot-check provider health / preview port discovery still work when used

Fixes #5410


Note

Medium Risk
Changes tracing and span parentage for all forkParked background work and port/secret hot paths; runtime behavior is unchanged but observability under startup traces will look different.

Overview
Addresses idle Private memory creep (#5410) by stopping long-lived server fibers from inheriting short-lived startup ParentSpan values, which kept parent spans and nested trace data alive indefinitely.

Adds withoutAmbientParentSpan in serverActivation.ts and runs all forkParked roots through it so background reactors no longer parent under immortal startup spans. The PortDiscovery poll loop uses the same helper, moves the retain-count idle check outside Effect.fn so no-op ticks do not create spans, and drops the wrapping span on PortDiscovery.make.

ServerSecretStore.get swaps its named span for withTracerEnabled(false) to avoid high-frequency secret reads under startup traces. A unit test asserts that ambient ParentSpan is stripped while normal effects still see it.

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

Note

Stop idle private-memory growth from immortal parent spans in background fibers

  • Adds withoutAmbientParentSpan helper in serverActivation.ts that strips Tracer.ParentSpan from an effect's context, preventing long-lived forked fibers from holding a reference to a parent span.
  • Applies withoutAmbientParentSpan in forkParked so all long-running roots it forks no longer inherit an ambient parent span.
  • Refactors PortDiscovery polling in PortScanner.ts so idle no-op ticks skip span creation entirely, and the background polling fiber runs without an inherited parent span.
  • Disables tracing on ServerSecretStore.get in ServerSecretStore.ts instead of creating a named span per call.
  • Behavioral Change: named tracing spans are no longer emitted for idle poll ticks, ServerSecretStore.get, or the PortDiscovery.make and forkParked roots.

Macroscope summarized 17580be.

Long-lived forkParked roots and PortDiscovery poll loops were inheriting
Effect ParentSpan from short-lived startup/make spans. Nested hot-path
spans (secret reads, idle poll ticks) kept parenting under those
immortal spans and drove ~25-30MB/h private growth while idle.

Detach ambient ParentSpan in forkParked, untrace idle port polls and
secret gets, and drop PortDiscovery.make's wrapping span.

Co-authored-by: Cursor <cursoragent@cursor.com>
Copilot AI lite review requested due to automatic review settings August 5, 2026 13:50
@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown

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: bd549d1b-e4f5-4f14-9e3f-1986fbe46c19

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

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@github-actions github-actions Bot added the vouch:unvouched PR author is not yet trusted in the VOUCHED list. label Aug 5, 2026
@github-actions github-actions Bot added the size:M 30-99 changed lines (additions + deletions). label Aug 5, 2026
Co-authored-by: Cursor <cursoragent@cursor.com>
Comment on lines 25 to +27
Effect.gen(function* () {
const activation = yield* ServerActivation;
const detached = withoutAmbientParentSpan(effect);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High src/serverActivation.ts:25

withoutAmbientParentSpan casts the context after removing Tracer.ParentSpan back to Context.Context<NoInfer<R>>, so the return type still claims to satisfy effects whose environment explicitly includes Tracer.ParentSpan. If an effect passed to withoutAmbientParentSpan (or to forkParked, which wraps every input with it) directly uses Tracer.ParentSpan, the code type-checks but the forked fiber fails at runtime with Service not found because the service was removed from the child context. The cast masks a missing-service defect rather than surfacing it as a type error. Consider constraining the input type so Tracer.ParentSpan cannot appear in R.

-    (context) => Context.omit(Tracer.ParentSpan)(context) as Context.Context<NoInfer<R>>,
+    (context) => Context.omit(Tracer.ParentSpan)(context) as Context.Context<Exclude<NoInfer<R>, Tracer.ParentSpan>>,
+    // Note: this cast is unsafe if R explicitly requires Tracer.ParentSpan;
+    // callers should ensure the input does not use that service directly.
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/serverActivation.ts around lines 25-27:

`withoutAmbientParentSpan` casts the context after removing `Tracer.ParentSpan` back to `Context.Context<NoInfer<R>>`, so the return type still claims to satisfy effects whose environment explicitly includes `Tracer.ParentSpan`. If an effect passed to `withoutAmbientParentSpan` (or to `forkParked`, which wraps every input with it) directly uses `Tracer.ParentSpan`, the code type-checks but the forked fiber fails at runtime with `Service not found` because the service was removed from the child context. The cast masks a missing-service defect rather than surfacing it as a type error. Consider constraining the input type so `Tracer.ParentSpan` cannot appear in `R`.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 17580be — return type is now Effect.Effect<A, E, Exclude<R, Tracer.ParentSpan>> with a matching context cast, so the stripped env is reflected in the type.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I'm unable to act on this request because you do not have permissions within this repository.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes idle private-memory growth in the desktop/server process by preventing long-lived fibers (parked roots + background pollers) from inheriting short-lived startup Tracer.ParentSpan context, and by reducing tracing work on hot/idle paths.

Changes:

  • Add withoutAmbientParentSpan and apply it in forkParked to detach inherited Tracer.ParentSpan for long-lived roots.
  • Reduce tracing overhead on idle/background loops (PortDiscovery polling, secret reads) and remove the PortDiscovery.make wrapping span.
  • Add a focused unit test validating that withoutAmbientParentSpan clears inherited Tracer.ParentSpan without affecting sibling effects.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
apps/server/src/serverRuntimeStartup.ts Documents why parked startup roots must detach ambient ParentSpan to avoid pinning startup spans.
apps/server/src/serverActivation.ts Introduces withoutAmbientParentSpan and applies it to all forkParked forks.
apps/server/src/serverActivation.test.ts Adds a unit test asserting ParentSpan detachment behavior.
apps/server/src/preview/PortScanner.ts Detaches the poll fiber from ambient parent spans and disables tracing in the poll loop; removes PortDiscovery.make span wrapper.
apps/server/src/auth/ServerSecretStore.ts Disables tracing for frequent secret reads to reduce idle trace volume and span retention.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

expect(stillHasParent).toBe(true);

const stripped = Context.omit(Tracer.ParentSpan)(Context.make(Tracer.ParentSpan, ambient));
expect(Context.getOption(stripped, Tracer.ParentSpan)._tag).toBe("None");

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 17580be — switched to Option.isNone.

Comment thread apps/server/src/preview/PortScanner.ts Outdated
Comment on lines +312 to +323
// Single layer-scoped polling fiber. Ticks are no-ops when no client is
// currently retained, so the cost is one Ref.get every POLL_INTERVAL.
yield* Effect.forkScoped(pollTick().pipe(Effect.repeat(Schedule.spaced(POLL_INTERVAL))));

// currently retained. Run detached from any ambient ParentSpan (e.g.
// PortDiscovery.make) and without tracing idle no-ops — otherwise every
// 3s tick parents under an immortal span and grows server private memory.
yield* Effect.forkScoped(
withoutAmbientParentSpan(
pollTick().pipe(
Effect.repeat(Schedule.spaced(POLL_INTERVAL)),
Effect.withTracerEnabled(false),
),
),
);

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — fixed in 17580be. Idle early-return now sits outside Effect.fn("PortDiscovery.pollTick") so no-op ticks create no span, and the blanket withTracerEnabled(false) on the poll loop is gone so active scans still get traced. ParentSpan detach stays via withoutAmbientParentSpan.

@macroscopeapp

macroscopeapp Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

1 blocking correctness issue found. This memory-leak fix modifies span/tracing behavior for long-lived server processes. An unresolved high-severity comment identifies a type cast that could mask runtime 'Service not found' errors, which warrants human review to ensure correctness.

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

Keep active PortDiscovery polls traced, tighten withoutAmbientParentSpan's R, and use Option.isNone in the test.

Co-authored-by: Cursor <cursoragent@cursor.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:M 30-99 changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Desktop server private memory climbs ~25-30 MB/h while idle (immortal Effect parent spans + always-on tracing)

2 participants