From 036f7d378f6d6ee61de1ad3aa079ca45fbee0755 Mon Sep 17 00:00:00 2001 From: ranxianglei Date: Wed, 24 Jun 2026 09:36:19 +0800 Subject: [PATCH 1/3] fix(.github): enable blank issues in local Gitea config.yml had blank_issues_enabled: false (inherited from upstream opencode). Gitea reads this GitHub-compatible config and hides the 'Open a blank issue' option, so users could only pick from the 3 templates. Flip to true so plain issues are creatable. --- .github/ISSUE_TEMPLATE/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 52eec90991..459ce25d05 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,4 +1,4 @@ -blank_issues_enabled: false +blank_issues_enabled: true contact_links: - name: 💬 Discord Community url: https://discord.gg/opencode From 422f729a5689845a3fa922e0df962e99d18da059 Mon Sep 17 00:00:00 2001 From: ranxianglei Date: Wed, 24 Jun 2026 21:14:05 +0800 Subject: [PATCH 2/3] fix(run): wait for background/child sessions before exiting In opencode run mode, the event loop only watched the main session's idle status and exited as soon as it went idle. Background agents spawned via delegate_task(run_in_background=true) run in child sessions via prompt_async; when the main session finished the process exited, disposing the instance and cancelling all runners, which killed the still-running background agents. Track every session's busy/idle status in the event loop and gate execute()'s return on a 'done' promise that resolves only once the main session is idle AND no background/child sessions remain busy. Also resolve the promise if the SSE stream closes unexpectedly to avoid hanging. --- packages/opencode/src/cli/cmd/run.ts | 34 +++++++++++++++++++++++----- 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/packages/opencode/src/cli/cmd/run.ts b/packages/opencode/src/cli/cmd/run.ts index a05b273e44..fee9f379b4 100644 --- a/packages/opencode/src/cli/cmd/run.ts +++ b/packages/opencode/src/cli/cmd/run.ts @@ -441,8 +441,19 @@ export const RunCommand = effectCmd({ const events = await sdk.event.subscribe() let error: string | undefined + // Controls when execute() returns: resolves only after the main session is + // idle AND no background/child sessions are still running. Without this, + // background agents (e.g. delegate_task with run_in_background) get killed + // when the process exits right after the main session finishes. + let resolveDone: () => void + const done = new Promise((resolve) => (resolveDone = resolve)) + async function loop() { const toggles = new Map() + // Track every session that is busy/retry so execute() doesn't exit while + // background (child) sessions are still running. + const busy = new Set() + let mainIdle = false for await (const event of events.stream) { if ( @@ -533,12 +544,15 @@ export const RunCommand = effectCmd({ UI.error(err) } - if ( - event.type === "session.status" && - event.properties.sessionID === sessionID && - event.properties.status.type === "idle" - ) { - break + if (event.type === "session.status") { + const { sessionID: sid, status } = event.properties + if (status.type === "idle") { + busy.delete(sid) + if (sid === sessionID) mainIdle = true + if (mainIdle && busy.size === 0) resolveDone() + } else { + busy.add(sid) + } } if (event.type === "permission.asked") { @@ -563,6 +577,9 @@ export const RunCommand = effectCmd({ } } } + // Stream closed (e.g. server shutdown) before the exit condition was + // met — unblock execute() so it doesn't hang forever. + resolveDone() } // Validate agent if specified @@ -659,6 +676,11 @@ export const RunCommand = effectCmd({ parts: [...files, { type: "text", text: message }], }) } + + // prompt()/command() return once the main session is idle, but background + // (child) sessions may still be running. Wait for all of them to finish + // before returning so disposing the instance doesn't kill them. + await done } if (args.attach) { From 7f40c6ea5d620fd3d6b7c88ca5413612570bbedb Mon Sep 17 00:00:00 2001 From: ranxianglei Date: Wed, 24 Jun 2026 21:46:27 +0800 Subject: [PATCH 3/3] fix(run): force exit after all sessions idle Plugin background handles (omo-stable's unawaited prompt promises and task-manager timers) keep the Node.js event loop alive after every session is idle, causing the run command to hang indefinitely. Verified at runtime: done resolves correctly when all sessions idle (confirmed via debug instrumentation), execute() returns, but the process hangs on dangling plugin handles. Add an unreffed 2s fallback exit timer after await done. unref() means it never prevents natural exit (no plugin handles -> process exits immediately, timer is moot); with plugin handles it force-exits after 2s. Without background agents behavior is unchanged (process exits naturally before the timer fires). --- packages/opencode/src/cli/cmd/run.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/opencode/src/cli/cmd/run.ts b/packages/opencode/src/cli/cmd/run.ts index fee9f379b4..0d49aecc52 100644 --- a/packages/opencode/src/cli/cmd/run.ts +++ b/packages/opencode/src/cli/cmd/run.ts @@ -681,6 +681,13 @@ export const RunCommand = effectCmd({ // (child) sessions may still be running. Wait for all of them to finish // before returning so disposing the instance doesn't kill them. await done + // Plugin background handles (e.g. unawaited prompt promises, task-manager + // timers) can keep the event loop alive after every session is idle. The + // run command is one-shot, so once all sessions are done we exit + // deterministically. unref() ensures this timer never prevents a natural + // exit when no plugin handles are pending. + const fallbackExit = setTimeout(() => process.exit(0), 2000) + fallbackExit.unref?.() } if (args.attach) {