merge: integrate origin/dev into modular-enforcement branch

Resolves all merge conflicts, preserving our split module structure
while integrating all dev changes:
- Custom agent summaries support (parseRegisteredAgentSummaries)
- Background notification queue (enqueueNotificationForParent)
- Atlas shared git-worktree module (collectGitDiffStats, formatFileChanges)
- Ralph-loop withTimeout + DEFAULT_API_TIMEOUT=5000
- Session recovery assistant_prefill_unsupported error type
- Atlas agentOverrides forwarding
- Config handler plan model demotion (buildPlanDemoteConfig)
- Delegate-task agentOverrides, promptSyncWithModelSuggestionRetry, variant
- LSP init timeout + stale init detection
- isPlanFamily function + task-continuation-enforcer hook
- Handoff command
This commit is contained in:
YeonGyu-Kim
2026-02-08 17:34:47 +09:00
74 changed files with 4016 additions and 654 deletions
+83 -69
View File
@@ -1,4 +1,6 @@
import type { ResolvedServer } from "./types"
import { registerLspManagerProcessCleanup } from "./lsp-manager-process-cleanup"
import { cleanupTempDirectoryLspClients } from "./lsp-manager-temp-directory-cleanup"
import { LSPClient } from "./lsp-client"
interface ManagedClient {
client: LSPClient
@@ -6,52 +8,31 @@ interface ManagedClient {
refCount: number
initPromise?: Promise<void>
isInitializing: boolean
initializingSince?: number
}
class LSPServerManager {
private static instance: LSPServerManager
private clients = new Map<string, ManagedClient>()
private cleanupInterval: ReturnType<typeof setInterval> | null = null
private readonly IDLE_TIMEOUT = 5 * 60 * 1000
private readonly INIT_TIMEOUT = 60 * 1000
private constructor() {
this.startCleanupTimer()
this.registerProcessCleanup()
}
private registerProcessCleanup(): void {
// Synchronous cleanup for 'exit' event (cannot await)
const syncCleanup = () => {
for (const [, managed] of this.clients) {
try {
// Fire-and-forget during sync exit - process is terminating
void managed.client.stop().catch(() => {})
} catch {}
}
this.clients.clear()
if (this.cleanupInterval) {
clearInterval(this.cleanupInterval)
this.cleanupInterval = null
}
}
// Async cleanup for signal handlers - properly await all stops
const asyncCleanup = async () => {
const stopPromises: Promise<void>[] = []
for (const [, managed] of this.clients) {
stopPromises.push(managed.client.stop().catch(() => {}))
}
await Promise.allSettled(stopPromises)
this.clients.clear()
if (this.cleanupInterval) {
clearInterval(this.cleanupInterval)
this.cleanupInterval = null
}
}
process.on("exit", syncCleanup)
// Don't call process.exit() here; other handlers (background-agent manager) handle final exit.
process.on("SIGINT", () => void asyncCleanup().catch(() => {}))
process.on("SIGTERM", () => void asyncCleanup().catch(() => {}))
if (process.platform === "win32") {
process.on("SIGBREAK", () => void asyncCleanup().catch(() => {}))
}
registerLspManagerProcessCleanup({
getClients: () => this.clients.entries(),
clearClients: () => {
this.clients.clear()
},
clearCleanupInterval: () => {
if (this.cleanupInterval) {
clearInterval(this.cleanupInterval)
this.cleanupInterval = null
}
},
})
}
static getInstance(): LSPServerManager {
@@ -85,17 +66,46 @@ class LSPServerManager {
async getClient(root: string, server: ResolvedServer): Promise<LSPClient> {
const key = this.getKey(root, server.id)
let managed = this.clients.get(key)
if (managed) {
const now = Date.now()
if (
managed.isInitializing &&
managed.initializingSince !== undefined &&
now - managed.initializingSince >= this.INIT_TIMEOUT
) {
// Stale init can permanently block subsequent calls (e.g., LSP process hang)
try {
await managed.client.stop()
} catch {}
this.clients.delete(key)
managed = undefined
}
}
if (managed) {
if (managed.initPromise) {
await managed.initPromise
try {
await managed.initPromise
} catch {
// Failed init should not keep the key blocked forever.
try {
await managed.client.stop()
} catch {}
this.clients.delete(key)
managed = undefined
}
}
if (managed.client.isAlive()) {
managed.refCount++
managed.lastUsedAt = Date.now()
return managed.client
if (managed) {
if (managed.client.isAlive()) {
managed.refCount++
managed.lastUsedAt = Date.now()
return managed.client
}
try {
await managed.client.stop()
} catch {}
this.clients.delete(key)
}
await managed.client.stop()
this.clients.delete(key)
}
const client = new LSPClient(root, server)
@@ -103,19 +113,30 @@ class LSPServerManager {
await client.start()
await client.initialize()
})()
const initStartedAt = Date.now()
this.clients.set(key, {
client,
lastUsedAt: Date.now(),
lastUsedAt: initStartedAt,
refCount: 1,
initPromise,
isInitializing: true,
initializingSince: initStartedAt,
})
await initPromise
try {
await initPromise
} catch (error) {
this.clients.delete(key)
try {
await client.stop()
} catch {}
throw error
}
const m = this.clients.get(key)
if (m) {
m.initPromise = undefined
m.isInitializing = false
m.initializingSince = undefined
}
return client
@@ -130,21 +151,30 @@ class LSPServerManager {
await client.initialize()
})()
const initStartedAt = Date.now()
this.clients.set(key, {
client,
lastUsedAt: Date.now(),
lastUsedAt: initStartedAt,
refCount: 0,
initPromise,
isInitializing: true,
initializingSince: initStartedAt,
})
initPromise.then(() => {
const m = this.clients.get(key)
if (m) {
m.initPromise = undefined
m.isInitializing = false
}
})
initPromise
.then(() => {
const m = this.clients.get(key)
if (m) {
m.initPromise = undefined
m.isInitializing = false
m.initializingSince = undefined
}
})
.catch(() => {
// Warmup failures must not permanently block future initialization.
this.clients.delete(key)
void client.stop().catch(() => {})
})
}
releaseClient(root: string, serverId: string): void {
@@ -174,23 +204,7 @@ class LSPServerManager {
}
async cleanupTempDirectoryClients(): Promise<void> {
const keysToRemove: string[] = []
for (const [key, managed] of this.clients.entries()) {
const isTempDir = key.startsWith("/tmp/") || key.startsWith("/var/folders/")
const isIdle = managed.refCount === 0
if (isTempDir && isIdle) {
keysToRemove.push(key)
}
}
for (const key of keysToRemove) {
const managed = this.clients.get(key)
if (managed) {
this.clients.delete(key)
try {
await managed.client.stop()
} catch {}
}
}
await cleanupTempDirectoryLspClients(this.clients)
}
}