Skip to content

Commit d19370d

Browse files
authored
Merge branch 'dev' into fix/stats-day-filter
2 parents 18823bc + 2719063 commit d19370d

155 files changed

Lines changed: 3244 additions & 3117 deletions

File tree

Some content is hidden

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

bun.lock

Lines changed: 16 additions & 7 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-gFbo3B6TFAmin2marXlwUyfchTX6ogsaUFEzBIl4zaI=",
4+
"aarch64-linux": "sha256-HUKL7zBVtb1KPaoAgfSfAzjDoAPRUe2WNFHDrsoqEF8=",
5+
"aarch64-darwin": "sha256-qWPRkuVA3nDEEaVZ0Ex4sYsFFarSRJSyOn+KJm1D3U0=",
6+
"x86_64-darwin": "sha256-FxhOYMXkxjn/9xQPeVX/gfQT/KjHT4wIBqzVDZuYlos="
77
}
88
}

package.json

Lines changed: 2 additions & 2 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,7 +47,7 @@
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",
50+
"effect": "4.0.0-beta.46",
5151
"ai": "6.0.149",
5252
"cross-spawn": "7.0.6",
5353
"hono": "4.10.7",

packages/opencode/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
},
4444
"devDependencies": {
4545
"@babel/core": "7.28.4",
46-
"@effect/language-service": "0.79.0",
46+
"@effect/language-service": "0.84.2",
4747
"@octokit/webhooks-types": "7.6.1",
4848
"@opencode-ai/script": "workspace:*",
4949
"@parcel/watcher-darwin-arm64": "2.5.1",

packages/opencode/specs/effect-migration.md

Lines changed: 35 additions & 3 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,11 +219,11 @@ 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

@@ -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 & 2 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,
@@ -181,7 +181,7 @@ export namespace Account {
181181
readonly poll: (input: Login) => Effect.Effect<PollResult, AccountError>
182182
}
183183

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

186186
export const layer: Layer.Layer<Service, never, AccountRepo | HttpClient.HttpClient> = Layer.effect(
187187
Service,

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: 2 additions & 2 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,

packages/opencode/src/auth/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import path from "path"
2-
import { Effect, Layer, Record, Result, Schema, ServiceMap } from "effect"
2+
import { Effect, Layer, Record, Result, Schema, Context } from "effect"
33
import { makeRuntime } from "@/effect/run-service"
44
import { zod } from "@/util/effect-zod"
55
import { Global } from "../global"
@@ -49,7 +49,7 @@ export namespace Auth {
4949
readonly remove: (key: string) => Effect.Effect<void, AuthError>
5050
}
5151

52-
export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/Auth") {}
52+
export class Service extends Context.Service<Service, Interface>()("@opencode/Auth") {}
5353

5454
export const layer = Layer.effect(
5555
Service,

0 commit comments

Comments
 (0)