From bfd2f91d5b4b7ee28346bfcfd1481a8c0370574c Mon Sep 17 00:00:00 2001 From: Spoon <212802214+spoons-and-mirrors@users.noreply.github.com> Date: Sun, 18 Jan 2026 20:11:22 +0100 Subject: [PATCH 0001/1177] feat(hook): command execute before hook (#9267) --- packages/opencode/src/session/prompt.ts | 10 ++++++++++ packages/plugin/src/index.ts | 4 ++++ 2 files changed, 14 insertions(+) diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 0d3d25feb8de..f4793d1a7987 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -1702,6 +1702,16 @@ NOTE: At any point in time through this workflow you should feel free to ask the : await lastModel(input.sessionID) : taskModel + await Plugin.trigger( + "command.execute.before", + { + command: input.command, + sessionID: input.sessionID, + arguments: input.arguments, + }, + { parts }, + ) + const result = (await prompt({ sessionID: input.sessionID, messageID: input.messageID, diff --git a/packages/plugin/src/index.ts b/packages/plugin/src/index.ts index e57eff579e63..36a4657d74c5 100644 --- a/packages/plugin/src/index.ts +++ b/packages/plugin/src/index.ts @@ -173,6 +173,10 @@ export interface Hooks { output: { temperature: number; topP: number; topK: number; options: Record }, ) => Promise "permission.ask"?: (input: Permission, output: { status: "ask" | "deny" | "allow" }) => Promise + "command.execute.before"?: ( + input: { command: string; sessionID: string; arguments: string }, + output: { parts: Part[] }, + ) => Promise "tool.execute.before"?: ( input: { tool: string; sessionID: string; callID: string }, output: { args: any }, From 501ef2d989afde09b54299d309442a7b1a39a680 Mon Sep 17 00:00:00 2001 From: Vladimir Glafirov Date: Sun, 18 Jan 2026 20:11:34 +0100 Subject: [PATCH 0002/1177] fix: update gitlab-ai-provider to 1.3.2 (#9279) --- bun.lock | 4 ++-- packages/opencode/package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/bun.lock b/bun.lock index 9cda088153c1..a9cabb31114a 100644 --- a/bun.lock +++ b/bun.lock @@ -281,7 +281,7 @@ "@ai-sdk/vercel": "1.0.31", "@ai-sdk/xai": "2.0.51", "@clack/prompts": "1.0.0-alpha.1", - "@gitlab/gitlab-ai-provider": "3.1.1", + "@gitlab/gitlab-ai-provider": "3.1.2", "@hono/standard-validator": "0.1.5", "@hono/zod-validator": "catalog:", "@modelcontextprotocol/sdk": "1.25.2", @@ -917,7 +917,7 @@ "@fontsource/inter": ["@fontsource/inter@5.2.8", "", {}, "sha512-P6r5WnJoKiNVV+zvW2xM13gNdFhAEpQ9dQJHt3naLvfg+LkF2ldgSLiF4T41lf1SQCM9QmkqPTn4TH568IRagg=="], - "@gitlab/gitlab-ai-provider": ["@gitlab/gitlab-ai-provider@3.1.1", "", { "dependencies": { "@anthropic-ai/sdk": "^0.71.0", "@anycable/core": "^0.9.2", "graphql-request": "^6.1.0", "isomorphic-ws": "^5.0.0", "socket.io-client": "^4.8.1", "vscode-jsonrpc": "^8.2.1", "zod": "^3.25.76" }, "peerDependencies": { "@ai-sdk/provider": ">=2.0.0", "@ai-sdk/provider-utils": ">=3.0.0" } }, "sha512-7AtFrCflq2NzC99bj7YaqbQDCZyaScM1+L4ujllV5syiRTFE239Uhnd/yEkPXa7sUAnNRfN3CWusCkQ2zK/q9g=="], + "@gitlab/gitlab-ai-provider": ["@gitlab/gitlab-ai-provider@3.1.2", "", { "dependencies": { "@anthropic-ai/sdk": "^0.71.0", "@anycable/core": "^0.9.2", "graphql-request": "^6.1.0", "isomorphic-ws": "^5.0.0", "socket.io-client": "^4.8.1", "vscode-jsonrpc": "^8.2.1", "zod": "^3.25.76" }, "peerDependencies": { "@ai-sdk/provider": ">=2.0.0", "@ai-sdk/provider-utils": ">=3.0.0" } }, "sha512-p0NZhZJSavWDX9r/Px/mOK2YIC803GZa8iRzcg3f1C6S0qfea/HBTe4/NWvT2+2kWIwhCePGuI4FN2UFiUWXUg=="], "@graphql-typed-document-node/core": ["@graphql-typed-document-node/core@3.2.0", "", { "peerDependencies": { "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ=="], diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 757e6efde901..e19181934709 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -70,7 +70,7 @@ "@ai-sdk/vercel": "1.0.31", "@ai-sdk/xai": "2.0.51", "@clack/prompts": "1.0.0-alpha.1", - "@gitlab/gitlab-ai-provider": "3.1.1", + "@gitlab/gitlab-ai-provider": "3.1.2", "@hono/standard-validator": "0.1.5", "@hono/zod-validator": "catalog:", "@modelcontextprotocol/sdk": "1.25.2", From 38c641a2fc6d45c504d419609359f64710a4e732 Mon Sep 17 00:00:00 2001 From: zerone0x Date: Mon, 19 Jan 2026 03:17:49 +0800 Subject: [PATCH 0003/1177] fix(tool): treat .fbs files as text instead of images (#9276) Co-authored-by: Claude --- packages/opencode/src/tool/read.ts | 4 +++- packages/opencode/test/tool/read.test.ts | 29 ++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/tool/read.ts b/packages/opencode/src/tool/read.ts index ce4ab28619dd..3b1484cbc0f8 100644 --- a/packages/opencode/src/tool/read.ts +++ b/packages/opencode/src/tool/read.ts @@ -59,7 +59,9 @@ export const ReadTool = Tool.define("read", { throw new Error(`File not found: ${filepath}`) } - const isImage = file.type.startsWith("image/") && file.type !== "image/svg+xml" + // Exclude SVG (XML-based) and vnd.fastbidsheet (.fbs extension, commonly FlatBuffers schema files) + const isImage = + file.type.startsWith("image/") && file.type !== "image/svg+xml" && file.type !== "image/vnd.fastbidsheet" const isPdf = file.type === "application/pdf" if (isImage || isPdf) { const mime = file.type diff --git a/packages/opencode/test/tool/read.test.ts b/packages/opencode/test/tool/read.test.ts index 04ffc80ea67c..7250bd2fd1ef 100644 --- a/packages/opencode/test/tool/read.test.ts +++ b/packages/opencode/test/tool/read.test.ts @@ -300,4 +300,33 @@ describe("tool.read truncation", () => { }, }) }) + + test(".fbs files (FlatBuffers schema) are read as text, not images", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + // FlatBuffers schema content + const fbsContent = `namespace MyGame; + +table Monster { + pos:Vec3; + name:string; + inventory:[ubyte]; +} + +root_type Monster;` + await Bun.write(path.join(dir, "schema.fbs"), fbsContent) + }, + }) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const read = await ReadTool.init() + const result = await read.execute({ filePath: path.join(tmp.path, "schema.fbs") }, ctx) + // Should be read as text, not as image + expect(result.attachments).toBeUndefined() + expect(result.output).toContain("namespace MyGame") + expect(result.output).toContain("table Monster") + }, + }) + }) }) From c29d44fcef12b393f82407d6fbd26b0ce8aa979a Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Sun, 18 Jan 2026 13:22:39 -0600 Subject: [PATCH 0004/1177] docs: note untracked files in review --- packages/opencode/src/command/template/review.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/opencode/src/command/template/review.txt b/packages/opencode/src/command/template/review.txt index 1ffa0fca0b46..9f6fbfcc3a8d 100644 --- a/packages/opencode/src/command/template/review.txt +++ b/packages/opencode/src/command/template/review.txt @@ -13,6 +13,7 @@ Based on the input provided, determine which type of review to perform: 1. **No arguments (default)**: Review all uncommitted changes - Run: `git diff` for unstaged changes - Run: `git diff --cached` for staged changes + - Run: `git status --short` to identify untracked (net new) files 2. **Commit hash** (40-char SHA or short hash): Review that specific commit - Run: `git show $ARGUMENTS` @@ -33,6 +34,7 @@ Use best judgement when processing input. **Diffs alone are not enough.** After getting the diff, read the entire file(s) being modified to understand the full context. Code that looks wrong in isolation may be correct given surrounding logic—and vice versa. - Use the diff to identify which files changed +- Use `git status --short` to identify untracked files, then read their full contents - Read the full file to understand existing patterns, control flow, and error handling - Check for existing style guide or conventions files (CONVENTIONS.md, AGENTS.md, .editorconfig, etc.) From 19cf9344e12891f92662498ee2c9f132ac78480b Mon Sep 17 00:00:00 2001 From: Github Action Date: Sun, 18 Jan 2026 19:24:21 +0000 Subject: [PATCH 0005/1177] Update node_modules hashes --- nix/hashes.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/nix/hashes.json b/nix/hashes.json index 16a1c1f398b0..5bbdf921bbdb 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,8 +1,8 @@ { "nodeModules": { - "x86_64-linux": "sha256-4zchRpxzvHnPMcwumgL9yaX0deIXS5IGPp131eYsSvg=", - "aarch64-linux": "sha256-3/BSRsl5pI0Iz3qAFZxIkOehFLZ2Ox9UsbdDHYzqlVg=", - "aarch64-darwin": "sha256-86d/G1q6xiHSSlm+/irXoKLb/yLQbV348uuSrBV70+Q=", - "x86_64-darwin": "sha256-WYaP44PWRGtoG1DIuUJUH4DvuaCuFhlJZ9fPzGsiIfE=" + "x86_64-linux": "sha256-D1VXuKJagfq3mxh8Xs8naHoYNJUJzAM9JLJqpHcItDk=", + "aarch64-linux": "sha256-9wXcg50Sv56Wb2x5NWe15olNGE/uMiDkmGRmqPoeW1U=", + "aarch64-darwin": "sha256-i5eTTjsNAARwcw69sd6wuse2BKTUi/Vfgo4M28l+RoY=", + "x86_64-darwin": "sha256-oFtQnIzgTS2zcjkhBTnXxYqr20KXdA2I+b908piLs+c=" } } From d841e70d2646d84c31f839e8cf7f94bc9bda66a8 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Sun, 18 Jan 2026 14:21:08 -0600 Subject: [PATCH 0006/1177] fix: bad variants for grok models --- packages/opencode/src/provider/transform.ts | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index 79892db4ccae..b803bd66ce1d 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -325,9 +325,24 @@ export namespace ProviderTransform { const id = model.id.toLowerCase() if (id.includes("deepseek") || id.includes("minimax") || id.includes("glm") || id.includes("mistral")) return {} + // see: https://docs.x.ai/docs/guides/reasoning#control-how-hard-the-model-thinks + if (id.includes("grok") && id.includes("grok-3-mini")) { + if (model.api.npm === "@openrouter/ai-sdk-provider") { + return { + low: { reasoning: { effort: "low" } }, + high: { reasoning: { effort: "high" } }, + } + } + return { + low: { reasoningEffort: "low" }, + high: { reasoningEffort: "high" }, + } + } + if (id.includes("grok")) return {} + switch (model.api.npm) { case "@openrouter/ai-sdk-provider": - if (!model.id.includes("gpt") && !model.id.includes("gemini-3") && !model.id.includes("grok-4")) return {} + if (!model.id.includes("gpt") && !model.id.includes("gemini-3")) return {} return Object.fromEntries(OPENAI_EFFORTS.map((effort) => [effort, { reasoning: { effort } }])) // TODO: YOU CANNOT SET max_tokens if this is set!!! From 0d8e706facd193610572f1d5b8ddeba80de0b63a Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Sun, 18 Jan 2026 14:44:39 -0600 Subject: [PATCH 0007/1177] test: fix transfomr test --- .../opencode/test/provider/transform.test.ts | 38 +++++++++++++++++-- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/packages/opencode/test/provider/transform.test.ts b/packages/opencode/test/provider/transform.test.ts index dcf16c65cbd7..2b8f1872f56f 100644 --- a/packages/opencode/test/provider/transform.test.ts +++ b/packages/opencode/test/provider/transform.test.ts @@ -1140,7 +1140,7 @@ describe("ProviderTransform.variants", () => { expect(Object.keys(result)).toEqual(["none", "minimal", "low", "medium", "high", "xhigh"]) }) - test("grok-4 returns OPENAI_EFFORTS with reasoning", () => { + test("grok-4 returns empty object", () => { const model = createMockModel({ id: "openrouter/grok-4", providerID: "openrouter", @@ -1151,7 +1151,23 @@ describe("ProviderTransform.variants", () => { }, }) const result = ProviderTransform.variants(model) - expect(Object.keys(result)).toEqual(["none", "minimal", "low", "medium", "high", "xhigh"]) + expect(result).toEqual({}) + }) + + test("grok-3-mini returns low and high with reasoning", () => { + const model = createMockModel({ + id: "openrouter/grok-3-mini", + providerID: "openrouter", + api: { + id: "grok-3-mini", + url: "https://openrouter.ai", + npm: "@openrouter/ai-sdk-provider", + }, + }) + const result = ProviderTransform.variants(model) + expect(Object.keys(result)).toEqual(["low", "high"]) + expect(result.low).toEqual({ reasoning: { effort: "low" } }) + expect(result.high).toEqual({ reasoning: { effort: "high" } }) }) }) @@ -1210,7 +1226,7 @@ describe("ProviderTransform.variants", () => { }) describe("@ai-sdk/xai", () => { - test("returns WIDELY_SUPPORTED_EFFORTS with reasoningEffort", () => { + test("grok-3 returns empty object", () => { const model = createMockModel({ id: "xai/grok-3", providerID: "xai", @@ -1221,7 +1237,21 @@ describe("ProviderTransform.variants", () => { }, }) const result = ProviderTransform.variants(model) - expect(Object.keys(result)).toEqual(["low", "medium", "high"]) + expect(result).toEqual({}) + }) + + test("grok-3-mini returns low and high with reasoningEffort", () => { + const model = createMockModel({ + id: "xai/grok-3-mini", + providerID: "xai", + api: { + id: "grok-3-mini", + url: "https://api.x.ai", + npm: "@ai-sdk/xai", + }, + }) + const result = ProviderTransform.variants(model) + expect(Object.keys(result)).toEqual(["low", "high"]) expect(result.low).toEqual({ reasoningEffort: "low" }) expect(result.high).toEqual({ reasoningEffort: "high" }) }) From b4d4a1ea7d2e590e3963b36580989404377e4ce4 Mon Sep 17 00:00:00 2001 From: Alan Pogrebinschi Date: Sun, 18 Jan 2026 14:46:04 -0800 Subject: [PATCH 0008/1177] docs: clarify agent tool access and explore vs general distinction (#9300) --- packages/web/src/content/docs/agents.mdx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/web/src/content/docs/agents.mdx b/packages/web/src/content/docs/agents.mdx index 22bed7f16a44..ea1f779cd375 100644 --- a/packages/web/src/content/docs/agents.mdx +++ b/packages/web/src/content/docs/agents.mdx @@ -21,7 +21,7 @@ There are two types of agents in OpenCode; primary agents and subagents. ### Primary agents -Primary agents are the main assistants you interact with directly. You can cycle through them using the **Tab** key, or your configured `switch_agent` keybind. These agents handle your main conversation and can access all configured tools. +Primary agents are the main assistants you interact with directly. You can cycle through them using the **Tab** key, or your configured `switch_agent` keybind. These agents handle your main conversation. Tool access is configured via permissions — for example, Build has all tools enabled while Plan is restricted. :::tip You can use the **Tab** key to switch between primary agents during a session. @@ -72,7 +72,7 @@ This agent is useful when you want the LLM to analyze code, suggest changes, or _Mode_: `subagent` -A general-purpose agent for researching complex questions, searching for code, and executing multi-step tasks. Use when searching for keywords or files and you're not confident you'll find the right match in the first few tries. +A general-purpose agent for researching complex questions and executing multi-step tasks. Has full tool access (except todo), so it can make file changes when needed. Use this to run multiple units of work in parallel. --- @@ -80,7 +80,7 @@ A general-purpose agent for researching complex questions, searching for code, a _Mode_: `subagent` -A fast agent specialized for exploring codebases. Use this when you need to quickly find files by patterns, search code for keywords, or answer questions about the codebase. +A fast, read-only agent for exploring codebases. Cannot modify files. Use this when you need to quickly find files by patterns, search code for keywords, or answer questions about the codebase. --- From e81bb86795c062dae736568c9c4a4426e8fe9474 Mon Sep 17 00:00:00 2001 From: Luke Parker <10430890+Hona@users.noreply.github.com> Date: Mon, 19 Jan 2026 09:27:30 +1000 Subject: [PATCH 0009/1177] fix: Windows evaluating text on copy (#9293) --- .../src/cli/cmd/tui/util/clipboard.ts | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/util/clipboard.ts b/packages/opencode/src/cli/cmd/tui/util/clipboard.ts index 2526f41714c7..0e287fbc41ae 100644 --- a/packages/opencode/src/cli/cmd/tui/util/clipboard.ts +++ b/packages/opencode/src/cli/cmd/tui/util/clipboard.ts @@ -125,9 +125,25 @@ export namespace Clipboard { if (os === "win32") { console.log("clipboard: using powershell") return async (text: string) => { - // need to escape backticks because powershell uses them as escape code - const escaped = text.replace(/"/g, '""').replace(/`/g, "``") - await $`powershell -NonInteractive -NoProfile -Command "Set-Clipboard -Value \"${escaped}\""`.nothrow().quiet() + // Pipe via stdin to avoid PowerShell string interpolation ($env:FOO, $(), etc.) + const proc = Bun.spawn( + [ + "powershell.exe", + "-NonInteractive", + "-NoProfile", + "-Command", + "[Console]::InputEncoding = [System.Text.Encoding]::UTF8; Set-Clipboard -Value ([Console]::In.ReadToEnd())", + ], + { + stdin: "pipe", + stdout: "ignore", + stderr: "ignore", + }, + ) + + proc.stdin.write(text) + proc.stdin.end() + await proc.exited.catch(() => {}) } } From bee2f654090f92f607fbf4f7d1ff669ae76ede39 Mon Sep 17 00:00:00 2001 From: Frank Date: Sun, 18 Jan 2026 19:18:58 -0500 Subject: [PATCH 0010/1177] zen: fix checkout link for black users --- packages/console/core/src/billing.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/console/core/src/billing.ts b/packages/console/core/src/billing.ts index f052e6fc6fe7..36e8a76b79d3 100644 --- a/packages/console/core/src/billing.ts +++ b/packages/console/core/src/billing.ts @@ -218,6 +218,7 @@ export namespace Billing { customer: customer.customerID, customer_update: { name: "auto", + address: "auto", }, } : { From d939a3ad547f1794ab39a5455517bedfc310f286 Mon Sep 17 00:00:00 2001 From: Luke Parker <10430890+Hona@users.noreply.github.com> Date: Mon, 19 Jan 2026 13:42:10 +1000 Subject: [PATCH 0011/1177] feat(tui): use mouse for permission buttons (#9305) --- .../opencode/src/cli/cmd/tui/routes/session/permission.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/permission.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/permission.tsx index eab2adb100c4..c4ff4c04b0c7 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/permission.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/permission.tsx @@ -280,6 +280,7 @@ export function PermissionPrompt(props: { request: PermissionRequest }) { reply: "reject", requestID: props.request.id, }) + return } sdk.client.permission.reply({ reply: "once", @@ -456,6 +457,11 @@ function Prompt>(props: { paddingLeft={1} paddingRight={1} backgroundColor={option === store.selected ? theme.warning : theme.backgroundMenu} + onMouseOver={() => setStore("selected", option)} + onMouseUp={() => { + setStore("selected", option) + props.onSelect(option) + }} > {props.options[option]} From 2fc4ab9687219aae4cef5fba042264f7638c5ebc Mon Sep 17 00:00:00 2001 From: Caleb Norton Date: Sun, 18 Jan 2026 21:46:00 -0600 Subject: [PATCH 0012/1177] ci: simplify nix hash updates (#9309) --- .github/workflows/update-nix-hashes.yml | 171 ++++-------------------- flake.nix | 24 +++- nix/node_modules.nix | 85 ++++++++++++ nix/opencode.nix | 83 +----------- 4 files changed, 140 insertions(+), 223 deletions(-) create mode 100644 nix/node_modules.nix diff --git a/.github/workflows/update-nix-hashes.yml b/.github/workflows/update-nix-hashes.yml index f9817fe1eac4..6e937da52704 100644 --- a/.github/workflows/update-nix-hashes.yml +++ b/.github/workflows/update-nix-hashes.yml @@ -10,32 +10,22 @@ on: - "bun.lock" - "package.json" - "packages/*/package.json" + - "flake.lock" - ".github/workflows/update-nix-hashes.yml" pull_request: paths: - "bun.lock" - "package.json" - "packages/*/package.json" + - "flake.lock" - ".github/workflows/update-nix-hashes.yml" jobs: - compute-node-modules-hash: + update-node-modules-hashes: if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository - strategy: - fail-fast: false - matrix: - include: - - system: x86_64-linux - host: blacksmith-4vcpu-ubuntu-2404 - - system: aarch64-linux - host: blacksmith-4vcpu-ubuntu-2404-arm - - system: x86_64-darwin - host: macos-15-intel - - system: aarch64-darwin - host: macos-latest - runs-on: ${{ matrix.host }} + runs-on: blacksmith-4vcpu-ubuntu-2404 env: - SYSTEM: ${{ matrix.system }} + TITLE: node_modules hashes steps: - name: Checkout repository @@ -49,104 +39,6 @@ jobs: - name: Setup Nix uses: nixbuild/nix-quick-install-action@v34 - - name: Compute node_modules hash - run: | - set -euo pipefail - - DUMMY="sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" - HASH_FILE="nix/hashes.json" - OUTPUT_FILE="hash-${SYSTEM}.txt" - - export NIX_KEEP_OUTPUTS=1 - export NIX_KEEP_DERIVATIONS=1 - - BUILD_LOG=$(mktemp) - TMP_JSON=$(mktemp) - trap 'rm -f "$BUILD_LOG" "$TMP_JSON"' EXIT - - if [ ! -f "$HASH_FILE" ]; then - mkdir -p "$(dirname "$HASH_FILE")" - echo '{"nodeModules":{}}' > "$HASH_FILE" - fi - - # Set dummy hash to force nix to rebuild and reveal correct hash - jq --arg system "$SYSTEM" --arg value "$DUMMY" \ - '.nodeModules = (.nodeModules // {}) | .nodeModules[$system] = $value' "$HASH_FILE" > "$TMP_JSON" - mv "$TMP_JSON" "$HASH_FILE" - - MODULES_ATTR=".#packages.${SYSTEM}.default.node_modules" - DRV_PATH="$(nix eval --raw "${MODULES_ATTR}.drvPath")" - - echo "Building node_modules for ${SYSTEM} to discover correct hash..." - echo "Attempting to realize derivation: ${DRV_PATH}" - REALISE_OUT=$(nix-store --realise "$DRV_PATH" --keep-failed 2>&1 | tee "$BUILD_LOG" || true) - - BUILD_PATH=$(echo "$REALISE_OUT" | grep "^/nix/store/" | head -n1 || true) - CORRECT_HASH="" - - if [ -n "$BUILD_PATH" ] && [ -d "$BUILD_PATH" ]; then - echo "Realized node_modules output: $BUILD_PATH" - CORRECT_HASH=$(nix hash path --sri "$BUILD_PATH" 2>/dev/null || true) - fi - - # Try to extract hash from build log - if [ -z "$CORRECT_HASH" ]; then - CORRECT_HASH="$(grep -E 'got:\s+sha256-[A-Za-z0-9+/=]+' "$BUILD_LOG" | awk '{print $2}' | head -n1 || true)" - fi - - if [ -z "$CORRECT_HASH" ]; then - CORRECT_HASH="$(grep -A2 'hash mismatch' "$BUILD_LOG" | grep 'got:' | awk '{print $2}' | sed 's/sha256:/sha256-/' || true)" - fi - - # Try to hash from kept failed build directory - if [ -z "$CORRECT_HASH" ]; then - KEPT_DIR=$(grep -oE "build directory.*'[^']+'" "$BUILD_LOG" | grep -oE "'/[^']+'" | tr -d "'" | head -n1 || true) - if [ -z "$KEPT_DIR" ]; then - KEPT_DIR=$(grep -oE '/nix/var/nix/builds/[^ ]+' "$BUILD_LOG" | head -n1 || true) - fi - - if [ -n "$KEPT_DIR" ] && [ -d "$KEPT_DIR" ]; then - HASH_PATH="$KEPT_DIR" - [ -d "$KEPT_DIR/build" ] && HASH_PATH="$KEPT_DIR/build" - - if [ -d "$HASH_PATH/node_modules" ]; then - CORRECT_HASH=$(nix hash path --sri "$HASH_PATH" 2>/dev/null || true) - fi - fi - fi - - if [ -z "$CORRECT_HASH" ]; then - echo "Failed to determine correct node_modules hash for ${SYSTEM}." - cat "$BUILD_LOG" - exit 1 - fi - - echo "$CORRECT_HASH" > "$OUTPUT_FILE" - echo "Hash for ${SYSTEM}: $CORRECT_HASH" - - - name: Upload hash artifact - uses: actions/upload-artifact@v6 - with: - name: hash-${{ matrix.system }} - path: hash-${{ matrix.system }}.txt - retention-days: 1 - - commit-node-modules-hashes: - needs: compute-node-modules-hash - if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository - runs-on: blacksmith-4vcpu-ubuntu-2404 - env: - TITLE: node_modules hashes - - steps: - - name: Checkout repository - uses: actions/checkout@v6 - with: - token: ${{ secrets.GITHUB_TOKEN }} - fetch-depth: 0 - ref: ${{ github.head_ref || github.ref_name }} - repository: ${{ github.event.pull_request.head.repo.full_name || github.repository }} - - name: Configure git run: | git config --global user.email "action@github.com" @@ -159,54 +51,47 @@ jobs: BRANCH="${TARGET_BRANCH:-${GITHUB_REF_NAME}}" git pull --rebase --autostash origin "$BRANCH" - - name: Download all hash artifacts - uses: actions/download-artifact@v7 - with: - pattern: hash-* - merge-multiple: true - - - name: Merge hashes into hashes.json + - name: Compute all node_modules hashes run: | set -euo pipefail HASH_FILE="nix/hashes.json" + SYSTEMS="x86_64-linux aarch64-linux x86_64-darwin aarch64-darwin" if [ ! -f "$HASH_FILE" ]; then mkdir -p "$(dirname "$HASH_FILE")" echo '{"nodeModules":{}}' > "$HASH_FILE" fi - echo "Merging hashes into ${HASH_FILE}..." + for SYSTEM in $SYSTEMS; do + echo "Computing hash for ${SYSTEM}..." + BUILD_LOG=$(mktemp) + trap 'rm -f "$BUILD_LOG"' EXIT - shopt -s nullglob - files=(hash-*.txt) - if [ ${#files[@]} -eq 0 ]; then - echo "No hash files found, nothing to update" - exit 0 - fi + # The updater derivations use fakeHash, so they will fail and reveal the correct hash + UPDATER_ATTR=".#packages.x86_64-linux.${SYSTEM}_node_modules" - EXPECTED_SYSTEMS="x86_64-linux aarch64-linux x86_64-darwin aarch64-darwin" - for sys in $EXPECTED_SYSTEMS; do - if [ ! -f "hash-${sys}.txt" ]; then - echo "WARNING: Missing hash file for $sys" + nix build "$UPDATER_ATTR" --no-link 2>&1 | tee "$BUILD_LOG" || true + + CORRECT_HASH="$(grep -E 'got:\s+sha256-[A-Za-z0-9+/=]+' "$BUILD_LOG" | awk '{print $2}' | head -n1 || true)" + + if [ -z "$CORRECT_HASH" ]; then + CORRECT_HASH="$(grep -A2 'hash mismatch' "$BUILD_LOG" | grep 'got:' | awk '{print $2}' | sed 's/sha256:/sha256-/' || true)" fi - done - for f in "${files[@]}"; do - system="${f#hash-}" - system="${system%.txt}" - hash=$(cat "$f") - if [ -z "$hash" ]; then - echo "WARNING: Empty hash for $system, skipping" - continue + if [ -z "$CORRECT_HASH" ]; then + echo "Failed to determine correct node_modules hash for ${SYSTEM}." + cat "$BUILD_LOG" + exit 1 fi - echo " $system: $hash" - jq --arg sys "$system" --arg h "$hash" \ - '.nodeModules = (.nodeModules // {}) | .nodeModules[$sys] = $h' "$HASH_FILE" > "${HASH_FILE}.tmp" + + echo " ${SYSTEM}: ${CORRECT_HASH}" + jq --arg sys "$SYSTEM" --arg h "$CORRECT_HASH" \ + '.nodeModules[$sys] = $h' "$HASH_FILE" > "${HASH_FILE}.tmp" mv "${HASH_FILE}.tmp" "$HASH_FILE" done - echo "All hashes merged:" + echo "All hashes computed:" cat "$HASH_FILE" - name: Commit ${{ env.TITLE }} changes diff --git a/flake.nix b/flake.nix index 20833fc49ed4..0f4250937410 100644 --- a/flake.nix +++ b/flake.nix @@ -33,17 +33,37 @@ packages = forEachSystem ( pkgs: let - opencode = pkgs.callPackage ./nix/opencode.nix { + node_modules = pkgs.callPackage ./nix/node_modules.nix { inherit rev; }; + opencode = pkgs.callPackage ./nix/opencode.nix { + inherit node_modules; + }; desktop = pkgs.callPackage ./nix/desktop.nix { inherit opencode; }; + # nixpkgs cpu naming to bun cpu naming + cpuMap = { x86_64 = "x64"; aarch64 = "arm64"; }; + # matrix of node_modules builds - these will always fail due to fakeHash usage + # but allow computation of the correct hash from any build machine for any cpu/os + # see the update-nix-hashes workflow for usage + moduleUpdaters = pkgs.lib.listToAttrs ( + pkgs.lib.concatMap (cpu: + map (os: { + name = "${cpu}_${os}_node_modules"; + value = node_modules.override { + bunCpu = cpuMap.${cpu}; + bunOs = os; + hash = pkgs.lib.fakeHash; + }; + }) [ "linux" "darwin" ] + ) [ "x86_64" "aarch64" ] + ); in { default = opencode; inherit opencode desktop; - } + } // moduleUpdaters ); }; } diff --git a/nix/node_modules.nix b/nix/node_modules.nix new file mode 100644 index 000000000000..981a60ef9baf --- /dev/null +++ b/nix/node_modules.nix @@ -0,0 +1,85 @@ +{ + lib, + stdenvNoCC, + bun, + bunCpu ? if stdenvNoCC.hostPlatform.isAarch64 then "arm64" else "x64", + bunOs ? if stdenvNoCC.hostPlatform.isLinux then "linux" else "darwin", + rev ? "dirty", + hash ? + (lib.pipe ./hashes.json [ + builtins.readFile + builtins.fromJSON + ]).nodeModules.${stdenvNoCC.hostPlatform.system}, +}: +let + packageJson = lib.pipe ../packages/opencode/package.json [ + builtins.readFile + builtins.fromJSON + ]; +in +stdenvNoCC.mkDerivation { + pname = "opencode-node_modules"; + version = "${packageJson.version}-${rev}"; + + src = lib.fileset.toSource { + root = ../.; + fileset = lib.fileset.intersection (lib.fileset.fromSource (lib.sources.cleanSource ../.)) ( + lib.fileset.unions [ + ../packages + ../bun.lock + ../package.json + ../patches + ../install + ] + ); + }; + + impureEnvVars = lib.fetchers.proxyImpureEnvVars ++ [ + "GIT_PROXY_COMMAND" + "SOCKS_SERVER" + ]; + + nativeBuildInputs = [ + bun + ]; + + dontConfigure = true; + + buildPhase = '' + runHook preBuild + export HOME=$(mktemp -d) + export BUN_INSTALL_CACHE_DIR=$(mktemp -d) + bun install \ + --cpu="${bunCpu}" \ + --os="${bunOs}" \ + --frozen-lockfile \ + --ignore-scripts \ + --no-progress \ + --linker=isolated + bun --bun ${./scripts/canonicalize-node-modules.ts} + bun --bun ${./scripts/normalize-bun-binaries.ts} + runHook postBuild + ''; + + installPhase = '' + runHook preInstall + + mkdir -p $out + find . -type d -name node_modules -exec cp -R --parents {} $out \; + + runHook postInstall + ''; + + dontFixup = true; + + outputHashAlgo = "sha256"; + outputHashMode = "recursive"; + outputHash = hash; + + meta.platforms = [ + "aarch64-linux" + "x86_64-linux" + "aarch64-darwin" + "x86_64-darwin" + ]; +} diff --git a/nix/opencode.nix b/nix/opencode.nix index 4d6f8e9b423a..23d9fbe34e04 100644 --- a/nix/opencode.nix +++ b/nix/opencode.nix @@ -1,6 +1,7 @@ { lib, stdenvNoCC, + callPackage, bun, sysctl, makeBinaryWrapper, @@ -9,81 +10,12 @@ installShellFiles, versionCheckHook, writableTmpDirAsHomeHook, - rev ? "dirty", + node_modules ? callPackage ./node-modules.nix { }, }: -let - packageJson = lib.pipe ../packages/opencode/package.json [ - builtins.readFile - builtins.fromJSON - ]; -in stdenvNoCC.mkDerivation (finalAttrs: { pname = "opencode"; - version = "${packageJson.version}-${rev}"; - - src = lib.fileset.toSource { - root = ../.; - fileset = lib.fileset.intersection (lib.fileset.fromSource (lib.sources.cleanSource ../.)) ( - lib.fileset.unions [ - ../packages - ../bun.lock - ../package.json - ../patches - ../install - ] - ); - }; - - node_modules = stdenvNoCC.mkDerivation { - pname = "${finalAttrs.pname}-node_modules"; - inherit (finalAttrs) version src; - - impureEnvVars = lib.fetchers.proxyImpureEnvVars ++ [ - "GIT_PROXY_COMMAND" - "SOCKS_SERVER" - ]; - - nativeBuildInputs = [ - bun - ]; - - dontConfigure = true; - - buildPhase = '' - runHook preBuild - export HOME=$(mktemp -d) - export BUN_INSTALL_CACHE_DIR=$(mktemp -d) - bun install \ - --cpu="${if stdenvNoCC.hostPlatform.isAarch64 then "arm64" else "x64"}" \ - --os="${if stdenvNoCC.hostPlatform.isLinux then "linux" else "darwin"}" \ - --frozen-lockfile \ - --ignore-scripts \ - --no-progress \ - --linker=isolated - bun --bun ${./scripts/canonicalize-node-modules.ts} - bun --bun ${./scripts/normalize-bun-binaries.ts} - runHook postBuild - ''; - - installPhase = '' - runHook preInstall - - mkdir -p $out - find . -type d -name node_modules -exec cp -R --parents {} $out \; - - runHook postInstall - ''; - - dontFixup = true; - - outputHashAlgo = "sha256"; - outputHashMode = "recursive"; - outputHash = - (lib.pipe ./hashes.json [ - builtins.readFile - builtins.fromJSON - ]).nodeModules.${stdenvNoCC.hostPlatform.system}; - }; + inherit (node_modules) version src; + inherit node_modules; nativeBuildInputs = [ bun @@ -159,11 +91,6 @@ stdenvNoCC.mkDerivation (finalAttrs: { homepage = "https://opencode.ai/"; license = lib.licenses.mit; mainProgram = "opencode"; - platforms = [ - "aarch64-linux" - "x86_64-linux" - "aarch64-darwin" - "x86_64-darwin" - ]; + inherit (node_modules.meta) platforms; }; }) From 6b481b5fb07134cac6f3df4fa2195aa56476295c Mon Sep 17 00:00:00 2001 From: Thiago Malek <14800002+thmalek@users.noreply.github.com> Date: Mon, 19 Jan 2026 02:22:31 -0300 Subject: [PATCH 0013/1177] fix(opencode): use streamObject when using openai oauth in agent generation (#9231) --- packages/opencode/src/agent/agent.ts | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/packages/opencode/src/agent/agent.ts b/packages/opencode/src/agent/agent.ts index 0725933d731d..2b44308f1301 100644 --- a/packages/opencode/src/agent/agent.ts +++ b/packages/opencode/src/agent/agent.ts @@ -1,10 +1,12 @@ import { Config } from "../config/config" import z from "zod" import { Provider } from "../provider/provider" -import { generateObject, type ModelMessage } from "ai" +import { generateObject, streamObject, type ModelMessage } from "ai" import { SystemPrompt } from "../session/system" import { Instance } from "../project/instance" import { Truncate } from "../tool/truncation" +import { Auth } from "../auth" +import { ProviderTransform } from "../provider/transform" import PROMPT_GENERATE from "./generate.txt" import PROMPT_COMPACTION from "./prompt/compaction.txt" @@ -276,10 +278,12 @@ export namespace Agent { const defaultModel = input.model ?? (await Provider.defaultModel()) const model = await Provider.getModel(defaultModel.providerID, defaultModel.modelID) const language = await Provider.getLanguage(model) + const system = SystemPrompt.header(defaultModel.providerID) system.push(PROMPT_GENERATE) const existing = await list() - const result = await generateObject({ + + const params = { experimental_telemetry: { isEnabled: cfg.experimental?.openTelemetry, metadata: { @@ -305,7 +309,24 @@ export namespace Agent { whenToUse: z.string(), systemPrompt: z.string(), }), - }) + } satisfies Parameters[0] + + if (defaultModel.providerID === "openai" && (await Auth.get(defaultModel.providerID))?.type === "oauth") { + const result = streamObject({ + ...params, + providerOptions: ProviderTransform.providerOptions(model, { + instructions: SystemPrompt.instructions(), + store: false, + }), + onError: () => {}, + }) + for await (const part of result.fullStream) { + if (part.type === "error") throw part.error + } + return result.object + } + + const result = await generateObject(params) return result.object } } From fc6c9cbbd262daa0f98338ed3c79270fbfa086ad Mon Sep 17 00:00:00 2001 From: Christopher Tso Date: Mon, 19 Jan 2026 16:30:28 +1100 Subject: [PATCH 0014/1177] fix(github-copilot): auto-route GPT-5+ models to Responses API (#5877) Co-authored-by: Claude --- packages/opencode/src/provider/provider.ts | 22 +++++++++++++-------- packages/opencode/src/provider/transform.ts | 6 +++++- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index bcb115edf419..d4d4b3e2680f 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -41,6 +41,18 @@ import { ProviderTransform } from "./transform" export namespace Provider { const log = Log.create({ service: "provider" }) + function isGpt5OrLater(modelID: string): boolean { + const match = /^gpt-(\d+)/.exec(modelID) + if (!match) { + return false + } + return Number(match[1]) >= 5 + } + + function shouldUseCopilotResponsesApi(modelID: string): boolean { + return isGpt5OrLater(modelID) && !modelID.startsWith("gpt-5-mini") + } + const BUNDLED_PROVIDERS: Record SDK> = { "@ai-sdk/amazon-bedrock": createAmazonBedrock, "@ai-sdk/anthropic": createAnthropic, @@ -120,10 +132,7 @@ export namespace Provider { return { autoload: false, async getModel(sdk: any, modelID: string, _options?: Record) { - if (modelID.includes("codex")) { - return sdk.responses(modelID) - } - return sdk.chat(modelID) + return shouldUseCopilotResponsesApi(modelID) ? sdk.responses(modelID) : sdk.chat(modelID) }, options: {}, } @@ -132,10 +141,7 @@ export namespace Provider { return { autoload: false, async getModel(sdk: any, modelID: string, _options?: Record) { - if (modelID.includes("codex")) { - return sdk.responses(modelID) - } - return sdk.chat(modelID) + return shouldUseCopilotResponsesApi(modelID) ? sdk.responses(modelID) : sdk.chat(modelID) }, options: {}, } diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index b803bd66ce1d..2cacb61aaf46 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -524,7 +524,11 @@ export namespace ProviderTransform { const result: Record = {} // openai and providers using openai package should set store to false by default. - if (input.model.providerID === "openai" || input.model.api.npm === "@ai-sdk/openai") { + if ( + input.model.providerID === "openai" || + input.model.api.npm === "@ai-sdk/openai" || + input.model.api.npm === "@ai-sdk/github-copilot" + ) { result["store"] = false } From e2f1f4d81e152f19f6f9d2f8ed873f310296eba4 Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Sun, 18 Jan 2026 21:33:23 -0800 Subject: [PATCH 0015/1177] add scheduler, cleanup module (#9346) --- packages/opencode/src/project/bootstrap.ts | 4 ++ packages/opencode/src/scheduler/index.ts | 61 ++++++++++++++++++ packages/opencode/src/snapshot/index.ts | 37 +++++++++++ packages/opencode/src/tool/truncation.ts | 15 +++-- packages/opencode/test/scheduler.test.ts | 73 ++++++++++++++++++++++ 5 files changed, 186 insertions(+), 4 deletions(-) create mode 100644 packages/opencode/src/scheduler/index.ts create mode 100644 packages/opencode/test/scheduler.test.ts diff --git a/packages/opencode/src/project/bootstrap.ts b/packages/opencode/src/project/bootstrap.ts index 56fe4d13e664..efdcaba99094 100644 --- a/packages/opencode/src/project/bootstrap.ts +++ b/packages/opencode/src/project/bootstrap.ts @@ -11,6 +11,8 @@ import { Instance } from "./instance" import { Vcs } from "./vcs" import { Log } from "@/util/log" import { ShareNext } from "@/share/share-next" +import { Snapshot } from "../snapshot" +import { Truncate } from "../tool/truncation" export async function InstanceBootstrap() { Log.Default.info("bootstrapping", { directory: Instance.directory }) @@ -22,6 +24,8 @@ export async function InstanceBootstrap() { FileWatcher.init() File.init() Vcs.init() + Snapshot.init() + Truncate.init() Bus.subscribe(Command.Event.Executed, async (payload) => { if (payload.properties.name === Command.Default.INIT) { diff --git a/packages/opencode/src/scheduler/index.ts b/packages/opencode/src/scheduler/index.ts new file mode 100644 index 000000000000..cfafa7b9ced0 --- /dev/null +++ b/packages/opencode/src/scheduler/index.ts @@ -0,0 +1,61 @@ +import { Instance } from "../project/instance" +import { Log } from "../util/log" + +export namespace Scheduler { + const log = Log.create({ service: "scheduler" }) + + export type Task = { + id: string + interval: number + run: () => Promise + scope?: "instance" | "global" + } + + type Timer = ReturnType + type Entry = { + tasks: Map + timers: Map + } + + const create = (): Entry => { + const tasks = new Map() + const timers = new Map() + return { tasks, timers } + } + + const shared = create() + + const state = Instance.state( + () => create(), + async (entry) => { + for (const timer of entry.timers.values()) { + clearInterval(timer) + } + entry.tasks.clear() + entry.timers.clear() + }, + ) + + export function register(task: Task) { + const scope = task.scope ?? "instance" + const entry = scope === "global" ? shared : state() + const current = entry.timers.get(task.id) + if (current && scope === "global") return + if (current) clearInterval(current) + + entry.tasks.set(task.id, task) + void run(task) + const timer = setInterval(() => { + void run(task) + }, task.interval) + timer.unref() + entry.timers.set(task.id, timer) + } + + async function run(task: Task) { + log.info("run", { id: task.id }) + await task.run().catch((error) => { + log.error("run failed", { id: task.id, error }) + }) + } +} diff --git a/packages/opencode/src/snapshot/index.ts b/packages/opencode/src/snapshot/index.ts index 69f2abc7903a..46c97cf8dfd2 100644 --- a/packages/opencode/src/snapshot/index.ts +++ b/packages/opencode/src/snapshot/index.ts @@ -6,9 +6,46 @@ import { Global } from "../global" import z from "zod" import { Config } from "../config/config" import { Instance } from "../project/instance" +import { Scheduler } from "../scheduler" export namespace Snapshot { const log = Log.create({ service: "snapshot" }) + const hour = 60 * 60 * 1000 + const prune = "7.days" + + export function init() { + Scheduler.register({ + id: "snapshot.cleanup", + interval: hour, + run: cleanup, + scope: "instance", + }) + } + + export async function cleanup() { + if (Instance.project.vcs !== "git") return + const cfg = await Config.get() + if (cfg.snapshot === false) return + const git = gitdir() + const exists = await fs + .stat(git) + .then(() => true) + .catch(() => false) + if (!exists) return + const result = await $`git --git-dir ${git} --work-tree ${Instance.worktree} gc --prune=${prune}` + .quiet() + .cwd(Instance.directory) + .nothrow() + if (result.exitCode !== 0) { + log.warn("cleanup failed", { + exitCode: result.exitCode, + stderr: result.stderr.toString(), + stdout: result.stdout.toString(), + }) + return + } + log.info("cleanup", { prune }) + } export async function track() { if (Instance.project.vcs !== "git") return diff --git a/packages/opencode/src/tool/truncation.ts b/packages/opencode/src/tool/truncation.ts index 4172b6447e69..84e799c1310e 100644 --- a/packages/opencode/src/tool/truncation.ts +++ b/packages/opencode/src/tool/truncation.ts @@ -2,9 +2,9 @@ import fs from "fs/promises" import path from "path" import { Global } from "../global" import { Identifier } from "../id/id" -import { lazy } from "../util/lazy" import { PermissionNext } from "../permission/next" import type { Agent } from "../agent/agent" +import { Scheduler } from "../scheduler" export namespace Truncate { export const MAX_LINES = 2000 @@ -12,6 +12,7 @@ export namespace Truncate { export const DIR = path.join(Global.Path.data, "tool-output") export const GLOB = path.join(DIR, "*") const RETENTION_MS = 7 * 24 * 60 * 60 * 1000 // 7 days + const HOUR_MS = 60 * 60 * 1000 export type Result = { content: string; truncated: false } | { content: string; truncated: true; outputPath: string } @@ -21,6 +22,15 @@ export namespace Truncate { direction?: "head" | "tail" } + export function init() { + Scheduler.register({ + id: "tool.truncation.cleanup", + interval: HOUR_MS, + run: cleanup, + scope: "global", + }) + } + export async function cleanup() { const cutoff = Identifier.timestamp(Identifier.create("tool", false, Date.now() - RETENTION_MS)) const glob = new Bun.Glob("tool_*") @@ -31,8 +41,6 @@ export namespace Truncate { } } - const init = lazy(cleanup) - function hasTaskTool(agent?: Agent.Info): boolean { if (!agent?.permission) return false const rule = PermissionNext.evaluate("task", "*", agent.permission) @@ -81,7 +89,6 @@ export namespace Truncate { const unit = hitBytes ? "bytes" : "lines" const preview = out.join("\n") - await init() const id = Identifier.ascending("tool") const filepath = path.join(DIR, id) await Bun.write(Bun.file(filepath), text) diff --git a/packages/opencode/test/scheduler.test.ts b/packages/opencode/test/scheduler.test.ts new file mode 100644 index 000000000000..328daad9b833 --- /dev/null +++ b/packages/opencode/test/scheduler.test.ts @@ -0,0 +1,73 @@ +import { describe, expect, test } from "bun:test" +import { Scheduler } from "../src/scheduler" +import { Instance } from "../src/project/instance" +import { tmpdir } from "./fixture/fixture" + +describe("Scheduler.register", () => { + const hour = 60 * 60 * 1000 + + test("defaults to instance scope per directory", async () => { + await using one = await tmpdir({ git: true }) + await using two = await tmpdir({ git: true }) + const runs = { count: 0 } + const id = "scheduler.instance." + Math.random().toString(36).slice(2) + const task = { + id, + interval: hour, + run: async () => { + runs.count += 1 + }, + } + + await Instance.provide({ + directory: one.path, + fn: async () => { + Scheduler.register(task) + await Instance.dispose() + }, + }) + expect(runs.count).toBe(1) + + await Instance.provide({ + directory: two.path, + fn: async () => { + Scheduler.register(task) + await Instance.dispose() + }, + }) + expect(runs.count).toBe(2) + }) + + test("global scope runs once across instances", async () => { + await using one = await tmpdir({ git: true }) + await using two = await tmpdir({ git: true }) + const runs = { count: 0 } + const id = "scheduler.global." + Math.random().toString(36).slice(2) + const task = { + id, + interval: hour, + run: async () => { + runs.count += 1 + }, + scope: "global" as const, + } + + await Instance.provide({ + directory: one.path, + fn: async () => { + Scheduler.register(task) + await Instance.dispose() + }, + }) + expect(runs.count).toBe(1) + + await Instance.provide({ + directory: two.path, + fn: async () => { + Scheduler.register(task) + await Instance.dispose() + }, + }) + expect(runs.count).toBe(1) + }) +}) From 260ab60c0b9ba1667a326c1b19ea46473156df0c Mon Sep 17 00:00:00 2001 From: NateSmyth Date: Mon, 19 Jan 2026 01:11:54 -0500 Subject: [PATCH 0016/1177] fix: track reasoning by output_index for copilot compatibility (#9124) Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> --- packages/opencode/src/provider/provider.ts | 14 ++--- .../openai-responses-language-model.ts | 59 ++++++++++++------- 2 files changed, 46 insertions(+), 27 deletions(-) diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index d4d4b3e2680f..ad57867df477 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -615,13 +615,13 @@ export namespace Provider { }, experimentalOver200K: model.cost?.context_over_200k ? { - cache: { - read: model.cost.context_over_200k.cache_read ?? 0, - write: model.cost.context_over_200k.cache_write ?? 0, - }, - input: model.cost.context_over_200k.input, - output: model.cost.context_over_200k.output, - } + cache: { + read: model.cost.context_over_200k.cache_read ?? 0, + write: model.cost.context_over_200k.cache_write ?? 0, + }, + input: model.cost.context_over_200k.input, + output: model.cost.context_over_200k.output, + } : undefined, }, limit: { diff --git a/packages/opencode/src/provider/sdk/openai-compatible/src/responses/openai-responses-language-model.ts b/packages/opencode/src/provider/sdk/openai-compatible/src/responses/openai-responses-language-model.ts index 94b0edaf3f4a..0990b7e0077c 100644 --- a/packages/opencode/src/provider/sdk/openai-compatible/src/responses/openai-responses-language-model.ts +++ b/packages/opencode/src/provider/sdk/openai-compatible/src/responses/openai-responses-language-model.ts @@ -815,14 +815,20 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV2 { // flag that checks if there have been client-side tool calls (not executed by openai) let hasFunctionCall = false + // Track reasoning by output_index instead of item_id + // GitHub Copilot rotates encrypted item IDs on every event const activeReasoning: Record< - string, + number, { + canonicalId: string // the item.id from output_item.added encryptedContent?: string | null summaryParts: number[] } > = {} + // Track current active reasoning output_index for correlating summary events + let currentReasoningOutputIndex: number | null = null + // Track a stable text part id for the current assistant message. // Copilot may change item_id across text deltas; normalize to one id. let currentTextId: string | null = null @@ -933,10 +939,12 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV2 { }, }) } else if (isResponseOutputItemAddedReasoningChunk(value)) { - activeReasoning[value.item.id] = { + activeReasoning[value.output_index] = { + canonicalId: value.item.id, encryptedContent: value.item.encrypted_content, summaryParts: [0], } + currentReasoningOutputIndex = value.output_index controller.enqueue({ type: "reasoning-start", @@ -1091,22 +1099,25 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV2 { currentTextId = null } } else if (isResponseOutputItemDoneReasoningChunk(value)) { - const activeReasoningPart = activeReasoning[value.item.id] + const activeReasoningPart = activeReasoning[value.output_index] if (activeReasoningPart) { for (const summaryIndex of activeReasoningPart.summaryParts) { controller.enqueue({ type: "reasoning-end", - id: `${value.item.id}:${summaryIndex}`, + id: `${activeReasoningPart.canonicalId}:${summaryIndex}`, providerMetadata: { openai: { - itemId: value.item.id, + itemId: activeReasoningPart.canonicalId, reasoningEncryptedContent: value.item.encrypted_content ?? null, }, }, }) } + delete activeReasoning[value.output_index] + if (currentReasoningOutputIndex === value.output_index) { + currentReasoningOutputIndex = null + } } - delete activeReasoning[value.item.id] } } else if (isResponseFunctionCallArgumentsDeltaChunk(value)) { const toolCall = ongoingToolCalls[value.output_index] @@ -1198,32 +1209,40 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV2 { logprobs.push(value.logprobs) } } else if (isResponseReasoningSummaryPartAddedChunk(value)) { + const activeItem = + currentReasoningOutputIndex !== null ? activeReasoning[currentReasoningOutputIndex] : null + // the first reasoning start is pushed in isResponseOutputItemAddedReasoningChunk. - if (value.summary_index > 0) { - activeReasoning[value.item_id]?.summaryParts.push(value.summary_index) + if (activeItem && value.summary_index > 0) { + activeItem.summaryParts.push(value.summary_index) controller.enqueue({ type: "reasoning-start", - id: `${value.item_id}:${value.summary_index}`, + id: `${activeItem.canonicalId}:${value.summary_index}`, providerMetadata: { openai: { - itemId: value.item_id, - reasoningEncryptedContent: activeReasoning[value.item_id]?.encryptedContent ?? null, + itemId: activeItem.canonicalId, + reasoningEncryptedContent: activeItem.encryptedContent ?? null, }, }, }) } } else if (isResponseReasoningSummaryTextDeltaChunk(value)) { - controller.enqueue({ - type: "reasoning-delta", - id: `${value.item_id}:${value.summary_index}`, - delta: value.delta, - providerMetadata: { - openai: { - itemId: value.item_id, + const activeItem = + currentReasoningOutputIndex !== null ? activeReasoning[currentReasoningOutputIndex] : null + + if (activeItem) { + controller.enqueue({ + type: "reasoning-delta", + id: `${activeItem.canonicalId}:${value.summary_index}`, + delta: value.delta, + providerMetadata: { + openai: { + itemId: activeItem.canonicalId, + }, }, - }, - }) + }) + } } else if (isResponseFinishedChunk(value)) { finishReason = mapOpenAIResponseFinishReason({ finishReason: value.response.incomplete_details?.reason, From 6f847a794b919bab586172b2848464da33f1e452 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Mon, 19 Jan 2026 06:12:36 +0000 Subject: [PATCH 0017/1177] chore: generate --- packages/opencode/src/provider/provider.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index ad57867df477..d4d4b3e2680f 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -615,13 +615,13 @@ export namespace Provider { }, experimentalOver200K: model.cost?.context_over_200k ? { - cache: { - read: model.cost.context_over_200k.cache_read ?? 0, - write: model.cost.context_over_200k.cache_write ?? 0, - }, - input: model.cost.context_over_200k.input, - output: model.cost.context_over_200k.output, - } + cache: { + read: model.cost.context_over_200k.cache_read ?? 0, + write: model.cost.context_over_200k.cache_write ?? 0, + }, + input: model.cost.context_over_200k.input, + output: model.cost.context_over_200k.output, + } : undefined, }, limit: { From 86df915df02b4d25332de4837574cbe0a89bc9b3 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Mon, 19 Jan 2026 00:07:07 -0600 Subject: [PATCH 0018/1177] chore: cleanup provider code to assign copilot sdk earlier in flow --- packages/opencode/src/provider/provider.ts | 23 ++++++++-------------- 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index d4d4b3e2680f..513c8524de91 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -598,11 +598,14 @@ export namespace Provider { providerID: provider.id, name: model.name, family: model.family, - api: { - id: model.id, - url: provider.api!, - npm: model.provider?.npm ?? provider.npm ?? "@ai-sdk/openai-compatible", - }, + api: { + id: model.id, + url: provider.api!, + npm: iife(() => { + if (provider.id.startsWith("github-copilot")) return "@ai-sdk/github-copilot" + return model.provider?.npm ?? provider.npm ?? "@ai-sdk/openai-compatible" + }), + }, status: model.status ?? "active", headers: model.headers ?? {}, options: model.options ?? {}, @@ -908,16 +911,6 @@ export namespace Provider { continue } - if (providerID === "github-copilot" || providerID === "github-copilot-enterprise") { - provider.models = mapValues(provider.models, (model) => ({ - ...model, - api: { - ...model.api, - npm: "@ai-sdk/github-copilot", - }, - })) - } - const configProvider = config.provider?.[providerID] for (const [modelID, model] of Object.entries(provider.models)) { From 91787ceb3e023507ec643eac43db211d8f68a52d Mon Sep 17 00:00:00 2001 From: Caleb Norton Date: Mon, 19 Jan 2026 00:14:14 -0600 Subject: [PATCH 0019/1177] fix: nix ci - swapped dash/underscore (#9352) --- flake.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flake.nix b/flake.nix index 0f4250937410..e4d214a0b930 100644 --- a/flake.nix +++ b/flake.nix @@ -50,7 +50,7 @@ moduleUpdaters = pkgs.lib.listToAttrs ( pkgs.lib.concatMap (cpu: map (os: { - name = "${cpu}_${os}_node_modules"; + name = "${cpu}-${os}_node_modules"; value = node_modules.override { bunCpu = cpuMap.${cpu}; bunOs = os; From 9d1803d00080b4ce88705862e367fc1961dfb00e Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Mon, 19 Jan 2026 06:14:40 +0000 Subject: [PATCH 0020/1177] chore: generate --- packages/opencode/src/provider/provider.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index 513c8524de91..fdd4ccdfb619 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -598,14 +598,14 @@ export namespace Provider { providerID: provider.id, name: model.name, family: model.family, - api: { - id: model.id, - url: provider.api!, - npm: iife(() => { - if (provider.id.startsWith("github-copilot")) return "@ai-sdk/github-copilot" - return model.provider?.npm ?? provider.npm ?? "@ai-sdk/openai-compatible" - }), - }, + api: { + id: model.id, + url: provider.api!, + npm: iife(() => { + if (provider.id.startsWith("github-copilot")) return "@ai-sdk/github-copilot" + return model.provider?.npm ?? provider.npm ?? "@ai-sdk/openai-compatible" + }), + }, status: model.status ?? "active", headers: model.headers ?? {}, options: model.options ?? {}, From 4a7809f600f30a08d4ac3afd3ec4fc39f41983f7 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Mon, 19 Jan 2026 00:18:31 -0600 Subject: [PATCH 0021/1177] add proper variant support to copilot --- packages/opencode/src/provider/transform.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index 2cacb61aaf46..f6b7ec8cbcc9 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -349,6 +349,18 @@ export namespace ProviderTransform { case "@ai-sdk/gateway": return Object.fromEntries(OPENAI_EFFORTS.map((effort) => [effort, { reasoningEffort: effort }])) + case "@ai-sdk/github-copilot": + return Object.fromEntries( + WIDELY_SUPPORTED_EFFORTS.map((effort) => [ + effort, + { + reasoningEffort: effort, + reasoningSummary: "auto", + include: ["reasoning.encrypted_content"], + }, + ]), + ) + case "@ai-sdk/cerebras": // https://v5.ai-sdk.dev/providers/ai-sdk-providers/cerebras case "@ai-sdk/togetherai": From 3515b4ff7d21da9f5783df1705ad8fd382a5b7e0 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Mon, 19 Jan 2026 01:06:26 -0600 Subject: [PATCH 0022/1177] omit todo tools for openai models --- packages/opencode/src/tool/registry.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/opencode/src/tool/registry.ts b/packages/opencode/src/tool/registry.ts index faa5f72bcce1..dad9914a289f 100644 --- a/packages/opencode/src/tool/registry.ts +++ b/packages/opencode/src/tool/registry.ts @@ -143,6 +143,11 @@ export namespace ToolRegistry { if (t.id === "apply_patch") return usePatch if (t.id === "edit" || t.id === "write") return !usePatch + // omit todo tools for openai models + if (t.id === "todoread" || t.id === "todowrite") { + if (model.modelID.includes("gpt-")) return false + } + return true }) .map(async (t) => { From 4299450d7d474d350bd06b9e810a5d1250957a00 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Mon, 19 Jan 2026 01:31:30 -0600 Subject: [PATCH 0023/1177] tweak apply_patch tool description --- packages/opencode/src/tool/apply_patch.ts | 3 +- packages/opencode/src/tool/apply_patch.txt | 34 +++++++++++++++++++++- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/packages/opencode/src/tool/apply_patch.ts b/packages/opencode/src/tool/apply_patch.ts index d070eaefa978..7b0ba6150ce4 100644 --- a/packages/opencode/src/tool/apply_patch.ts +++ b/packages/opencode/src/tool/apply_patch.ts @@ -12,13 +12,14 @@ import { assertExternalDirectory } from "./external-directory" import { trimDiff } from "./edit" import { LSP } from "../lsp" import { Filesystem } from "../util/filesystem" +import DESCRIPTION from "./apply_patch.txt" const PatchParams = z.object({ patchText: z.string().describe("The full patch text that describes all changes to be made"), }) export const ApplyPatchTool = Tool.define("apply_patch", { - description: "Use the `apply_patch` tool to edit files. This is a FREEFORM tool, so do not wrap the patch in JSON.", + description: DESCRIPTION, parameters: PatchParams, async execute(params, ctx) { if (!params.patchText) { diff --git a/packages/opencode/src/tool/apply_patch.txt b/packages/opencode/src/tool/apply_patch.txt index 1af0606109f3..e195cd9cb187 100644 --- a/packages/opencode/src/tool/apply_patch.txt +++ b/packages/opencode/src/tool/apply_patch.txt @@ -1 +1,33 @@ -Use the `apply_patch` tool to edit files. This is a FREEFORM tool, so do not wrap the patch in JSON. +Use the `apply_patch` tool to edit files. This is a FREEFORM tool, so do not wrap the patch in JSON. Your patch language is a stripped‑down, file‑oriented diff format designed to be easy to parse and safe to apply. You can think of it as a high‑level envelope: + +*** Begin Patch +[ one or more file sections ] +*** End Patch + +Within that envelope, you get a sequence of file operations. +You MUST include a header to specify the action you are taking. +Each operation starts with one of three headers: + +*** Add File: - create a new file. Every following line is a + line (the initial contents). +*** Delete File: - remove an existing file. Nothing follows. +*** Update File: - patch an existing file in place (optionally with a rename). + +Example patch: + +``` +*** Begin Patch +*** Add File: hello.txt ++Hello world +*** Update File: src/app.py +*** Move to: src/main.py +@@ def greet(): +-print("Hi") ++print("Hello, world!") +*** Delete File: obsolete.txt +*** End Patch +``` + +It is important to remember: + +- You must include a header with your intended action (Add/Delete/Update) +- You must prefix new lines with `+` even when creating a new file From 13276aee8255ea809a975dc70808af08273773f2 Mon Sep 17 00:00:00 2001 From: Slone <50995948+Slone123c@users.noreply.github.com> Date: Mon, 19 Jan 2026 18:32:41 +0800 Subject: [PATCH 0024/1177] fix(desktop): apply getComputedStyle polyfill on all platforms (#9369) --- packages/desktop/src/index.tsx | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/packages/desktop/src/index.tsx b/packages/desktop/src/index.tsx index 0d9e383790a8..6cd77d7d557f 100644 --- a/packages/desktop/src/index.tsx +++ b/packages/desktop/src/index.tsx @@ -26,17 +26,16 @@ if (import.meta.env.DEV && !(root instanceof HTMLElement)) { ) } -const isWindows = ostype() === "windows" -if (isWindows) { - const originalGetComputedStyle = window.getComputedStyle - window.getComputedStyle = ((elt: Element, pseudoElt?: string | null) => { - if (!(elt instanceof Element)) { - // WebView2 can call into Floating UI with non-elements; fall back to a safe element. - return originalGetComputedStyle(document.documentElement, pseudoElt ?? undefined) - } - return originalGetComputedStyle(elt, pseudoElt ?? undefined) - }) as typeof window.getComputedStyle -} +// Floating UI can call getComputedStyle with non-elements (e.g., null refs, virtual elements). +// This happens on all platforms (WebView2 on Windows, WKWebView on macOS), not just Windows. +const originalGetComputedStyle = window.getComputedStyle +window.getComputedStyle = ((elt: Element, pseudoElt?: string | null) => { + if (!(elt instanceof Element)) { + // Fall back to a safe element when a non-element is passed. + return originalGetComputedStyle(document.documentElement, pseudoElt ?? undefined) + } + return originalGetComputedStyle(elt, pseudoElt ?? undefined) +}) as typeof window.getComputedStyle let update: Update | null = null From 08005d755b240dac3ec208aee504a76af7052de7 Mon Sep 17 00:00:00 2001 From: Mani Sundararajan <10191300+itsrainingmani@users.noreply.github.com> Date: Mon, 19 Jan 2026 05:34:40 -0500 Subject: [PATCH 0025/1177] refactor(desktop): tweak share button to prevent layout shift (#9322) --- packages/app/src/components/session/session-header.tsx | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/app/src/components/session/session-header.tsx b/packages/app/src/components/session/session-header.tsx index 96ed762c448f..7cded4bce29c 100644 --- a/packages/app/src/components/session/session-header.tsx +++ b/packages/app/src/components/session/session-header.tsx @@ -244,7 +244,11 @@ export function SessionHeader() { } trigger={ - @@ -293,12 +297,12 @@ export function SessionHeader() { - +