Conversation
SIGUSR2 asks the TUI worker to reload config, which disposes every instance. Instance disposal cancels the session runners that instance owns, so a signal that lands while the model is streaming interrupts the run. Desktop environments send this signal on theme changes - Omarchy's omarchy-theme-set runs `killall -SIGUSR2 opencode` - so switching themes mid-run aborts the in-flight request. Theme refresh does not depend on the worker reload: the TUI re-detects the terminal palette and re-scans theme files from its own SIGUSR2 handler. Wait for every instance to have no busy session before invalidating config and disposing, and coalesce signals that arrive while waiting, so the reload is deferred rather than dropped.
Scope: config reload (SIGUSR2 from desktop theme changes, config writes) no longer disposes instances mid-run: the reload RPC now waits for all sessions across all loaded instances to go idle before invalidating config, and concurrent reload signals coalesce onto one in-flight promise.
|
|
Just as a workaround I place import type { Plugin } from "@opencode-ai/plugin"
import { createServer } from "http"
import { mkdirSync, writeFileSync, unlinkSync } from "fs"
const PORT_DIR = "/tmp/opencode-theme-port"
const portFile = () => `${PORT_DIR}/${process.pid}`
export const ThemeReloadOnIdle: Plugin = async () => {
const busy = new Set<string>()
let pending = false
try {
mkdirSync(PORT_DIR, { recursive: true })
} catch {}
const flush = () => {
if (!pending || busy.size > 0) return
pending = false
try {
process.kill(process.pid, "SIGUSR2")
} catch {}
}
const server = createServer((req, res) => {
if (req.method === "POST" && req.url === "/theme-changed") {
pending = true
flush()
res.statusCode = 204
res.end()
return
}
res.statusCode = 404
res.end()
})
server.listen(0, "127.0.0.1", () => {
const addr = server.address()
if (addr && typeof addr === "object") {
try {
writeFileSync(portFile(), String(addr.port))
} catch {}
}
})
return {
event: async ({ event }) => {
const e = event as { type: string; properties?: any }
if (e.type === "session.status") {
const { sessionID, status } = e.properties ?? {}
if (!sessionID || !status) return
if (status.type === "busy") busy.add(sessionID)
else if (status.type === "idle") busy.delete(sessionID)
} else if (e.type === "session.idle") {
const { sessionID } = e.properties ?? {}
if (sessionID) busy.delete(sessionID)
} else {
return
}
flush()
},
dispose: async () => {
busy.clear()
pending = false
server.close()
try {
unlinkSync(portFile())
} catch {}
},
}
}then have this in a bash hook running after color generation: # --- OPENCODE ---
for pid in $(pgrep -x .opencode); do
port=$(cat /tmp/opencode-theme-port/$pid 2>/dev/null) || continue
curl --max-time 1 -X POST http://127.0.0.1:$port/theme-changed || true
doneThe hook finds the stored ports opencode instances are listening in with a small bun server and when it is hit the plugin schedules a dispatch of SIGUSR2 to its own pid but delays until session becomes idle. Hope this hack helps while upstream reaches a built in solution :) |
|
Automated PR Cleanup Thank you for contributing to opencode. Due to the high volume of PRs from users and AI agents, we periodically close older PRs using automated criteria so maintainers can focus review time on the most active and community-supported contributions. This PR was closed because it matched the following cleanup criteria:
PRs created within the last month are not affected by this cleanup. If you believe this PR was closed incorrectly, or if you are still actively working on it, please leave a comment explaining why it should be reopened. A maintainer can review and reopen it if appropriate. Thanks again for taking the time to contribute. |
|
I've rebased this onto current dev and reopened it as #49162, with your commit kept as the first one. It adds a fix for instances that load while an earlier one is still booting (the idle check now re-snapshots the store until stable) and end-to-end tests for the reload path. If you'd rather carry it yourself, happy to close mine in favor of a reopen here. |
Issue for this PR
Closes #42621
Type of change
What does this PR do?
SIGUSR2 makes the TUI worker reload config, and that reload disposes every instance. Instance disposal cancels the session runners the instance owns, so a signal that lands mid-run kills whatever is in flight. Desktop environments send SIGUSR2 on theme changes (Omarchy runs
killall -SIGUSR2 opencode), so switching themes while opencode is working aborts the request.The reload now waits until no instance has a busy session before invalidating config and disposing, so the reload is deferred rather than dropped. Signals that arrive while waiting join the pending reload instead of stacking.
Themes still update immediately — that path never went through the worker. The TUI re-detects the terminal palette and re-scans theme files from its own SIGUSR2 handler.
InstanceStore.list()is new; the wait needs it to checkSessionStatusper instance.How did you verify your code works?
Manually, before and after, using a shell command so no model call is involved:
bun dev, then!sleep 45, thenkill -SIGUSR2 <tui pid>from another terminal.disposing all instances~5ms after the signal.disposing all instancesappears once the session goes idle — so the config reload still happens, just later.Tests:
test/server/global-lifecycle.test.ts(new):awaitSessionsIdlereturns while idle, and blocks until a busy session goes idle. Checked that it fails if the busy predicate is broken.test/project/instance.test.ts: coverage forInstanceStore.list().bun typecheckclean inpackages/opencode;bun test test/server/httpapi-global.test.ts test/project/passes.Checklist