Skip to content

Commit 63e7373

Browse files
EBrownclaude
andcommitted
feat: port remaining Tier 1+2 upstream fixes and features
Tier 1 bug fixes: - Fix O(n²) bash output concatenation with StreamingOutput class (anomalyco#9693) - Fix memory leaks in Bus.once, Format, Plugin, ShareNext, Bootstrap (anomalyco#13514) - Fix FileTime race condition using actual file mtime instead of JS clock - Free memory on compaction prune: clear output/attachments/metadata (anomalyco#7049) - Throttle reasoning-delta storage writes to 50ms intervals (anomalyco#11328) - Handle SIGHUP/SIGTERM to prevent orphaned processes (anomalyco#12718) - Add process.once("close") handler for bash tool reliability Tier 2 features: - Support 1M context window for Anthropic models via beta header (anomalyco#14375) - Input-only token counting for compaction with limit.input models - MCP lazy loading: on-demand tool discovery via mcp_search tool (anomalyco#8771) - MCP servers listed in system prompt when lazy mode enabled - StreamingOutput: output_filter regex for build diagnostics - LSP server cleanup callback for temp directory removal - Extract formatSize utility from uninstall to shared util/format - GitHub CI: fix Bus subscription leak in session event handler Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent d87d4f0 commit 63e7373

29 files changed

Lines changed: 907 additions & 239 deletions

File tree

packages/opencode/src/bus/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ export namespace Bus {
8080
const unsub = subscribe(def, (event) => {
8181
if (callback(event)) unsub()
8282
})
83+
return unsub
8384
}
8485

8586
export function subscribeAll(callback: (event: any) => void) {

packages/opencode/src/cli/cmd/github.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -483,6 +483,7 @@ export const GithubRunCommand = cmd({
483483
let session: { id: string; title: string; version: string }
484484
let shareId: string | undefined
485485
let exitCode = 0
486+
let unsubSessionEvents: (() => void) | undefined
486487
type PromptFiles = Awaited<ReturnType<typeof getUserPrompt>>["promptFiles"]
487488
const triggerCommentId = isCommentEvent
488489
? (payload as IssueCommentEvent | PullRequestReviewCommentEvent).comment.id
@@ -533,7 +534,7 @@ export const GithubRunCommand = cmd({
533534
},
534535
],
535536
})
536-
subscribeSessionEvents()
537+
unsubSessionEvents = subscribeSessionEvents()
537538
shareId = await (async () => {
538539
if (share === false) return
539540
if (!share && repoData.data.private) return
@@ -671,6 +672,7 @@ export const GithubRunCommand = cmd({
671672
// Also output the clean error message for the action to capture
672673
//core.setOutput("prepare_error", e.message);
673674
} finally {
675+
unsubSessionEvents?.()
674676
if (!useGithubToken) {
675677
await restoreGitConfig()
676678
await revokeAppToken()
@@ -868,7 +870,7 @@ export const GithubRunCommand = cmd({
868870
}
869871

870872
let text = ""
871-
Bus.subscribe(MessageV2.Event.PartUpdated, async (evt) => {
873+
return Bus.subscribe(MessageV2.Event.PartUpdated, async (evt) => {
872874
if (evt.properties.part.sessionID !== session.id) return
873875
//if (evt.properties.part.messageID === messageID) return
874876
const part = evt.properties.part

packages/opencode/src/cli/cmd/serve.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,12 @@ export const ServeCommand = cmd({
1111
builder: (yargs) => withNetworkOptions(yargs),
1212
describe: "starts a headless opencode server",
1313
handler: async (args) => {
14+
for (const signal of ["SIGHUP", "SIGTERM"] as const) {
15+
process.once(signal, () => {
16+
process.kill(process.pid, signal)
17+
})
18+
}
19+
1420
if (!Flag.OPENCODE_SERVER_PASSWORD) {
1521
console.log("Warning: OPENCODE_SERVER_PASSWORD is not set; server is unsecured.")
1622
}

packages/opencode/src/cli/cmd/tui/attach.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,12 @@ export const AttachCommand = cmd({
4040
describe: "basic auth password (defaults to OPENCODE_SERVER_PASSWORD)",
4141
}),
4242
handler: async (args) => {
43+
for (const signal of ["SIGHUP", "SIGTERM"] as const) {
44+
process.once(signal, () => {
45+
process.kill(process.pid, signal)
46+
})
47+
}
48+
4349
const unguard = win32InstallCtrlCGuard()
4450
try {
4551
win32DisableProcessedInput()

packages/opencode/src/cli/cmd/tui/thread.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,13 @@ export const TuiThreadCommand = cmd({
162162
worker.terminate()
163163
}
164164

165+
for (const signal of ["SIGHUP", "SIGTERM"] as const) {
166+
process.once(signal, async () => {
167+
await client.call("shutdown", undefined).catch(() => {})
168+
process.kill(process.pid, signal)
169+
})
170+
}
171+
165172
const prompt = await input(args.prompt)
166173
const config = await Instance.provide({
167174
directory: cwd,

packages/opencode/src/cli/cmd/tui/worker.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,26 @@ process.on("uncaughtException", (e) => {
3333
})
3434
})
3535

36+
for (const signal of ["SIGHUP", "SIGTERM"] as const) {
37+
process.once(signal, () => {
38+
rpc.shutdown().finally(() => {
39+
process.kill(process.pid, signal)
40+
})
41+
})
42+
}
43+
44+
if (process.platform !== "win32") {
45+
const monitor = setInterval(() => {
46+
// Avoid stale parent PID checks; rely on reparent-to-init (ppid=1) instead.
47+
if (process.ppid !== 1) return
48+
clearInterval(monitor)
49+
rpc.shutdown().finally(() => {
50+
process.kill(process.pid, "SIGTERM")
51+
})
52+
}, 2000)
53+
monitor.unref()
54+
}
55+
3656
// Subscribe to global events and forward them via RPC
3757
GlobalBus.on("event", (event) => {
3858
Rpc.emit("global.event", event)

packages/opencode/src/cli/cmd/uninstall.ts

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { UI } from "../ui"
33
import * as prompts from "@clack/prompts"
44
import { Installation } from "../../installation"
55
import { Global } from "../../global"
6+
import { formatSize } from "../../util/format"
67
import { $ } from "bun"
78
import fs from "fs/promises"
89
import path from "path"
@@ -340,13 +341,6 @@ async function getDirectorySize(dir: string): Promise<number> {
340341
return total
341342
}
342343

343-
function formatSize(bytes: number): string {
344-
if (bytes < 1024) return `${bytes} B`
345-
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
346-
if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
347-
return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`
348-
}
349-
350344
function shortenPath(p: string): string {
351345
const home = os.homedir()
352346
if (p.startsWith(home)) {

packages/opencode/src/config/config.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1190,6 +1190,12 @@ export namespace Config {
11901190
.positive()
11911191
.optional()
11921192
.describe("Timeout in milliseconds for model context protocol (MCP) requests"),
1193+
mcp_lazy: z
1194+
.boolean()
1195+
.optional()
1196+
.describe(
1197+
"Enable lazy loading of MCP tools. When enabled, MCP tools are not loaded into context automatically. Instead, use the mcp_search tool to discover and call MCP tools on-demand.",
1198+
),
11931199
tool_aliases: z
11941200
.record(z.string(), z.string())
11951201
.optional()

packages/opencode/src/file/time.ts

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,24 +9,36 @@ export namespace FileTime {
99
// All tools that overwrite existing files should run their
1010
// assert/read/write/update sequence inside withLock(filepath, ...)
1111
// so concurrent writes to the same file are serialized.
12-
export const state = Instance.state(() => {
13-
const read: {
14-
[sessionID: string]: {
15-
[path: string]: Date | undefined
12+
export const state = Instance.state(
13+
() => {
14+
const read: {
15+
[sessionID: string]: {
16+
[path: string]: Date | undefined
17+
}
18+
} = {}
19+
const locks = new Map<string, Promise<void>>()
20+
return {
21+
read,
22+
locks,
1623
}
17-
} = {}
18-
const locks = new Map<string, Promise<void>>()
19-
return {
20-
read,
21-
locks,
22-
}
23-
})
24+
},
25+
async (current) => {
26+
for (const key of Object.keys(current.read)) {
27+
delete current.read[key]
28+
}
29+
current.locks.clear()
30+
},
31+
)
2432

2533
export function read(sessionID: string, file: string) {
2634
log.info("read", { sessionID, file })
2735
const { read } = state()
2836
read[sessionID] = read[sessionID] || {}
29-
read[sessionID][file] = new Date()
37+
// Use the file's actual mtime when available so the assert() comparison
38+
// is filesystem-time vs filesystem-time. Using new Date() caused false
39+
// positives on Windows where NTFS mtime can lag behind JS clock.
40+
const mtime = Filesystem.stat(file)?.mtime
41+
read[sessionID][file] = mtime ?? new Date()
3042
}
3143

3244
export function get(sessionID: string, file: string) {

packages/opencode/src/format/index.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,9 +101,12 @@ export namespace Format {
101101
return result
102102
}
103103

104+
let unsub: (() => void) | undefined
105+
104106
export function init() {
105107
log.info("init")
106-
Bus.subscribe(File.Event.Edited, async (payload) => {
108+
unsub?.()
109+
unsub = Bus.subscribe(File.Event.Edited, async (payload) => {
107110
const file = payload.properties.file
108111
log.info("formatting", { file })
109112
const ext = path.extname(file)

0 commit comments

Comments
 (0)