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.
Files changed (134) hide show
  1. package/dist/cli/index.js +75 -26
  2. package/dist/cli-node/index.js +75 -26
  3. package/dist/config/schema/background-task.d.ts +1 -0
  4. package/dist/config/schema/oh-my-opencode-config.d.ts +1 -0
  5. package/dist/features/background-agent/constants.d.ts +0 -7
  6. package/dist/features/background-agent/manager.d.ts +2 -1
  7. package/dist/features/background-agent/parent-wake-dispatched-tracker.d.ts +0 -1
  8. package/dist/features/background-agent/subagent-spawn-limits.d.ts +8 -0
  9. package/dist/features/task-toast-manager/manager.d.ts +0 -24
  10. package/dist/hooks/anthropic-context-window-limit-recovery/message-storage-directory.d.ts +0 -3
  11. package/dist/hooks/anthropic-context-window-limit-recovery/state.d.ts +1 -0
  12. package/dist/hooks/anthropic-context-window-limit-recovery/storage/empty-messages.d.ts +0 -1
  13. package/dist/hooks/claude-code-hooks/session-hook-state.d.ts +1 -0
  14. package/dist/hooks/claude-code-hooks/stop.d.ts +2 -0
  15. package/dist/hooks/claude-code-hooks/todo.d.ts +0 -1
  16. package/dist/hooks/claude-code-hooks/transcript.d.ts +2 -0
  17. package/dist/hooks/fsync-skip-warning/index.d.ts +4 -0
  18. package/dist/hooks/hashline-edit-diff-enhancer/hook.d.ts +1 -0
  19. package/dist/hooks/hashline-edit-diff-enhancer/pending-captures.d.ts +13 -0
  20. package/dist/hooks/keyword-detector/hook.d.ts +7 -0
  21. package/dist/hooks/keyword-detector/ultrawork/source-detector.d.ts +0 -1
  22. package/dist/hooks/model-fallback/hook.d.ts +1 -1
  23. package/dist/index.js +439 -218
  24. package/dist/oh-my-opencode.schema.json +5 -0
  25. package/dist/plugin-dispose.d.ts +3 -0
  26. package/dist/shared/agent-tool-restrictions.d.ts +0 -1
  27. package/dist/tools/delegate-task/sync-session-cleanup.d.ts +3 -0
  28. package/dist/tools/delegate-task/sync-session-poller.d.ts +1 -0
  29. package/dist/tools/delegate-task/types.d.ts +1 -0
  30. package/dist/tui.js +56 -3
  31. package/package.json +15 -15
  32. package/packages/git-bash-mcp/dist/cli.js +1 -0
  33. package/packages/lsp-core/src/lsp/client-wrapper.test.ts +97 -0
  34. package/packages/lsp-core/src/lsp/client-wrapper.ts +21 -6
  35. package/packages/lsp-core/src/lsp/constants.ts +5 -0
  36. package/packages/lsp-core/src/lsp/errors.ts +15 -0
  37. package/packages/lsp-core/src/lsp/manager-lifecycle.test.ts +262 -0
  38. package/packages/lsp-core/src/lsp/manager.ts +106 -13
  39. package/packages/lsp-core/src/lsp/workspace-markers.ts +18 -1
  40. package/packages/lsp-core/src/mcp.ts +3 -0
  41. package/packages/lsp-daemon/dist/cli.js +114 -18
  42. package/packages/lsp-daemon/dist/client.js +114 -18
  43. package/packages/lsp-daemon/dist/index.js +114 -18
  44. package/packages/lsp-daemon/dist/proxy.js +3 -0
  45. package/packages/lsp-tools-mcp/dist/cli.js +114 -18
  46. package/packages/lsp-tools-mcp/dist/lsp/manager.js +93 -12
  47. package/packages/lsp-tools-mcp/dist/mcp.js +114 -18
  48. package/packages/lsp-tools-mcp/dist/tools.js +113 -18
  49. package/packages/omo-codex/plugin/.codex-plugin/plugin.json +1 -1
  50. package/packages/omo-codex/plugin/components/bootstrap/hooks/hooks.json +1 -1
  51. package/packages/omo-codex/plugin/components/bootstrap/package.json +1 -1
  52. package/packages/omo-codex/plugin/components/codegraph/dist/cli.js +31 -2
  53. package/packages/omo-codex/plugin/components/codegraph/dist/serve.js +31 -2
  54. package/packages/omo-codex/plugin/components/codegraph/package.json +1 -1
  55. package/packages/omo-codex/plugin/components/codegraph/src/mcp-unavailable.ts +12 -0
  56. package/packages/omo-codex/plugin/components/codegraph/test/serve-unavailable-idle.test.ts +47 -0
  57. package/packages/omo-codex/plugin/components/comment-checker/hooks/hooks.json +1 -1
  58. package/packages/omo-codex/plugin/components/comment-checker/package.json +1 -1
  59. package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +2 -2
  60. package/packages/omo-codex/plugin/components/git-bash/package.json +1 -1
  61. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/hooks/hooks.json +1 -1
  62. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/package.json +1 -1
  63. package/packages/omo-codex/plugin/components/lsp/dist/.omo-runtime-manifest.json +3 -3
  64. package/packages/omo-codex/plugin/components/lsp/dist/cli.js +113 -18
  65. package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +2 -2
  66. package/packages/omo-codex/plugin/components/lsp/package.json +1 -1
  67. package/packages/omo-codex/plugin/components/rules/hooks/hooks.json +4 -4
  68. package/packages/omo-codex/plugin/components/rules/package.json +1 -1
  69. package/packages/omo-codex/plugin/components/teammode/hooks/hooks.json +1 -1
  70. package/packages/omo-codex/plugin/components/teammode/package.json +1 -1
  71. package/packages/omo-codex/plugin/components/telemetry/hooks/hooks.json +1 -1
  72. package/packages/omo-codex/plugin/components/telemetry/package.json +1 -1
  73. package/packages/omo-codex/plugin/components/ultrawork/hooks/hooks.json +1 -1
  74. package/packages/omo-codex/plugin/components/ultrawork/package.json +1 -1
  75. package/packages/omo-codex/plugin/components/ulw-execute-continuation/hooks/hooks.json +2 -2
  76. package/packages/omo-codex/plugin/components/ulw-execute-continuation/package.json +1 -1
  77. package/packages/omo-codex/plugin/components/ulw-loop/AGENTS.md +1 -0
  78. package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint.js +2 -0
  79. package/packages/omo-codex/plugin/components/ulw-loop/dist/cli.js +100 -54
  80. package/packages/omo-codex/plugin/components/ulw-loop/dist/codex-goal-instruction.d.ts +2 -0
  81. package/packages/omo-codex/plugin/components/ulw-loop/dist/codex-goal-instruction.js +9 -6
  82. package/packages/omo-codex/plugin/components/ulw-loop/dist/quality-gate.d.ts +4 -2
  83. package/packages/omo-codex/plugin/components/ulw-loop/dist/quality-gate.js +7 -10
  84. package/packages/omo-codex/plugin/components/ulw-loop/dist/spawn-guard.js +3 -2
  85. package/packages/omo-codex/plugin/components/ulw-loop/dist/surface.d.ts +15 -0
  86. package/packages/omo-codex/plugin/components/ulw-loop/dist/surface.js +50 -0
  87. package/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json +4 -4
  88. package/packages/omo-codex/plugin/components/ulw-loop/package.json +1 -1
  89. package/packages/omo-codex/plugin/components/ulw-loop/src/checkpoint.ts +2 -0
  90. package/packages/omo-codex/plugin/components/ulw-loop/src/codex-goal-instruction.ts +16 -5
  91. package/packages/omo-codex/plugin/components/ulw-loop/src/quality-gate.ts +11 -14
  92. package/packages/omo-codex/plugin/components/ulw-loop/src/spawn-guard.ts +3 -2
  93. package/packages/omo-codex/plugin/components/ulw-loop/src/surface.ts +73 -0
  94. package/packages/omo-codex/plugin/components/ulw-loop/test/checkpoint-final.test.ts +44 -1
  95. package/packages/omo-codex/plugin/components/ulw-loop/test/codex-goal-instruction.test.ts +25 -0
  96. package/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/quality-gate-builder.ts +7 -3
  97. package/packages/omo-codex/plugin/components/ulw-loop/test/quality-gate-roles.test.ts +45 -2
  98. package/packages/omo-codex/plugin/components/ulw-loop/test/spawn-guard.test.ts +20 -0
  99. package/packages/omo-codex/plugin/components/ulw-loop/test/surface.test.ts +74 -0
  100. package/packages/omo-codex/plugin/hooks/post-compact-resetting-git-bash-mcp-reminder.json +1 -1
  101. package/packages/omo-codex/plugin/hooks/post-compact-resetting-lsp-diagnostics-cache.json +1 -1
  102. package/packages/omo-codex/plugin/hooks/post-compact-resetting-project-rule-cache.json +1 -1
  103. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-codegraph-init-guidance.json +1 -1
  104. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-comments.json +1 -1
  105. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-lsp-diagnostics.json +1 -1
  106. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-thread-title-hygiene.json +1 -1
  107. package/packages/omo-codex/plugin/hooks/post-tool-use-matching-project-rules.json +1 -1
  108. package/packages/omo-codex/plugin/hooks/pre-tool-use-enforcing-unlimited-goal-budget.json +1 -1
  109. package/packages/omo-codex/plugin/hooks/pre-tool-use-guarding-ulw-loop-spawns.json +1 -1
  110. package/packages/omo-codex/plugin/hooks/pre-tool-use-recommending-git-bash-mcp.json +1 -1
  111. package/packages/omo-codex/plugin/hooks/session-start-checking-auto-update.json +1 -1
  112. package/packages/omo-codex/plugin/hooks/session-start-checking-bootstrap-provisioning.json +1 -1
  113. package/packages/omo-codex/plugin/hooks/session-start-checking-codegraph-bootstrap.json +1 -1
  114. package/packages/omo-codex/plugin/hooks/session-start-loading-project-rules.json +1 -1
  115. package/packages/omo-codex/plugin/hooks/session-start-recording-session-telemetry.json +1 -1
  116. package/packages/omo-codex/plugin/hooks/stop-checking-ulw-execute-continuation.json +1 -1
  117. package/packages/omo-codex/plugin/hooks/stop-checking-ulw-loop-resume.json +1 -1
  118. package/packages/omo-codex/plugin/hooks/subagent-stop-checking-ulw-execute-continuation.json +1 -1
  119. package/packages/omo-codex/plugin/hooks/subagent-stop-verifying-lazycodex-executor-evidence.json +1 -1
  120. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ultrawork-trigger.json +1 -1
  121. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ulw-loop-steering.json +1 -1
  122. package/packages/omo-codex/plugin/hooks/user-prompt-submit-loading-project-rules.json +1 -1
  123. package/packages/omo-codex/plugin/package-lock.json +13 -13
  124. package/packages/omo-codex/plugin/package.json +1 -1
  125. package/packages/omo-codex/scripts/install-dist/install-local.mjs +2 -2
  126. package/dist/hooks/ralph-loop/completion-promise-detector-test-input.d.ts +0 -11
  127. package/dist/hooks/ralph-loop/loop-session-recovery.d.ts +0 -7
  128. package/dist/tools/call-omo-agent/message-storage-directory.d.ts +0 -1
  129. package/dist/tools/delegate-task/model-string-parser.d.ts +0 -1
  130. package/packages/lsp-tools-mcp/dist/lsp/cleanup-errors.d.ts +0 -1
  131. package/packages/lsp-tools-mcp/dist/lsp/constants.d.ts +0 -1
  132. package/packages/lsp-tools-mcp/dist/lsp/language-mappings.d.ts +0 -1
  133. package/packages/lsp-tools-mcp/dist/lsp/process-signal-cleanup.d.ts +0 -1
  134. 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
- stopClientBestEffort(victim.managed.client);
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 stopClientBestEffort(managed.client);
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
- await stopClientBestEffort(managed.client);
2878
- this.clients.delete(key);
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 stopClientBestEffort(client);
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
- stopClientBestEffort(managed.client);
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
- stopClientBestEffort(client);
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
- await Promise.allSettled(stopPromises);
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 WORKSPACE_MARKERS = [".git", "package.json", "pyproject.toml", "Cargo.toml", "go.mod", "pom.xml", "build.gradle"];
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
- for (const marker of WORKSPACE_MARKERS) {
4877
- if (existsSync11(join5(dir, marker))) {
4878
- return canonicalDir;
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
- stopClientBestEffort(victim.managed.client);
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 stopClientBestEffort(managed.client);
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
- await stopClientBestEffort(managed.client);
2880
- this.clients.delete(key);
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 stopClientBestEffort(client);
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
- stopClientBestEffort(managed.client);
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
- stopClientBestEffort(client);
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
- await Promise.allSettled(stopPromises);
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 WORKSPACE_MARKERS = [".git", "package.json", "pyproject.toml", "Cargo.toml", "go.mod", "pom.xml", "build.gradle"];
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
- for (const marker of WORKSPACE_MARKERS) {
4229
- if (existsSync10(join5(dir, marker))) {
4230
- return canonicalDir;
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();