oh-my-opencode 5.0.0-beta.30 → 5.0.0-beta.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.js +75 -26
- package/dist/cli-node/index.js +75 -26
- package/dist/config/schema/background-task.d.ts +1 -0
- package/dist/config/schema/oh-my-opencode-config.d.ts +1 -0
- package/dist/features/background-agent/constants.d.ts +0 -7
- package/dist/features/background-agent/manager.d.ts +2 -1
- package/dist/features/background-agent/parent-wake-dispatched-tracker.d.ts +0 -1
- package/dist/features/background-agent/subagent-spawn-limits.d.ts +8 -0
- package/dist/features/task-toast-manager/manager.d.ts +0 -24
- package/dist/hooks/anthropic-context-window-limit-recovery/message-storage-directory.d.ts +0 -3
- package/dist/hooks/anthropic-context-window-limit-recovery/state.d.ts +1 -0
- package/dist/hooks/anthropic-context-window-limit-recovery/storage/empty-messages.d.ts +0 -1
- package/dist/hooks/claude-code-hooks/session-hook-state.d.ts +1 -0
- package/dist/hooks/claude-code-hooks/stop.d.ts +2 -0
- package/dist/hooks/claude-code-hooks/todo.d.ts +0 -1
- package/dist/hooks/claude-code-hooks/transcript.d.ts +2 -0
- package/dist/hooks/fsync-skip-warning/index.d.ts +4 -0
- package/dist/hooks/hashline-edit-diff-enhancer/hook.d.ts +1 -0
- package/dist/hooks/hashline-edit-diff-enhancer/pending-captures.d.ts +13 -0
- package/dist/hooks/keyword-detector/hook.d.ts +7 -0
- package/dist/hooks/keyword-detector/ultrawork/source-detector.d.ts +0 -1
- package/dist/hooks/model-fallback/hook.d.ts +1 -1
- package/dist/index.js +439 -218
- package/dist/oh-my-opencode.schema.json +5 -0
- package/dist/plugin-dispose.d.ts +3 -0
- package/dist/shared/agent-tool-restrictions.d.ts +0 -1
- package/dist/tools/delegate-task/sync-session-cleanup.d.ts +3 -0
- package/dist/tools/delegate-task/sync-session-poller.d.ts +1 -0
- package/dist/tools/delegate-task/types.d.ts +1 -0
- package/dist/tui.js +56 -3
- package/package.json +15 -15
- package/packages/git-bash-mcp/dist/cli.js +1 -0
- package/packages/lsp-core/src/lsp/client-wrapper.test.ts +97 -0
- package/packages/lsp-core/src/lsp/client-wrapper.ts +21 -6
- package/packages/lsp-core/src/lsp/constants.ts +5 -0
- package/packages/lsp-core/src/lsp/errors.ts +15 -0
- package/packages/lsp-core/src/lsp/manager-lifecycle.test.ts +262 -0
- package/packages/lsp-core/src/lsp/manager.ts +106 -13
- package/packages/lsp-core/src/lsp/workspace-markers.ts +18 -1
- package/packages/lsp-core/src/mcp.ts +3 -0
- package/packages/lsp-daemon/dist/cli.js +114 -18
- package/packages/lsp-daemon/dist/client.js +114 -18
- package/packages/lsp-daemon/dist/index.js +114 -18
- package/packages/lsp-daemon/dist/proxy.js +3 -0
- package/packages/lsp-tools-mcp/dist/cli.js +114 -18
- package/packages/lsp-tools-mcp/dist/lsp/manager.js +93 -12
- package/packages/lsp-tools-mcp/dist/mcp.js +114 -18
- package/packages/lsp-tools-mcp/dist/tools.js +113 -18
- package/packages/omo-codex/plugin/.codex-plugin/plugin.json +1 -1
- package/packages/omo-codex/plugin/components/bootstrap/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/bootstrap/package.json +1 -1
- package/packages/omo-codex/plugin/components/codegraph/dist/cli.js +31 -2
- package/packages/omo-codex/plugin/components/codegraph/dist/serve.js +31 -2
- package/packages/omo-codex/plugin/components/codegraph/package.json +1 -1
- package/packages/omo-codex/plugin/components/codegraph/src/mcp-unavailable.ts +12 -0
- package/packages/omo-codex/plugin/components/codegraph/test/serve-unavailable-idle.test.ts +47 -0
- package/packages/omo-codex/plugin/components/comment-checker/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/comment-checker/package.json +1 -1
- package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +2 -2
- package/packages/omo-codex/plugin/components/git-bash/package.json +1 -1
- package/packages/omo-codex/plugin/components/lazycodex-executor-verify/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/lazycodex-executor-verify/package.json +1 -1
- package/packages/omo-codex/plugin/components/lsp/dist/.omo-runtime-manifest.json +3 -3
- package/packages/omo-codex/plugin/components/lsp/dist/cli.js +113 -18
- package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +2 -2
- package/packages/omo-codex/plugin/components/lsp/package.json +1 -1
- package/packages/omo-codex/plugin/components/rules/hooks/hooks.json +4 -4
- package/packages/omo-codex/plugin/components/rules/package.json +1 -1
- package/packages/omo-codex/plugin/components/teammode/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/teammode/package.json +1 -1
- package/packages/omo-codex/plugin/components/telemetry/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/telemetry/package.json +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/package.json +1 -1
- package/packages/omo-codex/plugin/components/ulw-execute-continuation/hooks/hooks.json +2 -2
- package/packages/omo-codex/plugin/components/ulw-execute-continuation/package.json +1 -1
- package/packages/omo-codex/plugin/components/ulw-loop/AGENTS.md +1 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint.js +2 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/cli.js +100 -54
- package/packages/omo-codex/plugin/components/ulw-loop/dist/codex-goal-instruction.d.ts +2 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/codex-goal-instruction.js +9 -6
- package/packages/omo-codex/plugin/components/ulw-loop/dist/quality-gate.d.ts +4 -2
- package/packages/omo-codex/plugin/components/ulw-loop/dist/quality-gate.js +7 -10
- package/packages/omo-codex/plugin/components/ulw-loop/dist/spawn-guard.js +3 -2
- package/packages/omo-codex/plugin/components/ulw-loop/dist/surface.d.ts +15 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/surface.js +50 -0
- package/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json +4 -4
- package/packages/omo-codex/plugin/components/ulw-loop/package.json +1 -1
- package/packages/omo-codex/plugin/components/ulw-loop/src/checkpoint.ts +2 -0
- package/packages/omo-codex/plugin/components/ulw-loop/src/codex-goal-instruction.ts +16 -5
- package/packages/omo-codex/plugin/components/ulw-loop/src/quality-gate.ts +11 -14
- package/packages/omo-codex/plugin/components/ulw-loop/src/spawn-guard.ts +3 -2
- package/packages/omo-codex/plugin/components/ulw-loop/src/surface.ts +73 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/checkpoint-final.test.ts +44 -1
- package/packages/omo-codex/plugin/components/ulw-loop/test/codex-goal-instruction.test.ts +25 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/quality-gate-builder.ts +7 -3
- package/packages/omo-codex/plugin/components/ulw-loop/test/quality-gate-roles.test.ts +45 -2
- package/packages/omo-codex/plugin/components/ulw-loop/test/spawn-guard.test.ts +20 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/surface.test.ts +74 -0
- package/packages/omo-codex/plugin/hooks/post-compact-resetting-git-bash-mcp-reminder.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-compact-resetting-lsp-diagnostics-cache.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-compact-resetting-project-rule-cache.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-codegraph-init-guidance.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-comments.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-lsp-diagnostics.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-thread-title-hygiene.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-matching-project-rules.json +1 -1
- package/packages/omo-codex/plugin/hooks/pre-tool-use-enforcing-unlimited-goal-budget.json +1 -1
- package/packages/omo-codex/plugin/hooks/pre-tool-use-guarding-ulw-loop-spawns.json +1 -1
- package/packages/omo-codex/plugin/hooks/pre-tool-use-recommending-git-bash-mcp.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-checking-auto-update.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-checking-bootstrap-provisioning.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-checking-codegraph-bootstrap.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-loading-project-rules.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-recording-session-telemetry.json +1 -1
- package/packages/omo-codex/plugin/hooks/stop-checking-ulw-execute-continuation.json +1 -1
- package/packages/omo-codex/plugin/hooks/stop-checking-ulw-loop-resume.json +1 -1
- package/packages/omo-codex/plugin/hooks/subagent-stop-checking-ulw-execute-continuation.json +1 -1
- package/packages/omo-codex/plugin/hooks/subagent-stop-verifying-lazycodex-executor-evidence.json +1 -1
- package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ultrawork-trigger.json +1 -1
- package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ulw-loop-steering.json +1 -1
- package/packages/omo-codex/plugin/hooks/user-prompt-submit-loading-project-rules.json +1 -1
- package/packages/omo-codex/plugin/package-lock.json +13 -13
- package/packages/omo-codex/plugin/package.json +1 -1
- package/packages/omo-codex/scripts/install-dist/install-local.mjs +2 -2
- package/dist/hooks/ralph-loop/completion-promise-detector-test-input.d.ts +0 -11
- package/dist/hooks/ralph-loop/loop-session-recovery.d.ts +0 -7
- package/dist/tools/call-omo-agent/message-storage-directory.d.ts +0 -1
- package/dist/tools/delegate-task/model-string-parser.d.ts +0 -1
- package/packages/lsp-tools-mcp/dist/lsp/cleanup-errors.d.ts +0 -1
- package/packages/lsp-tools-mcp/dist/lsp/constants.d.ts +0 -1
- package/packages/lsp-tools-mcp/dist/lsp/language-mappings.d.ts +0 -1
- package/packages/lsp-tools-mcp/dist/lsp/process-signal-cleanup.d.ts +0 -1
- package/packages/lsp-tools-mcp/dist/missing-dependency-result.d.ts +0 -1
|
@@ -27,6 +27,8 @@ var MAX_RESIDENT_CLIENTS = 6;
|
|
|
27
27
|
var REAPER_INTERVAL_MS = 60000;
|
|
28
28
|
var STOP_HARD_KILL_TIMEOUT_MS = 5000;
|
|
29
29
|
var STOP_SIGKILL_GRACE_MS = 1000;
|
|
30
|
+
var CLIENT_RESPAWN_RETRY_LIMIT = 2;
|
|
31
|
+
var CLIENT_RESPAWN_COOLDOWN_MS = 60000;
|
|
30
32
|
|
|
31
33
|
// ../lsp-core/src/lsp/errors.ts
|
|
32
34
|
class LspConnectionClosedError extends Error {
|
|
@@ -91,6 +93,16 @@ class LspProcessSpawnError extends Error {
|
|
|
91
93
|
this.name = "LspProcessSpawnError";
|
|
92
94
|
}
|
|
93
95
|
}
|
|
96
|
+
|
|
97
|
+
class LspClientRespawnBudgetExceededError extends Error {
|
|
98
|
+
constructor(serverId, root, retryLimit) {
|
|
99
|
+
super(`LSP server ${serverId} at ${root} failed to stay alive; respawn budget exhausted ` + `(${retryLimit} consecutive dead generations). Retrying after the cooldown may succeed.`);
|
|
100
|
+
this.serverId = serverId;
|
|
101
|
+
this.root = root;
|
|
102
|
+
this.retryLimit = retryLimit;
|
|
103
|
+
this.name = "LspClientRespawnBudgetExceededError";
|
|
104
|
+
}
|
|
105
|
+
}
|
|
94
106
|
function isLspDeadConnectionError(err) {
|
|
95
107
|
return err instanceof LspConnectionClosedError || err instanceof LspProcessExitedError;
|
|
96
108
|
}
|
|
@@ -2777,6 +2789,8 @@ function awaitWithSignal(promise, signal) {
|
|
|
2777
2789
|
class LspManager {
|
|
2778
2790
|
constructor(options = {}) {
|
|
2779
2791
|
this.clients = new Map;
|
|
2792
|
+
this.pendingStops = new Map;
|
|
2793
|
+
this.respawnBudgets = new Map;
|
|
2780
2794
|
this.reaperHandle = null;
|
|
2781
2795
|
this.signalDisposer = null;
|
|
2782
2796
|
this.disposed = false;
|
|
@@ -2802,17 +2816,52 @@ class LspManager {
|
|
|
2802
2816
|
getKey(root, serverId) {
|
|
2803
2817
|
return `${root}::${serverId}`;
|
|
2804
2818
|
}
|
|
2819
|
+
tombstoneStop(key, client) {
|
|
2820
|
+
const stop = stopClientBestEffort(client);
|
|
2821
|
+
this.pendingStops.set(key, stop);
|
|
2822
|
+
stop.finally(() => {
|
|
2823
|
+
if (this.pendingStops.get(key) === stop) {
|
|
2824
|
+
this.pendingStops.delete(key);
|
|
2825
|
+
}
|
|
2826
|
+
});
|
|
2827
|
+
return stop;
|
|
2828
|
+
}
|
|
2829
|
+
recordDeadGeneration(key) {
|
|
2830
|
+
let budget = this.respawnBudgets.get(key);
|
|
2831
|
+
if (budget === undefined) {
|
|
2832
|
+
budget = { deadGenerations: 0, exhaustedAt: null };
|
|
2833
|
+
this.respawnBudgets.set(key, budget);
|
|
2834
|
+
}
|
|
2835
|
+
budget.deadGenerations += 1;
|
|
2836
|
+
}
|
|
2837
|
+
markHealthyGeneration(key) {
|
|
2838
|
+
this.respawnBudgets.delete(key);
|
|
2839
|
+
}
|
|
2840
|
+
ensureRespawnAllowed(key, root, serverId) {
|
|
2841
|
+
const budget = this.respawnBudgets.get(key);
|
|
2842
|
+
if (budget === undefined || budget.deadGenerations < CLIENT_RESPAWN_RETRY_LIMIT)
|
|
2843
|
+
return;
|
|
2844
|
+
if (budget.exhaustedAt === null) {
|
|
2845
|
+
budget.exhaustedAt = this.now();
|
|
2846
|
+
throw new LspClientRespawnBudgetExceededError(serverId, root, CLIENT_RESPAWN_RETRY_LIMIT);
|
|
2847
|
+
}
|
|
2848
|
+
if (this.now() - budget.exhaustedAt < CLIENT_RESPAWN_COOLDOWN_MS) {
|
|
2849
|
+
throw new LspClientRespawnBudgetExceededError(serverId, root, CLIENT_RESPAWN_RETRY_LIMIT);
|
|
2850
|
+
}
|
|
2851
|
+
budget.deadGenerations = 0;
|
|
2852
|
+
budget.exhaustedAt = null;
|
|
2853
|
+
}
|
|
2805
2854
|
reapStale() {
|
|
2806
2855
|
const t = this.now();
|
|
2807
2856
|
for (const [key, managed] of this.clients) {
|
|
2808
2857
|
if (managed.isInitializing && managed.initializingSince !== null && t - managed.initializingSince > this.initTimeoutMs) {
|
|
2809
|
-
stopClientBestEffort(managed.client);
|
|
2810
2858
|
this.clients.delete(key);
|
|
2859
|
+
this.tombstoneStop(key, managed.client);
|
|
2811
2860
|
continue;
|
|
2812
2861
|
}
|
|
2813
2862
|
if (!managed.isInitializing && managed.refCount === 0 && managed.pendingWaiters === 0 && t - managed.lastUsedAt > this.idleTimeoutMs) {
|
|
2814
|
-
stopClientBestEffort(managed.client);
|
|
2815
2863
|
this.clients.delete(key);
|
|
2864
|
+
this.tombstoneStop(key, managed.client);
|
|
2816
2865
|
}
|
|
2817
2866
|
}
|
|
2818
2867
|
}
|
|
@@ -2822,7 +2871,7 @@ class LspManager {
|
|
|
2822
2871
|
if (!victim)
|
|
2823
2872
|
return;
|
|
2824
2873
|
this.clients.delete(victim.key);
|
|
2825
|
-
|
|
2874
|
+
this.tombstoneStop(victim.key, victim.managed.client);
|
|
2826
2875
|
}
|
|
2827
2876
|
}
|
|
2828
2877
|
leastRecentlyUsedIdleClient() {
|
|
@@ -2839,7 +2888,7 @@ class LspManager {
|
|
|
2839
2888
|
async tryDeleteIfOrphaned(key, managed) {
|
|
2840
2889
|
if (managed.refCount === 0 && managed.pendingWaiters === 0 && !managed.isInitializing && this.clients.get(key) === managed) {
|
|
2841
2890
|
this.clients.delete(key);
|
|
2842
|
-
await
|
|
2891
|
+
await this.tombstoneStop(key, managed.client);
|
|
2843
2892
|
}
|
|
2844
2893
|
}
|
|
2845
2894
|
async getClient(root, server, signal) {
|
|
@@ -2848,12 +2897,22 @@ class LspManager {
|
|
|
2848
2897
|
}
|
|
2849
2898
|
signal?.throwIfAborted();
|
|
2850
2899
|
const key = this.getKey(root, server.id);
|
|
2900
|
+
for (;; ) {
|
|
2901
|
+
const pendingStop = this.pendingStops.get(key);
|
|
2902
|
+
if (pendingStop === undefined)
|
|
2903
|
+
break;
|
|
2904
|
+
await awaitWithSignal(pendingStop, signal);
|
|
2905
|
+
signal?.throwIfAborted();
|
|
2906
|
+
}
|
|
2907
|
+
if (this.disposed) {
|
|
2908
|
+
throw new Error("LspManager has been disposed");
|
|
2909
|
+
}
|
|
2851
2910
|
let managed = this.clients.get(key);
|
|
2852
2911
|
if (managed) {
|
|
2853
2912
|
const t = this.now();
|
|
2854
2913
|
if (managed.isInitializing && managed.initializingSince !== null && t - managed.initializingSince > this.initTimeoutMs) {
|
|
2855
|
-
await stopClientBestEffort(managed.client);
|
|
2856
2914
|
this.clients.delete(key);
|
|
2915
|
+
await this.tombstoneStop(key, managed.client);
|
|
2857
2916
|
managed = undefined;
|
|
2858
2917
|
}
|
|
2859
2918
|
}
|
|
@@ -2874,15 +2933,20 @@ class LspManager {
|
|
|
2874
2933
|
signal.throwIfAborted();
|
|
2875
2934
|
}
|
|
2876
2935
|
if (!managed.client.isAlive()) {
|
|
2877
|
-
|
|
2878
|
-
this.clients.
|
|
2936
|
+
this.recordDeadGeneration(key);
|
|
2937
|
+
if (this.clients.get(key) === managed) {
|
|
2938
|
+
this.clients.delete(key);
|
|
2939
|
+
}
|
|
2940
|
+
await this.tombstoneStop(key, managed.client);
|
|
2879
2941
|
return this.getClient(root, server, signal);
|
|
2880
2942
|
}
|
|
2943
|
+
this.markHealthyGeneration(key);
|
|
2881
2944
|
managed.refCount++;
|
|
2882
2945
|
managed.lastUsedAt = this.now();
|
|
2883
2946
|
return managed.client;
|
|
2884
2947
|
}
|
|
2885
2948
|
this.evictForAdmission();
|
|
2949
|
+
this.ensureRespawnAllowed(key, root, server.id);
|
|
2886
2950
|
const client = this.clientFactory(root, server);
|
|
2887
2951
|
const initStartedAt = this.now();
|
|
2888
2952
|
const initPromise = (async () => {
|
|
@@ -2906,7 +2970,7 @@ class LspManager {
|
|
|
2906
2970
|
if (this.clients.get(key) === newManaged) {
|
|
2907
2971
|
this.clients.delete(key);
|
|
2908
2972
|
}
|
|
2909
|
-
await
|
|
2973
|
+
await this.tombstoneStop(key, client);
|
|
2910
2974
|
throw err;
|
|
2911
2975
|
}
|
|
2912
2976
|
newManaged.pendingWaiters--;
|
|
@@ -2917,6 +2981,15 @@ class LspManager {
|
|
|
2917
2981
|
await this.tryDeleteIfOrphaned(key, newManaged);
|
|
2918
2982
|
signal.throwIfAborted();
|
|
2919
2983
|
}
|
|
2984
|
+
if (!client.isAlive()) {
|
|
2985
|
+
this.recordDeadGeneration(key);
|
|
2986
|
+
if (this.clients.get(key) === newManaged) {
|
|
2987
|
+
this.clients.delete(key);
|
|
2988
|
+
}
|
|
2989
|
+
await this.tombstoneStop(key, client);
|
|
2990
|
+
return this.getClient(root, server, signal);
|
|
2991
|
+
}
|
|
2992
|
+
this.markHealthyGeneration(key);
|
|
2920
2993
|
newManaged.refCount++;
|
|
2921
2994
|
newManaged.lastUsedAt = this.now();
|
|
2922
2995
|
return client;
|
|
@@ -2937,15 +3010,20 @@ class LspManager {
|
|
|
2937
3010
|
if (client && managed.client !== client)
|
|
2938
3011
|
return;
|
|
2939
3012
|
this.clients.delete(key);
|
|
2940
|
-
|
|
3013
|
+
this.tombstoneStop(key, managed.client);
|
|
2941
3014
|
}
|
|
2942
3015
|
warmupClient(root, server) {
|
|
2943
3016
|
if (this.disposed)
|
|
2944
3017
|
return;
|
|
2945
3018
|
const key = this.getKey(root, server.id);
|
|
2946
|
-
if (this.clients.has(key))
|
|
3019
|
+
if (this.clients.has(key) || this.pendingStops.has(key))
|
|
2947
3020
|
return;
|
|
2948
3021
|
this.evictForAdmission();
|
|
3022
|
+
try {
|
|
3023
|
+
this.ensureRespawnAllowed(key, root, server.id);
|
|
3024
|
+
} catch {
|
|
3025
|
+
return;
|
|
3026
|
+
}
|
|
2949
3027
|
const client = this.clientFactory(root, server);
|
|
2950
3028
|
const initStartedAt = this.now();
|
|
2951
3029
|
const initPromise = (async () => {
|
|
@@ -2971,7 +3049,7 @@ class LspManager {
|
|
|
2971
3049
|
if (this.clients.get(key) === managed) {
|
|
2972
3050
|
this.clients.delete(key);
|
|
2973
3051
|
}
|
|
2974
|
-
|
|
3052
|
+
this.tombstoneStop(key, client);
|
|
2975
3053
|
});
|
|
2976
3054
|
}
|
|
2977
3055
|
isServerInitializing(root, serverId) {
|
|
@@ -3015,8 +3093,11 @@ class LspManager {
|
|
|
3015
3093
|
for (const managed of this.clients.values()) {
|
|
3016
3094
|
stopPromises.push(stopClientBestEffort(managed.client));
|
|
3017
3095
|
}
|
|
3096
|
+
const tombstonedStops = [...this.pendingStops.values()];
|
|
3018
3097
|
this.clients.clear();
|
|
3019
|
-
|
|
3098
|
+
this.respawnBudgets.clear();
|
|
3099
|
+
await Promise.allSettled([...stopPromises, ...tombstonedStops]);
|
|
3100
|
+
this.pendingStops.clear();
|
|
3020
3101
|
}
|
|
3021
3102
|
}
|
|
3022
3103
|
var _defaultInstance = null;
|
|
@@ -4038,6 +4119,7 @@ async function runJsonRpcStdioServer(config) {
|
|
|
4038
4119
|
const idleTimer = createIdleTimer(idleTimeoutMs, log, () => {
|
|
4039
4120
|
isClosed = true;
|
|
4040
4121
|
config.onIdleTimeout?.();
|
|
4122
|
+
config.input.destroy();
|
|
4041
4123
|
});
|
|
4042
4124
|
const watchdog = createParentWatchdog(config.parentWatchdog, (parentPid, pollIntervalMs) => {
|
|
4043
4125
|
isClosed = true;
|
|
@@ -4181,7 +4263,16 @@ import { existsSync as existsSync7, realpathSync as realpathSync4 } from "node:f
|
|
|
4181
4263
|
import { dirname as dirname7, join as join3 } from "node:path";
|
|
4182
4264
|
|
|
4183
4265
|
// ../lsp-core/src/lsp/workspace-markers.ts
|
|
4184
|
-
var
|
|
4266
|
+
var GIT_WORKSPACE_MARKER = ".git";
|
|
4267
|
+
var PROJECT_WORKSPACE_MARKERS = [
|
|
4268
|
+
"package.json",
|
|
4269
|
+
"pyproject.toml",
|
|
4270
|
+
"Cargo.toml",
|
|
4271
|
+
"go.mod",
|
|
4272
|
+
"pom.xml",
|
|
4273
|
+
"build.gradle"
|
|
4274
|
+
];
|
|
4275
|
+
var WORKSPACE_MARKERS = [GIT_WORKSPACE_MARKER, ...PROJECT_WORKSPACE_MARKERS];
|
|
4185
4276
|
|
|
4186
4277
|
// ../lsp-core/src/lsp/outside-context-workspace.ts
|
|
4187
4278
|
function findWorkspaceRootOutsideContext(directory) {
|
|
@@ -4870,20 +4961,25 @@ function findWorkspaceRoot(filePath) {
|
|
|
4870
4961
|
if (!isPathInside(cwd, abs))
|
|
4871
4962
|
return findWorkspaceRootOutsideContext(dir);
|
|
4872
4963
|
const fallbackRoot = nearestExistingDirectoryInsideContext(dir, cwd) ?? cwd;
|
|
4964
|
+
let nearestPackageRoot;
|
|
4873
4965
|
while (isPathInside(cwd, dir)) {
|
|
4874
4966
|
const canonicalDir = existingDirectoryInsideContext(dir, cwd);
|
|
4875
4967
|
if (canonicalDir !== undefined) {
|
|
4876
|
-
|
|
4877
|
-
|
|
4878
|
-
|
|
4879
|
-
|
|
4968
|
+
if (existsSync11(join5(dir, GIT_WORKSPACE_MARKER))) {
|
|
4969
|
+
return canonicalDir;
|
|
4970
|
+
}
|
|
4971
|
+
if (nearestPackageRoot === undefined && hasProjectWorkspaceMarker(dir)) {
|
|
4972
|
+
nearestPackageRoot = canonicalDir;
|
|
4880
4973
|
}
|
|
4881
4974
|
}
|
|
4882
4975
|
if (dir === cwd)
|
|
4883
4976
|
break;
|
|
4884
4977
|
dir = dirname10(dir);
|
|
4885
4978
|
}
|
|
4886
|
-
return fallbackRoot;
|
|
4979
|
+
return nearestPackageRoot ?? fallbackRoot;
|
|
4980
|
+
}
|
|
4981
|
+
function hasProjectWorkspaceMarker(directory) {
|
|
4982
|
+
return PROJECT_WORKSPACE_MARKERS.some((marker) => existsSync11(join5(directory, marker)));
|
|
4887
4983
|
}
|
|
4888
4984
|
function resolveReadablePathInsideContext(filePath) {
|
|
4889
4985
|
const cwd = contextCwd();
|
|
@@ -58,6 +58,9 @@ export async function runMcpStdioProxy(options = {}) {
|
|
|
58
58
|
await runJsonRpcStdioServer({
|
|
59
59
|
input,
|
|
60
60
|
output,
|
|
61
|
+
// Idle stays disabled: the opencode host does not respawn a stdio MCP
|
|
62
|
+
// server that exits — transport close marks it failed until a user
|
|
63
|
+
// reconnects via /mcp — so an idle kill would strand a live session.
|
|
61
64
|
idleTimeoutMs: 0,
|
|
62
65
|
parentWatchdog: options.parentWatchdog ?? {},
|
|
63
66
|
handler: (request, requestOptions) => {
|
|
@@ -29,6 +29,8 @@ var MAX_RESIDENT_CLIENTS = 6;
|
|
|
29
29
|
var REAPER_INTERVAL_MS = 60000;
|
|
30
30
|
var STOP_HARD_KILL_TIMEOUT_MS = 5000;
|
|
31
31
|
var STOP_SIGKILL_GRACE_MS = 1000;
|
|
32
|
+
var CLIENT_RESPAWN_RETRY_LIMIT = 2;
|
|
33
|
+
var CLIENT_RESPAWN_COOLDOWN_MS = 60000;
|
|
32
34
|
|
|
33
35
|
// ../lsp-core/src/lsp/errors.ts
|
|
34
36
|
class LspConnectionClosedError extends Error {
|
|
@@ -93,6 +95,16 @@ class LspProcessSpawnError extends Error {
|
|
|
93
95
|
this.name = "LspProcessSpawnError";
|
|
94
96
|
}
|
|
95
97
|
}
|
|
98
|
+
|
|
99
|
+
class LspClientRespawnBudgetExceededError extends Error {
|
|
100
|
+
constructor(serverId, root, retryLimit) {
|
|
101
|
+
super(`LSP server ${serverId} at ${root} failed to stay alive; respawn budget exhausted ` + `(${retryLimit} consecutive dead generations). Retrying after the cooldown may succeed.`);
|
|
102
|
+
this.serverId = serverId;
|
|
103
|
+
this.root = root;
|
|
104
|
+
this.retryLimit = retryLimit;
|
|
105
|
+
this.name = "LspClientRespawnBudgetExceededError";
|
|
106
|
+
}
|
|
107
|
+
}
|
|
96
108
|
function isLspDeadConnectionError(err) {
|
|
97
109
|
return err instanceof LspConnectionClosedError || err instanceof LspProcessExitedError;
|
|
98
110
|
}
|
|
@@ -2779,6 +2791,8 @@ function awaitWithSignal(promise, signal) {
|
|
|
2779
2791
|
class LspManager {
|
|
2780
2792
|
constructor(options = {}) {
|
|
2781
2793
|
this.clients = new Map;
|
|
2794
|
+
this.pendingStops = new Map;
|
|
2795
|
+
this.respawnBudgets = new Map;
|
|
2782
2796
|
this.reaperHandle = null;
|
|
2783
2797
|
this.signalDisposer = null;
|
|
2784
2798
|
this.disposed = false;
|
|
@@ -2804,17 +2818,52 @@ class LspManager {
|
|
|
2804
2818
|
getKey(root, serverId) {
|
|
2805
2819
|
return `${root}::${serverId}`;
|
|
2806
2820
|
}
|
|
2821
|
+
tombstoneStop(key, client) {
|
|
2822
|
+
const stop = stopClientBestEffort(client);
|
|
2823
|
+
this.pendingStops.set(key, stop);
|
|
2824
|
+
stop.finally(() => {
|
|
2825
|
+
if (this.pendingStops.get(key) === stop) {
|
|
2826
|
+
this.pendingStops.delete(key);
|
|
2827
|
+
}
|
|
2828
|
+
});
|
|
2829
|
+
return stop;
|
|
2830
|
+
}
|
|
2831
|
+
recordDeadGeneration(key) {
|
|
2832
|
+
let budget = this.respawnBudgets.get(key);
|
|
2833
|
+
if (budget === undefined) {
|
|
2834
|
+
budget = { deadGenerations: 0, exhaustedAt: null };
|
|
2835
|
+
this.respawnBudgets.set(key, budget);
|
|
2836
|
+
}
|
|
2837
|
+
budget.deadGenerations += 1;
|
|
2838
|
+
}
|
|
2839
|
+
markHealthyGeneration(key) {
|
|
2840
|
+
this.respawnBudgets.delete(key);
|
|
2841
|
+
}
|
|
2842
|
+
ensureRespawnAllowed(key, root, serverId) {
|
|
2843
|
+
const budget = this.respawnBudgets.get(key);
|
|
2844
|
+
if (budget === undefined || budget.deadGenerations < CLIENT_RESPAWN_RETRY_LIMIT)
|
|
2845
|
+
return;
|
|
2846
|
+
if (budget.exhaustedAt === null) {
|
|
2847
|
+
budget.exhaustedAt = this.now();
|
|
2848
|
+
throw new LspClientRespawnBudgetExceededError(serverId, root, CLIENT_RESPAWN_RETRY_LIMIT);
|
|
2849
|
+
}
|
|
2850
|
+
if (this.now() - budget.exhaustedAt < CLIENT_RESPAWN_COOLDOWN_MS) {
|
|
2851
|
+
throw new LspClientRespawnBudgetExceededError(serverId, root, CLIENT_RESPAWN_RETRY_LIMIT);
|
|
2852
|
+
}
|
|
2853
|
+
budget.deadGenerations = 0;
|
|
2854
|
+
budget.exhaustedAt = null;
|
|
2855
|
+
}
|
|
2807
2856
|
reapStale() {
|
|
2808
2857
|
const t = this.now();
|
|
2809
2858
|
for (const [key, managed] of this.clients) {
|
|
2810
2859
|
if (managed.isInitializing && managed.initializingSince !== null && t - managed.initializingSince > this.initTimeoutMs) {
|
|
2811
|
-
stopClientBestEffort(managed.client);
|
|
2812
2860
|
this.clients.delete(key);
|
|
2861
|
+
this.tombstoneStop(key, managed.client);
|
|
2813
2862
|
continue;
|
|
2814
2863
|
}
|
|
2815
2864
|
if (!managed.isInitializing && managed.refCount === 0 && managed.pendingWaiters === 0 && t - managed.lastUsedAt > this.idleTimeoutMs) {
|
|
2816
|
-
stopClientBestEffort(managed.client);
|
|
2817
2865
|
this.clients.delete(key);
|
|
2866
|
+
this.tombstoneStop(key, managed.client);
|
|
2818
2867
|
}
|
|
2819
2868
|
}
|
|
2820
2869
|
}
|
|
@@ -2824,7 +2873,7 @@ class LspManager {
|
|
|
2824
2873
|
if (!victim)
|
|
2825
2874
|
return;
|
|
2826
2875
|
this.clients.delete(victim.key);
|
|
2827
|
-
|
|
2876
|
+
this.tombstoneStop(victim.key, victim.managed.client);
|
|
2828
2877
|
}
|
|
2829
2878
|
}
|
|
2830
2879
|
leastRecentlyUsedIdleClient() {
|
|
@@ -2841,7 +2890,7 @@ class LspManager {
|
|
|
2841
2890
|
async tryDeleteIfOrphaned(key, managed) {
|
|
2842
2891
|
if (managed.refCount === 0 && managed.pendingWaiters === 0 && !managed.isInitializing && this.clients.get(key) === managed) {
|
|
2843
2892
|
this.clients.delete(key);
|
|
2844
|
-
await
|
|
2893
|
+
await this.tombstoneStop(key, managed.client);
|
|
2845
2894
|
}
|
|
2846
2895
|
}
|
|
2847
2896
|
async getClient(root, server, signal) {
|
|
@@ -2850,12 +2899,22 @@ class LspManager {
|
|
|
2850
2899
|
}
|
|
2851
2900
|
signal?.throwIfAborted();
|
|
2852
2901
|
const key = this.getKey(root, server.id);
|
|
2902
|
+
for (;; ) {
|
|
2903
|
+
const pendingStop = this.pendingStops.get(key);
|
|
2904
|
+
if (pendingStop === undefined)
|
|
2905
|
+
break;
|
|
2906
|
+
await awaitWithSignal(pendingStop, signal);
|
|
2907
|
+
signal?.throwIfAborted();
|
|
2908
|
+
}
|
|
2909
|
+
if (this.disposed) {
|
|
2910
|
+
throw new Error("LspManager has been disposed");
|
|
2911
|
+
}
|
|
2853
2912
|
let managed = this.clients.get(key);
|
|
2854
2913
|
if (managed) {
|
|
2855
2914
|
const t = this.now();
|
|
2856
2915
|
if (managed.isInitializing && managed.initializingSince !== null && t - managed.initializingSince > this.initTimeoutMs) {
|
|
2857
|
-
await stopClientBestEffort(managed.client);
|
|
2858
2916
|
this.clients.delete(key);
|
|
2917
|
+
await this.tombstoneStop(key, managed.client);
|
|
2859
2918
|
managed = undefined;
|
|
2860
2919
|
}
|
|
2861
2920
|
}
|
|
@@ -2876,15 +2935,20 @@ class LspManager {
|
|
|
2876
2935
|
signal.throwIfAborted();
|
|
2877
2936
|
}
|
|
2878
2937
|
if (!managed.client.isAlive()) {
|
|
2879
|
-
|
|
2880
|
-
this.clients.
|
|
2938
|
+
this.recordDeadGeneration(key);
|
|
2939
|
+
if (this.clients.get(key) === managed) {
|
|
2940
|
+
this.clients.delete(key);
|
|
2941
|
+
}
|
|
2942
|
+
await this.tombstoneStop(key, managed.client);
|
|
2881
2943
|
return this.getClient(root, server, signal);
|
|
2882
2944
|
}
|
|
2945
|
+
this.markHealthyGeneration(key);
|
|
2883
2946
|
managed.refCount++;
|
|
2884
2947
|
managed.lastUsedAt = this.now();
|
|
2885
2948
|
return managed.client;
|
|
2886
2949
|
}
|
|
2887
2950
|
this.evictForAdmission();
|
|
2951
|
+
this.ensureRespawnAllowed(key, root, server.id);
|
|
2888
2952
|
const client = this.clientFactory(root, server);
|
|
2889
2953
|
const initStartedAt = this.now();
|
|
2890
2954
|
const initPromise = (async () => {
|
|
@@ -2908,7 +2972,7 @@ class LspManager {
|
|
|
2908
2972
|
if (this.clients.get(key) === newManaged) {
|
|
2909
2973
|
this.clients.delete(key);
|
|
2910
2974
|
}
|
|
2911
|
-
await
|
|
2975
|
+
await this.tombstoneStop(key, client);
|
|
2912
2976
|
throw err;
|
|
2913
2977
|
}
|
|
2914
2978
|
newManaged.pendingWaiters--;
|
|
@@ -2919,6 +2983,15 @@ class LspManager {
|
|
|
2919
2983
|
await this.tryDeleteIfOrphaned(key, newManaged);
|
|
2920
2984
|
signal.throwIfAborted();
|
|
2921
2985
|
}
|
|
2986
|
+
if (!client.isAlive()) {
|
|
2987
|
+
this.recordDeadGeneration(key);
|
|
2988
|
+
if (this.clients.get(key) === newManaged) {
|
|
2989
|
+
this.clients.delete(key);
|
|
2990
|
+
}
|
|
2991
|
+
await this.tombstoneStop(key, client);
|
|
2992
|
+
return this.getClient(root, server, signal);
|
|
2993
|
+
}
|
|
2994
|
+
this.markHealthyGeneration(key);
|
|
2922
2995
|
newManaged.refCount++;
|
|
2923
2996
|
newManaged.lastUsedAt = this.now();
|
|
2924
2997
|
return client;
|
|
@@ -2939,15 +3012,20 @@ class LspManager {
|
|
|
2939
3012
|
if (client && managed.client !== client)
|
|
2940
3013
|
return;
|
|
2941
3014
|
this.clients.delete(key);
|
|
2942
|
-
|
|
3015
|
+
this.tombstoneStop(key, managed.client);
|
|
2943
3016
|
}
|
|
2944
3017
|
warmupClient(root, server) {
|
|
2945
3018
|
if (this.disposed)
|
|
2946
3019
|
return;
|
|
2947
3020
|
const key = this.getKey(root, server.id);
|
|
2948
|
-
if (this.clients.has(key))
|
|
3021
|
+
if (this.clients.has(key) || this.pendingStops.has(key))
|
|
2949
3022
|
return;
|
|
2950
3023
|
this.evictForAdmission();
|
|
3024
|
+
try {
|
|
3025
|
+
this.ensureRespawnAllowed(key, root, server.id);
|
|
3026
|
+
} catch {
|
|
3027
|
+
return;
|
|
3028
|
+
}
|
|
2951
3029
|
const client = this.clientFactory(root, server);
|
|
2952
3030
|
const initStartedAt = this.now();
|
|
2953
3031
|
const initPromise = (async () => {
|
|
@@ -2973,7 +3051,7 @@ class LspManager {
|
|
|
2973
3051
|
if (this.clients.get(key) === managed) {
|
|
2974
3052
|
this.clients.delete(key);
|
|
2975
3053
|
}
|
|
2976
|
-
|
|
3054
|
+
this.tombstoneStop(key, client);
|
|
2977
3055
|
});
|
|
2978
3056
|
}
|
|
2979
3057
|
isServerInitializing(root, serverId) {
|
|
@@ -3017,8 +3095,11 @@ class LspManager {
|
|
|
3017
3095
|
for (const managed of this.clients.values()) {
|
|
3018
3096
|
stopPromises.push(stopClientBestEffort(managed.client));
|
|
3019
3097
|
}
|
|
3098
|
+
const tombstonedStops = [...this.pendingStops.values()];
|
|
3020
3099
|
this.clients.clear();
|
|
3021
|
-
|
|
3100
|
+
this.respawnBudgets.clear();
|
|
3101
|
+
await Promise.allSettled([...stopPromises, ...tombstonedStops]);
|
|
3102
|
+
this.pendingStops.clear();
|
|
3022
3103
|
}
|
|
3023
3104
|
}
|
|
3024
3105
|
var _defaultInstance = null;
|
|
@@ -3212,6 +3293,7 @@ async function runJsonRpcStdioServer(config) {
|
|
|
3212
3293
|
const idleTimer = createIdleTimer(idleTimeoutMs, log, () => {
|
|
3213
3294
|
isClosed = true;
|
|
3214
3295
|
config.onIdleTimeout?.();
|
|
3296
|
+
config.input.destroy();
|
|
3215
3297
|
});
|
|
3216
3298
|
const watchdog = createParentWatchdog(config.parentWatchdog, (parentPid, pollIntervalMs) => {
|
|
3217
3299
|
isClosed = true;
|
|
@@ -3533,7 +3615,16 @@ import { existsSync as existsSync6, realpathSync as realpathSync4 } from "node:f
|
|
|
3533
3615
|
import { dirname as dirname5, join as join3 } from "node:path";
|
|
3534
3616
|
|
|
3535
3617
|
// ../lsp-core/src/lsp/workspace-markers.ts
|
|
3536
|
-
var
|
|
3618
|
+
var GIT_WORKSPACE_MARKER = ".git";
|
|
3619
|
+
var PROJECT_WORKSPACE_MARKERS = [
|
|
3620
|
+
"package.json",
|
|
3621
|
+
"pyproject.toml",
|
|
3622
|
+
"Cargo.toml",
|
|
3623
|
+
"go.mod",
|
|
3624
|
+
"pom.xml",
|
|
3625
|
+
"build.gradle"
|
|
3626
|
+
];
|
|
3627
|
+
var WORKSPACE_MARKERS = [GIT_WORKSPACE_MARKER, ...PROJECT_WORKSPACE_MARKERS];
|
|
3537
3628
|
|
|
3538
3629
|
// ../lsp-core/src/lsp/outside-context-workspace.ts
|
|
3539
3630
|
function findWorkspaceRootOutsideContext(directory) {
|
|
@@ -4222,20 +4313,25 @@ function findWorkspaceRoot(filePath) {
|
|
|
4222
4313
|
if (!isPathInside(cwd, abs))
|
|
4223
4314
|
return findWorkspaceRootOutsideContext(dir);
|
|
4224
4315
|
const fallbackRoot = nearestExistingDirectoryInsideContext(dir, cwd) ?? cwd;
|
|
4316
|
+
let nearestPackageRoot;
|
|
4225
4317
|
while (isPathInside(cwd, dir)) {
|
|
4226
4318
|
const canonicalDir = existingDirectoryInsideContext(dir, cwd);
|
|
4227
4319
|
if (canonicalDir !== undefined) {
|
|
4228
|
-
|
|
4229
|
-
|
|
4230
|
-
|
|
4231
|
-
|
|
4320
|
+
if (existsSync10(join5(dir, GIT_WORKSPACE_MARKER))) {
|
|
4321
|
+
return canonicalDir;
|
|
4322
|
+
}
|
|
4323
|
+
if (nearestPackageRoot === undefined && hasProjectWorkspaceMarker(dir)) {
|
|
4324
|
+
nearestPackageRoot = canonicalDir;
|
|
4232
4325
|
}
|
|
4233
4326
|
}
|
|
4234
4327
|
if (dir === cwd)
|
|
4235
4328
|
break;
|
|
4236
4329
|
dir = dirname8(dir);
|
|
4237
4330
|
}
|
|
4238
|
-
return fallbackRoot;
|
|
4331
|
+
return nearestPackageRoot ?? fallbackRoot;
|
|
4332
|
+
}
|
|
4333
|
+
function hasProjectWorkspaceMarker(directory) {
|
|
4334
|
+
return PROJECT_WORKSPACE_MARKERS.some((marker) => existsSync10(join5(directory, marker)));
|
|
4239
4335
|
}
|
|
4240
4336
|
function resolveReadablePathInsideContext(filePath) {
|
|
4241
4337
|
const cwd = contextCwd();
|