fix(server): run editor discovery concurrently and cache the result - #5050

Open
capad-xyz wants to merge 3 commits into
pingdotgg:mainfrom
capad-xyz:fix/concurrent-cached-editor-discovery
Open

fix(server): run editor discovery concurrently and cache the result#5050
capad-xyz wants to merge 3 commits into
pingdotgg:mainfrom
capad-xyz:fix/concurrent-cached-editor-discovery

Conversation

@capad-xyz

@capad-xyzcapad-xyz commented Jul 30, 2026

Copy link
Copy Markdown

Problem

server.getConfig blocks on external editor discovery, which probes ~20 editor commands sequentially, each walking every PATH entry against every PATHEXT variant. On hosts with long PATHs this exceeds the 5s EDITOR_DISCOVERY_TIMEOUT on every call, so discovery is interrupted, callers get an empty editor list, and the next call starts over from scratch. Every client connection pays the full 5s on its first RPC.

Fixes#4210. Also addresses the empty-list symptom in #4697.

Fix

Two changes in ExternalLauncher:

  1. Probe concurrently.Effect.forEach(..., { concurrency: EDITORS.length }) instead of a sequential loop. Wall time becomes the slowest single probe rather than the sum.
  2. Run discovery in a detached fiber and let callers join it.Effect.cached around Effect.forkDetach, so callers share one scan via Fiber.join.

The second point is deliberate. Caching the scan directly is unsafe here: Effect.cached is cachedWithTTL(self, Duration.infinity), and cachedInvalidateWithTTL memoizes the exit through onExit(self, ...) — including an interrupted exit — and with an infinite TTL never recomputes. A caller hitting the 5s timeout mid-scan would pin that interrupted exit for the life of the process. Caching the fork instead memoizes a fiber handle, which is produced instantly and cannot carry an interrupted exit; a caller that times out cancels only its own wait while the scan runs to completion in the background. On a #4210-class machine (15-99s scans) editors now resolve on a subsequent getConfig instead of never.

EDITOR_DISCOVERY_TIMEOUT in ws.ts is untouched and still bounds the cold call.

Results are unchanged.Effect.forEach preserves input order, and per-editor command preference (resolveAvailableCommand) is untouched, so the returned list is identical to what the sequential loop produced — same editors, same order. There are no per-editor timeouts, so a slow-but-present editor cannot be dropped from the result.

Relation to #4739

#4739 targeted the same issue and was closed unmerged. This takes a different approach: it adds caching rather than only bounding latency, and it avoids per-editor timeouts, which is what changed selection behaviour there. Happy to close this if you would rather solve it another way — the measurements below may still be useful for whatever lands.

Measurements

Windows 11, 46-entry PATH, 12-entry PATHEXT, desktop nightly 0.0.32-nightly.20260730.958 (patched bundle):

beforeafter
ws.rpc.server.getConfig5,000-5,004 ms on every call, discovery always interrupted4,789 ms once (cold, completes), then 0-1 ms
externalLauncher.buildAvailableEditors5,000 ms Interrupted, every time4,787 ms Success once, then cached

Discovery now completes, so clients receive a real editor list instead of a permanently empty one.

Tests

vp test run src/process/externalLauncher.test.ts — 6 passed. Two added:

  • discovery is cached per service instance
  • discovery survives a caller giving up: a first caller is interrupted mid-scan, and a later call must still resolve. Fails on the pre-fix commit with All fibers interrupted without error.

Not covered

This does not address the Android store app disconnect (#4901); that reproduces with a fast getConfig too and is protocol skew on the client build. This only removes the server-side stall.

Diagnosed and implemented with Claude Code (Fable 5).


Note

Medium Risk
Changes async/caching behavior on a hot RPC path (getConfig); logic is localized to ExternalLauncher with tests, but discovery can still be slow on the first cold call and cached results won't reflect PATH changes until process restart.

Overview
External editor discovery no longer blocks server.getConfig for the full sequential PATH scan on every call. buildAvailableEditors now probes all editors in parallel (Effect.forEach with full concurrency), and ExternalLaunchermemoizes discovery by caching a detached fiber (Effect.cached + forkDetach + Fiber.join) so callers share one scan and a timed-out waiter does not poison later results.

Editor list order and membership stay the same as the old sequential loop. Tests add coverage for per-instance caching and for recovery when an earlier caller is interrupted mid-discovery.

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

@coderabbitai

coderabbitaiBot commented Jul 30, 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: fcbdc8e9-b5de-448e-bc59-9b6666f3b913

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-actionsgithub-actionsBot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:M 30-99 changed lines (additions + deletions). labels Jul 30, 2026
Comment threadapps/server/src/process/externalLauncher.ts Outdated
@macroscopeapp

macroscopeappBot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

This PR introduces caching and concurrent execution for editor discovery, changing state management and interruption semantics. While tests are included, the new caching layer and detached fiber pattern represent meaningful runtime behavior changes that warrant human review.

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

capad-xyzand others added 2 commits July 31, 2026 04:26
Effect.cached memoizes the inner effect's exit, so an interrupted first
scan poisoned the cache permanently. Cache the fork instead and let
callers join a detached fiber, so a timed-out caller cancels only its
own wait.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:M30-99 changed lines (additions + deletions).vouch:unvouchedPR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Provider page stays on "Checking provider status" because server.getConfig blocks on editor discovery

1 participant

@capad-xyz
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

fix(server): run editor discovery concurrently and cache the result - #5050

Open
capad-xyz wants to merge 3 commits into
pingdotgg:mainfrom
capad-xyz:fix/concurrent-cached-editor-discovery
Open

fix(server): run editor discovery concurrently and cache the result#5050
capad-xyz wants to merge 3 commits into
pingdotgg:mainfrom
capad-xyz:fix/concurrent-cached-editor-discovery

Conversation

@capad-xyz

@capad-xyzcapad-xyz commented Jul 30, 2026

Copy link
Copy Markdown

Problem

server.getConfig blocks on external editor discovery, which probes ~20 editor commands sequentially, each walking every PATH entry against every PATHEXT variant. On hosts with long PATHs this exceeds the 5s EDITOR_DISCOVERY_TIMEOUT on every call, so discovery is interrupted, callers get an empty editor list, and the next call starts over from scratch. Every client connection pays the full 5s on its first RPC.

Fixes#4210. Also addresses the empty-list symptom in #4697.

Fix

Two changes in ExternalLauncher:

  1. Probe concurrently.Effect.forEach(..., { concurrency: EDITORS.length }) instead of a sequential loop. Wall time becomes the slowest single probe rather than the sum.
  2. Run discovery in a detached fiber and let callers join it.Effect.cached around Effect.forkDetach, so callers share one scan via Fiber.join.

The second point is deliberate. Caching the scan directly is unsafe here: Effect.cached is cachedWithTTL(self, Duration.infinity), and cachedInvalidateWithTTL memoizes the exit through onExit(self, ...) — including an interrupted exit — and with an infinite TTL never recomputes. A caller hitting the 5s timeout mid-scan would pin that interrupted exit for the life of the process. Caching the fork instead memoizes a fiber handle, which is produced instantly and cannot carry an interrupted exit; a caller that times out cancels only its own wait while the scan runs to completion in the background. On a #4210-class machine (15-99s scans) editors now resolve on a subsequent getConfig instead of never.

EDITOR_DISCOVERY_TIMEOUT in ws.ts is untouched and still bounds the cold call.

Results are unchanged.Effect.forEach preserves input order, and per-editor command preference (resolveAvailableCommand) is untouched, so the returned list is identical to what the sequential loop produced — same editors, same order. There are no per-editor timeouts, so a slow-but-present editor cannot be dropped from the result.

Relation to #4739

#4739 targeted the same issue and was closed unmerged. This takes a different approach: it adds caching rather than only bounding latency, and it avoids per-editor timeouts, which is what changed selection behaviour there. Happy to close this if you would rather solve it another way — the measurements below may still be useful for whatever lands.

Measurements

Windows 11, 46-entry PATH, 12-entry PATHEXT, desktop nightly 0.0.32-nightly.20260730.958 (patched bundle):

beforeafter
ws.rpc.server.getConfig5,000-5,004 ms on every call, discovery always interrupted4,789 ms once (cold, completes), then 0-1 ms
externalLauncher.buildAvailableEditors5,000 ms Interrupted, every time4,787 ms Success once, then cached

Discovery now completes, so clients receive a real editor list instead of a permanently empty one.

Tests

vp test run src/process/externalLauncher.test.ts — 6 passed. Two added:

  • discovery is cached per service instance
  • discovery survives a caller giving up: a first caller is interrupted mid-scan, and a later call must still resolve. Fails on the pre-fix commit with All fibers interrupted without error.

Not covered

This does not address the Android store app disconnect (#4901); that reproduces with a fast getConfig too and is protocol skew on the client build. This only removes the server-side stall.

Diagnosed and implemented with Claude Code (Fable 5).


Note

Medium Risk
Changes async/caching behavior on a hot RPC path (getConfig); logic is localized to ExternalLauncher with tests, but discovery can still be slow on the first cold call and cached results won't reflect PATH changes until process restart.

Overview
External editor discovery no longer blocks server.getConfig for the full sequential PATH scan on every call. buildAvailableEditors now probes all editors in parallel (Effect.forEach with full concurrency), and ExternalLaunchermemoizes discovery by caching a detached fiber (Effect.cached + forkDetach + Fiber.join) so callers share one scan and a timed-out waiter does not poison later results.

Editor list order and membership stay the same as the old sequential loop. Tests add coverage for per-instance caching and for recovery when an earlier caller is interrupted mid-discovery.

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

@coderabbitai

coderabbitaiBot commented Jul 30, 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: fcbdc8e9-b5de-448e-bc59-9b6666f3b913

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-actionsgithub-actionsBot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:M 30-99 changed lines (additions + deletions). labels Jul 30, 2026
Comment threadapps/server/src/process/externalLauncher.ts Outdated
@macroscopeapp

macroscopeappBot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

This PR introduces caching and concurrent execution for editor discovery, changing state management and interruption semantics. While tests are included, the new caching layer and detached fiber pattern represent meaningful runtime behavior changes that warrant human review.

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

capad-xyzand others added 2 commits July 31, 2026 04:26
Effect.cached memoizes the inner effect's exit, so an interrupted first
scan poisoned the cache permanently. Cache the fork instead and let
callers join a detached fiber, so a timed-out caller cancels only its
own wait.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:M30-99 changed lines (additions + deletions).vouch:unvouchedPR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Provider page stays on "Checking provider status" because server.getConfig blocks on editor discovery

1 participant

@capad-xyz
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(server): run editor discovery concurrently and cache the result - #5050

Open
capad-xyz wants to merge 3 commits into
pingdotgg:mainfrom
capad-xyz:fix/concurrent-cached-editor-discovery
Open

fix(server): run editor discovery concurrently and cache the result#5050
capad-xyz wants to merge 3 commits into
pingdotgg:mainfrom
capad-xyz:fix/concurrent-cached-editor-discovery

Conversation

@capad-xyz

@capad-xyzcapad-xyz commented Jul 30, 2026

Copy link
Copy Markdown

Problem

server.getConfig blocks on external editor discovery, which probes ~20 editor commands sequentially, each walking every PATH entry against every PATHEXT variant. On hosts with long PATHs this exceeds the 5s EDITOR_DISCOVERY_TIMEOUT on every call, so discovery is interrupted, callers get an empty editor list, and the next call starts over from scratch. Every client connection pays the full 5s on its first RPC.

Fixes#4210. Also addresses the empty-list symptom in #4697.

Fix

Two changes in ExternalLauncher:

  1. Probe concurrently.Effect.forEach(..., { concurrency: EDITORS.length }) instead of a sequential loop. Wall time becomes the slowest single probe rather than the sum.
  2. Run discovery in a detached fiber and let callers join it.Effect.cached around Effect.forkDetach, so callers share one scan via Fiber.join.

The second point is deliberate. Caching the scan directly is unsafe here: Effect.cached is cachedWithTTL(self, Duration.infinity), and cachedInvalidateWithTTL memoizes the exit through onExit(self, ...) — including an interrupted exit — and with an infinite TTL never recomputes. A caller hitting the 5s timeout mid-scan would pin that interrupted exit for the life of the process. Caching the fork instead memoizes a fiber handle, which is produced instantly and cannot carry an interrupted exit; a caller that times out cancels only its own wait while the scan runs to completion in the background. On a #4210-class machine (15-99s scans) editors now resolve on a subsequent getConfig instead of never.

EDITOR_DISCOVERY_TIMEOUT in ws.ts is untouched and still bounds the cold call.

Results are unchanged.Effect.forEach preserves input order, and per-editor command preference (resolveAvailableCommand) is untouched, so the returned list is identical to what the sequential loop produced — same editors, same order. There are no per-editor timeouts, so a slow-but-present editor cannot be dropped from the result.

Relation to #4739

#4739 targeted the same issue and was closed unmerged. This takes a different approach: it adds caching rather than only bounding latency, and it avoids per-editor timeouts, which is what changed selection behaviour there. Happy to close this if you would rather solve it another way — the measurements below may still be useful for whatever lands.

Measurements

Windows 11, 46-entry PATH, 12-entry PATHEXT, desktop nightly 0.0.32-nightly.20260730.958 (patched bundle):

beforeafter
ws.rpc.server.getConfig5,000-5,004 ms on every call, discovery always interrupted4,789 ms once (cold, completes), then 0-1 ms
externalLauncher.buildAvailableEditors5,000 ms Interrupted, every time4,787 ms Success once, then cached

Discovery now completes, so clients receive a real editor list instead of a permanently empty one.

Tests

vp test run src/process/externalLauncher.test.ts — 6 passed. Two added:

  • discovery is cached per service instance
  • discovery survives a caller giving up: a first caller is interrupted mid-scan, and a later call must still resolve. Fails on the pre-fix commit with All fibers interrupted without error.

Not covered

This does not address the Android store app disconnect (#4901); that reproduces with a fast getConfig too and is protocol skew on the client build. This only removes the server-side stall.

Diagnosed and implemented with Claude Code (Fable 5).


Note

Medium Risk
Changes async/caching behavior on a hot RPC path (getConfig); logic is localized to ExternalLauncher with tests, but discovery can still be slow on the first cold call and cached results won't reflect PATH changes until process restart.

Overview
External editor discovery no longer blocks server.getConfig for the full sequential PATH scan on every call. buildAvailableEditors now probes all editors in parallel (Effect.forEach with full concurrency), and ExternalLaunchermemoizes discovery by caching a detached fiber (Effect.cached + forkDetach + Fiber.join) so callers share one scan and a timed-out waiter does not poison later results.

Editor list order and membership stay the same as the old sequential loop. Tests add coverage for per-instance caching and for recovery when an earlier caller is interrupted mid-discovery.

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

@coderabbitai

coderabbitaiBot commented Jul 30, 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: fcbdc8e9-b5de-448e-bc59-9b6666f3b913

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-actionsgithub-actionsBot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:M 30-99 changed lines (additions + deletions). labels Jul 30, 2026
Comment threadapps/server/src/process/externalLauncher.ts Outdated
@macroscopeapp

macroscopeappBot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

This PR introduces caching and concurrent execution for editor discovery, changing state management and interruption semantics. While tests are included, the new caching layer and detached fiber pattern represent meaningful runtime behavior changes that warrant human review.

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

capad-xyzand others added 2 commits July 31, 2026 04:26
Effect.cached memoizes the inner effect's exit, so an interrupted first
scan poisoned the cache permanently. Cache the fork instead and let
callers join a detached fiber, so a timed-out caller cancels only its
own wait.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:M30-99 changed lines (additions + deletions).vouch:unvouchedPR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Provider page stays on "Checking provider status" because server.getConfig blocks on editor discovery

1 participant

@capad-xyz
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(server): run editor discovery concurrently and cache the result - #5050

Open
capad-xyz wants to merge 3 commits into
pingdotgg:mainfrom
capad-xyz:fix/concurrent-cached-editor-discovery
Open

fix(server): run editor discovery concurrently and cache the result#5050
capad-xyz wants to merge 3 commits into
pingdotgg:mainfrom
capad-xyz:fix/concurrent-cached-editor-discovery

Conversation

@capad-xyz

@capad-xyzcapad-xyz commented Jul 30, 2026

Copy link
Copy Markdown

Problem

server.getConfig blocks on external editor discovery, which probes ~20 editor commands sequentially, each walking every PATH entry against every PATHEXT variant. On hosts with long PATHs this exceeds the 5s EDITOR_DISCOVERY_TIMEOUT on every call, so discovery is interrupted, callers get an empty editor list, and the next call starts over from scratch. Every client connection pays the full 5s on its first RPC.

Fixes#4210. Also addresses the empty-list symptom in #4697.

Fix

Two changes in ExternalLauncher:

  1. Probe concurrently.Effect.forEach(..., { concurrency: EDITORS.length }) instead of a sequential loop. Wall time becomes the slowest single probe rather than the sum.
  2. Run discovery in a detached fiber and let callers join it.Effect.cached around Effect.forkDetach, so callers share one scan via Fiber.join.

The second point is deliberate. Caching the scan directly is unsafe here: Effect.cached is cachedWithTTL(self, Duration.infinity), and cachedInvalidateWithTTL memoizes the exit through onExit(self, ...) — including an interrupted exit — and with an infinite TTL never recomputes. A caller hitting the 5s timeout mid-scan would pin that interrupted exit for the life of the process. Caching the fork instead memoizes a fiber handle, which is produced instantly and cannot carry an interrupted exit; a caller that times out cancels only its own wait while the scan runs to completion in the background. On a #4210-class machine (15-99s scans) editors now resolve on a subsequent getConfig instead of never.

EDITOR_DISCOVERY_TIMEOUT in ws.ts is untouched and still bounds the cold call.

Results are unchanged.Effect.forEach preserves input order, and per-editor command preference (resolveAvailableCommand) is untouched, so the returned list is identical to what the sequential loop produced — same editors, same order. There are no per-editor timeouts, so a slow-but-present editor cannot be dropped from the result.

Relation to #4739

#4739 targeted the same issue and was closed unmerged. This takes a different approach: it adds caching rather than only bounding latency, and it avoids per-editor timeouts, which is what changed selection behaviour there. Happy to close this if you would rather solve it another way — the measurements below may still be useful for whatever lands.

Measurements

Windows 11, 46-entry PATH, 12-entry PATHEXT, desktop nightly 0.0.32-nightly.20260730.958 (patched bundle):

beforeafter
ws.rpc.server.getConfig5,000-5,004 ms on every call, discovery always interrupted4,789 ms once (cold, completes), then 0-1 ms
externalLauncher.buildAvailableEditors5,000 ms Interrupted, every time4,787 ms Success once, then cached

Discovery now completes, so clients receive a real editor list instead of a permanently empty one.

Tests

vp test run src/process/externalLauncher.test.ts — 6 passed. Two added:

  • discovery is cached per service instance
  • discovery survives a caller giving up: a first caller is interrupted mid-scan, and a later call must still resolve. Fails on the pre-fix commit with All fibers interrupted without error.

Not covered

This does not address the Android store app disconnect (#4901); that reproduces with a fast getConfig too and is protocol skew on the client build. This only removes the server-side stall.

Diagnosed and implemented with Claude Code (Fable 5).


Note

Medium Risk
Changes async/caching behavior on a hot RPC path (getConfig); logic is localized to ExternalLauncher with tests, but discovery can still be slow on the first cold call and cached results won't reflect PATH changes until process restart.

Overview
External editor discovery no longer blocks server.getConfig for the full sequential PATH scan on every call. buildAvailableEditors now probes all editors in parallel (Effect.forEach with full concurrency), and ExternalLaunchermemoizes discovery by caching a detached fiber (Effect.cached + forkDetach + Fiber.join) so callers share one scan and a timed-out waiter does not poison later results.

Editor list order and membership stay the same as the old sequential loop. Tests add coverage for per-instance caching and for recovery when an earlier caller is interrupted mid-discovery.

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

@coderabbitai

coderabbitaiBot commented Jul 30, 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: fcbdc8e9-b5de-448e-bc59-9b6666f3b913

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-actionsgithub-actionsBot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:M 30-99 changed lines (additions + deletions). labels Jul 30, 2026
Comment threadapps/server/src/process/externalLauncher.ts Outdated
@macroscopeapp

macroscopeappBot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

This PR introduces caching and concurrent execution for editor discovery, changing state management and interruption semantics. While tests are included, the new caching layer and detached fiber pattern represent meaningful runtime behavior changes that warrant human review.

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

capad-xyzand others added 2 commits July 31, 2026 04:26
Effect.cached memoizes the inner effect's exit, so an interrupted first
scan poisoned the cache permanently. Cache the fork instead and let
callers join a detached fiber, so a timed-out caller cancels only its
own wait.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:M30-99 changed lines (additions + deletions).vouch:unvouchedPR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Provider page stays on "Checking provider status" because server.getConfig blocks on editor discovery

1 participant

@capad-xyz
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

fix(server): run editor discovery concurrently and cache the result - #5050

Open
capad-xyz wants to merge 3 commits into
pingdotgg:mainfrom
capad-xyz:fix/concurrent-cached-editor-discovery
Open

fix(server): run editor discovery concurrently and cache the result#5050
capad-xyz wants to merge 3 commits into
pingdotgg:mainfrom
capad-xyz:fix/concurrent-cached-editor-discovery

Conversation

@capad-xyz

@capad-xyzcapad-xyz commented Jul 30, 2026

Copy link
Copy Markdown

Problem

server.getConfig blocks on external editor discovery, which probes ~20 editor commands sequentially, each walking every PATH entry against every PATHEXT variant. On hosts with long PATHs this exceeds the 5s EDITOR_DISCOVERY_TIMEOUT on every call, so discovery is interrupted, callers get an empty editor list, and the next call starts over from scratch. Every client connection pays the full 5s on its first RPC.

Fixes#4210. Also addresses the empty-list symptom in #4697.

Fix

Two changes in ExternalLauncher:

  1. Probe concurrently.Effect.forEach(..., { concurrency: EDITORS.length }) instead of a sequential loop. Wall time becomes the slowest single probe rather than the sum.
  2. Run discovery in a detached fiber and let callers join it.Effect.cached around Effect.forkDetach, so callers share one scan via Fiber.join.

The second point is deliberate. Caching the scan directly is unsafe here: Effect.cached is cachedWithTTL(self, Duration.infinity), and cachedInvalidateWithTTL memoizes the exit through onExit(self, ...) — including an interrupted exit — and with an infinite TTL never recomputes. A caller hitting the 5s timeout mid-scan would pin that interrupted exit for the life of the process. Caching the fork instead memoizes a fiber handle, which is produced instantly and cannot carry an interrupted exit; a caller that times out cancels only its own wait while the scan runs to completion in the background. On a #4210-class machine (15-99s scans) editors now resolve on a subsequent getConfig instead of never.

EDITOR_DISCOVERY_TIMEOUT in ws.ts is untouched and still bounds the cold call.

Results are unchanged.Effect.forEach preserves input order, and per-editor command preference (resolveAvailableCommand) is untouched, so the returned list is identical to what the sequential loop produced — same editors, same order. There are no per-editor timeouts, so a slow-but-present editor cannot be dropped from the result.

Relation to #4739

#4739 targeted the same issue and was closed unmerged. This takes a different approach: it adds caching rather than only bounding latency, and it avoids per-editor timeouts, which is what changed selection behaviour there. Happy to close this if you would rather solve it another way — the measurements below may still be useful for whatever lands.

Measurements

Windows 11, 46-entry PATH, 12-entry PATHEXT, desktop nightly 0.0.32-nightly.20260730.958 (patched bundle):

beforeafter
ws.rpc.server.getConfig5,000-5,004 ms on every call, discovery always interrupted4,789 ms once (cold, completes), then 0-1 ms
externalLauncher.buildAvailableEditors5,000 ms Interrupted, every time4,787 ms Success once, then cached

Discovery now completes, so clients receive a real editor list instead of a permanently empty one.

Tests

vp test run src/process/externalLauncher.test.ts — 6 passed. Two added:

  • discovery is cached per service instance
  • discovery survives a caller giving up: a first caller is interrupted mid-scan, and a later call must still resolve. Fails on the pre-fix commit with All fibers interrupted without error.

Not covered

This does not address the Android store app disconnect (#4901); that reproduces with a fast getConfig too and is protocol skew on the client build. This only removes the server-side stall.

Diagnosed and implemented with Claude Code (Fable 5).


Note

Medium Risk
Changes async/caching behavior on a hot RPC path (getConfig); logic is localized to ExternalLauncher with tests, but discovery can still be slow on the first cold call and cached results won't reflect PATH changes until process restart.

Overview
External editor discovery no longer blocks server.getConfig for the full sequential PATH scan on every call. buildAvailableEditors now probes all editors in parallel (Effect.forEach with full concurrency), and ExternalLaunchermemoizes discovery by caching a detached fiber (Effect.cached + forkDetach + Fiber.join) so callers share one scan and a timed-out waiter does not poison later results.

Editor list order and membership stay the same as the old sequential loop. Tests add coverage for per-instance caching and for recovery when an earlier caller is interrupted mid-discovery.

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

@coderabbitai

coderabbitaiBot commented Jul 30, 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: fcbdc8e9-b5de-448e-bc59-9b6666f3b913

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-actionsgithub-actionsBot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:M 30-99 changed lines (additions + deletions). labels Jul 30, 2026
Comment threadapps/server/src/process/externalLauncher.ts Outdated
@macroscopeapp

macroscopeappBot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

This PR introduces caching and concurrent execution for editor discovery, changing state management and interruption semantics. While tests are included, the new caching layer and detached fiber pattern represent meaningful runtime behavior changes that warrant human review.

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

capad-xyzand others added 2 commits July 31, 2026 04:26
Effect.cached memoizes the inner effect's exit, so an interrupted first
scan poisoned the cache permanently. Cache the fork instead and let
callers join a detached fiber, so a timed-out caller cancels only its
own wait.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:M30-99 changed lines (additions + deletions).vouch:unvouchedPR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Provider page stays on "Checking provider status" because server.getConfig blocks on editor discovery

1 participant

@capad-xyz
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(server): run editor discovery concurrently and cache the result - #5050

Open
capad-xyz wants to merge 3 commits into
pingdotgg:mainfrom
capad-xyz:fix/concurrent-cached-editor-discovery
Open

fix(server): run editor discovery concurrently and cache the result#5050
capad-xyz wants to merge 3 commits into
pingdotgg:mainfrom
capad-xyz:fix/concurrent-cached-editor-discovery

Conversation

@capad-xyz

@capad-xyzcapad-xyz commented Jul 30, 2026

Copy link
Copy Markdown

Problem

server.getConfig blocks on external editor discovery, which probes ~20 editor commands sequentially, each walking every PATH entry against every PATHEXT variant. On hosts with long PATHs this exceeds the 5s EDITOR_DISCOVERY_TIMEOUT on every call, so discovery is interrupted, callers get an empty editor list, and the next call starts over from scratch. Every client connection pays the full 5s on its first RPC.

Fixes#4210. Also addresses the empty-list symptom in #4697.

Fix

Two changes in ExternalLauncher:

  1. Probe concurrently.Effect.forEach(..., { concurrency: EDITORS.length }) instead of a sequential loop. Wall time becomes the slowest single probe rather than the sum.
  2. Run discovery in a detached fiber and let callers join it.Effect.cached around Effect.forkDetach, so callers share one scan via Fiber.join.

The second point is deliberate. Caching the scan directly is unsafe here: Effect.cached is cachedWithTTL(self, Duration.infinity), and cachedInvalidateWithTTL memoizes the exit through onExit(self, ...) — including an interrupted exit — and with an infinite TTL never recomputes. A caller hitting the 5s timeout mid-scan would pin that interrupted exit for the life of the process. Caching the fork instead memoizes a fiber handle, which is produced instantly and cannot carry an interrupted exit; a caller that times out cancels only its own wait while the scan runs to completion in the background. On a #4210-class machine (15-99s scans) editors now resolve on a subsequent getConfig instead of never.

EDITOR_DISCOVERY_TIMEOUT in ws.ts is untouched and still bounds the cold call.

Results are unchanged.Effect.forEach preserves input order, and per-editor command preference (resolveAvailableCommand) is untouched, so the returned list is identical to what the sequential loop produced — same editors, same order. There are no per-editor timeouts, so a slow-but-present editor cannot be dropped from the result.

Relation to #4739

#4739 targeted the same issue and was closed unmerged. This takes a different approach: it adds caching rather than only bounding latency, and it avoids per-editor timeouts, which is what changed selection behaviour there. Happy to close this if you would rather solve it another way — the measurements below may still be useful for whatever lands.

Measurements

Windows 11, 46-entry PATH, 12-entry PATHEXT, desktop nightly 0.0.32-nightly.20260730.958 (patched bundle):

beforeafter
ws.rpc.server.getConfig5,000-5,004 ms on every call, discovery always interrupted4,789 ms once (cold, completes), then 0-1 ms
externalLauncher.buildAvailableEditors5,000 ms Interrupted, every time4,787 ms Success once, then cached

Discovery now completes, so clients receive a real editor list instead of a permanently empty one.

Tests

vp test run src/process/externalLauncher.test.ts — 6 passed. Two added:

  • discovery is cached per service instance
  • discovery survives a caller giving up: a first caller is interrupted mid-scan, and a later call must still resolve. Fails on the pre-fix commit with All fibers interrupted without error.

Not covered

This does not address the Android store app disconnect (#4901); that reproduces with a fast getConfig too and is protocol skew on the client build. This only removes the server-side stall.

Diagnosed and implemented with Claude Code (Fable 5).


Note

Medium Risk
Changes async/caching behavior on a hot RPC path (getConfig); logic is localized to ExternalLauncher with tests, but discovery can still be slow on the first cold call and cached results won't reflect PATH changes until process restart.

Overview
External editor discovery no longer blocks server.getConfig for the full sequential PATH scan on every call. buildAvailableEditors now probes all editors in parallel (Effect.forEach with full concurrency), and ExternalLaunchermemoizes discovery by caching a detached fiber (Effect.cached + forkDetach + Fiber.join) so callers share one scan and a timed-out waiter does not poison later results.

Editor list order and membership stay the same as the old sequential loop. Tests add coverage for per-instance caching and for recovery when an earlier caller is interrupted mid-discovery.

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

@coderabbitai

coderabbitaiBot commented Jul 30, 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: fcbdc8e9-b5de-448e-bc59-9b6666f3b913

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-actionsgithub-actionsBot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:M 30-99 changed lines (additions + deletions). labels Jul 30, 2026
Comment threadapps/server/src/process/externalLauncher.ts Outdated
@macroscopeapp

macroscopeappBot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

This PR introduces caching and concurrent execution for editor discovery, changing state management and interruption semantics. While tests are included, the new caching layer and detached fiber pattern represent meaningful runtime behavior changes that warrant human review.

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

capad-xyzand others added 2 commits July 31, 2026 04:26
Effect.cached memoizes the inner effect's exit, so an interrupted first
scan poisoned the cache permanently. Cache the fork instead and let
callers join a detached fiber, so a timed-out caller cancels only its
own wait.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:M30-99 changed lines (additions + deletions).vouch:unvouchedPR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Provider page stays on "Checking provider status" because server.getConfig blocks on editor discovery

1 participant

@capad-xyz
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(server): run editor discovery concurrently and cache the result - #5050

Open
capad-xyz wants to merge 3 commits into
pingdotgg:mainfrom
capad-xyz:fix/concurrent-cached-editor-discovery
Open

fix(server): run editor discovery concurrently and cache the result#5050
capad-xyz wants to merge 3 commits into
pingdotgg:mainfrom
capad-xyz:fix/concurrent-cached-editor-discovery

Conversation

@capad-xyz

@capad-xyzcapad-xyz commented Jul 30, 2026

Copy link
Copy Markdown

Problem

server.getConfig blocks on external editor discovery, which probes ~20 editor commands sequentially, each walking every PATH entry against every PATHEXT variant. On hosts with long PATHs this exceeds the 5s EDITOR_DISCOVERY_TIMEOUT on every call, so discovery is interrupted, callers get an empty editor list, and the next call starts over from scratch. Every client connection pays the full 5s on its first RPC.

Fixes#4210. Also addresses the empty-list symptom in #4697.

Fix

Two changes in ExternalLauncher:

  1. Probe concurrently.Effect.forEach(..., { concurrency: EDITORS.length }) instead of a sequential loop. Wall time becomes the slowest single probe rather than the sum.
  2. Run discovery in a detached fiber and let callers join it.Effect.cached around Effect.forkDetach, so callers share one scan via Fiber.join.

The second point is deliberate. Caching the scan directly is unsafe here: Effect.cached is cachedWithTTL(self, Duration.infinity), and cachedInvalidateWithTTL memoizes the exit through onExit(self, ...) — including an interrupted exit — and with an infinite TTL never recomputes. A caller hitting the 5s timeout mid-scan would pin that interrupted exit for the life of the process. Caching the fork instead memoizes a fiber handle, which is produced instantly and cannot carry an interrupted exit; a caller that times out cancels only its own wait while the scan runs to completion in the background. On a #4210-class machine (15-99s scans) editors now resolve on a subsequent getConfig instead of never.

EDITOR_DISCOVERY_TIMEOUT in ws.ts is untouched and still bounds the cold call.

Results are unchanged.Effect.forEach preserves input order, and per-editor command preference (resolveAvailableCommand) is untouched, so the returned list is identical to what the sequential loop produced — same editors, same order. There are no per-editor timeouts, so a slow-but-present editor cannot be dropped from the result.

Relation to #4739

#4739 targeted the same issue and was closed unmerged. This takes a different approach: it adds caching rather than only bounding latency, and it avoids per-editor timeouts, which is what changed selection behaviour there. Happy to close this if you would rather solve it another way — the measurements below may still be useful for whatever lands.

Measurements

Windows 11, 46-entry PATH, 12-entry PATHEXT, desktop nightly 0.0.32-nightly.20260730.958 (patched bundle):

beforeafter
ws.rpc.server.getConfig5,000-5,004 ms on every call, discovery always interrupted4,789 ms once (cold, completes), then 0-1 ms
externalLauncher.buildAvailableEditors5,000 ms Interrupted, every time4,787 ms Success once, then cached

Discovery now completes, so clients receive a real editor list instead of a permanently empty one.

Tests

vp test run src/process/externalLauncher.test.ts — 6 passed. Two added:

  • discovery is cached per service instance
  • discovery survives a caller giving up: a first caller is interrupted mid-scan, and a later call must still resolve. Fails on the pre-fix commit with All fibers interrupted without error.

Not covered

This does not address the Android store app disconnect (#4901); that reproduces with a fast getConfig too and is protocol skew on the client build. This only removes the server-side stall.

Diagnosed and implemented with Claude Code (Fable 5).


Note

Medium Risk
Changes async/caching behavior on a hot RPC path (getConfig); logic is localized to ExternalLauncher with tests, but discovery can still be slow on the first cold call and cached results won't reflect PATH changes until process restart.

Overview
External editor discovery no longer blocks server.getConfig for the full sequential PATH scan on every call. buildAvailableEditors now probes all editors in parallel (Effect.forEach with full concurrency), and ExternalLaunchermemoizes discovery by caching a detached fiber (Effect.cached + forkDetach + Fiber.join) so callers share one scan and a timed-out waiter does not poison later results.

Editor list order and membership stay the same as the old sequential loop. Tests add coverage for per-instance caching and for recovery when an earlier caller is interrupted mid-discovery.

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

@coderabbitai

coderabbitaiBot commented Jul 30, 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: fcbdc8e9-b5de-448e-bc59-9b6666f3b913

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-actionsgithub-actionsBot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:M 30-99 changed lines (additions + deletions). labels Jul 30, 2026
Comment threadapps/server/src/process/externalLauncher.ts Outdated
@macroscopeapp

macroscopeappBot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

This PR introduces caching and concurrent execution for editor discovery, changing state management and interruption semantics. While tests are included, the new caching layer and detached fiber pattern represent meaningful runtime behavior changes that warrant human review.

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

capad-xyzand others added 2 commits July 31, 2026 04:26
Effect.cached memoizes the inner effect's exit, so an interrupted first
scan poisoned the cache permanently. Cache the fork instead and let
callers join a detached fiber, so a timed-out caller cancels only its
own wait.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:M30-99 changed lines (additions + deletions).vouch:unvouchedPR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Provider page stays on "Checking provider status" because server.getConfig blocks on editor discovery

1 participant

@capad-xyz
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

fix(server): run editor discovery concurrently and cache the result - #5050

Open
capad-xyz wants to merge 3 commits into
pingdotgg:mainfrom
capad-xyz:fix/concurrent-cached-editor-discovery
Open

fix(server): run editor discovery concurrently and cache the result#5050
capad-xyz wants to merge 3 commits into
pingdotgg:mainfrom
capad-xyz:fix/concurrent-cached-editor-discovery

Conversation

@capad-xyz

@capad-xyzcapad-xyz commented Jul 30, 2026

Copy link
Copy Markdown

Problem

server.getConfig blocks on external editor discovery, which probes ~20 editor commands sequentially, each walking every PATH entry against every PATHEXT variant. On hosts with long PATHs this exceeds the 5s EDITOR_DISCOVERY_TIMEOUT on every call, so discovery is interrupted, callers get an empty editor list, and the next call starts over from scratch. Every client connection pays the full 5s on its first RPC.

Fixes#4210. Also addresses the empty-list symptom in #4697.

Fix

Two changes in ExternalLauncher:

  1. Probe concurrently.Effect.forEach(..., { concurrency: EDITORS.length }) instead of a sequential loop. Wall time becomes the slowest single probe rather than the sum.
  2. Run discovery in a detached fiber and let callers join it.Effect.cached around Effect.forkDetach, so callers share one scan via Fiber.join.

The second point is deliberate. Caching the scan directly is unsafe here: Effect.cached is cachedWithTTL(self, Duration.infinity), and cachedInvalidateWithTTL memoizes the exit through onExit(self, ...) — including an interrupted exit — and with an infinite TTL never recomputes. A caller hitting the 5s timeout mid-scan would pin that interrupted exit for the life of the process. Caching the fork instead memoizes a fiber handle, which is produced instantly and cannot carry an interrupted exit; a caller that times out cancels only its own wait while the scan runs to completion in the background. On a #4210-class machine (15-99s scans) editors now resolve on a subsequent getConfig instead of never.

EDITOR_DISCOVERY_TIMEOUT in ws.ts is untouched and still bounds the cold call.

Results are unchanged.Effect.forEach preserves input order, and per-editor command preference (resolveAvailableCommand) is untouched, so the returned list is identical to what the sequential loop produced — same editors, same order. There are no per-editor timeouts, so a slow-but-present editor cannot be dropped from the result.

Relation to #4739

#4739 targeted the same issue and was closed unmerged. This takes a different approach: it adds caching rather than only bounding latency, and it avoids per-editor timeouts, which is what changed selection behaviour there. Happy to close this if you would rather solve it another way — the measurements below may still be useful for whatever lands.

Measurements

Windows 11, 46-entry PATH, 12-entry PATHEXT, desktop nightly 0.0.32-nightly.20260730.958 (patched bundle):

beforeafter
ws.rpc.server.getConfig5,000-5,004 ms on every call, discovery always interrupted4,789 ms once (cold, completes), then 0-1 ms
externalLauncher.buildAvailableEditors5,000 ms Interrupted, every time4,787 ms Success once, then cached

Discovery now completes, so clients receive a real editor list instead of a permanently empty one.

Tests

vp test run src/process/externalLauncher.test.ts — 6 passed. Two added:

  • discovery is cached per service instance
  • discovery survives a caller giving up: a first caller is interrupted mid-scan, and a later call must still resolve. Fails on the pre-fix commit with All fibers interrupted without error.

Not covered

This does not address the Android store app disconnect (#4901); that reproduces with a fast getConfig too and is protocol skew on the client build. This only removes the server-side stall.

Diagnosed and implemented with Claude Code (Fable 5).


Note

Medium Risk
Changes async/caching behavior on a hot RPC path (getConfig); logic is localized to ExternalLauncher with tests, but discovery can still be slow on the first cold call and cached results won't reflect PATH changes until process restart.

Overview
External editor discovery no longer blocks server.getConfig for the full sequential PATH scan on every call. buildAvailableEditors now probes all editors in parallel (Effect.forEach with full concurrency), and ExternalLaunchermemoizes discovery by caching a detached fiber (Effect.cached + forkDetach + Fiber.join) so callers share one scan and a timed-out waiter does not poison later results.

Editor list order and membership stay the same as the old sequential loop. Tests add coverage for per-instance caching and for recovery when an earlier caller is interrupted mid-discovery.

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

@coderabbitai

coderabbitaiBot commented Jul 30, 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: fcbdc8e9-b5de-448e-bc59-9b6666f3b913

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-actionsgithub-actionsBot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:M 30-99 changed lines (additions + deletions). labels Jul 30, 2026
Comment threadapps/server/src/process/externalLauncher.ts Outdated
@macroscopeapp

macroscopeappBot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

This PR introduces caching and concurrent execution for editor discovery, changing state management and interruption semantics. While tests are included, the new caching layer and detached fiber pattern represent meaningful runtime behavior changes that warrant human review.

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

capad-xyzand others added 2 commits July 31, 2026 04:26
Effect.cached memoizes the inner effect's exit, so an interrupted first
scan poisoned the cache permanently. Cache the fork instead and let
callers join a detached fiber, so a timed-out caller cancels only its
own wait.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:M30-99 changed lines (additions + deletions).vouch:unvouchedPR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Provider page stays on "Checking provider status" because server.getConfig blocks on editor discovery

1 participant

@capad-xyz