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
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
3370
|
-
this.clients.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
4227
|
-
|
|
4228
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
4025
|
-
this.clients.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
4882
|
-
|
|
4883
|
-
|
|
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();
|