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
@@ -184,6 +184,7 @@ async function runJsonRpcStdioServer(config) {
184
184
  const idleTimer = createIdleTimer(idleTimeoutMs, log, () => {
185
185
  isClosed = true;
186
186
  config.onIdleTimeout?.();
187
+ config.input.destroy();
187
188
  });
188
189
  const watchdog = createParentWatchdog(config.parentWatchdog, (parentPid, pollIntervalMs) => {
189
190
  isClosed = true;
@@ -563,6 +564,16 @@ class LspProcessSpawnError extends Error {
563
564
  this.name = "LspProcessSpawnError";
564
565
  }
565
566
  }
567
+
568
+ class LspClientRespawnBudgetExceededError extends Error {
569
+ constructor(serverId, root, retryLimit) {
570
+ super(`LSP server ${serverId} at ${root} failed to stay alive; respawn budget exhausted ` + `(${retryLimit} consecutive dead generations). Retrying after the cooldown may succeed.`);
571
+ this.serverId = serverId;
572
+ this.root = root;
573
+ this.retryLimit = retryLimit;
574
+ this.name = "LspClientRespawnBudgetExceededError";
575
+ }
576
+ }
566
577
  function isLspDeadConnectionError(err) {
567
578
  return err instanceof LspConnectionClosedError || err instanceof LspProcessExitedError;
568
579
  }
@@ -596,6 +607,8 @@ var MAX_RESIDENT_CLIENTS = 6;
596
607
  var REAPER_INTERVAL_MS = 60000;
597
608
  var STOP_HARD_KILL_TIMEOUT_MS = 5000;
598
609
  var STOP_SIGKILL_GRACE_MS = 1000;
610
+ var CLIENT_RESPAWN_RETRY_LIMIT = 2;
611
+ var CLIENT_RESPAWN_COOLDOWN_MS = 60000;
599
612
 
600
613
  // ../lsp-core/src/lsp/json-rpc-connection.ts
601
614
  var HEADER_SEPARATOR2 = `\r
@@ -3269,6 +3282,8 @@ function awaitWithSignal(promise, signal) {
3269
3282
  class LspManager {
3270
3283
  constructor(options = {}) {
3271
3284
  this.clients = new Map;
3285
+ this.pendingStops = new Map;
3286
+ this.respawnBudgets = new Map;
3272
3287
  this.reaperHandle = null;
3273
3288
  this.signalDisposer = null;
3274
3289
  this.disposed = false;
@@ -3294,17 +3309,52 @@ class LspManager {
3294
3309
  getKey(root, serverId) {
3295
3310
  return `${root}::${serverId}`;
3296
3311
  }
3312
+ tombstoneStop(key, client) {
3313
+ const stop = stopClientBestEffort(client);
3314
+ this.pendingStops.set(key, stop);
3315
+ stop.finally(() => {
3316
+ if (this.pendingStops.get(key) === stop) {
3317
+ this.pendingStops.delete(key);
3318
+ }
3319
+ });
3320
+ return stop;
3321
+ }
3322
+ recordDeadGeneration(key) {
3323
+ let budget = this.respawnBudgets.get(key);
3324
+ if (budget === undefined) {
3325
+ budget = { deadGenerations: 0, exhaustedAt: null };
3326
+ this.respawnBudgets.set(key, budget);
3327
+ }
3328
+ budget.deadGenerations += 1;
3329
+ }
3330
+ markHealthyGeneration(key) {
3331
+ this.respawnBudgets.delete(key);
3332
+ }
3333
+ ensureRespawnAllowed(key, root, serverId) {
3334
+ const budget = this.respawnBudgets.get(key);
3335
+ if (budget === undefined || budget.deadGenerations < CLIENT_RESPAWN_RETRY_LIMIT)
3336
+ return;
3337
+ if (budget.exhaustedAt === null) {
3338
+ budget.exhaustedAt = this.now();
3339
+ throw new LspClientRespawnBudgetExceededError(serverId, root, CLIENT_RESPAWN_RETRY_LIMIT);
3340
+ }
3341
+ if (this.now() - budget.exhaustedAt < CLIENT_RESPAWN_COOLDOWN_MS) {
3342
+ throw new LspClientRespawnBudgetExceededError(serverId, root, CLIENT_RESPAWN_RETRY_LIMIT);
3343
+ }
3344
+ budget.deadGenerations = 0;
3345
+ budget.exhaustedAt = null;
3346
+ }
3297
3347
  reapStale() {
3298
3348
  const t = this.now();
3299
3349
  for (const [key, managed] of this.clients) {
3300
3350
  if (managed.isInitializing && managed.initializingSince !== null && t - managed.initializingSince > this.initTimeoutMs) {
3301
- stopClientBestEffort(managed.client);
3302
3351
  this.clients.delete(key);
3352
+ this.tombstoneStop(key, managed.client);
3303
3353
  continue;
3304
3354
  }
3305
3355
  if (!managed.isInitializing && managed.refCount === 0 && managed.pendingWaiters === 0 && t - managed.lastUsedAt > this.idleTimeoutMs) {
3306
- stopClientBestEffort(managed.client);
3307
3356
  this.clients.delete(key);
3357
+ this.tombstoneStop(key, managed.client);
3308
3358
  }
3309
3359
  }
3310
3360
  }
@@ -3314,7 +3364,7 @@ class LspManager {
3314
3364
  if (!victim)
3315
3365
  return;
3316
3366
  this.clients.delete(victim.key);
3317
- stopClientBestEffort(victim.managed.client);
3367
+ this.tombstoneStop(victim.key, victim.managed.client);
3318
3368
  }
3319
3369
  }
3320
3370
  leastRecentlyUsedIdleClient() {
@@ -3331,7 +3381,7 @@ class LspManager {
3331
3381
  async tryDeleteIfOrphaned(key, managed) {
3332
3382
  if (managed.refCount === 0 && managed.pendingWaiters === 0 && !managed.isInitializing && this.clients.get(key) === managed) {
3333
3383
  this.clients.delete(key);
3334
- await stopClientBestEffort(managed.client);
3384
+ await this.tombstoneStop(key, managed.client);
3335
3385
  }
3336
3386
  }
3337
3387
  async getClient(root, server2, signal) {
@@ -3340,12 +3390,22 @@ class LspManager {
3340
3390
  }
3341
3391
  signal?.throwIfAborted();
3342
3392
  const key = this.getKey(root, server2.id);
3393
+ for (;; ) {
3394
+ const pendingStop = this.pendingStops.get(key);
3395
+ if (pendingStop === undefined)
3396
+ break;
3397
+ await awaitWithSignal(pendingStop, signal);
3398
+ signal?.throwIfAborted();
3399
+ }
3400
+ if (this.disposed) {
3401
+ throw new Error("LspManager has been disposed");
3402
+ }
3343
3403
  let managed = this.clients.get(key);
3344
3404
  if (managed) {
3345
3405
  const t = this.now();
3346
3406
  if (managed.isInitializing && managed.initializingSince !== null && t - managed.initializingSince > this.initTimeoutMs) {
3347
- await stopClientBestEffort(managed.client);
3348
3407
  this.clients.delete(key);
3408
+ await this.tombstoneStop(key, managed.client);
3349
3409
  managed = undefined;
3350
3410
  }
3351
3411
  }
@@ -3366,15 +3426,20 @@ class LspManager {
3366
3426
  signal.throwIfAborted();
3367
3427
  }
3368
3428
  if (!managed.client.isAlive()) {
3369
- await stopClientBestEffort(managed.client);
3370
- this.clients.delete(key);
3429
+ this.recordDeadGeneration(key);
3430
+ if (this.clients.get(key) === managed) {
3431
+ this.clients.delete(key);
3432
+ }
3433
+ await this.tombstoneStop(key, managed.client);
3371
3434
  return this.getClient(root, server2, signal);
3372
3435
  }
3436
+ this.markHealthyGeneration(key);
3373
3437
  managed.refCount++;
3374
3438
  managed.lastUsedAt = this.now();
3375
3439
  return managed.client;
3376
3440
  }
3377
3441
  this.evictForAdmission();
3442
+ this.ensureRespawnAllowed(key, root, server2.id);
3378
3443
  const client = this.clientFactory(root, server2);
3379
3444
  const initStartedAt = this.now();
3380
3445
  const initPromise = (async () => {
@@ -3398,7 +3463,7 @@ class LspManager {
3398
3463
  if (this.clients.get(key) === newManaged) {
3399
3464
  this.clients.delete(key);
3400
3465
  }
3401
- await stopClientBestEffort(client);
3466
+ await this.tombstoneStop(key, client);
3402
3467
  throw err;
3403
3468
  }
3404
3469
  newManaged.pendingWaiters--;
@@ -3409,6 +3474,15 @@ class LspManager {
3409
3474
  await this.tryDeleteIfOrphaned(key, newManaged);
3410
3475
  signal.throwIfAborted();
3411
3476
  }
3477
+ if (!client.isAlive()) {
3478
+ this.recordDeadGeneration(key);
3479
+ if (this.clients.get(key) === newManaged) {
3480
+ this.clients.delete(key);
3481
+ }
3482
+ await this.tombstoneStop(key, client);
3483
+ return this.getClient(root, server2, signal);
3484
+ }
3485
+ this.markHealthyGeneration(key);
3412
3486
  newManaged.refCount++;
3413
3487
  newManaged.lastUsedAt = this.now();
3414
3488
  return client;
@@ -3429,15 +3503,20 @@ class LspManager {
3429
3503
  if (client && managed.client !== client)
3430
3504
  return;
3431
3505
  this.clients.delete(key);
3432
- stopClientBestEffort(managed.client);
3506
+ this.tombstoneStop(key, managed.client);
3433
3507
  }
3434
3508
  warmupClient(root, server2) {
3435
3509
  if (this.disposed)
3436
3510
  return;
3437
3511
  const key = this.getKey(root, server2.id);
3438
- if (this.clients.has(key))
3512
+ if (this.clients.has(key) || this.pendingStops.has(key))
3439
3513
  return;
3440
3514
  this.evictForAdmission();
3515
+ try {
3516
+ this.ensureRespawnAllowed(key, root, server2.id);
3517
+ } catch {
3518
+ return;
3519
+ }
3441
3520
  const client = this.clientFactory(root, server2);
3442
3521
  const initStartedAt = this.now();
3443
3522
  const initPromise = (async () => {
@@ -3463,7 +3542,7 @@ class LspManager {
3463
3542
  if (this.clients.get(key) === managed) {
3464
3543
  this.clients.delete(key);
3465
3544
  }
3466
- stopClientBestEffort(client);
3545
+ this.tombstoneStop(key, client);
3467
3546
  });
3468
3547
  }
3469
3548
  isServerInitializing(root, serverId) {
@@ -3507,8 +3586,11 @@ class LspManager {
3507
3586
  for (const managed of this.clients.values()) {
3508
3587
  stopPromises.push(stopClientBestEffort(managed.client));
3509
3588
  }
3589
+ const tombstonedStops = [...this.pendingStops.values()];
3510
3590
  this.clients.clear();
3511
- await Promise.allSettled(stopPromises);
3591
+ this.respawnBudgets.clear();
3592
+ await Promise.allSettled([...stopPromises, ...tombstonedStops]);
3593
+ this.pendingStops.clear();
3512
3594
  }
3513
3595
  }
3514
3596
  var _defaultInstance = null;
@@ -3531,7 +3613,16 @@ import { existsSync as existsSync6, realpathSync as realpathSync4 } from "node:f
3531
3613
  import { dirname as dirname5, join as join3 } from "node:path";
3532
3614
 
3533
3615
  // ../lsp-core/src/lsp/workspace-markers.ts
3534
- var WORKSPACE_MARKERS = [".git", "package.json", "pyproject.toml", "Cargo.toml", "go.mod", "pom.xml", "build.gradle"];
3616
+ var GIT_WORKSPACE_MARKER = ".git";
3617
+ var PROJECT_WORKSPACE_MARKERS = [
3618
+ "package.json",
3619
+ "pyproject.toml",
3620
+ "Cargo.toml",
3621
+ "go.mod",
3622
+ "pom.xml",
3623
+ "build.gradle"
3624
+ ];
3625
+ var WORKSPACE_MARKERS = [GIT_WORKSPACE_MARKER, ...PROJECT_WORKSPACE_MARKERS];
3535
3626
 
3536
3627
  // ../lsp-core/src/lsp/outside-context-workspace.ts
3537
3628
  function findWorkspaceRootOutsideContext(directory) {
@@ -4220,20 +4311,25 @@ function findWorkspaceRoot(filePath) {
4220
4311
  if (!isPathInside(cwd, abs))
4221
4312
  return findWorkspaceRootOutsideContext(dir);
4222
4313
  const fallbackRoot = nearestExistingDirectoryInsideContext(dir, cwd) ?? cwd;
4314
+ let nearestPackageRoot;
4223
4315
  while (isPathInside(cwd, dir)) {
4224
4316
  const canonicalDir = existingDirectoryInsideContext(dir, cwd);
4225
4317
  if (canonicalDir !== undefined) {
4226
- for (const marker of WORKSPACE_MARKERS) {
4227
- if (existsSync10(join5(dir, marker))) {
4228
- return canonicalDir;
4229
- }
4318
+ if (existsSync10(join5(dir, GIT_WORKSPACE_MARKER))) {
4319
+ return canonicalDir;
4320
+ }
4321
+ if (nearestPackageRoot === undefined && hasProjectWorkspaceMarker(dir)) {
4322
+ nearestPackageRoot = canonicalDir;
4230
4323
  }
4231
4324
  }
4232
4325
  if (dir === cwd)
4233
4326
  break;
4234
4327
  dir = dirname8(dir);
4235
4328
  }
4236
- return fallbackRoot;
4329
+ return nearestPackageRoot ?? fallbackRoot;
4330
+ }
4331
+ function hasProjectWorkspaceMarker(directory) {
4332
+ return PROJECT_WORKSPACE_MARKERS.some((marker) => existsSync10(join5(directory, marker)));
4237
4333
  }
4238
4334
  function resolveReadablePathInsideContext(filePath) {
4239
4335
  const cwd = contextCwd();
@@ -1007,6 +1007,7 @@ async function runJsonRpcStdioServer(config) {
1007
1007
  const idleTimer = createIdleTimer(idleTimeoutMs, log, () => {
1008
1008
  isClosed = true;
1009
1009
  config.onIdleTimeout?.();
1010
+ config.input.destroy();
1010
1011
  });
1011
1012
  const watchdog = createParentWatchdog(config.parentWatchdog, (parentPid, pollIntervalMs) => {
1012
1013
  isClosed = true;
@@ -1218,6 +1219,16 @@ class LspProcessSpawnError extends Error {
1218
1219
  this.name = "LspProcessSpawnError";
1219
1220
  }
1220
1221
  }
1222
+
1223
+ class LspClientRespawnBudgetExceededError extends Error {
1224
+ constructor(serverId, root, retryLimit) {
1225
+ super(`LSP server ${serverId} at ${root} failed to stay alive; respawn budget exhausted ` + `(${retryLimit} consecutive dead generations). Retrying after the cooldown may succeed.`);
1226
+ this.serverId = serverId;
1227
+ this.root = root;
1228
+ this.retryLimit = retryLimit;
1229
+ this.name = "LspClientRespawnBudgetExceededError";
1230
+ }
1231
+ }
1221
1232
  function isLspDeadConnectionError(err) {
1222
1233
  return err instanceof LspConnectionClosedError || err instanceof LspProcessExitedError;
1223
1234
  }
@@ -1251,6 +1262,8 @@ var MAX_RESIDENT_CLIENTS = 6;
1251
1262
  var REAPER_INTERVAL_MS = 60000;
1252
1263
  var STOP_HARD_KILL_TIMEOUT_MS = 5000;
1253
1264
  var STOP_SIGKILL_GRACE_MS = 1000;
1265
+ var CLIENT_RESPAWN_RETRY_LIMIT = 2;
1266
+ var CLIENT_RESPAWN_COOLDOWN_MS = 60000;
1254
1267
 
1255
1268
  // ../lsp-core/src/lsp/json-rpc-connection.ts
1256
1269
  var HEADER_SEPARATOR2 = `\r
@@ -3924,6 +3937,8 @@ function awaitWithSignal2(promise, signal) {
3924
3937
  class LspManager {
3925
3938
  constructor(options = {}) {
3926
3939
  this.clients = new Map;
3940
+ this.pendingStops = new Map;
3941
+ this.respawnBudgets = new Map;
3927
3942
  this.reaperHandle = null;
3928
3943
  this.signalDisposer = null;
3929
3944
  this.disposed = false;
@@ -3949,17 +3964,52 @@ class LspManager {
3949
3964
  getKey(root, serverId) {
3950
3965
  return `${root}::${serverId}`;
3951
3966
  }
3967
+ tombstoneStop(key, client) {
3968
+ const stop = stopClientBestEffort(client);
3969
+ this.pendingStops.set(key, stop);
3970
+ stop.finally(() => {
3971
+ if (this.pendingStops.get(key) === stop) {
3972
+ this.pendingStops.delete(key);
3973
+ }
3974
+ });
3975
+ return stop;
3976
+ }
3977
+ recordDeadGeneration(key) {
3978
+ let budget = this.respawnBudgets.get(key);
3979
+ if (budget === undefined) {
3980
+ budget = { deadGenerations: 0, exhaustedAt: null };
3981
+ this.respawnBudgets.set(key, budget);
3982
+ }
3983
+ budget.deadGenerations += 1;
3984
+ }
3985
+ markHealthyGeneration(key) {
3986
+ this.respawnBudgets.delete(key);
3987
+ }
3988
+ ensureRespawnAllowed(key, root, serverId) {
3989
+ const budget = this.respawnBudgets.get(key);
3990
+ if (budget === undefined || budget.deadGenerations < CLIENT_RESPAWN_RETRY_LIMIT)
3991
+ return;
3992
+ if (budget.exhaustedAt === null) {
3993
+ budget.exhaustedAt = this.now();
3994
+ throw new LspClientRespawnBudgetExceededError(serverId, root, CLIENT_RESPAWN_RETRY_LIMIT);
3995
+ }
3996
+ if (this.now() - budget.exhaustedAt < CLIENT_RESPAWN_COOLDOWN_MS) {
3997
+ throw new LspClientRespawnBudgetExceededError(serverId, root, CLIENT_RESPAWN_RETRY_LIMIT);
3998
+ }
3999
+ budget.deadGenerations = 0;
4000
+ budget.exhaustedAt = null;
4001
+ }
3952
4002
  reapStale() {
3953
4003
  const t = this.now();
3954
4004
  for (const [key, managed] of this.clients) {
3955
4005
  if (managed.isInitializing && managed.initializingSince !== null && t - managed.initializingSince > this.initTimeoutMs) {
3956
- stopClientBestEffort(managed.client);
3957
4006
  this.clients.delete(key);
4007
+ this.tombstoneStop(key, managed.client);
3958
4008
  continue;
3959
4009
  }
3960
4010
  if (!managed.isInitializing && managed.refCount === 0 && managed.pendingWaiters === 0 && t - managed.lastUsedAt > this.idleTimeoutMs) {
3961
- stopClientBestEffort(managed.client);
3962
4011
  this.clients.delete(key);
4012
+ this.tombstoneStop(key, managed.client);
3963
4013
  }
3964
4014
  }
3965
4015
  }
@@ -3969,7 +4019,7 @@ class LspManager {
3969
4019
  if (!victim)
3970
4020
  return;
3971
4021
  this.clients.delete(victim.key);
3972
- stopClientBestEffort(victim.managed.client);
4022
+ this.tombstoneStop(victim.key, victim.managed.client);
3973
4023
  }
3974
4024
  }
3975
4025
  leastRecentlyUsedIdleClient() {
@@ -3986,7 +4036,7 @@ class LspManager {
3986
4036
  async tryDeleteIfOrphaned(key, managed) {
3987
4037
  if (managed.refCount === 0 && managed.pendingWaiters === 0 && !managed.isInitializing && this.clients.get(key) === managed) {
3988
4038
  this.clients.delete(key);
3989
- await stopClientBestEffort(managed.client);
4039
+ await this.tombstoneStop(key, managed.client);
3990
4040
  }
3991
4041
  }
3992
4042
  async getClient(root, server2, signal) {
@@ -3995,12 +4045,22 @@ class LspManager {
3995
4045
  }
3996
4046
  signal?.throwIfAborted();
3997
4047
  const key = this.getKey(root, server2.id);
4048
+ for (;; ) {
4049
+ const pendingStop = this.pendingStops.get(key);
4050
+ if (pendingStop === undefined)
4051
+ break;
4052
+ await awaitWithSignal2(pendingStop, signal);
4053
+ signal?.throwIfAborted();
4054
+ }
4055
+ if (this.disposed) {
4056
+ throw new Error("LspManager has been disposed");
4057
+ }
3998
4058
  let managed = this.clients.get(key);
3999
4059
  if (managed) {
4000
4060
  const t = this.now();
4001
4061
  if (managed.isInitializing && managed.initializingSince !== null && t - managed.initializingSince > this.initTimeoutMs) {
4002
- await stopClientBestEffort(managed.client);
4003
4062
  this.clients.delete(key);
4063
+ await this.tombstoneStop(key, managed.client);
4004
4064
  managed = undefined;
4005
4065
  }
4006
4066
  }
@@ -4021,15 +4081,20 @@ class LspManager {
4021
4081
  signal.throwIfAborted();
4022
4082
  }
4023
4083
  if (!managed.client.isAlive()) {
4024
- await stopClientBestEffort(managed.client);
4025
- this.clients.delete(key);
4084
+ this.recordDeadGeneration(key);
4085
+ if (this.clients.get(key) === managed) {
4086
+ this.clients.delete(key);
4087
+ }
4088
+ await this.tombstoneStop(key, managed.client);
4026
4089
  return this.getClient(root, server2, signal);
4027
4090
  }
4091
+ this.markHealthyGeneration(key);
4028
4092
  managed.refCount++;
4029
4093
  managed.lastUsedAt = this.now();
4030
4094
  return managed.client;
4031
4095
  }
4032
4096
  this.evictForAdmission();
4097
+ this.ensureRespawnAllowed(key, root, server2.id);
4033
4098
  const client = this.clientFactory(root, server2);
4034
4099
  const initStartedAt = this.now();
4035
4100
  const initPromise = (async () => {
@@ -4053,7 +4118,7 @@ class LspManager {
4053
4118
  if (this.clients.get(key) === newManaged) {
4054
4119
  this.clients.delete(key);
4055
4120
  }
4056
- await stopClientBestEffort(client);
4121
+ await this.tombstoneStop(key, client);
4057
4122
  throw err;
4058
4123
  }
4059
4124
  newManaged.pendingWaiters--;
@@ -4064,6 +4129,15 @@ class LspManager {
4064
4129
  await this.tryDeleteIfOrphaned(key, newManaged);
4065
4130
  signal.throwIfAborted();
4066
4131
  }
4132
+ if (!client.isAlive()) {
4133
+ this.recordDeadGeneration(key);
4134
+ if (this.clients.get(key) === newManaged) {
4135
+ this.clients.delete(key);
4136
+ }
4137
+ await this.tombstoneStop(key, client);
4138
+ return this.getClient(root, server2, signal);
4139
+ }
4140
+ this.markHealthyGeneration(key);
4067
4141
  newManaged.refCount++;
4068
4142
  newManaged.lastUsedAt = this.now();
4069
4143
  return client;
@@ -4084,15 +4158,20 @@ class LspManager {
4084
4158
  if (client && managed.client !== client)
4085
4159
  return;
4086
4160
  this.clients.delete(key);
4087
- stopClientBestEffort(managed.client);
4161
+ this.tombstoneStop(key, managed.client);
4088
4162
  }
4089
4163
  warmupClient(root, server2) {
4090
4164
  if (this.disposed)
4091
4165
  return;
4092
4166
  const key = this.getKey(root, server2.id);
4093
- if (this.clients.has(key))
4167
+ if (this.clients.has(key) || this.pendingStops.has(key))
4094
4168
  return;
4095
4169
  this.evictForAdmission();
4170
+ try {
4171
+ this.ensureRespawnAllowed(key, root, server2.id);
4172
+ } catch {
4173
+ return;
4174
+ }
4096
4175
  const client = this.clientFactory(root, server2);
4097
4176
  const initStartedAt = this.now();
4098
4177
  const initPromise = (async () => {
@@ -4118,7 +4197,7 @@ class LspManager {
4118
4197
  if (this.clients.get(key) === managed) {
4119
4198
  this.clients.delete(key);
4120
4199
  }
4121
- stopClientBestEffort(client);
4200
+ this.tombstoneStop(key, client);
4122
4201
  });
4123
4202
  }
4124
4203
  isServerInitializing(root, serverId) {
@@ -4162,8 +4241,11 @@ class LspManager {
4162
4241
  for (const managed of this.clients.values()) {
4163
4242
  stopPromises.push(stopClientBestEffort(managed.client));
4164
4243
  }
4244
+ const tombstonedStops = [...this.pendingStops.values()];
4165
4245
  this.clients.clear();
4166
- await Promise.allSettled(stopPromises);
4246
+ this.respawnBudgets.clear();
4247
+ await Promise.allSettled([...stopPromises, ...tombstonedStops]);
4248
+ this.pendingStops.clear();
4167
4249
  }
4168
4250
  }
4169
4251
  var _defaultInstance = null;
@@ -4186,7 +4268,16 @@ import { existsSync as existsSync7, realpathSync as realpathSync4 } from "node:f
4186
4268
  import { dirname as dirname7, join as join3 } from "node:path";
4187
4269
 
4188
4270
  // ../lsp-core/src/lsp/workspace-markers.ts
4189
- var WORKSPACE_MARKERS = [".git", "package.json", "pyproject.toml", "Cargo.toml", "go.mod", "pom.xml", "build.gradle"];
4271
+ var GIT_WORKSPACE_MARKER = ".git";
4272
+ var PROJECT_WORKSPACE_MARKERS = [
4273
+ "package.json",
4274
+ "pyproject.toml",
4275
+ "Cargo.toml",
4276
+ "go.mod",
4277
+ "pom.xml",
4278
+ "build.gradle"
4279
+ ];
4280
+ var WORKSPACE_MARKERS = [GIT_WORKSPACE_MARKER, ...PROJECT_WORKSPACE_MARKERS];
4190
4281
 
4191
4282
  // ../lsp-core/src/lsp/outside-context-workspace.ts
4192
4283
  function findWorkspaceRootOutsideContext(directory) {
@@ -4875,20 +4966,25 @@ function findWorkspaceRoot(filePath) {
4875
4966
  if (!isPathInside(cwd, abs))
4876
4967
  return findWorkspaceRootOutsideContext(dir);
4877
4968
  const fallbackRoot = nearestExistingDirectoryInsideContext(dir, cwd) ?? cwd;
4969
+ let nearestPackageRoot;
4878
4970
  while (isPathInside(cwd, dir)) {
4879
4971
  const canonicalDir = existingDirectoryInsideContext(dir, cwd);
4880
4972
  if (canonicalDir !== undefined) {
4881
- for (const marker of WORKSPACE_MARKERS) {
4882
- if (existsSync11(join5(dir, marker))) {
4883
- return canonicalDir;
4884
- }
4973
+ if (existsSync11(join5(dir, GIT_WORKSPACE_MARKER))) {
4974
+ return canonicalDir;
4975
+ }
4976
+ if (nearestPackageRoot === undefined && hasProjectWorkspaceMarker(dir)) {
4977
+ nearestPackageRoot = canonicalDir;
4885
4978
  }
4886
4979
  }
4887
4980
  if (dir === cwd)
4888
4981
  break;
4889
4982
  dir = dirname10(dir);
4890
4983
  }
4891
- return fallbackRoot;
4984
+ return nearestPackageRoot ?? fallbackRoot;
4985
+ }
4986
+ function hasProjectWorkspaceMarker(directory) {
4987
+ return PROJECT_WORKSPACE_MARKERS.some((marker) => existsSync11(join5(directory, marker)));
4892
4988
  }
4893
4989
  function resolveReadablePathInsideContext(filePath) {
4894
4990
  const cwd = contextCwd();