Skip to content

Commit 4721b31

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

File tree

170 files changed

+51600
-46253
lines changed

Some content is hidden

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

170 files changed

+51600
-46253
lines changed

bun.lock

Lines changed: 37 additions & 25 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

nix/hashes.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
{
22
"nodeModules": {
3-
"x86_64-linux": "sha256-285KZ7rZLRoc6XqCZRHc25NE+mmpGh/BVeMpv8aPQtQ=",
4-
"aarch64-linux": "sha256-qIwmY4TP4CI7R7G6A5OMYRrorVNXjkg25tTtVpIHm2o=",
5-
"aarch64-darwin": "sha256-RwvnZQhdYZ0u7h7evyfxuPLHHX9eO/jXTAxIFc8B+IE=",
6-
"x86_64-darwin": "sha256-vVj40al+TEeMpbe5XG2GmJEpN+eQAvtr9W0T98l5PBE="
3+
"x86_64-linux": "sha256-fNRQYkucjXr1D61HJRScJpDa6+oBdyhgTBxCu+PE2kQ=",
4+
"aarch64-linux": "sha256-V8J6kn2nSdXrplyqi6aIqNlHcVjSxvye+yC/YFO7PF4=",
5+
"aarch64-darwin": "sha256-6cLmUJVUycGALCmslXuloVGBSlFOSHRjsWjx7KOW8rg=",
6+
"x86_64-darwin": "sha256-kcOSO3NFIJh79ylLotG41ovWLQfH5kh1WYFghUu+4HE="
77
}
88
}

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
"packages/slack"
2727
],
2828
"catalog": {
29-
"@effect/platform-node": "4.0.0-beta.43",
29+
"@effect/platform-node": "4.0.0-beta.46",
3030
"@types/bun": "1.3.11",
3131
"@types/cross-spawn": "6.0.6",
3232
"@octokit/rest": "22.0.0",
@@ -47,8 +47,8 @@
4747
"dompurify": "3.3.1",
4848
"drizzle-kit": "1.0.0-beta.19-d95b7a4",
4949
"drizzle-orm": "1.0.0-beta.19-d95b7a4",
50-
"effect": "4.0.0-beta.43",
51-
"ai": "6.0.149",
50+
"effect": "4.0.0-beta.46",
51+
"ai": "6.0.158",
5252
"cross-spawn": "7.0.6",
5353
"hono": "4.10.7",
5454
"hono-openapi": "1.1.2",

packages/console/app/src/routes/download/index.css

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -316,7 +316,8 @@
316316

317317
/* Download Hero Section */
318318
[data-component="download-hero"] {
319-
display: grid;
319+
/* display: grid; */
320+
display: none;
320321
grid-template-columns: 260px 1fr;
321322
gap: 4rem;
322323
padding-bottom: 2rem;

packages/opencode/package.json

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,16 @@
3939
"bun": "./src/pty/pty.bun.ts",
4040
"node": "./src/pty/pty.node.ts",
4141
"default": "./src/pty/pty.bun.ts"
42+
},
43+
"#hono": {
44+
"bun": "./src/server/adapter.bun.ts",
45+
"node": "./src/server/adapter.node.ts",
46+
"default": "./src/server/adapter.bun.ts"
4247
}
4348
},
4449
"devDependencies": {
4550
"@babel/core": "7.28.4",
46-
"@effect/language-service": "0.79.0",
51+
"@effect/language-service": "0.84.2",
4752
"@octokit/webhooks-types": "7.6.1",
4853
"@opencode-ai/script": "workspace:*",
4954
"@parcel/watcher-darwin-arm64": "2.5.1",
@@ -78,7 +83,7 @@
7883
"@actions/core": "1.11.1",
7984
"@actions/github": "6.0.1",
8085
"@agentclientprotocol/sdk": "0.16.1",
81-
"@ai-sdk/amazon-bedrock": "4.0.83",
86+
"@ai-sdk/amazon-bedrock": "4.0.93",
8287
"@ai-sdk/anthropic": "3.0.67",
8388
"@ai-sdk/azure": "3.0.49",
8489
"@ai-sdk/cerebras": "2.0.41",
@@ -90,7 +95,7 @@
9095
"@ai-sdk/groq": "3.0.31",
9196
"@ai-sdk/mistral": "3.0.27",
9297
"@ai-sdk/openai": "3.0.48",
93-
"@ai-sdk/openai-compatible": "2.0.37",
98+
"@ai-sdk/openai-compatible": "2.0.41",
9499
"@ai-sdk/perplexity": "3.0.26",
95100
"@ai-sdk/provider": "3.0.8",
96101
"@ai-sdk/provider-utils": "4.0.23",
@@ -100,7 +105,6 @@
100105
"@aws-sdk/credential-providers": "3.993.0",
101106
"@clack/prompts": "1.0.0-alpha.1",
102107
"@effect/platform-node": "catalog:",
103-
"@gitlab/gitlab-ai-provider": "3.6.0",
104108
"@gitlab/opencode-gitlab-auth": "1.3.3",
105109
"@hono/node-server": "1.19.11",
106110
"@hono/node-ws": "1.3.0",
@@ -116,7 +120,7 @@
116120
"@opencode-ai/script": "workspace:*",
117121
"@opencode-ai/sdk": "workspace:*",
118122
"@opencode-ai/util": "workspace:*",
119-
"@openrouter/ai-sdk-provider": "2.4.2",
123+
"@openrouter/ai-sdk-provider": "2.5.1",
120124
"@opentui/core": "0.1.97",
121125
"@opentui/solid": "0.1.97",
122126
"@parcel/watcher": "2.5.1",

packages/opencode/specs/effect-migration.md

Lines changed: 42 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export namespace Foo {
2323
readonly get: (id: FooID) => Effect.Effect<FooInfo, FooError>
2424
}
2525

26-
export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/Foo") {}
26+
export class Service extends Context.Service<Service, Interface>()("@opencode/Foo") {}
2727

2828
export const layer = Layer.effect(
2929
Service,
@@ -219,34 +219,34 @@ Fully migrated (single namespace, InstanceState where needed, flattened facade):
219219
- [x] `Instruction``session/instruction.ts`
220220
- [x] `Provider``provider/provider.ts`
221221
- [x] `Storage``storage/storage.ts`
222+
- [x] `ShareNext``share/share-next.ts`
222223

223224
Still open:
224225

225-
- [ ] `SessionTodo``session/todo.ts`
226-
- [ ] `ShareNext``share/share-next.ts`
226+
- [x] `SessionTodo``session/todo.ts`
227227
- [ ] `SyncEvent``sync/index.ts`
228228
- [ ] `Workspace``control-plane/workspace.ts`
229229

230230
## Tool interface → Effect
231231

232-
Once individual tools are effectified, change `Tool.Info` (`tool/tool.ts`) so `init` and `execute` return `Effect` instead of `Promise`. This lets tool implementations compose natively with the Effect pipeline rather than being wrapped in `Effect.promise()` at the call site. Requires:
232+
`Tool.Def.execute` and `Tool.Info.init` already return `Effect` on this branch. Tool definitions should now stay Effect-native all the way through initialization instead of using Promise-returning init callbacks. Tools can still use lazy init callbacks when they need instance-bound state at init time, but those callbacks should return `Effect`, not `Promise`. Remaining work is:
233233

234-
1. Migrate each tool to return Effects
235-
2. Update `Tool.define()` factory to work with Effects
236-
3. Update `SessionPrompt` to `yield*` tool results instead of `await`ing
234+
1. Migrate each tool body to return Effects
235+
2. Keep `Tool.define()` inputs Effect-native
236+
3. Update remaining callers to `yield*` tool initialization instead of `await`ing
237237

238238
### Tool migration details
239239

240-
Until the tool interface itself returns `Effect`, use this transitional pattern for migrated tools:
240+
With `Tool.Info.init()` now effectful, use this transitional pattern for migrated tools that still need Promise-based boundaries internally:
241241

242242
- `Tool.defineEffect(...)` should `yield*` the services the tool depends on and close over them in the returned tool definition.
243-
- Keep the bridge at the Promise boundary only. Prefer a single `Effect.runPromise(...)` in the temporary `async execute(...)` implementation, and move the inner logic into `Effect.fn(...)` helpers instead of scattering `runPromise` islands through the tool body.
243+
- Keep the bridge at the Promise boundary only inside the tool body when required by external APIs. Do not return Promise-based init callbacks from `Tool.define()`.
244244
- If a tool starts requiring new services, wire them into `ToolRegistry.defaultLayer` so production callers resolve the same dependencies as tests.
245245

246246
Tool tests should use the existing Effect helpers in `packages/opencode/test/lib/effect.ts`:
247247

248248
- Use `testEffect(...)` / `it.live(...)` instead of creating fake local wrappers around effectful tools.
249-
- Yield the real tool export, then initialize it: `const info = yield* ReadTool`, `const tool = yield* Effect.promise(() => info.init())`.
249+
- Yield the real tool export, then initialize it: `const info = yield* ReadTool`, `const tool = yield* info.init()`.
250250
- Run tests inside a real instance with `provideTmpdirInstance(...)` or `provideInstance(tmpdirScoped(...))` so instance-scoped services resolve exactly as they do in production.
251251

252252
This keeps migrated tool tests aligned with the production service graph today, and makes the eventual `Tool.Info``Effect` cleanup mostly mechanical later.
@@ -308,3 +308,35 @@ Current raw fs users that will convert during tool migration:
308308
- [ ] `util/flock.ts` — file-based distributed lock with heartbeat → Effect.repeat + addFinalizer
309309
- [ ] `util/process.ts` — child process spawn wrapper → return Effect instead of Promise
310310
- [ ] `util/lazy.ts` — replace uses in Effect code with Effect.cached; keep for sync-only code
311+
312+
## Destroying the facades
313+
314+
Every service currently exports async facade functions at the bottom of its namespace — `export async function read(...) { return runPromise(...) }` — backed by a per-service `makeRuntime`. These exist because cyclic imports used to force each service to build its own independent runtime. Now that the layer DAG is acyclic and `AppRuntime` (`src/effect/app-runtime.ts`) composes everything into one `ManagedRuntime`, we're removing them.
315+
316+
### Process
317+
318+
For each service, the migration is roughly:
319+
320+
1. **Find callers.** `grep -n "Namespace\.(methodA|methodB|...)"` across `src/` and `test/`. Skip the service file itself.
321+
2. **Migrate production callers.** For each effectful caller that does `Effect.tryPromise(() => Namespace.method(...))`:
322+
- Add the service to the caller's layer R type (`Layer.Layer<Self, never, ... | Namespace.Service>`)
323+
- Yield it at the top of the layer: `const ns = yield* Namespace.Service`
324+
- Replace `Effect.tryPromise(() => Namespace.method(...))` with `yield* ns.method(...)` (or `ns.method(...).pipe(Effect.orElseSucceed(...))` for the common fallback case)
325+
- Add `Layer.provide(Namespace.defaultLayer)` to the caller's own `defaultLayer` chain
326+
3. **Fix tests that used the caller's raw `.layer`.** Any test that composes `Caller.layer` (not `defaultLayer`) needs to also provide the newly-required service tag. The fastest fix is usually switching to `Caller.defaultLayer` since it now pulls in the new dependency.
327+
4. **Migrate test callers of the facade.** Tests calling `Namespace.method(...)` directly get converted to full effectful style using `testEffect(Namespace.defaultLayer)` + `it.live` / `it.effect` + `yield* svc.method(...)`. Don't wrap the test body in `Effect.promise(async () => {...})` — do the whole thing in `Effect.gen` and use `AppFileSystem.Service` / `tmpdirScoped` / `Effect.addFinalizer` for what used to be raw `fs` / `Bun.write` / `try/finally`.
328+
5. **Delete the facades.** Once `grep` shows zero callers, remove the `export async function` block AND the `makeRuntime(...)` line from the service namespace. Also remove the now-unused `import { makeRuntime }`.
329+
330+
### Pitfalls
331+
332+
- **Layer caching inside tests.** `testEffect(layer)` constructs the Storage (or whatever) service once and memoizes it. If a test then tries `inner.pipe(Effect.provide(customStorage))` to swap in a differently-configured Storage, the outer cached one wins and the inner provision is a no-op. Fix: wrap the overriding layer in `Layer.fresh(...)`, which forces a new instance to be built instead of hitting the memoMap cache. This lets a single `testEffect(...)` serve both simple and per-test-customized cases.
333+
- **`Effect.tryPromise``yield*` drops the Promise layer.** The old code was `Effect.tryPromise(() => Storage.read(...))` — a `tryPromise` wrapper because the facade returned a Promise. The new code is `yield* storage.read(...)` directly — the service method already returns an Effect, so no wrapper is needed. Don't reach for `Effect.promise` or `Effect.tryPromise` during migration; if you're using them on a service method call, you're doing it wrong.
334+
- **Raw `.layer` test callers break silently in the type checker.** When you add a new R requirement to a service's `.layer`, any test that composes it raw (not `defaultLayer`) becomes under-specified. `tsgo` will flag this — the error looks like `Type 'Storage.Service' is not assignable to type '... | Service | TestConsole'`. Usually the fix is to switch that composition to `defaultLayer`, or add `Layer.provide(NewDep.defaultLayer)` to the custom composition.
335+
- **Tests that do async setup with `fs`, `Bun.write`, `tmpdir`.** Convert these to `AppFileSystem.Service` calls inside `Effect.gen`, and use `tmpdirScoped()` instead of `tmpdir()` so cleanup happens via the scope finalizer. For file operations on the actual filesystem (not via a service), a small helper like `const writeJson = Effect.fnUntraced(function* (file, value) { const fs = yield* AppFileSystem.Service; yield* fs.makeDirectory(path.dirname(file), { recursive: true }); yield* fs.writeFileString(file, JSON.stringify(value, null, 2)) })` keeps the migration tests clean.
336+
337+
### Migration log
338+
339+
- `SessionStatus` — migrated 2026-04-11. Replaced the last route and retry-policy callers with `AppRuntime.runPromise(SessionStatus.Service.use(...))` and removed the `makeRuntime(...)` facade.
340+
- `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.
341+
- `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.
342+
- `Storage` — migrated 2026-04-10. One production caller (`Session.diff`) and all storage.test.ts tests converted to effectful style. Facades and `makeRuntime` removed.

packages/opencode/src/account/index.ts

Lines changed: 2 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Cache, Clock, Duration, Effect, Layer, Option, Schema, SchemaGetter, ServiceMap } from "effect"
1+
import { Cache, Clock, Duration, Effect, Layer, Option, Schema, SchemaGetter, Context } from "effect"
22
import {
33
FetchHttpClient,
44
HttpClient,
@@ -7,7 +7,6 @@ import {
77
HttpClientResponse,
88
} from "effect/unstable/http"
99

10-
import { makeRuntime } from "@/effect/run-service"
1110
import { withTransientReadRetry } from "@/util/effect-http-client"
1211
import { AccountRepo, type AccountRow } from "./repo"
1312
import { normalizeServerUrl } from "./url"
@@ -181,7 +180,7 @@ export namespace Account {
181180
readonly poll: (input: Login) => Effect.Effect<PollResult, AccountError>
182181
}
183182

184-
export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/Account") {}
183+
export class Service extends Context.Service<Service, Interface>()("@opencode/Account") {}
185184

186185
export const layer: Layer.Layer<Service, never, AccountRepo | HttpClient.HttpClient> = Layer.effect(
187186
Service,
@@ -454,18 +453,4 @@ export namespace Account {
454453
)
455454

456455
export const defaultLayer = layer.pipe(Layer.provide(AccountRepo.layer), Layer.provide(FetchHttpClient.layer))
457-
458-
export const { runPromise } = makeRuntime(Service, defaultLayer)
459-
460-
export async function active(): Promise<Info | undefined> {
461-
return Option.getOrUndefined(await runPromise((service) => service.active()))
462-
}
463-
464-
export async function orgsByAccount(): Promise<readonly AccountOrgs[]> {
465-
return runPromise((service) => service.orgsByAccount())
466-
}
467-
468-
export async function switchOrg(accountID: AccountID, orgID: OrgID) {
469-
return runPromise((service) => service.use(accountID, Option.some(orgID)))
470-
}
471456
}

packages/opencode/src/account/repo.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { eq } from "drizzle-orm"
2-
import { Effect, Layer, Option, Schema, ServiceMap } from "effect"
2+
import { Effect, Layer, Option, Schema, Context } from "effect"
33

44
import { Database } from "@/storage/db"
55
import { AccountStateTable, AccountTable } from "./account.sql"
@@ -38,7 +38,7 @@ export namespace AccountRepo {
3838
}
3939
}
4040

41-
export class AccountRepo extends ServiceMap.Service<AccountRepo, AccountRepo.Service>()("@opencode/AccountRepo") {
41+
export class AccountRepo extends Context.Service<AccountRepo, AccountRepo.Service>()("@opencode/AccountRepo") {
4242
static readonly layer: Layer.Layer<AccountRepo> = Layer.effect(
4343
AccountRepo,
4444
Effect.gen(function* () {

packages/opencode/src/account/schema.ts

Lines changed: 6 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,22 @@
11
import { Schema } from "effect"
22
import type * as HttpClientError from "effect/unstable/http/HttpClientError"
33

4-
import { withStatics } from "@/util/schema"
5-
6-
export const AccountID = Schema.String.pipe(
7-
Schema.brand("AccountID"),
8-
withStatics((s) => ({ make: (id: string) => s.makeUnsafe(id) })),
9-
)
4+
export const AccountID = Schema.String.pipe(Schema.brand("AccountID"))
105
export type AccountID = Schema.Schema.Type<typeof AccountID>
116

12-
export const OrgID = Schema.String.pipe(
13-
Schema.brand("OrgID"),
14-
withStatics((s) => ({ make: (id: string) => s.makeUnsafe(id) })),
15-
)
7+
export const OrgID = Schema.String.pipe(Schema.brand("OrgID"))
168
export type OrgID = Schema.Schema.Type<typeof OrgID>
179

18-
export const AccessToken = Schema.String.pipe(
19-
Schema.brand("AccessToken"),
20-
withStatics((s) => ({ make: (token: string) => s.makeUnsafe(token) })),
21-
)
10+
export const AccessToken = Schema.String.pipe(Schema.brand("AccessToken"))
2211
export type AccessToken = Schema.Schema.Type<typeof AccessToken>
2312

24-
export const RefreshToken = Schema.String.pipe(
25-
Schema.brand("RefreshToken"),
26-
withStatics((s) => ({ make: (token: string) => s.makeUnsafe(token) })),
27-
)
13+
export const RefreshToken = Schema.String.pipe(Schema.brand("RefreshToken"))
2814
export type RefreshToken = Schema.Schema.Type<typeof RefreshToken>
2915

30-
export const DeviceCode = Schema.String.pipe(
31-
Schema.brand("DeviceCode"),
32-
withStatics((s) => ({ make: (code: string) => s.makeUnsafe(code) })),
33-
)
16+
export const DeviceCode = Schema.String.pipe(Schema.brand("DeviceCode"))
3417
export type DeviceCode = Schema.Schema.Type<typeof DeviceCode>
3518

36-
export const UserCode = Schema.String.pipe(
37-
Schema.brand("UserCode"),
38-
withStatics((s) => ({ make: (code: string) => s.makeUnsafe(code) })),
39-
)
19+
export const UserCode = Schema.String.pipe(Schema.brand("UserCode"))
4020
export type UserCode = Schema.Schema.Type<typeof UserCode>
4121

4222
export class Info extends Schema.Class<Info>("Account")({

packages/opencode/src/agent/agent.ts

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import { Global } from "@/global"
1919
import path from "path"
2020
import { Plugin } from "@/plugin"
2121
import { Skill } from "../skill"
22-
import { Effect, ServiceMap, Layer } from "effect"
22+
import { Effect, Context, Layer } from "effect"
2323
import { InstanceState } from "@/effect/instance-state"
2424
import { makeRuntime } from "@/effect/run-service"
2525

@@ -67,7 +67,7 @@ export namespace Agent {
6767

6868
type State = Omit<Interface, "generate">
6969

70-
export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/Agent") {}
70+
export class Service extends Context.Service<Service, Interface>()("@opencode/Agent") {}
7171

7272
export const layer = Layer.effect(
7373
Service,
@@ -398,13 +398,11 @@ export namespace Agent {
398398
}),
399399
)
400400

401-
export const defaultLayer = Layer.suspend(() =>
402-
layer.pipe(
403-
Layer.provide(Provider.defaultLayer),
404-
Layer.provide(Auth.defaultLayer),
405-
Layer.provide(Config.defaultLayer),
406-
Layer.provide(Skill.defaultLayer),
407-
),
401+
export const defaultLayer = layer.pipe(
402+
Layer.provide(Provider.defaultLayer),
403+
Layer.provide(Auth.defaultLayer),
404+
Layer.provide(Config.defaultLayer),
405+
Layer.provide(Skill.defaultLayer),
408406
)
409407

410408
const { runPromise } = makeRuntime(Service, defaultLayer)

0 commit comments

Comments
 (0)