Merge branch 'dev' into kit/lsp-effect-refactor

This commit is contained in:
Kit Langton 2026-04-17 20:43:38 -04:00 committed by GitHub
commit 370e47a5d0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
30 changed files with 322 additions and 295 deletions

View file

@ -29,7 +29,7 @@
}, },
"packages/app": { "packages/app": {
"name": "@opencode-ai/app", "name": "@opencode-ai/app",
"version": "1.4.10", "version": "1.4.11",
"dependencies": { "dependencies": {
"@kobalte/core": "catalog:", "@kobalte/core": "catalog:",
"@opencode-ai/sdk": "workspace:*", "@opencode-ai/sdk": "workspace:*",
@ -83,7 +83,7 @@
}, },
"packages/console/app": { "packages/console/app": {
"name": "@opencode-ai/console-app", "name": "@opencode-ai/console-app",
"version": "1.4.10", "version": "1.4.11",
"dependencies": { "dependencies": {
"@cloudflare/vite-plugin": "1.15.2", "@cloudflare/vite-plugin": "1.15.2",
"@ibm/plex": "6.4.1", "@ibm/plex": "6.4.1",
@ -117,7 +117,7 @@
}, },
"packages/console/core": { "packages/console/core": {
"name": "@opencode-ai/console-core", "name": "@opencode-ai/console-core",
"version": "1.4.10", "version": "1.4.11",
"dependencies": { "dependencies": {
"@aws-sdk/client-sts": "3.782.0", "@aws-sdk/client-sts": "3.782.0",
"@jsx-email/render": "1.1.1", "@jsx-email/render": "1.1.1",
@ -144,7 +144,7 @@
}, },
"packages/console/function": { "packages/console/function": {
"name": "@opencode-ai/console-function", "name": "@opencode-ai/console-function",
"version": "1.4.10", "version": "1.4.11",
"dependencies": { "dependencies": {
"@ai-sdk/anthropic": "3.0.64", "@ai-sdk/anthropic": "3.0.64",
"@ai-sdk/openai": "3.0.48", "@ai-sdk/openai": "3.0.48",
@ -168,7 +168,7 @@
}, },
"packages/console/mail": { "packages/console/mail": {
"name": "@opencode-ai/console-mail", "name": "@opencode-ai/console-mail",
"version": "1.4.10", "version": "1.4.11",
"dependencies": { "dependencies": {
"@jsx-email/all": "2.2.3", "@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3", "@jsx-email/cli": "1.4.3",
@ -192,7 +192,7 @@
}, },
"packages/desktop": { "packages/desktop": {
"name": "@opencode-ai/desktop", "name": "@opencode-ai/desktop",
"version": "1.4.10", "version": "1.4.11",
"dependencies": { "dependencies": {
"@opencode-ai/app": "workspace:*", "@opencode-ai/app": "workspace:*",
"@opencode-ai/ui": "workspace:*", "@opencode-ai/ui": "workspace:*",
@ -225,7 +225,7 @@
}, },
"packages/desktop-electron": { "packages/desktop-electron": {
"name": "@opencode-ai/desktop-electron", "name": "@opencode-ai/desktop-electron",
"version": "1.4.10", "version": "1.4.11",
"dependencies": { "dependencies": {
"effect": "catalog:", "effect": "catalog:",
"electron-context-menu": "4.1.2", "electron-context-menu": "4.1.2",
@ -268,7 +268,7 @@
}, },
"packages/enterprise": { "packages/enterprise": {
"name": "@opencode-ai/enterprise", "name": "@opencode-ai/enterprise",
"version": "1.4.10", "version": "1.4.11",
"dependencies": { "dependencies": {
"@opencode-ai/shared": "workspace:*", "@opencode-ai/shared": "workspace:*",
"@opencode-ai/ui": "workspace:*", "@opencode-ai/ui": "workspace:*",
@ -297,7 +297,7 @@
}, },
"packages/function": { "packages/function": {
"name": "@opencode-ai/function", "name": "@opencode-ai/function",
"version": "1.4.10", "version": "1.4.11",
"dependencies": { "dependencies": {
"@octokit/auth-app": "8.0.1", "@octokit/auth-app": "8.0.1",
"@octokit/rest": "catalog:", "@octokit/rest": "catalog:",
@ -313,7 +313,7 @@
}, },
"packages/opencode": { "packages/opencode": {
"name": "opencode", "name": "opencode",
"version": "1.4.10", "version": "1.4.11",
"bin": { "bin": {
"opencode": "./bin/opencode", "opencode": "./bin/opencode",
}, },
@ -458,7 +458,7 @@
}, },
"packages/plugin": { "packages/plugin": {
"name": "@opencode-ai/plugin", "name": "@opencode-ai/plugin",
"version": "1.4.10", "version": "1.4.11",
"dependencies": { "dependencies": {
"@opencode-ai/sdk": "workspace:*", "@opencode-ai/sdk": "workspace:*",
"effect": "catalog:", "effect": "catalog:",
@ -493,7 +493,7 @@
}, },
"packages/sdk/js": { "packages/sdk/js": {
"name": "@opencode-ai/sdk", "name": "@opencode-ai/sdk",
"version": "1.4.10", "version": "1.4.11",
"dependencies": { "dependencies": {
"cross-spawn": "catalog:", "cross-spawn": "catalog:",
}, },
@ -508,7 +508,7 @@
}, },
"packages/shared": { "packages/shared": {
"name": "@opencode-ai/shared", "name": "@opencode-ai/shared",
"version": "1.4.10", "version": "1.4.11",
"bin": { "bin": {
"opencode": "./bin/opencode", "opencode": "./bin/opencode",
}, },
@ -532,7 +532,7 @@
}, },
"packages/slack": { "packages/slack": {
"name": "@opencode-ai/slack", "name": "@opencode-ai/slack",
"version": "1.4.10", "version": "1.4.11",
"dependencies": { "dependencies": {
"@opencode-ai/sdk": "workspace:*", "@opencode-ai/sdk": "workspace:*",
"@slack/bolt": "^3.17.1", "@slack/bolt": "^3.17.1",
@ -567,7 +567,7 @@
}, },
"packages/ui": { "packages/ui": {
"name": "@opencode-ai/ui", "name": "@opencode-ai/ui",
"version": "1.4.10", "version": "1.4.11",
"dependencies": { "dependencies": {
"@kobalte/core": "catalog:", "@kobalte/core": "catalog:",
"@opencode-ai/sdk": "workspace:*", "@opencode-ai/sdk": "workspace:*",
@ -616,7 +616,7 @@
}, },
"packages/web": { "packages/web": {
"name": "@opencode-ai/web", "name": "@opencode-ai/web",
"version": "1.4.10", "version": "1.4.11",
"dependencies": { "dependencies": {
"@astrojs/cloudflare": "12.6.3", "@astrojs/cloudflare": "12.6.3",
"@astrojs/markdown-remark": "6.3.1", "@astrojs/markdown-remark": "6.3.1",

View file

@ -1,6 +1,6 @@
{ {
"name": "@opencode-ai/app", "name": "@opencode-ai/app",
"version": "1.4.10", "version": "1.4.11",
"description": "", "description": "",
"type": "module", "type": "module",
"exports": { "exports": {

View file

@ -1,6 +1,6 @@
{ {
"name": "@opencode-ai/console-app", "name": "@opencode-ai/console-app",
"version": "1.4.10", "version": "1.4.11",
"type": "module", "type": "module",
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {

View file

@ -1,7 +1,7 @@
{ {
"$schema": "https://json.schemastore.org/package.json", "$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/console-core", "name": "@opencode-ai/console-core",
"version": "1.4.10", "version": "1.4.11",
"private": true, "private": true,
"type": "module", "type": "module",
"license": "MIT", "license": "MIT",

View file

@ -1,6 +1,6 @@
{ {
"name": "@opencode-ai/console-function", "name": "@opencode-ai/console-function",
"version": "1.4.10", "version": "1.4.11",
"$schema": "https://json.schemastore.org/package.json", "$schema": "https://json.schemastore.org/package.json",
"private": true, "private": true,
"type": "module", "type": "module",

View file

@ -1,6 +1,6 @@
{ {
"name": "@opencode-ai/console-mail", "name": "@opencode-ai/console-mail",
"version": "1.4.10", "version": "1.4.11",
"dependencies": { "dependencies": {
"@jsx-email/all": "2.2.3", "@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3", "@jsx-email/cli": "1.4.3",

View file

@ -1,7 +1,7 @@
{ {
"name": "@opencode-ai/desktop-electron", "name": "@opencode-ai/desktop-electron",
"private": true, "private": true,
"version": "1.4.10", "version": "1.4.11",
"type": "module", "type": "module",
"license": "MIT", "license": "MIT",
"homepage": "https://opencode.ai", "homepage": "https://opencode.ai",

View file

@ -1,7 +1,7 @@
{ {
"name": "@opencode-ai/desktop", "name": "@opencode-ai/desktop",
"private": true, "private": true,
"version": "1.4.10", "version": "1.4.11",
"type": "module", "type": "module",
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {

View file

@ -1,6 +1,6 @@
{ {
"name": "@opencode-ai/enterprise", "name": "@opencode-ai/enterprise",
"version": "1.4.10", "version": "1.4.11",
"private": true, "private": true,
"type": "module", "type": "module",
"license": "MIT", "license": "MIT",

View file

@ -1,7 +1,7 @@
id = "opencode" id = "opencode"
name = "OpenCode" name = "OpenCode"
description = "The open source coding agent." description = "The open source coding agent."
version = "1.4.10" version = "1.4.11"
schema_version = 1 schema_version = 1
authors = ["Anomaly"] authors = ["Anomaly"]
repository = "https://github.com/anomalyco/opencode" repository = "https://github.com/anomalyco/opencode"
@ -11,26 +11,26 @@ name = "OpenCode"
icon = "./icons/opencode.svg" icon = "./icons/opencode.svg"
[agent_servers.opencode.targets.darwin-aarch64] [agent_servers.opencode.targets.darwin-aarch64]
archive = "https://github.com/anomalyco/opencode/releases/download/v1.4.10/opencode-darwin-arm64.zip" archive = "https://github.com/anomalyco/opencode/releases/download/v1.4.11/opencode-darwin-arm64.zip"
cmd = "./opencode" cmd = "./opencode"
args = ["acp"] args = ["acp"]
[agent_servers.opencode.targets.darwin-x86_64] [agent_servers.opencode.targets.darwin-x86_64]
archive = "https://github.com/anomalyco/opencode/releases/download/v1.4.10/opencode-darwin-x64.zip" archive = "https://github.com/anomalyco/opencode/releases/download/v1.4.11/opencode-darwin-x64.zip"
cmd = "./opencode" cmd = "./opencode"
args = ["acp"] args = ["acp"]
[agent_servers.opencode.targets.linux-aarch64] [agent_servers.opencode.targets.linux-aarch64]
archive = "https://github.com/anomalyco/opencode/releases/download/v1.4.10/opencode-linux-arm64.tar.gz" archive = "https://github.com/anomalyco/opencode/releases/download/v1.4.11/opencode-linux-arm64.tar.gz"
cmd = "./opencode" cmd = "./opencode"
args = ["acp"] args = ["acp"]
[agent_servers.opencode.targets.linux-x86_64] [agent_servers.opencode.targets.linux-x86_64]
archive = "https://github.com/anomalyco/opencode/releases/download/v1.4.10/opencode-linux-x64.tar.gz" archive = "https://github.com/anomalyco/opencode/releases/download/v1.4.11/opencode-linux-x64.tar.gz"
cmd = "./opencode" cmd = "./opencode"
args = ["acp"] args = ["acp"]
[agent_servers.opencode.targets.windows-x86_64] [agent_servers.opencode.targets.windows-x86_64]
archive = "https://github.com/anomalyco/opencode/releases/download/v1.4.10/opencode-windows-x64.zip" archive = "https://github.com/anomalyco/opencode/releases/download/v1.4.11/opencode-windows-x64.zip"
cmd = "./opencode.exe" cmd = "./opencode.exe"
args = ["acp"] args = ["acp"]

View file

@ -1,6 +1,6 @@
{ {
"name": "@opencode-ai/function", "name": "@opencode-ai/function",
"version": "1.4.10", "version": "1.4.11",
"$schema": "https://json.schemastore.org/package.json", "$schema": "https://json.schemastore.org/package.json",
"private": true, "private": true,
"type": "module", "type": "module",

View file

@ -1,6 +1,6 @@
{ {
"$schema": "https://json.schemastore.org/package.json", "$schema": "https://json.schemastore.org/package.json",
"version": "1.4.10", "version": "1.4.11",
"name": "opencode", "name": "opencode",
"type": "module", "type": "module",
"license": "MIT", "license": "MIT",

View file

@ -1,12 +1,13 @@
# Facade removal checklist # Facade removal checklist
Concrete inventory of the remaining `makeRuntime(...)`-backed service facades in `packages/opencode`. Concrete inventory of the remaining `makeRuntime(...)`-backed facades in `packages/opencode`.
As of 2026-04-13, latest `origin/dev`: Current status on this branch:
- `src/` still has 15 `makeRuntime(...)` call sites. - `src/` has 5 `makeRuntime(...)` call sites total.
- 13 of those are still in scope for facade removal. - 2 are intentionally excluded from this checklist: `src/bus/index.ts` and `src/effect/cross-spawn-spawner.ts`.
- 2 are excluded from this checklist: `bus/index.ts` and `effect/cross-spawn-spawner.ts`. - 1 is tracked primarily by the instance-context migration rather than facade removal: `src/project/instance.ts`.
- That leaves 2 live runtime-backed service facades still worth tracking here: `src/npm/index.ts` and `src/cli/cmd/tui/config/tui.ts`.
Recent progress: Recent progress:
@ -15,8 +16,9 @@ Recent progress:
## Priority hotspots ## Priority hotspots
- `server/instance/session.ts` still depends on `Session`, `SessionPrompt`, `SessionRevert`, `SessionCompaction`, `SessionSummary`, `ShareSession`, `Agent`, and `Permission` facades. - `src/cli/cmd/tui/config/tui.ts` still exports `makeRuntime(...)` plus async facade helpers for `get()` and `waitForDependencies()`.
- `src/effect/app-runtime.ts` still references many facade namespaces directly, so it should stay in view during each deletion. - `src/npm/index.ts` still exports `makeRuntime(...)` plus async facade helpers for `install()`, `add()`, `outdated()`, and `which()`.
- `src/project/instance.ts` still uses a dedicated runtime for project boot, but that file is really part of the broader legacy instance-context transition tracked in `instance-context.md`.
## Completed Batches ## Completed Batches
@ -184,53 +186,34 @@ These were the recurring mistakes and useful corrections from the first two batc
5. For CLI readability, extract file-local preload helpers when the handler starts doing config load + service load + batched effect fanout inline. 5. For CLI readability, extract file-local preload helpers when the handler starts doing config load + service load + batched effect fanout inline.
6. When rebasing a facade branch after nearby merges, prefer the already-cleaned service/test version over older inline facade-era code. 6. When rebasing a facade branch after nearby merges, prefer the already-cleaned service/test version over older inline facade-era code.
## Next batch ## Remaining work
Recommended next five, in order: Most of the original facade-removal backlog is already done. The practical remaining work is narrower now:
1. `src/permission/index.ts` 1. remove the `Npm` runtime-backed facade from `src/npm/index.ts`
2. `src/agent/agent.ts` 2. remove the `TuiConfig` runtime-backed facade from `src/cli/cmd/tui/config/tui.ts`
3. `src/session/summary.ts` 3. keep `src/project/instance.ts` in the separate instance-context migration, not this checklist
4. `src/session/revert.ts`
5. `src/mcp/auth.ts`
Why this batch:
- It keeps pushing the session-adjacent cleanup without jumping straight into `session/index.ts` or `session/prompt.ts`.
- `Permission`, `Agent`, `SessionSummary`, and `SessionRevert` all reduce fanout in `server/instance/session.ts`.
- `McpAuth` is small and closely related to the just-landed `MCP` cleanup.
After that batch, the expected follow-up is the main session cluster:
1. `src/session/index.ts`
2. `src/session/prompt.ts`
3. `src/session/compaction.ts`
## Checklist ## Checklist
- [ ] `src/session/index.ts` (`Session`) - facades: `create`, `fork`, `get`, `setTitle`, `setArchived`, `setPermission`, `setRevert`, `messages`, `children`, `remove`, `updateMessage`, `removeMessage`, `removePart`, `updatePart`; main callers: `server/instance/session.ts`, `cli/cmd/session.ts`, `cli/cmd/export.ts`, `cli/cmd/github.ts`; tests: `test/server/session-actions.test.ts`, `test/server/session-list.test.ts`, `test/server/global-session-list.test.ts` - [ ] `src/npm/index.ts` (`Npm`) - still exports runtime-backed async facade helpers on top of `Npm.Service`
- [ ] `src/session/prompt.ts` (`SessionPrompt`) - facades: `prompt`, `resolvePromptParts`, `cancel`, `loop`, `shell`, `command`; main callers: `server/instance/session.ts`, `cli/cmd/github.ts`; tests: `test/session/prompt.test.ts`, `test/session/prompt-effect.test.ts`, `test/session/structured-output-integration.test.ts` - [ ] `src/cli/cmd/tui/config/tui.ts` (`TuiConfig`) - still exports runtime-backed async facade helpers on top of `TuiConfig.Service`
- [ ] `src/session/revert.ts` (`SessionRevert`) - facades: `revert`, `unrevert`, `cleanup`; main callers: `server/instance/session.ts`; tests: `test/session/revert-compact.test.ts` - [x] `src/session/session.ts` / `src/session/prompt.ts` / `src/session/revert.ts` / `src/session/summary.ts` - service-local facades removed
- [ ] `src/session/compaction.ts` (`SessionCompaction`) - facades: `isOverflow`, `prune`, `create`; main callers: `server/instance/session.ts`; tests: `test/session/compaction.test.ts` - [x] `src/agent/agent.ts` (`Agent`) - service-local facades removed
- [ ] `src/session/summary.ts` (`SessionSummary`) - facades: `summarize`, `diff`; main callers: `session/prompt.ts`, `session/processor.ts`, `server/instance/session.ts`; tests: `test/session/snapshot-tool-race.test.ts` - [x] `src/permission/index.ts` (`Permission`) - service-local facades removed
- [ ] `src/share/session.ts` (`ShareSession`) - facades: `create`, `share`, `unshare`; main callers: `server/instance/session.ts`, `cli/cmd/github.ts` - [x] `src/worktree/index.ts` (`Worktree`) - service-local facades removed
- [ ] `src/agent/agent.ts` (`Agent`) - facades: `get`, `list`, `defaultAgent`, `generate`; main callers: `cli/cmd/agent.ts`, `server/instance/session.ts`, `server/instance/experimental.ts`; tests: `test/agent/agent.test.ts` - [x] `src/plugin/index.ts` (`Plugin`) - service-local facades removed
- [ ] `src/permission/index.ts` (`Permission`) - facades: `ask`, `reply`, `list`; main callers: `server/instance/permission.ts`, `server/instance/session.ts`, `session/llm.ts`; tests: `test/permission/next.test.ts` - [x] `src/snapshot/index.ts` (`Snapshot`) - service-local facades removed
- [x] `src/file/index.ts` (`File`) - facades removed and merged. - [x] `src/file/index.ts` (`File`) - facades removed and merged
- [x] `src/lsp/index.ts` (`LSP`) - facades removed and merged. - [x] `src/lsp/index.ts` (`LSP`) - facades removed and merged
- [x] `src/mcp/index.ts` (`MCP`) - facades removed and merged. - [x] `src/mcp/index.ts` (`MCP`) - facades removed and merged
- [x] `src/config/config.ts` (`Config`) - facades removed and merged. - [x] `src/config/config.ts` (`Config`) - facades removed and merged
- [x] `src/provider/provider.ts` (`Provider`) - facades removed and merged. - [x] `src/provider/provider.ts` (`Provider`) - facades removed and merged
- [x] `src/pty/index.ts` (`Pty`) - facades removed and merged. - [x] `src/pty/index.ts` (`Pty`) - facades removed and merged
- [x] `src/skill/index.ts` (`Skill`) - facades removed and merged. - [x] `src/skill/index.ts` (`Skill`) - facades removed and merged
- [x] `src/project/vcs.ts` (`Vcs`) - facades removed and merged. - [x] `src/project/vcs.ts` (`Vcs`) - facades removed and merged
- [x] `src/tool/registry.ts` (`ToolRegistry`) - facades removed and merged. - [x] `src/tool/registry.ts` (`ToolRegistry`) - facades removed and merged
- [ ] `src/worktree/index.ts` (`Worktree`) - facades: `makeWorktreeInfo`, `createFromInfo`, `create`, `remove`, `reset`; main callers: `control-plane/adaptors/worktree.ts`, `server/instance/experimental.ts`; tests: `test/project/worktree.test.ts`, `test/project/worktree-remove.test.ts` - [x] `src/auth/index.ts` (`Auth`) - facades removed and merged
- [x] `src/auth/index.ts` (`Auth`) - facades removed and merged.
- [ ] `src/mcp/auth.ts` (`McpAuth`) - facades: `get`, `getForUrl`, `all`, `set`, `remove`, `updateTokens`, `updateClientInfo`, `updateCodeVerifier`, `updateOAuthState`; main callers: `mcp/oauth-provider.ts`, `cli/cmd/mcp.ts`; tests: `test/mcp/oauth-auto-connect.test.ts`
- [ ] `src/plugin/index.ts` (`Plugin`) - facades: `trigger`, `list`, `init`; main callers: `agent/agent.ts`, `session/llm.ts`, `project/bootstrap.ts`; tests: `test/plugin/trigger.test.ts`, `test/provider/provider.test.ts`
- [ ] `src/project/project.ts` (`Project`) - facades: `fromDirectory`, `discover`, `initGit`, `update`, `sandboxes`, `addSandbox`, `removeSandbox`; main callers: `project/instance.ts`, `server/instance/project.ts`, `server/instance/experimental.ts`; tests: `test/project/project.test.ts`, `test/project/migrate-global.test.ts`
- [ ] `src/snapshot/index.ts` (`Snapshot`) - facades: `init`, `track`, `patch`, `restore`, `revert`, `diff`, `diffFull`; main callers: `project/bootstrap.ts`, `cli/cmd/debug/snapshot.ts`; tests: `test/snapshot/snapshot.test.ts`, `test/session/revert-compact.test.ts`
## Excluded `makeRuntime(...)` sites ## Excluded `makeRuntime(...)` sites

View file

@ -76,7 +76,7 @@ Many route boundaries still use Zod-first validators. That does not block all ex
### Mixed handler styles ### Mixed handler styles
Many current `server/instance/*.ts` handlers still call async facades directly. Migrating those to composed `Effect.gen(...)` handlers is the low-risk step to do first. Many current `server/routes/instance/*.ts` handlers still mix composed Effect code with smaller Promise- or ALS-backed seams. Migrating those to consistent `Effect.gen(...)` handlers is the low-risk step to do first.
### Non-JSON routes ### Non-JSON routes
@ -90,7 +90,7 @@ The current server composition, middleware, and docs flow are Hono-centered toda
### 1. Finish the prerequisites first ### 1. Finish the prerequisites first
- continue route-handler effectification in `server/instance/*.ts` - continue route-handler effectification in `server/routes/instance/*.ts`
- continue schema migration toward Effect Schema-first DTOs and errors - continue schema migration toward Effect Schema-first DTOs and errors
- keep removing service facades - keep removing service facades
@ -98,9 +98,9 @@ The current server composition, middleware, and docs flow are Hono-centered toda
Introduce one small `HttpApi` group for plain JSON endpoints only. Good initial candidates are the least stateful endpoints in: Introduce one small `HttpApi` group for plain JSON endpoints only. Good initial candidates are the least stateful endpoints in:
- `server/instance/question.ts` - `server/routes/instance/question.ts`
- `server/instance/provider.ts` - `server/routes/instance/provider.ts`
- `server/instance/permission.ts` - `server/routes/instance/permission.ts`
Avoid `session.ts`, SSE, websocket, and TUI-facing routes first. Avoid `session.ts`, SSE, websocket, and TUI-facing routes first.
@ -155,9 +155,9 @@ This gives:
As each route group is ported to `HttpApi`: As each route group is ported to `HttpApi`:
1. change its `root` path from `/experimental/httpapi/<group>` to `/<group>` 1. add `.get(...)` / `.post(...)` bridge entries to the flag block in `server/routes/instance/index.ts`
2. add `.all("/<group>", handler)` / `.all("/<group>/*", handler)` to the flag block in `instance/index.ts` 2. for partial ports (e.g. only `GET /provider/auth`), bridge only the specific path
3. for partial ports (e.g. only `GET /provider/auth`), bridge only the specific path 3. keep the legacy Hono route registered behind it for OpenAPI / SDK generation until the spec pipeline changes
4. verify SDK output is unchanged 4. verify SDK output is unchanged
Leave streaming-style endpoints on Hono until there is a clear reason to move them. Leave streaming-style endpoints on Hono until there is a clear reason to move them.
@ -267,7 +267,7 @@ Use the same sequence for each route group.
3. Apply the schema migration ordering above so those types are Effect Schema-first. 3. Apply the schema migration ordering above so those types are Effect Schema-first.
4. Define the `HttpApi` contract separately from the handlers. 4. Define the `HttpApi` contract separately from the handlers.
5. Implement handlers by yielding the existing service from context. 5. Implement handlers by yielding the existing service from context.
6. Mount the new surface in parallel under an experimental prefix. 6. Mount the new surface in parallel behind the `OPENCODE_EXPERIMENTAL_HTTPAPI` bridge.
7. Regenerate the SDK and verify zero diff against `dev` (see SDK shape rule above). 7. Regenerate the SDK and verify zero diff against `dev` (see SDK shape rule above).
8. Add one end-to-end test and one OpenAPI-focused test. 8. Add one end-to-end test and one OpenAPI-focused test.
9. Compare ergonomics before migrating the next endpoint. 9. Compare ergonomics before migrating the next endpoint.
@ -286,20 +286,20 @@ Placement rule:
- keep `HttpApi` code under `src/server`, not `src/effect` - keep `HttpApi` code under `src/server`, not `src/effect`
- `src/effect` should stay focused on runtimes, layers, instance state, and shared Effect plumbing - `src/effect` should stay focused on runtimes, layers, instance state, and shared Effect plumbing
- place each `HttpApi` slice next to the HTTP boundary it serves - place each `HttpApi` slice next to the HTTP boundary it serves
- for instance-scoped routes, prefer `src/server/instance/httpapi/*` - for instance-scoped routes, prefer `src/server/routes/instance/httpapi/*`
- if control-plane routes ever migrate, prefer `src/server/control/httpapi/*` - if control-plane routes ever migrate, prefer `src/server/routes/control/httpapi/*`
Suggested file layout for a repeatable spike: Suggested file layout for a repeatable spike:
- `src/server/instance/httpapi/question.ts` — contract and handler layer for one route group - `src/server/routes/instance/httpapi/question.ts` — contract and handler layer for one route group
- `src/server/instance/httpapi/server.ts` — standalone Effect HTTP server that composes all groups - `src/server/routes/instance/httpapi/server.ts` — bridged Effect HTTP layer that composes all groups
- `test/server/question-httpapi.test.ts` — end-to-end test against the real service - route or OpenAPI verification should live alongside the existing server tests; there is no dedicated `question-httpapi` test file on this branch
Suggested responsibilities: Suggested responsibilities:
- `question.ts` defines the `HttpApi` contract and `HttpApiBuilder.group(...)` handlers - `question.ts` defines the `HttpApi` contract and `HttpApiBuilder.group(...)` handlers
- `server.ts` composes all route groups into one `HttpRouter.serve` layer with shared middleware (auth, instance lookup) - `server.ts` composes all route groups into one `HttpRouter.toWebHandler(...)` bridge with shared middleware (auth, instance lookup)
- tests use `ExperimentalHttpApiServer.layerTest` to run against a real in-process HTTP server - tests should verify the bridged routes through the normal server surface
## Example migration shape ## Example migration shape
@ -319,33 +319,33 @@ Each route-group spike should follow the same shape.
- keep handler bodies thin - keep handler bodies thin
- keep transport mapping at the HTTP boundary only - keep transport mapping at the HTTP boundary only
### 3. Standalone server ### 3. Bridged server
- the Effect HTTP server is self-contained in `httpapi/server.ts` - the Effect HTTP layer is composed in `httpapi/server.ts`
- it is **not** mounted into the Hono app — no bridge, no `toWebHandler` - it is mounted into the Hono app via `HttpRouter.toWebHandler(...)`
- route paths use the `/experimental/httpapi` prefix so they match the eventual cutover - routes keep their normal instance paths and are gated by the `OPENCODE_EXPERIMENTAL_HTTPAPI` flag
- each route group exposes its own OpenAPI doc endpoint - the legacy Hono handlers stay registered after the bridge so current OpenAPI / SDK generation still works
### 4. Verification ### 4. Verification
- seed real state through the existing service - seed real state through the existing service
- call the experimental endpoints - call the bridged endpoints with the flag enabled
- assert that the service behavior is unchanged - assert that the service behavior is unchanged
- assert that the generated OpenAPI contains the migrated paths and schemas - assert that the generated OpenAPI contains the migrated paths and schemas
## Boundary composition ## Boundary composition
The standalone Effect server owns its own middleware stack. It does not share middleware with the Hono server. The Effect `HttpApi` layer owns its own auth and instance middleware, but it is currently mounted inside the existing Hono server.
### Auth ### Auth
- the standalone server implements auth as an `HttpApiMiddleware.Service` using `HttpApiSecurity.basic` - the bridged `HttpApi` layer implements auth as an `HttpApiMiddleware.Service` using `HttpApiSecurity.basic`
- each route group's `HttpApi` is wrapped with `.middleware(Authorization)` before being served - each route group's `HttpApi` is wrapped with `.middleware(Authorization)` before being served
- this is independent of the Hono `AuthMiddleware` — when the Effect server eventually replaces Hono, this becomes the only auth layer - this is independent of the Hono auth layer; the current bridge keeps the responsibility local to the `HttpApi` slice
### Instance and workspace lookup ### Instance and workspace lookup
- the standalone server resolves instance context via an `HttpRouter.middleware` that reads `x-opencode-directory` headers and `directory` query params - the bridged `HttpApi` layer resolves instance context via an `HttpRouter.middleware` that reads `x-opencode-directory` headers and `directory` query params
- this is the Effect equivalent of the Hono `WorkspaceRouterMiddleware` - this is the Effect equivalent of the Hono `WorkspaceRouterMiddleware`
- `HttpApi` handlers yield services from context and assume the correct instance has already been provided - `HttpApi` handlers yield services from context and assume the correct instance has already been provided
@ -360,7 +360,7 @@ The standalone Effect server owns its own middleware stack. It does not share mi
The first slice is successful if: The first slice is successful if:
- the standalone Effect server starts and serves the endpoints independently of the Hono server - the bridged endpoints serve correctly through the existing Hono host when the flag is enabled
- the handlers reuse the existing Effect service - the handlers reuse the existing Effect service
- request decoding and response shapes are schema-defined from canonical Effect schemas - request decoding and response shapes are schema-defined from canonical Effect schemas
- any remaining Zod boundary usage is derived from `.zod` or clearly temporary - any remaining Zod boundary usage is derived from `.zod` or clearly temporary

View file

@ -157,7 +157,7 @@ Direct legacy usage means any source file that still calls one of:
- `Instance.reload(...)` - `Instance.reload(...)`
- `Instance.dispose()` / `Instance.disposeAll()` - `Instance.dispose()` / `Instance.disposeAll()`
Current total: `54` files in `packages/opencode/src`. Current total: `56` files in `packages/opencode/src`.
### Core bridge and plumbing ### Core bridge and plumbing
@ -177,13 +177,13 @@ Migration rule:
These are the current request-entry seams that still create or consume instance context through the legacy helper. These are the current request-entry seams that still create or consume instance context through the legacy helper.
- `src/server/instance/middleware.ts` - `src/server/routes/instance/middleware.ts`
- `src/server/instance/index.ts` - `src/server/routes/instance/index.ts`
- `src/server/instance/project.ts` - `src/server/routes/instance/project.ts`
- `src/server/instance/workspace.ts` - `src/server/routes/control/workspace.ts`
- `src/server/instance/file.ts` - `src/server/routes/instance/file.ts`
- `src/server/instance/experimental.ts` - `src/server/routes/instance/experimental.ts`
- `src/server/instance/global.ts` - `src/server/routes/global.ts`
Migration rule: Migration rule:
@ -239,7 +239,7 @@ Migration rule:
These modules are already the best near-term migration targets because they are in Effect code but still read sync getters from the legacy helper. These modules are already the best near-term migration targets because they are in Effect code but still read sync getters from the legacy helper.
- `src/agent/agent.ts` - `src/agent/agent.ts`
- `src/config/tui-migrate.ts` - `src/cli/cmd/tui/config/tui-migrate.ts`
- `src/file/index.ts` - `src/file/index.ts`
- `src/file/watcher.ts` - `src/file/watcher.ts`
- `src/format/formatter.ts` - `src/format/formatter.ts`
@ -250,7 +250,7 @@ These modules are already the best near-term migration targets because they are
- `src/project/vcs.ts` - `src/project/vcs.ts`
- `src/provider/provider.ts` - `src/provider/provider.ts`
- `src/pty/index.ts` - `src/pty/index.ts`
- `src/session/index.ts` - `src/session/session.ts`
- `src/session/instruction.ts` - `src/session/instruction.ts`
- `src/session/llm.ts` - `src/session/llm.ts`
- `src/session/system.ts` - `src/session/system.ts`

View file

@ -4,11 +4,11 @@ Small follow-ups that do not fit neatly into the main facade, route, tool, or sc
## Config / TUI ## Config / TUI
- [ ] `config/tui.ts` - finish the internal Effect migration after the `Instance.state(...)` removal. - [ ] `cli/cmd/tui/config/tui.ts` - finish the internal Effect migration.
Keep the current precedence and migration semantics intact while converting the remaining internal async helpers (`loadState`, `mergeFile`, `loadFile`, `load`) to `Effect.gen(...)` / `Effect.fn(...)`. Keep the current precedence and migration semantics intact while converting the remaining internal async helpers (`loadState`, `mergeFile`, `loadFile`, `load`) to `Effect.gen(...)` / `Effect.fn(...)`.
- [ ] `config/tui.ts` callers - once the internal service is stable, migrate plain async callers to use `TuiConfig.Service` directly where that actually simplifies the code. - [ ] `cli/cmd/tui/config/tui.ts` callers - once the internal service is stable, migrate plain async callers to use `TuiConfig.Service` directly where that actually simplifies the code.
Likely first callers: `cli/cmd/tui/attach.ts`, `cli/cmd/tui/thread.ts`, `cli/cmd/tui/plugin/runtime.ts`. Likely first callers: `cli/cmd/tui/attach.ts`, `cli/cmd/tui/thread.ts`, `cli/cmd/tui/plugin/runtime.ts`.
- [ ] `env/index.ts` - move the last production `Instance.state(...)` usage onto `InstanceState` (or its replacement) so `Instance.state` can be deleted. - [x] `env/index.ts` - already uses `InstanceState.make(...)`.
## ConfigPaths ## ConfigPaths
@ -21,14 +21,12 @@ Small follow-ups that do not fit neatly into the main facade, route, tool, or sc
- `readFile(...)` - `readFile(...)`
- `parseText(...)` - `parseText(...)`
- [ ] `config/config.ts` - switch internal config loading from `Effect.promise(() => ConfigPaths.*(...))` to `yield* paths.*(...)` once the service exists. - [ ] `config/config.ts` - switch internal config loading from `Effect.promise(() => ConfigPaths.*(...))` to `yield* paths.*(...)` once the service exists.
- [ ] `config/tui.ts` - switch TUI config loading from async `ConfigPaths.*` wrappers to the `ConfigPaths.Service` once that service exists. - [ ] `cli/cmd/tui/config/tui.ts` - switch TUI config loading from async `ConfigPaths.*` wrappers to the `ConfigPaths.Service` once that service exists.
- [ ] `config/tui-migrate.ts` - decide whether to leave this as a plain async module using wrapper functions or effectify it fully after `ConfigPaths.Service` lands. - [ ] `cli/cmd/tui/config/tui-migrate.ts` - decide whether to leave this as a plain async module using wrapper functions or effectify it fully after `ConfigPaths.Service` lands.
## Instance cleanup ## Instance cleanup
- [ ] `project/instance.ts` - remove `Instance.state(...)` once `env/index.ts` is migrated. - [ ] `project/instance.ts` - keep shrinking the legacy ALS / Promise cache after the remaining `Instance.*` callers move over.
- [ ] `project/state.ts` - delete the bespoke per-instance state helper after the last production caller is gone.
- [ ] `test/project/state.test.ts` - replace or delete the old `Instance.state(...)` tests after the removal.
## Notes ## Notes

View file

@ -19,53 +19,43 @@ See `instance-context.md` for the phased plan to remove the legacy ALS / promise
## Service shape ## Service shape
Every service follows the same pattern — a single namespace with the service definition, layer, `runPromise`, and async facade functions: Every service follows the same pattern: one module, flat top-level exports, traced Effect methods, and a self-reexport at the bottom when the file is the public module.
```ts ```ts
export namespace Foo { export interface Interface {
export interface Interface { readonly get: (id: FooID) => Effect.Effect<FooInfo, FooError>
readonly get: (id: FooID) => Effect.Effect<FooInfo, FooError>
}
export class Service extends Context.Service<Service, Interface>()("@opencode/Foo") {}
export const layer = Layer.effect(
Service,
Effect.gen(function* () {
// For instance-scoped services:
const state = yield* InstanceState.make<State>(
Effect.fn("Foo.state")(() => Effect.succeed({ ... })),
)
const get = Effect.fn("Foo.get")(function* (id: FooID) {
const s = yield* InstanceState.get(state)
// ...
})
return Service.of({ get })
}),
)
// Optional: wire dependencies
export const defaultLayer = layer.pipe(Layer.provide(FooDep.layer))
// Per-service runtime (inside the namespace)
const { runPromise } = makeRuntime(Service, defaultLayer)
// Async facade functions
export async function get(id: FooID) {
return runPromise((svc) => svc.get(id))
}
} }
export class Service extends Context.Service<Service, Interface>()("@opencode/Foo") {}
export const layer = Layer.effect(
Service,
Effect.gen(function* () {
const state = yield* InstanceState.make<State>(
Effect.fn("Foo.state")(() => Effect.succeed({ ... })),
)
const get = Effect.fn("Foo.get")(function* (id: FooID) {
const s = yield* InstanceState.get(state)
// ...
})
return Service.of({ get })
}),
)
export const defaultLayer = layer.pipe(Layer.provide(FooDep.layer))
export * as Foo from "."
``` ```
Rules: Rules:
- Keep everything in one namespace, one file — no separate `service.ts` / `index.ts` split - Keep the service surface in one module; prefer flat top-level exports over `export namespace Foo { ... }`
- `runPromise` goes inside the namespace (not exported unless tests need it) - Use `Effect.fn("Foo.method")` for Effect methods
- Facade functions are plain `async function` — no `fn()` wrappers - Use a self-reexport (`export * as Foo from "."` or `"./foo"`) for the public namespace projection
- Use `Effect.fn("Namespace.method")` for all Effect functions (for tracing) - Avoid service-local `makeRuntime(...)` facades unless a file is still intentionally in the older migration phase
- No `Layer.fresh` — InstanceState handles per-directory isolation - No `Layer.fresh` for normal per-directory isolation; use `InstanceState`
## Schema → Zod interop ## Schema → Zod interop
@ -266,7 +256,7 @@ Tool-specific filesystem cleanup notes live in `tools.md`.
## Destroying the facades ## Destroying the facades
This phase is still broadly open. As of 2026-04-13 there are still 15 `makeRuntime(...)` call sites under `src/`, with 13 still in scope for facade removal. The live checklist now lives in `facades.md`. This phase is no longer broadly open. There are 5 `makeRuntime(...)` call sites under `src/`, and only a small subset are still ordinary facade-removal targets. The live checklist now lives in `facades.md`.
These facades 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. These facades 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.
@ -297,11 +287,11 @@ For each service, the migration is roughly:
- `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. - `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.
- `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. - `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.
- `Storage` — migrated 2026-04-10. One production caller (`Session.diff`) and all storage.test.ts tests converted to effectful style. Facades and `makeRuntime` removed. - `Storage` — migrated 2026-04-10. One production caller (`Session.diff`) and all storage.test.ts tests converted to effectful style. Facades and `makeRuntime` removed.
- `SessionRunState` — migrated 2026-04-11. Single caller in `server/instance/session.ts` converted; facade removed. - `SessionRunState` — migrated 2026-04-11. Single caller in `server/routes/instance/session.ts` converted; facade removed.
- `Account` — migrated 2026-04-11. Callers in `server/instance/experimental.ts` and `cli/cmd/account.ts` converted; facade removed. - `Account` — migrated 2026-04-11. Callers in `server/routes/instance/experimental.ts` and `cli/cmd/account.ts` converted; facade removed.
- `Instruction` — migrated 2026-04-11. Test-only callers converted; facade removed. - `Instruction` — migrated 2026-04-11. Test-only callers converted; facade removed.
- `FileWatcher` — migrated 2026-04-11. Callers in `project/bootstrap.ts` and test converted; facade removed. - `FileWatcher` — migrated 2026-04-11. Callers in `project/bootstrap.ts` and test converted; facade removed.
- `Question` — migrated 2026-04-11. Callers in `server/instance/question.ts` and test converted; facade removed. - `Question` — migrated 2026-04-11. Callers in `server/routes/instance/question.ts` and test converted; facade removed.
- `Truncate` — migrated 2026-04-11. Caller in `tool/tool.ts` and test converted; facade removed. - `Truncate` — migrated 2026-04-11. Caller in `tool/tool.ts` and test converted; facade removed.
## Route handler effectification ## Route handler effectification

View file

@ -39,28 +39,26 @@ This eliminates multiple `runPromise` round-trips and lets handlers compose natu
## Current route files ## Current route files
Current instance route files live under `src/server/instance`, not `server/routes`. Current instance route files live under `src/server/routes/instance`.
The main migration targets are: Files that are already mostly on the intended service-yielding shape:
- [ ] `server/instance/session.ts` — heaviest; still has many direct facade calls for Session, SessionPrompt, SessionRevert, SessionCompaction, SessionShare, SessionSummary, Agent, Bus - [x] `server/routes/instance/question.ts` — handlers yield `Question.Service`
- [ ] `server/instance/global.ts` — still has direct facade calls for Config and instance lifecycle actions - [x] `server/routes/instance/provider.ts` — handlers yield `Provider.Service`, `ProviderAuth.Service`, and `Config.Service`
- [ ] `server/instance/provider.ts` — still has direct facade calls for Config and Provider - [x] `server/routes/instance/permission.ts` — handlers yield `Permission.Service`
- [ ] `server/instance/question.ts` — partially converted; still worth tracking here until it consistently uses the composed style - [x] `server/routes/instance/mcp.ts` — handlers mostly yield `MCP.Service`
- [ ] `server/instance/pty.ts` — still calls Pty facades directly - [x] `server/routes/instance/pty.ts` — handlers yield `Pty.Service`
- [ ] `server/instance/experimental.ts` — mixed state; some handlers are already composed, others still use facades
Additional route files that still participate in the migration: Files still worth tracking here:
- [ ] `server/instance/index.ts` — Vcs, Agent, Skill, LSP, Format - [ ] `server/routes/instance/session.ts` — still the heaviest mixed file; many handlers are composed, but the file still mixes patterns and has direct `Bus.publish(...)` / `Session.list(...)` usage
- [ ] `server/instance/file.ts` — Ripgrep, File, LSP - [ ] `server/routes/instance/index.ts` — mostly converted, but still has direct `Instance.dispose()` / `Instance.*` reads for `/instance/dispose` and `/path`
- [ ] `server/instance/mcp.ts` — MCP facade-heavy - [ ] `server/routes/instance/file.ts` — most handlers yield services, but `/find` still passes `Instance.directory` directly into ripgrep and `/find/symbol` is still stubbed
- [ ] `server/instance/permission.ts` — Permission - [ ] `server/routes/instance/experimental.ts` — mixed state; many handlers are composed, but some still rely on `runRequest(...)` or direct `Instance.project` reads
- [ ] `server/instance/workspace.ts` — Workspace - [ ] `server/routes/instance/middleware.ts` — still enters the instance via `Instance.provide(...)`
- [ ] `server/instance/tui.ts` — Bus and Session - [ ] `server/routes/global.ts` — still uses `Instance.disposeAll()` and remains partly outside the fully-composed style
- [ ] `server/instance/middleware.ts` — Session and Workspace lookups
## Notes ## Notes
- Some handlers already use `AppRuntime.runPromise(Effect.gen(...))` in isolated places. Keep pushing those files toward one consistent style. - Route conversion is now less about facade removal and more about removing the remaining direct `Instance.*` reads, `Instance.provide(...)` boundaries, and small Promise-style bridges inside route files.
- Route conversion is closely tied to facade removal. As services lose `makeRuntime`-backed async exports, route handlers should switch to yielding the service directly. - `jsonRequest(...)` / `runRequest(...)` already provide a good intermediate shape for many handlers. The remaining cleanup is mostly consistency work in the heavier files.

View file

@ -40,13 +40,13 @@ Everything still lives in `packages/opencode`.
Important current facts: Important current facts:
- there is no `packages/core` or `packages/cli` workspace yet - there is no `packages/core` or `packages/cli` workspace yet
- `packages/server` now exists as a minimal scaffold package, but it does not own any real route contracts, handlers, or runtime composition yet - there is no `packages/server` workspace yet on this branch
- the main host server is still Hono-based in `src/server/server.ts` - the main host server is still Hono-based in `src/server/server.ts`
- current OpenAPI generation is Hono-based through `Server.openapi()` and `cli/cmd/generate.ts` - current OpenAPI generation is Hono-based through `Server.openapi()` and `cli/cmd/generate.ts`
- the Effect runtime and app layer are centralized in `src/effect/app-runtime.ts` and `src/effect/run-service.ts` - the Effect runtime and app layer are centralized in `src/effect/app-runtime.ts` and `src/effect/run-service.ts`
- there is already one experimental Effect `HttpApi` slice at `src/server/instance/httpapi/question.ts` - there are already bridged Effect `HttpApi` slices under `src/server/routes/instance/httpapi/*`
- that experimental slice is mounted under `/experimental/httpapi/question` - those slices are mounted into the Hono server behind `OPENCODE_EXPERIMENTAL_HTTPAPI`
- that experimental slice already has an end-to-end test at `test/server/question-httpapi.test.ts` - the bridge currently covers `question`, `permission`, `provider`, partial `config`, and partial `project` routes
This means the package split should start from an extraction path, not from greenfield package ownership. This means the package split should start from an extraction path, not from greenfield package ownership.
@ -209,17 +209,19 @@ Current host and route composition:
- `src/server/server.ts` - `src/server/server.ts`
- `src/server/control/index.ts` - `src/server/control/index.ts`
- `src/server/instance/index.ts` - `src/server/routes/instance/index.ts`
- `src/server/middleware.ts` - `src/server/middleware.ts`
- `src/server/adapter.bun.ts` - `src/server/adapter.bun.ts`
- `src/server/adapter.node.ts` - `src/server/adapter.node.ts`
Current experimental `HttpApi` slice: Current bridged `HttpApi` slices:
- `src/server/instance/httpapi/question.ts` - `src/server/routes/instance/httpapi/question.ts`
- `src/server/instance/httpapi/index.ts` - `src/server/routes/instance/httpapi/permission.ts`
- `src/server/instance/experimental.ts` - `src/server/routes/instance/httpapi/provider.ts`
- `test/server/question-httpapi.test.ts` - `src/server/routes/instance/httpapi/config.ts`
- `src/server/routes/instance/httpapi/project.ts`
- `src/server/routes/instance/httpapi/server.ts`
Current OpenAPI flow: Current OpenAPI flow:
@ -245,7 +247,7 @@ Keep in `packages/opencode` for now:
- `src/server/server.ts` - `src/server/server.ts`
- `src/server/control/index.ts` - `src/server/control/index.ts`
- `src/server/instance/*.ts` - `src/server/routes/**/*.ts`
- `src/server/middleware.ts` - `src/server/middleware.ts`
- `src/server/adapter.*.ts` - `src/server/adapter.*.ts`
- `src/effect/app-runtime.ts` - `src/effect/app-runtime.ts`
@ -305,14 +307,13 @@ Bad early migration targets:
## First vertical slice ## First vertical slice
The first slice for the package split is the existing experimental `question` group. The first slice for the package split is still the existing `question` `HttpApi` group.
Why `question` first: Why `question` first:
- it already exists as an experimental `HttpApi` slice - it already exists as an experimental `HttpApi` slice
- it already follows the desired contract and implementation split in one file - it already follows the desired contract and implementation split in one file
- it is already mounted through the current Hono host - it is already mounted through the current Hono host
- it already has an end-to-end test
- it is JSON-only - it is JSON-only
- it has low blast radius - it has low blast radius
@ -357,7 +358,7 @@ Done means:
Scope: Scope:
- extract the pure `HttpApi` contract from `src/server/instance/httpapi/question.ts` - extract the pure `HttpApi` contract from `src/server/routes/instance/httpapi/question.ts`
- place it in `packages/server/src/definition/question.ts` - place it in `packages/server/src/definition/question.ts`
- aggregate it in `packages/server/src/definition/api.ts` - aggregate it in `packages/server/src/definition/api.ts`
- generate OpenAPI in `packages/server/src/openapi.ts` - generate OpenAPI in `packages/server/src/openapi.ts`
@ -399,8 +400,9 @@ Scope:
- replace local experimental question route wiring in `packages/opencode` - replace local experimental question route wiring in `packages/opencode`
- keep the same mount path: - keep the same mount path:
- `/experimental/httpapi/question` - `/question`
- `/experimental/httpapi/question/doc` - `/question/:requestID/reply`
- `/question/:requestID/reject`
Rules: Rules:
@ -569,7 +571,7 @@ For package-split PRs, validate the smallest useful thing.
Typical validation for the first waves: Typical validation for the first waves:
- `bun typecheck` in the touched package directory or directories - `bun typecheck` in the touched package directory or directories
- the relevant route test, especially `test/server/question-httpapi.test.ts` - the relevant server / route coverage for the migrated slice
- merged OpenAPI coverage if the PR touches spec generation - merged OpenAPI coverage if the PR touches spec generation
Do not run tests from repo root. Do not run tests from repo root.

View file

@ -36,7 +36,7 @@ This keeps tool tests aligned with the production service graph and makes follow
## Exported tools ## Exported tools
These exported tool definitions already exist in `src/tool` and are on the current Effect-native `Tool.define(...)` path: These exported tool definitions currently use `Tool.define(...)` in `src/tool`:
- [x] `apply_patch.ts` - [x] `apply_patch.ts`
- [x] `bash.ts` - [x] `bash.ts`
@ -45,7 +45,6 @@ These exported tool definitions already exist in `src/tool` and are on the curre
- [x] `glob.ts` - [x] `glob.ts`
- [x] `grep.ts` - [x] `grep.ts`
- [x] `invalid.ts` - [x] `invalid.ts`
- [x] `ls.ts`
- [x] `lsp.ts` - [x] `lsp.ts`
- [x] `multiedit.ts` - [x] `multiedit.ts`
- [x] `plan.ts` - [x] `plan.ts`
@ -60,7 +59,7 @@ These exported tool definitions already exist in `src/tool` and are on the curre
Notes: Notes:
- `batch.ts` is no longer a current tool file and should not be tracked here. - There is no current `ls.ts` tool file on this branch.
- `truncate.ts` is an Effect service used by tools, not a tool definition itself. - `truncate.ts` is an Effect service used by tools, not a tool definition itself.
- `mcp-exa.ts`, `external-directory.ts`, and `schema.ts` are support modules, not standalone tool definitions. - `mcp-exa.ts`, `external-directory.ts`, and `schema.ts` are support modules, not standalone tool definitions.
@ -73,7 +72,7 @@ Current spot cleanups worth tracking:
- [ ] `read.ts` — still bridges to Node stream / `readline` helpers and Promise-based binary detection - [ ] `read.ts` — still bridges to Node stream / `readline` helpers and Promise-based binary detection
- [ ] `bash.ts` — already uses Effect child-process primitives; only keep tracking shell-specific platform bridges and parser/loading details as they come up - [ ] `bash.ts` — already uses Effect child-process primitives; only keep tracking shell-specific platform bridges and parser/loading details as they come up
- [ ] `webfetch.ts` — already uses `HttpClient`; remaining work is limited to smaller boundary helpers like HTML text extraction - [ ] `webfetch.ts` — already uses `HttpClient`; remaining work is limited to smaller boundary helpers like HTML text extraction
- [ ] `file/ripgrep.ts` — adjacent to tool migration; still has raw fs/process usage that affects `grep.ts` and `ls.ts` - [ ] `file/ripgrep.ts` — adjacent to tool migration; still has raw fs/process usage that affects `grep.ts` and file-search routes
- [ ] `patch/index.ts` — adjacent to tool migration; still has raw fs usage behind patch application - [ ] `patch/index.ts` — adjacent to tool migration; still has raw fs usage behind patch application
Notable items that are already effectively on the target path and do not need separate migration bullets right now: Notable items that are already effectively on the target path and do not need separate migration bullets right now:
@ -83,7 +82,6 @@ Notable items that are already effectively on the target path and do not need se
- `write.ts` - `write.ts`
- `codesearch.ts` - `codesearch.ts`
- `websearch.ts` - `websearch.ts`
- `ls.ts`
- `multiedit.ts` - `multiedit.ts`
- `edit.ts` - `edit.ts`

View file

@ -1,6 +1,6 @@
import { Schema } from "effect" import { Schema } from "effect"
import { SessionEvent } from "./session-event" import { SessionEvent } from "./session-event"
import { produce } from "immer" import { castDraft, produce } from "immer"
export const ID = SessionEvent.ID export const ID = SessionEvent.ID
export type ID = Schema.Schema.Type<typeof ID> export type ID = Schema.Schema.Type<typeof ID>
@ -70,7 +70,9 @@ export class ToolStateError extends Schema.Class<ToolStateError>("Session.Entry.
metadata: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional), metadata: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional),
}) {} }) {}
export const ToolState = Schema.Union([ToolStatePending, ToolStateRunning, ToolStateCompleted, ToolStateError]) export const ToolState = Schema.Union([ToolStatePending, ToolStateRunning, ToolStateCompleted, ToolStateError]).pipe(
Schema.toTaggedUnion("status"),
)
export type ToolState = Schema.Schema.Type<typeof ToolState> export type ToolState = Schema.Schema.Type<typeof ToolState>
export class AssistantTool extends Schema.Class<AssistantTool>("Session.Entry.Assistant.Tool")({ export class AssistantTool extends Schema.Class<AssistantTool>("Session.Entry.Assistant.Tool")({
@ -96,7 +98,9 @@ export class AssistantReasoning extends Schema.Class<AssistantReasoning>("Sessio
text: Schema.String, text: Schema.String,
}) {} }) {}
export const AssistantContent = Schema.Union([AssistantText, AssistantReasoning, AssistantTool]) export const AssistantContent = Schema.Union([AssistantText, AssistantReasoning, AssistantTool]).pipe(
Schema.toTaggedUnion("type"),
)
export type AssistantContent = Schema.Schema.Type<typeof AssistantContent> export type AssistantContent = Schema.Schema.Type<typeof AssistantContent>
export class Assistant extends Schema.Class<Assistant>("Session.Entry.Assistant")({ export class Assistant extends Schema.Class<Assistant>("Session.Entry.Assistant")({
@ -126,7 +130,7 @@ export class Compaction extends Schema.Class<Compaction>("Session.Entry.Compacti
...Base, ...Base,
}) {} }) {}
export const Entry = Schema.Union([User, Synthetic, Assistant, Compaction]) export const Entry = Schema.Union([User, Synthetic, Assistant, Compaction]).pipe(Schema.toTaggedUnion("type"))
export type Entry = Schema.Schema.Type<typeof Entry> export type Entry = Schema.Schema.Type<typeof Entry>
@ -141,19 +145,29 @@ export function step(old: History, event: SessionEvent.Event): History {
return produce(old, (draft) => { return produce(old, (draft) => {
const lastAssistant = draft.entries.findLast((x) => x.type === "assistant") const lastAssistant = draft.entries.findLast((x) => x.type === "assistant")
const pendingAssistant = lastAssistant && !lastAssistant.time.completed ? lastAssistant : undefined const pendingAssistant = lastAssistant && !lastAssistant.time.completed ? lastAssistant : undefined
type DraftContent = NonNullable<typeof pendingAssistant>["content"][number]
type DraftTool = Extract<DraftContent, { type: "tool" }>
switch (event.type) { const latestTool = (callID?: string) =>
case "prompt": { pendingAssistant?.content.findLast(
(item): item is DraftTool => item.type === "tool" && (callID === undefined || item.callID === callID),
)
const latestText = () => pendingAssistant?.content.findLast((item) => item.type === "text")
const latestReasoning = () => pendingAssistant?.content.findLast((item) => item.type === "reasoning")
SessionEvent.Event.match(event, {
prompt: (event) => {
const entry = User.fromEvent(event)
if (pendingAssistant) { if (pendingAssistant) {
// @ts-expect-error draft.pending.push(castDraft(entry))
draft.pending.push(User.fromEvent(event)) return
break
} }
// @ts-expect-error draft.entries.push(castDraft(entry))
draft.entries.push(User.fromEvent(event)) },
break synthetic: (event) => {
} draft.entries.push(new Synthetic({ ...event, time: { created: event.timestamp } }))
case "step.started": { },
"step.started": (event) => {
if (pendingAssistant) pendingAssistant.time.completed = event.timestamp if (pendingAssistant) pendingAssistant.time.completed = event.timestamp
draft.entries.push({ draft.entries.push({
id: event.id, id: event.id,
@ -163,27 +177,28 @@ export function step(old: History, event: SessionEvent.Event): History {
}, },
content: [], content: [],
}) })
break },
} "step.ended": (event) => {
case "text.started": { if (!pendingAssistant) return
if (!pendingAssistant) break pendingAssistant.time.completed = event.timestamp
pendingAssistant.cost = event.cost
pendingAssistant.tokens = event.tokens
},
"text.started": () => {
if (!pendingAssistant) return
pendingAssistant.content.push({ pendingAssistant.content.push({
type: "text", type: "text",
text: "", text: "",
}) })
break },
} "text.delta": (event) => {
case "text.delta": { if (!pendingAssistant) return
if (!pendingAssistant) break const match = latestText()
const match = pendingAssistant.content.findLast((x) => x.type === "text")
if (match) match.text += event.delta if (match) match.text += event.delta
break },
} "text.ended": () => {},
case "text.ended": { "tool.input.started": (event) => {
break if (!pendingAssistant) return
}
case "tool.input.started": {
if (!pendingAssistant) break
pendingAssistant.content.push({ pendingAssistant.content.push({
type: "tool", type: "tool",
callID: event.callID, callID: event.callID,
@ -196,21 +211,17 @@ export function step(old: History, event: SessionEvent.Event): History {
input: "", input: "",
}, },
}) })
break },
} "tool.input.delta": (event) => {
case "tool.input.delta": { if (!pendingAssistant) return
if (!pendingAssistant) break const match = latestTool(event.callID)
const match = pendingAssistant.content.findLast((x) => x.type === "tool")
// oxlint-disable-next-line no-base-to-string -- event.delta is a Schema.String (runtime string) // oxlint-disable-next-line no-base-to-string -- event.delta is a Schema.String (runtime string)
if (match) match.state.input += event.delta if (match) match.state.input += event.delta
break },
} "tool.input.ended": () => {},
case "tool.input.ended": { "tool.called": (event) => {
break if (!pendingAssistant) return
} const match = latestTool(event.callID)
case "tool.called": {
if (!pendingAssistant) break
const match = pendingAssistant.content.findLast((x) => x.type === "tool")
if (match) { if (match) {
match.time.ran = event.timestamp match.time.ran = event.timestamp
match.state = { match.state = {
@ -218,11 +229,10 @@ export function step(old: History, event: SessionEvent.Event): History {
input: event.input, input: event.input,
} }
} }
break },
} "tool.success": (event) => {
case "tool.success": { if (!pendingAssistant) return
if (!pendingAssistant) break const match = latestTool(event.callID)
const match = pendingAssistant.content.findLast((x) => x.type === "tool")
if (match && match.state.status === "running") { if (match && match.state.status === "running") {
match.state = { match.state = {
status: "completed", status: "completed",
@ -230,15 +240,13 @@ export function step(old: History, event: SessionEvent.Event): History {
output: event.output ?? "", output: event.output ?? "",
title: event.title, title: event.title,
metadata: event.metadata ?? {}, metadata: event.metadata ?? {},
// @ts-expect-error attachments: [...(event.attachments ?? [])],
attachments: event.attachments ?? [],
} }
} }
break },
} "tool.error": (event) => {
case "tool.error": { if (!pendingAssistant) return
if (!pendingAssistant) break const match = latestTool(event.callID)
const match = pendingAssistant.content.findLast((x) => x.type === "tool")
if (match && match.state.status === "running") { if (match && match.state.status === "running") {
match.state = { match.state = {
status: "error", status: "error",
@ -247,36 +255,29 @@ export function step(old: History, event: SessionEvent.Event): History {
metadata: event.metadata ?? {}, metadata: event.metadata ?? {},
} }
} }
break },
} "reasoning.started": () => {
case "reasoning.started": { if (!pendingAssistant) return
if (!pendingAssistant) break
pendingAssistant.content.push({ pendingAssistant.content.push({
type: "reasoning", type: "reasoning",
text: "", text: "",
}) })
break },
} "reasoning.delta": (event) => {
case "reasoning.delta": { if (!pendingAssistant) return
if (!pendingAssistant) break const match = latestReasoning()
const match = pendingAssistant.content.findLast((x) => x.type === "reasoning")
if (match) match.text += event.delta if (match) match.text += event.delta
break },
} "reasoning.ended": (event) => {
case "reasoning.ended": { if (!pendingAssistant) return
if (!pendingAssistant) break const match = latestReasoning()
const match = pendingAssistant.content.findLast((x) => x.type === "reasoning")
if (match) match.text = event.text if (match) match.text = event.text
break },
} retried: () => {},
case "step.ended": { compacted: (event) => {
if (!pendingAssistant) break draft.entries.push(new Compaction({ ...event, type: "compaction", time: { created: event.timestamp } }))
pendingAssistant.time.completed = event.timestamp },
pendingAssistant.cost = event.cost })
pendingAssistant.tokens = event.tokens
break
}
}
}) })
} }

View file

@ -441,7 +441,7 @@ export namespace SessionEvent {
{ {
mode: "oneOf", mode: "oneOf",
}, },
) ).pipe(Schema.toTaggedUnion("type"))
export type Event = Schema.Schema.Type<typeof Event> export type Event = Schema.Schema.Type<typeof Event>
export type Type = Event["type"] export type Type = Event["type"]
} }

View file

@ -591,7 +591,64 @@ describe("session-entry step", () => {
) )
}) })
test.failing("records synthetic events", () => { test("routes tool events by callID when tool streams interleave", () => {
FastCheck.assert(
FastCheck.property(dict, dict, word, word, text, text, (a, b, titleA, titleB, deltaA, deltaB) => {
const next = run(
[
SessionEvent.Tool.Input.Started.create({ callID: "a", name: "bash", timestamp: time(1) }),
SessionEvent.Tool.Input.Started.create({ callID: "b", name: "grep", timestamp: time(2) }),
SessionEvent.Tool.Input.Delta.create({ callID: "a", delta: deltaA, timestamp: time(3) }),
SessionEvent.Tool.Input.Delta.create({ callID: "b", delta: deltaB, timestamp: time(4) }),
SessionEvent.Tool.Called.create({
callID: "a",
tool: "bash",
input: a,
provider: { executed: true },
timestamp: time(5),
}),
SessionEvent.Tool.Called.create({
callID: "b",
tool: "grep",
input: b,
provider: { executed: true },
timestamp: time(6),
}),
SessionEvent.Tool.Success.create({
callID: "a",
title: titleA,
output: "done-a",
provider: { executed: true },
timestamp: time(7),
}),
SessionEvent.Tool.Success.create({
callID: "b",
title: titleB,
output: "done-b",
provider: { executed: true },
timestamp: time(8),
}),
],
active(),
)
const first = tool(next, "a")
const second = tool(next, "b")
expect(first?.state.status).toBe("completed")
expect(second?.state.status).toBe("completed")
if (first?.state.status !== "completed" || second?.state.status !== "completed") return
expect(first.state.input).toEqual(a)
expect(second.state.input).toEqual(b)
expect(first.state.title).toBe(titleA)
expect(second.state.title).toBe(titleB)
}),
{ numRuns: 50 },
)
})
test("records synthetic events", () => {
FastCheck.assert( FastCheck.assert(
FastCheck.property(word, (body) => { FastCheck.property(word, (body) => {
const next = SessionEntry.step(history(), SessionEvent.Synthetic.create({ text: body, timestamp: time(1) })) const next = SessionEntry.step(history(), SessionEvent.Synthetic.create({ text: body, timestamp: time(1) }))
@ -604,7 +661,7 @@ describe("session-entry step", () => {
) )
}) })
test.failing("records compaction events", () => { test("records compaction events", () => {
FastCheck.assert( FastCheck.assert(
FastCheck.property(FastCheck.boolean(), maybe(FastCheck.boolean()), (auto, overflow) => { FastCheck.property(FastCheck.boolean(), maybe(FastCheck.boolean()), (auto, overflow) => {
const next = SessionEntry.step( const next = SessionEntry.step(

View file

@ -1,7 +1,7 @@
{ {
"$schema": "https://json.schemastore.org/package.json", "$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/plugin", "name": "@opencode-ai/plugin",
"version": "1.4.10", "version": "1.4.11",
"type": "module", "type": "module",
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {

View file

@ -1,7 +1,7 @@
{ {
"$schema": "https://json.schemastore.org/package.json", "$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/sdk", "name": "@opencode-ai/sdk",
"version": "1.4.10", "version": "1.4.11",
"type": "module", "type": "module",
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {

View file

@ -1,6 +1,6 @@
{ {
"$schema": "https://json.schemastore.org/package.json", "$schema": "https://json.schemastore.org/package.json",
"version": "1.4.10", "version": "1.4.11",
"name": "@opencode-ai/shared", "name": "@opencode-ai/shared",
"type": "module", "type": "module",
"license": "MIT", "license": "MIT",

View file

@ -1,6 +1,6 @@
{ {
"name": "@opencode-ai/slack", "name": "@opencode-ai/slack",
"version": "1.4.10", "version": "1.4.11",
"type": "module", "type": "module",
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {

View file

@ -1,6 +1,6 @@
{ {
"name": "@opencode-ai/ui", "name": "@opencode-ai/ui",
"version": "1.4.10", "version": "1.4.11",
"type": "module", "type": "module",
"license": "MIT", "license": "MIT",
"exports": { "exports": {

View file

@ -2,7 +2,7 @@
"name": "@opencode-ai/web", "name": "@opencode-ai/web",
"type": "module", "type": "module",
"license": "MIT", "license": "MIT",
"version": "1.4.10", "version": "1.4.11",
"scripts": { "scripts": {
"dev": "astro dev", "dev": "astro dev",
"dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev", "dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev",

View file

@ -2,7 +2,7 @@
"name": "opencode", "name": "opencode",
"displayName": "opencode", "displayName": "opencode",
"description": "opencode for VS Code", "description": "opencode for VS Code",
"version": "1.4.10", "version": "1.4.11",
"publisher": "sst-dev", "publisher": "sst-dev",
"repository": { "repository": {
"type": "git", "type": "git",