diff --git a/packages/opencode/src/file/watcher.ts b/packages/opencode/src/file/watcher.ts index d5985b58..7d190c60 100644 --- a/packages/opencode/src/file/watcher.ts +++ b/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() }, ) diff --git a/packages/opencode/src/index.ts b/packages/opencode/src/index.ts index 45ccd3ca..a0cce76a 100644 --- a/packages/opencode/src/index.ts +++ b/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() diff --git a/packages/opencode/src/lsp/index.ts b/packages/opencode/src/lsp/index.ts index 72a9cae2..d533815f 100644 --- a/packages/opencode/src/lsp/index.ts +++ b/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() + } }, ) diff --git a/packages/opencode/src/mcp/index.ts b/packages/opencode/src/mcp/index.ts index b0e72b53..fa3513bb 100644 --- a/packages/opencode/src/mcp/index.ts +++ b/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() + } }, ) diff --git a/packages/opencode/src/project/bootstrap.ts b/packages/opencode/src/project/bootstrap.ts index 45e85fd2..c7805aa7 100644 --- a/packages/opencode/src/project/bootstrap.ts +++ b/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() } diff --git a/packages/opencode/src/project/state.ts b/packages/opencode/src/project/state.ts index 6377833e..2ffef3b3 100644 --- a/packages/opencode/src/project/state.ts +++ b/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 } - const log = Log.create({ service: "state" }) - const recordsByKey = new Map>() + const entries = new Map>() export function create(root: () => string, init: () => S, dispose?: (state: Awaited) => Promise) { return () => { const key = root() - let entries = recordsByKey.get(key) - if (!entries) { - entries = new Map() - recordsByKey.set(key, entries) + let collection = entries.get(key) + if (!collection) { + collection = new Map() + 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[] = [] - 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 }) } } diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 184f4af8..7018978e 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -74,22 +74,13 @@ export namespace SessionPrompt { callback: (input: MessageV2.WithParts) => void }[] >() - const pending = new Set>() - - const track = (promise: Promise) => { - 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() + // 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: [