Skip to content

Commit 699563e

Browse files
authored
Merge branch 'dev' into snapshot-node-shim-stuff
2 parents 4721b31 + 8ffadde commit 699563e

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+1452
-67140
lines changed

.github/VOUCHED.td

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ kommander
2626
r44vc0rp
2727
rekram1-node
2828
-robinmordasiewicz
29+
simonklee
2930
-spider-yamet clawdbot/llm psychosis, spam pinging the team
3031
thdxr
3132
-toastythebot

.github/workflows/publish.yml

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ jobs:
114114
- build-cli
115115
- version
116116
runs-on: blacksmith-4vcpu-windows-2025
117-
if: github.repository == 'anomalyco/opencode' && github.ref_name != 'beta'
117+
if: github.repository == 'anomalyco/opencode'
118118
env:
119119
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
120120
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
@@ -213,7 +213,6 @@ jobs:
213213
needs:
214214
- build-cli
215215
- version
216-
if: github.ref_name != 'beta'
217216
continue-on-error: false
218217
env:
219218
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
@@ -390,7 +389,7 @@ jobs:
390389
needs:
391390
- build-cli
392391
- version
393-
if: github.repository == 'anomalyco/opencode' && github.ref_name != 'beta'
392+
if: github.repository == 'anomalyco/opencode'
394393
continue-on-error: false
395394
env:
396395
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
@@ -591,13 +590,12 @@ jobs:
591590
path: packages/opencode/dist
592591

593592
- uses: actions/download-artifact@v4
594-
if: github.ref_name != 'beta'
595593
with:
596594
name: opencode-cli-signed-windows
597595
path: packages/opencode/dist
598596

599597
- uses: actions/download-artifact@v4
600-
if: needs.version.outputs.release && github.ref_name != 'beta'
598+
if: needs.version.outputs.release
601599
with:
602600
pattern: latest-yml-*
603601
path: /tmp/latest-yml

packages/opencode/package.json

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,18 +14,11 @@
1414
"fix-node-pty": "bun run script/fix-node-pty.ts",
1515
"upgrade-opentui": "bun run script/upgrade-opentui.ts",
1616
"dev": "bun run --conditions=browser ./src/index.ts",
17-
"random": "echo 'Random script updated at $(date)' && echo 'Change queued successfully' && echo 'Another change made' && echo 'Yet another change' && echo 'One more change' && echo 'Final change' && echo 'Another final change' && echo 'Yet another final change'",
18-
"clean": "echo 'Cleaning up...' && rm -rf node_modules dist",
19-
"lint": "echo 'Running lint checks...' && bun test --coverage",
20-
"format": "echo 'Formatting code...' && bun run --prettier --write src/**/*.ts",
21-
"docs": "echo 'Generating documentation...' && find src -name '*.ts' -exec echo 'Processing: {}' \\;",
22-
"deploy": "echo 'Deploying application...' && bun run build && echo 'Deployment completed successfully'",
2317
"db": "bun drizzle-kit"
2418
},
2519
"bin": {
2620
"opencode": "./bin/opencode"
2721
},
28-
"randomField": "this-is-a-random-value-12345",
2922
"exports": {
3023
"./*": "./src/*.ts"
3124
},

packages/opencode/specs/effect-migration.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,7 @@ Fully migrated (single namespace, InstanceState where needed, flattened facade):
217217
- [x] `SessionSummary``session/summary.ts`
218218
- [x] `SessionRevert``session/revert.ts`
219219
- [x] `Instruction``session/instruction.ts`
220+
- [x] `SystemPrompt``session/system.ts`
220221
- [x] `Provider``provider/provider.ts`
221222
- [x] `Storage``storage/storage.ts`
222223
- [x] `ShareNext``share/share-next.ts`
@@ -340,3 +341,47 @@ For each service, the migration is roughly:
340341
- `ShareNext` — migrated 2026-04-11. Swapped remaining async callers to `AppRuntime.runPromise(ShareNext.Service.use(...))`, removed the `makeRuntime(...)` facade, and kept instance bootstrap on the shared app runtime.
341342
- `SessionTodo` — migrated 2026-04-10. Already matched the target service shape in `session/todo.ts`: single namespace, traced Effect methods, and no `makeRuntime(...)` facade remained; checklist updated to reflect the completed migration.
342343
- `Storage` — migrated 2026-04-10. One production caller (`Session.diff`) and all storage.test.ts tests converted to effectful style. Facades and `makeRuntime` removed.
344+
- `SessionRunState` — migrated 2026-04-11. Single caller in `server/routes/session.ts` converted; facade removed.
345+
- `Account` — migrated 2026-04-11. Callers in `server/routes/experimental.ts` and `cli/cmd/account.ts` converted; facade removed.
346+
- `Instruction` — migrated 2026-04-11. Test-only callers converted; facade removed.
347+
- `FileTime` — migrated 2026-04-11. Test-only callers converted; facade removed.
348+
- `FileWatcher` — migrated 2026-04-11. Callers in `project/bootstrap.ts` and test converted; facade removed.
349+
- `Question` — migrated 2026-04-11. Callers in `server/routes/question.ts` and test converted; facade removed.
350+
- `Truncate` — migrated 2026-04-11. Caller in `tool/tool.ts` and test converted; facade removed.
351+
352+
## Route handler effectification
353+
354+
Route handlers should wrap their entire body in a single `AppRuntime.runPromise(Effect.gen(...))` call, yielding services from context rather than calling facades one-by-one. This eliminates multiple `runPromise` round-trips and lets handlers compose naturally.
355+
356+
```ts
357+
// Before — one facade call per service
358+
;async (c) => {
359+
await SessionRunState.assertNotBusy(id)
360+
await Session.removeMessage({ sessionID: id, messageID })
361+
return c.json(true)
362+
}
363+
364+
// After — one Effect.gen, yield services from context
365+
;async (c) => {
366+
await AppRuntime.runPromise(
367+
Effect.gen(function* () {
368+
const state = yield* SessionRunState.Service
369+
const session = yield* Session.Service
370+
yield* state.assertNotBusy(id)
371+
yield* session.removeMessage({ sessionID: id, messageID })
372+
}),
373+
)
374+
return c.json(true)
375+
}
376+
```
377+
378+
When migrating, always use `{ concurrency: "unbounded" }` with `Effect.all` — route handlers should run independent service calls in parallel, not sequentially.
379+
380+
Route files to convert (each handler that calls facades should be wrapped):
381+
382+
- [ ] `server/routes/session.ts` — heaviest; uses Session, SessionPrompt, SessionRevert, SessionCompaction, SessionShare, SessionSummary, SessionRunState, Agent, Permission, Bus
383+
- [ ] `server/routes/global.ts` — uses Config, Project, Provider, Vcs, Snapshot, Agent
384+
- [ ] `server/routes/provider.ts` — uses Provider, Auth, Config
385+
- [ ] `server/routes/question.ts` — uses Question
386+
- [ ] `server/routes/pty.ts` — uses Pty
387+
- [ ] `server/routes/experimental.ts` — uses Account, ToolRegistry, Agent, MCP, Config
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import { Layer, ManagedRuntime } from "effect"
22
import { memoMap } from "./run-service"
33

4+
import { FileWatcher } from "@/file/watcher"
45
import { Format } from "@/format"
56
import { ShareNext } from "@/share/share-next"
67

7-
export const BootstrapLayer = Layer.mergeAll(Format.defaultLayer, ShareNext.defaultLayer)
8+
export const BootstrapLayer = Layer.mergeAll(Format.defaultLayer, ShareNext.defaultLayer, FileWatcher.defaultLayer)
89

910
export const BootstrapRuntime = ManagedRuntime.make(BootstrapLayer, { memoMap })

packages/opencode/src/file/time.ts

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { DateTime, Effect, Layer, Option, Semaphore, Context } from "effect"
22
import { InstanceState } from "@/effect/instance-state"
3-
import { makeRuntime } from "@/effect/run-service"
43
import { AppFileSystem } from "@/filesystem"
54
import { Flag } from "@/flag/flag"
65
import type { SessionID } from "@/session/schema"
@@ -112,22 +111,4 @@ export namespace FileTime {
112111
).pipe(Layer.orDie)
113112

114113
export const defaultLayer = layer.pipe(Layer.provide(AppFileSystem.defaultLayer))
115-
116-
const { runPromise } = makeRuntime(Service, defaultLayer)
117-
118-
export function read(sessionID: SessionID, file: string) {
119-
return runPromise((s) => s.read(sessionID, file))
120-
}
121-
122-
export function get(sessionID: SessionID, file: string) {
123-
return runPromise((s) => s.get(sessionID, file))
124-
}
125-
126-
export async function assert(sessionID: SessionID, filepath: string) {
127-
return runPromise((s) => s.assert(sessionID, filepath))
128-
}
129-
130-
export async function withLock<T>(filepath: string, fn: () => Promise<T>): Promise<T> {
131-
return runPromise((s) => s.withLock(filepath, () => Effect.promise(fn)))
132-
}
133114
}

packages/opencode/src/file/watcher.ts

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import z from "zod"
88
import { Bus } from "@/bus"
99
import { BusEvent } from "@/bus/bus-event"
1010
import { InstanceState } from "@/effect/instance-state"
11-
import { makeRuntime } from "@/effect/run-service"
1211
import { Flag } from "@/flag/flag"
1312
import { Git } from "@/git"
1413
import { Instance } from "@/project/instance"
@@ -161,10 +160,4 @@ export namespace FileWatcher {
161160
)
162161

163162
export const defaultLayer = layer.pipe(Layer.provide(Config.defaultLayer), Layer.provide(Git.defaultLayer))
164-
165-
const { runPromise } = makeRuntime(Service, defaultLayer)
166-
167-
export function init() {
168-
return runPromise((svc) => svc.init())
169-
}
170163
}

packages/opencode/src/plugin/github-copilot/copilot.ts

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { iife } from "@/util/iife"
55
import { Log } from "../../util/log"
66
import { setTimeout as sleep } from "node:timers/promises"
77
import { CopilotModels } from "./models"
8+
import { MessageV2 } from "@/session/message-v2"
89

910
const log = Log.create({ service: "plugin.copilot" })
1011

@@ -27,11 +28,27 @@ function base(enterpriseUrl?: string) {
2728
return enterpriseUrl ? `https://copilot-api.${normalizeDomain(enterpriseUrl)}` : "https://api.githubcopilot.com"
2829
}
2930

30-
function fix(model: Model): Model {
31+
// Check if a message is a synthetic user msg used to attach an image from a tool call
32+
function imgMsg(msg: any): boolean {
33+
if (msg?.role !== "user") return false
34+
35+
// Handle the 3 api formats
36+
37+
const content = msg.content
38+
if (typeof content === "string") return content === MessageV2.SYNTHETIC_ATTACHMENT_PROMPT
39+
if (!Array.isArray(content)) return false
40+
return content.some(
41+
(part: any) =>
42+
(part?.type === "text" || part?.type === "input_text") && part.text === MessageV2.SYNTHETIC_ATTACHMENT_PROMPT,
43+
)
44+
}
45+
46+
function fix(model: Model, url: string): Model {
3147
return {
3248
...model,
3349
api: {
3450
...model.api,
51+
url,
3552
npm: "@ai-sdk/github-copilot",
3653
},
3754
}
@@ -44,19 +61,23 @@ export async function CopilotAuthPlugin(input: PluginInput): Promise<Hooks> {
4461
id: "github-copilot",
4562
async models(provider, ctx) {
4663
if (ctx.auth?.type !== "oauth") {
47-
return Object.fromEntries(Object.entries(provider.models).map(([id, model]) => [id, fix(model)]))
64+
return Object.fromEntries(Object.entries(provider.models).map(([id, model]) => [id, fix(model, base())]))
4865
}
4966

67+
const auth = ctx.auth
68+
5069
return CopilotModels.get(
51-
base(ctx.auth.enterpriseUrl),
70+
base(auth.enterpriseUrl),
5271
{
53-
Authorization: `Bearer ${ctx.auth.refresh}`,
72+
Authorization: `Bearer ${auth.refresh}`,
5473
"User-Agent": `opencode/${Installation.VERSION}`,
5574
},
5675
provider.models,
5776
).catch((error) => {
5877
log.error("failed to fetch copilot models", { error })
59-
return Object.fromEntries(Object.entries(provider.models).map(([id, model]) => [id, fix(model)]))
78+
return Object.fromEntries(
79+
Object.entries(provider.models).map(([id, model]) => [id, fix(model, base(auth.enterpriseUrl))]),
80+
)
6081
})
6182
},
6283
},
@@ -66,10 +87,7 @@ export async function CopilotAuthPlugin(input: PluginInput): Promise<Hooks> {
6687
const info = await getAuth()
6788
if (!info || info.type !== "oauth") return {}
6889

69-
const baseURL = base(info.enterpriseUrl)
70-
7190
return {
72-
baseURL,
7391
apiKey: "",
7492
async fetch(request: RequestInfo | URL, init?: RequestInit) {
7593
const info = await getAuth()
@@ -88,7 +106,7 @@ export async function CopilotAuthPlugin(input: PluginInput): Promise<Hooks> {
88106
(msg: any) =>
89107
Array.isArray(msg.content) && msg.content.some((part: any) => part.type === "image_url"),
90108
),
91-
isAgent: last?.role !== "user",
109+
isAgent: last?.role !== "user" || imgMsg(last),
92110
}
93111
}
94112

@@ -100,7 +118,7 @@ export async function CopilotAuthPlugin(input: PluginInput): Promise<Hooks> {
100118
(item: any) =>
101119
Array.isArray(item?.content) && item.content.some((part: any) => part.type === "input_image"),
102120
),
103-
isAgent: last?.role !== "user",
121+
isAgent: last?.role !== "user" || imgMsg(last),
104122
}
105123
}
106124

@@ -122,7 +140,7 @@ export async function CopilotAuthPlugin(input: PluginInput): Promise<Hooks> {
122140
part.content.some((nested: any) => nested?.type === "image")),
123141
),
124142
),
125-
isAgent: !(last?.role === "user" && hasNonToolCalls),
143+
isAgent: !(last?.role === "user" && hasNonToolCalls) || imgMsg(last),
126144
}
127145
}
128146
} catch {}

packages/opencode/src/plugin/github-copilot/models.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,13 +52,15 @@ export namespace CopilotModels {
5252
(remote.capabilities.supports.vision ?? false) ||
5353
(remote.capabilities.limits.vision?.supported_media_types ?? []).some((item) => item.startsWith("image/"))
5454

55+
const isMsgApi = remote.supported_endpoints?.includes("/v1/messages")
56+
5557
return {
5658
id: key,
5759
providerID: "github-copilot",
5860
api: {
5961
id: remote.id,
60-
url,
61-
npm: "@ai-sdk/github-copilot",
62+
url: isMsgApi ? `${url}/v1` : url,
63+
npm: isMsgApi ? "@ai-sdk/anthropic" : "@ai-sdk/github-copilot",
6264
},
6365
// API response wins
6466
status: "active",

packages/opencode/src/project/bootstrap.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import { Plugin } from "../plugin"
22
import { Format } from "../format"
33
import { LSP } from "../lsp"
44
import { File } from "../file"
5-
import { FileWatcher } from "../file/watcher"
65
import { Snapshot } from "../snapshot"
76
import { Project } from "./project"
87
import { Vcs } from "./vcs"
@@ -11,6 +10,7 @@ import { Command } from "../command"
1110
import { Instance } from "./instance"
1211
import { Log } from "@/util/log"
1312
import { BootstrapRuntime } from "@/effect/bootstrap-runtime"
13+
import { FileWatcher } from "@/file/watcher"
1414
import { ShareNext } from "@/share/share-next"
1515

1616
export async function InstanceBootstrap() {
@@ -20,7 +20,7 @@ export async function InstanceBootstrap() {
2020
void BootstrapRuntime.runPromise(Format.Service.use((svc) => svc.init()))
2121
await LSP.init()
2222
File.init()
23-
FileWatcher.init()
23+
void BootstrapRuntime.runPromise(FileWatcher.Service.use((svc) => svc.init()))
2424
Vcs.init()
2525
Snapshot.init()
2626

0 commit comments

Comments
 (0)