Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions packages/opencode/src/cli/cmd/tui/thread.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,13 +152,37 @@ export const TuiThreadCommand = cmd({
process.on("unhandledRejection", error)
process.on("SIGUSR2", reload)

// altimate_change start — crash: flush worker traces on signals
// Bun Workers don't receive OS signals — only the main thread does.
// On SIGINT/SIGTERM/SIGHUP, terminate the worker and briefly wait so
// its "exit" handler fires and flushes all active session traces to disk.
// Bun.sleepSync gives the worker thread time to process the termination
// before the main thread continues to process.exit().
const emergencyTerminate = () => {
try {
worker.terminate()
Bun.sleepSync(50)
} catch {
// best-effort — crash handler must never throw
}
}
process.on("SIGINT", emergencyTerminate)
process.on("SIGTERM", emergencyTerminate)
process.on("SIGHUP", emergencyTerminate)
// altimate_change end

let stopped = false
const stop = async () => {
if (stopped) return
stopped = true
process.off("uncaughtException", error)
process.off("unhandledRejection", error)
process.off("SIGUSR2", reload)
// altimate_change start — crash: remove emergency handlers on clean shutdown
process.off("SIGINT", emergencyTerminate)
process.off("SIGTERM", emergencyTerminate)
process.off("SIGHUP", emergencyTerminate)
// altimate_change end
await withTimeout(client.call("shutdown", undefined), 5000).catch((error) => {
Log.Default.warn("worker shutdown failed", {
error: error instanceof Error ? error.message : String(error),
Expand Down
29 changes: 29 additions & 0 deletions packages/opencode/src/cli/cmd/tui/worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@ process.on("uncaughtException", (e) => {
Log.Default.error("exception", {
e: e instanceof Error ? e.message : e,
})
// altimate_change start — crash: flush traces on uncaught exception
// After logging, write all active traces to disk so crash context is preserved.
// The process may continue or exit depending on the exception — either way the
// trace snapshot will reflect the crash.
flushAllTracesSync(`Uncaught exception: ${e instanceof Error ? e.message : String(e)}`)
// altimate_change end
})

// Subscribe to global events and forward them via RPC
Expand Down Expand Up @@ -325,6 +331,29 @@ export const rpc = {

Rpc.listen(rpc)

// altimate_change start — crash: flush active traces on unexpected exit
// When the worker is terminated (via worker.terminate() from the main thread,
// or on uncaught exceptions), write all in-flight traces to disk synchronously.
//
// NOTE: Bun Workers do NOT receive OS signals (SIGINT, SIGTERM, SIGHUP) —
// those are delivered only to the main thread. Signal-based flush is handled
// in thread.ts by terminating the worker, which triggers the "exit" event here.
let hasFlushed = false
function flushAllTracesSync(reason: string) {
if (hasFlushed) return
hasFlushed = true
for (const [, trace] of sessionTraces) {
try {
trace.flushSync(reason)
} catch {
// flushSync is best-effort — must never throw in an exit handler
}
}
}

process.once("exit", () => { flushAllTracesSync("Process exited") })
// altimate_change end

function getAuthorizationHeader(): string | undefined {
const password = Flag.OPENCODE_SERVER_PASSWORD
if (!password) return undefined
Expand Down
25 changes: 24 additions & 1 deletion packages/opencode/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ import { Database } from "./storage/db"
// altimate_change start - telemetry import
import { Telemetry } from "./telemetry"
// altimate_change end
// altimate_change start — crash: import Trace for crash handlers
import { Trace } from "./altimate/observability/tracing"
// altimate_change end
// altimate_change start - welcome banner
import { showWelcomeBannerIfNeeded } from "./cli/welcome"
// altimate_change end
Expand All @@ -60,12 +63,32 @@ process.on("uncaughtException", (e) => {
Log.Default.error("exception", {
e: e instanceof Error ? e.message : e,
})
// altimate_change start — crash: flush active trace on uncaught exception
// Trace.active is set by run.ts (headless mode only — TUI traces live in
// the worker's isolated memory and are flushed via worker.terminate()).
// This is a safety net for the headless path where run.ts registers its
// own handlers but an exception could bubble past them.
try {
Trace.active?.flushSync(`Uncaught exception: ${e instanceof Error ? e.message : String(e)}`)
} catch {
// Trace module may not be initialized — best-effort
}
// altimate_change end
})

// Ensure the process exits on terminal hangup (eg. closing the terminal tab).
// Without this, long-running commands like `serve` block on a never-resolving
// promise and survive as orphaned processes.
process.on("SIGHUP", () => process.exit())
// altimate_change start — crash: flush active trace before SIGHUP exit
process.on("SIGHUP", () => {
try {
Trace.active?.flushSync("Terminal hangup (SIGHUP)")
} catch {
// best-effort
}
process.exit()
})
// altimate_change end

let cli = yargs(hideBin(process.argv))
.parserConfiguration({ "populate--": true })
Expand Down
Loading