Browse Source

Revert "fix: Explicitly exit CLI to prevent hanging subprocesses (#3083)"

This reverts commit a9624c0fffcab843e05817aac92ae1d28f571d0d.
Aiden Cline 4 months ago
parent
commit
1a6fd018f6

+ 1 - 2
packages/opencode/src/file/watcher.ts

@@ -63,8 +63,7 @@ export namespace FileWatcher {
       return { sub }
     },
     async (state) => {
-      if (!state.sub) return
-      await state.sub?.unsubscribe()
+      state.sub?.unsubscribe()
     },
   )
 

+ 4 - 6
packages/opencode/src/index.ts

@@ -22,6 +22,8 @@ import { AttachCommand } from "./cli/cmd/attach"
 import { AcpCommand } from "./cli/cmd/acp"
 import { EOL } from "os"
 
+const cancel = new AbortController()
+
 process.on("unhandledRejection", (e) => {
   Log.Default.error("rejection", {
     e: e instanceof Error ? e.message : e,
@@ -133,10 +135,6 @@ try {
     console.error(e)
   }
   process.exitCode = 1
-} finally {
-  // Some subprocesses don't react properly to SIGTERM and similar signals.
-  // Most notably, some docker-container-based MCP servers don't handle such signals unless
-  // run using `docker run --init`.
-  // Explicitly exit to avoid any hanging subprocesses.
-  process.exit();
 }
+
+cancel.abort()

+ 3 - 1
packages/opencode/src/lsp/index.ts

@@ -101,7 +101,9 @@ export namespace LSP {
       }
     },
     async (state) => {
-      await Promise.all(state.clients.map((client) => client.shutdown()))
+      for (const client of state.clients) {
+        await client.shutdown()
+      }
     },
   )
 

+ 3 - 1
packages/opencode/src/mcp/index.ts

@@ -145,7 +145,9 @@ export namespace MCP {
       }
     },
     async (state) => {
-      await Promise.all(Object.values(state.clients).map((client) => client.close()))
+      for (const client of Object.values(state.clients)) {
+        client.close()
+      }
     },
   )
 

+ 1 - 1
packages/opencode/src/project/bootstrap.ts

@@ -11,7 +11,7 @@ export async function InstanceBootstrap() {
   await Plugin.init()
   Share.init()
   Format.init()
-  await LSP.init()
+  LSP.init()
   FileWatcher.init()
   File.init()
 }

+ 9 - 41
packages/opencode/src/project/state.ts

@@ -1,26 +1,23 @@
-import { Log } from "@/util/log"
-
 export namespace State {
   interface Entry {
     state: any
     dispose?: (state: any) => Promise<void>
   }
 
-  const log = Log.create({ service: "state" })
-  const recordsByKey = new Map<string, Map<any, Entry>>()
+  const entries = new Map<string, Map<any, Entry>>()
 
   export function create<S>(root: () => string, init: () => S, dispose?: (state: Awaited<S>) => Promise<void>) {
     return () => {
       const key = root()
-      let entries = recordsByKey.get(key)
-      if (!entries) {
-        entries = new Map<string, Entry>()
-        recordsByKey.set(key, entries)
+      let collection = entries.get(key)
+      if (!collection) {
+        collection = new Map<string, Entry>()
+        entries.set(key, collection)
       }
-      const exists = entries.get(init)
+      const exists = collection.get(init)
       if (exists) return exists.state as S
       const state = init()
-      entries.set(init, {
+      collection.set(init, {
         state,
         dispose,
       })
@@ -29,38 +26,9 @@ export namespace State {
   }
 
   export async function dispose(key: string) {
-    const entries = recordsByKey.get(key)
-    if (!entries) return
-
-    log.info("waiting for state disposal to complete", { key })
-
-    let disposalFinished = false
-
-    setTimeout(() => {
-      if (!disposalFinished) {
-        log.warn(
-          "state disposal is taking an unusually long time - if it does not complete in a reasonable time, please report this as a bug",
-          { key },
-        )
-      }
-    }, 10000).unref()
-
-    const tasks: Promise<void>[] = []
-    for (const entry of entries.values()) {
+    for (const [_, entry] of entries.get(key)?.entries() ?? []) {
       if (!entry.dispose) continue
-
-      const task = Promise.resolve(entry.state)
-        .then((state) => entry.dispose!(state))
-        .catch((error) => {
-          log.error("Error while disposing state:", { error, key })
-        })
-
-      tasks.push(task)
+      await entry.dispose(await entry.state)
     }
-
-    await Promise.all(tasks)
-
-    disposalFinished = true
-    log.info("state disposal completed", { key })
   }
 }

+ 30 - 19
packages/opencode/src/session/prompt.ts

@@ -74,22 +74,13 @@ export namespace SessionPrompt {
           callback: (input: MessageV2.WithParts) => void
         }[]
       >()
-      const pending = new Set<Promise<void>>()
-
-      const track = (promise: Promise<void>) => {
-        pending.add(promise)
-        promise.finally(() => pending.delete(promise))
-      }
 
       return {
         queued,
-        pending,
-        track,
       }
     },
     async (current) => {
       current.queued.clear()
-      await Promise.allSettled([...current.pending])
     },
   )
 
@@ -200,6 +191,28 @@ export namespace SessionPrompt {
       processor,
     })
 
+    // const permUnsub = (() => {
+    //   const handled = new Set<string>()
+    //   const options = [
+    //     { optionId: "allow_once", kind: "allow_once", name: "Allow once" },
+    //     { optionId: "allow_always", kind: "allow_always", name: "Always allow" },
+    //     { optionId: "reject_once", kind: "reject_once", name: "Reject" },
+    //   ]
+    //   return Bus.subscribe(Permission.Event.Updated, async (event) => {
+    //     const info = event.properties
+    //     if (info.sessionID !== input.sessionID) return
+    //     if (handled.has(info.id)) return
+    //     handled.add(info.id)
+    //     const toolCallId = info.callID ?? info.id
+    //     const metadata = info.metadata ?? {}
+    //     // TODO: emit permission event to bus for ACP to handle
+    //     Permission.respond({ sessionID: info.sessionID, permissionID: info.id, response: "reject" })
+    //   })
+    // })()
+    // await using _permSub = defer(() => {
+    //   permUnsub?.()
+    // })
+
     const params = await Plugin.trigger(
       "chat.params",
       {
@@ -234,15 +247,13 @@ export namespace SessionPrompt {
       step++
       await processor.next(msgs.findLast((m) => m.info.role === "user")?.info.id!)
       if (step === 1) {
-        state().track(
-          ensureTitle({
-            session,
-            history: msgs,
-            message: userMsg,
-            providerID: model.providerID,
-            modelID: model.info.id,
-          }),
-        )
+        ensureTitle({
+          session,
+          history: msgs,
+          message: userMsg,
+          providerID: model.providerID,
+          modelID: model.info.id,
+        })
         SessionSummary.summarize({
           sessionID: input.sessionID,
           messageID: userMsg.info.id,
@@ -1719,7 +1730,7 @@ export namespace SessionPrompt {
         thinkingBudget: 0,
       }
     }
-    await generateText({
+    generateText({
       maxOutputTokens: small.info.reasoning ? 1500 : 20,
       providerOptions: ProviderTransform.providerOptions(small.npm, small.providerID, options),
       messages: [