From b0073f24ec6480ac059376a75fef20cc18dd1c52 Mon Sep 17 00:00:00 2001 From: Vishal Rana Date: Sat, 31 Oct 2026 10:38:16 -0700 Subject: [PATCH 1/2] fix(derive): never name a process that reused a PID as an MCP server's host On Windows a process keeps the PID of its exited parent, and a newer process can reuse it. Process placement already stops the parent chain at such a parent, but it set an MCP server's SharedHost from the raw parent PID, so the server's memory could be grouped under that unrelated newer process. SharedHost now skips a sampled parent that started after the server, using the same check as processChain, which is now shared as reusedParent. --- internal/derive/buckets.go | 6 +++++- internal/derive/buckets_test.go | 15 +++++++++++++++ internal/derive/launch_surface.go | 8 +++++++- 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/internal/derive/buckets.go b/internal/derive/buckets.go index c02ecdef..cd76dbea 100644 --- a/internal/derive/buckets.go +++ b/internal/derive/buckets.go @@ -98,7 +98,11 @@ func PlaceProcesses(procs []model.Proc, sessions []model.Session, sigs *signatur // MCP evidence wins over the bundle: editors may run MCP servers // with their own bundled runtime. if mcp[cur.PID] && shared == 0 { - shared = cur.PPID + // A sampled process that reused the exited parent's PID is + // not the host. + if parent, ok := byPID[cur.PPID]; !ok || !reusedParent(parent, cur) { + shared = cur.PPID + } likely = true } else if AppBundle(cur.Exe) != "" && !proven[cur.PID] { appBoundary = true diff --git a/internal/derive/buckets_test.go b/internal/derive/buckets_test.go index dcc8deab..78b9e9ea 100644 --- a/internal/derive/buckets_test.go +++ b/internal/derive/buckets_test.go @@ -67,6 +67,21 @@ func TestBucketsEditorExtensionHostMCP(t *testing.T) { t.Fatal(got) } } + +// A process that reused the PID of an MCP server's exited parent started after +// the server, so it is not the server's shared host. +func TestBucketsReusedParentPIDIsNotSharedHost(t *testing.T) { + ps := editorBucketFixture() + now := time.Now() + ps[0].StartTime, ps[1].StartTime = now, now.Add(-time.Minute) + if got := PlaceProcesses(ps, nil, nil)[2]; got.Bucket != BucketLikely || got.SharedHost != 0 || got.Bundle != "" { + t.Fatal(got) + } + ps[0].StartTime = now.Add(-time.Hour) + if got := PlaceProcesses(ps, nil, nil)[2]; got.SharedHost != 1 { + t.Fatal(got) + } +} func TestBucketsEditorWorkerIsOther(t *testing.T) { ps := editorBucketFixture() ps[1].Cmdline = "node /x/tsserver.js" diff --git a/internal/derive/launch_surface.go b/internal/derive/launch_surface.go index fb69e60d..b377482b 100644 --- a/internal/derive/launch_surface.go +++ b/internal/derive/launch_surface.go @@ -20,7 +20,7 @@ func processChain(byPID map[int32]model.Proc, pid int32) iter.Seq[model.Proc] { return } parent, found := byPID[cur.PPID] - if !found || (!cur.StartTime.IsZero() && !parent.StartTime.IsZero() && parent.StartTime.After(cur.StartTime)) { + if !found || reusedParent(parent, cur) { return } cur = parent @@ -28,6 +28,12 @@ func processChain(byPID map[int32]model.Proc, pid int32) iter.Seq[model.Proc] { } } +// reusedParent reports that parent, the sampled process with child's PPID, +// started after child, so it reused the PID of child's exited parent. +func reusedParent(parent, child model.Proc) bool { + return !child.StartTime.IsZero() && !parent.StartTime.IsZero() && parent.StartTime.After(child.StartTime) +} + // LaunchSurface names the nearest app bundle in a sampled process's ancestry. // It performs no process inspection and never infers a product from argv. func LaunchSurface(byPID map[int32]model.Proc, pid int32) string { From c02bf0c24c6651f08e033e4a2feed7036f34fd19 Mon Sep 17 00:00:00 2001 From: Vishal Rana Date: Sat, 31 Oct 2026 10:45:18 -0700 Subject: [PATCH 2/2] fix(derive): take an MCP server's host from its walked parent chain Address review: the host is now the next process the parent chain walked, so it is never a reused, dead or zero PID. A server whose chain ends there has no host and is placed as other, not as shared with an editor that is gone. processChain keeps its own check, so no helper is added. --- internal/derive/buckets.go | 16 +++++++-------- internal/derive/buckets_test.go | 34 +++++++++++++++++++++---------- internal/derive/launch_surface.go | 8 +------- 3 files changed, 32 insertions(+), 26 deletions(-) diff --git a/internal/derive/buckets.go b/internal/derive/buckets.go index cd76dbea..b487bd49 100644 --- a/internal/derive/buckets.go +++ b/internal/derive/buckets.go @@ -78,7 +78,7 @@ func PlaceProcesses(procs []model.Proc, sessions []model.Session, sigs *signatur } placement := ProcPlacement{PID: p.PID, Bucket: BucketOther} placement.Memory, placement.Measure = ProcessMemory(p) - agent, likely, appBoundary := false, false, false + agent, likely, appBoundary, hostPending := false, false, false, false var shared int32 bundle := "" for cur := range processChain(byPID, p.PID) { @@ -97,13 +97,13 @@ func PlaceProcesses(procs []model.Proc, sessions []model.Session, sigs *signatur if !appBoundary { // MCP evidence wins over the bundle: editors may run MCP servers // with their own bundled runtime. - if mcp[cur.PID] && shared == 0 { - // A sampled process that reused the exited parent's PID is - // not the host. - if parent, ok := byPID[cur.PPID]; !ok || !reusedParent(parent, cur) { - shared = cur.PPID - } - likely = true + if hostPending { + // The host is the next process the chain walked, so it is + // a sampled, valid parent and never a reused or dead PID. + shared, likely, hostPending = cur.PID, true, false + } + if mcp[cur.PID] && shared == 0 && !likely { + hostPending = true } else if AppBundle(cur.Exe) != "" && !proven[cur.PID] { appBoundary = true } diff --git a/internal/derive/buckets_test.go b/internal/derive/buckets_test.go index 78b9e9ea..695f4d7d 100644 --- a/internal/derive/buckets_test.go +++ b/internal/derive/buckets_test.go @@ -68,18 +68,30 @@ func TestBucketsEditorExtensionHostMCP(t *testing.T) { } } -// A process that reused the PID of an MCP server's exited parent started after -// the server, so it is not the server's shared host. -func TestBucketsReusedParentPIDIsNotSharedHost(t *testing.T) { - ps := editorBucketFixture() +// An MCP server's shared host is the parent its chain walked: never a process +// that reused the PID of its exited parent, and never a parent missing from +// the sample. Without a host the server is not shared with an editor. +func TestBucketsMCPHostIsAWalkedParent(t *testing.T) { now := time.Now() - ps[0].StartTime, ps[1].StartTime = now, now.Add(-time.Minute) - if got := PlaceProcesses(ps, nil, nil)[2]; got.Bucket != BucketLikely || got.SharedHost != 0 || got.Bundle != "" { - t.Fatal(got) - } - ps[0].StartTime = now.Add(-time.Hour) - if got := PlaceProcesses(ps, nil, nil)[2]; got.SharedHost != 1 { - t.Fatal(got) + for _, tc := range []struct { + name string + mutate func([]model.Proc) []model.Proc + host int32 + bucket Bucket + }{ + {"live parent", func(ps []model.Proc) []model.Proc { return ps }, 1, BucketLikely}, + {"reused parent PID", func(ps []model.Proc) []model.Proc { + ps[0].StartTime, ps[1].StartTime = now, now.Add(-time.Minute) + return ps + }, 0, BucketOther}, + {"parent not sampled", func(ps []model.Proc) []model.Proc { return ps[1:] }, 0, BucketOther}, + } { + t.Run(tc.name, func(t *testing.T) { + got := PlaceProcesses(tc.mutate(editorBucketFixture()), nil, nil)[2] + if got.Bucket != tc.bucket || got.SharedHost != tc.host { + t.Fatal(got) + } + }) } } func TestBucketsEditorWorkerIsOther(t *testing.T) { diff --git a/internal/derive/launch_surface.go b/internal/derive/launch_surface.go index b377482b..fb69e60d 100644 --- a/internal/derive/launch_surface.go +++ b/internal/derive/launch_surface.go @@ -20,7 +20,7 @@ func processChain(byPID map[int32]model.Proc, pid int32) iter.Seq[model.Proc] { return } parent, found := byPID[cur.PPID] - if !found || reusedParent(parent, cur) { + if !found || (!cur.StartTime.IsZero() && !parent.StartTime.IsZero() && parent.StartTime.After(cur.StartTime)) { return } cur = parent @@ -28,12 +28,6 @@ func processChain(byPID map[int32]model.Proc, pid int32) iter.Seq[model.Proc] { } } -// reusedParent reports that parent, the sampled process with child's PPID, -// started after child, so it reused the PID of child's exited parent. -func reusedParent(parent, child model.Proc) bool { - return !child.StartTime.IsZero() && !parent.StartTime.IsZero() && parent.StartTime.After(child.StartTime) -} - // LaunchSurface names the nearest app bundle in a sampled process's ancestry. // It performs no process inspection and never infers a product from argv. func LaunchSurface(byPID map[int32]model.Proc, pid int32) string {