Ver código fonte

Merge branch 'dev' into snapshot-node-shim-stuff

Aiden Cline 5 dias atrás
pai
commit
4721b31d35
100 arquivos alterados com 12830 adições e 12053 exclusões
  1. 37 25
      bun.lock
  2. 4 4
      nix/hashes.json
  3. 3 3
      package.json
  4. 2 1
      packages/console/app/src/routes/download/index.css
  5. 9 5
      packages/opencode/package.json
  6. 42 10
      packages/opencode/specs/effect-migration.md
  7. 2 17
      packages/opencode/src/account/index.ts
  8. 2 2
      packages/opencode/src/account/repo.ts
  9. 6 26
      packages/opencode/src/account/schema.ts
  10. 7 9
      packages/opencode/src/agent/agent.ts
  11. 2 2
      packages/opencode/src/auth/index.ts
  12. 4 3
      packages/opencode/src/bus/index.ts
  13. 6 5
      packages/opencode/src/cli/cmd/account.ts
  14. 10 7
      packages/opencode/src/cli/cmd/debug/agent.ts
  15. 8 4
      packages/opencode/src/cli/cmd/github.ts
  16. 3 2
      packages/opencode/src/cli/cmd/import.ts
  17. 18 7
      packages/opencode/src/cli/cmd/pr.ts
  18. 12 1
      packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx
  19. 8 2
      packages/opencode/src/cli/cmd/tui/thread.ts
  20. 2 1
      packages/opencode/src/cli/cmd/uninstall.ts
  21. 8 3
      packages/opencode/src/cli/cmd/upgrade.ts
  22. 4 3
      packages/opencode/src/cli/upgrade.ts
  23. 6 10
      packages/opencode/src/command/index.ts
  24. 23 19
      packages/opencode/src/config/config.ts
  25. 1 2
      packages/opencode/src/control-plane/schema.ts
  26. 2 2
      packages/opencode/src/control-plane/workspace-context.ts
  27. 9 0
      packages/opencode/src/effect/bootstrap-runtime.ts
  28. 13 0
      packages/opencode/src/effect/cross-spawn-spawner.ts
  29. 3 3
      packages/opencode/src/effect/instance-ref.ts
  30. 8 5
      packages/opencode/src/effect/instance-state.ts
  31. 67 0
      packages/opencode/src/effect/logger.ts
  32. 32 25
      packages/opencode/src/effect/oltp.ts
  33. 4 4
      packages/opencode/src/effect/run-service.ts
  34. 2 2
      packages/opencode/src/file/index.ts
  35. 2 2
      packages/opencode/src/file/ripgrep.ts
  36. 6 6
      packages/opencode/src/file/time.ts
  37. 2 2
      packages/opencode/src/file/watcher.ts
  38. 2 2
      packages/opencode/src/filesystem/index.ts
  39. 2 17
      packages/opencode/src/format/index.ts
  40. 2 13
      packages/opencode/src/git/index.ts
  41. 2 17
      packages/opencode/src/installation/index.ts
  42. 13 2
      packages/opencode/src/lsp/index.ts
  43. 2 2
      packages/opencode/src/mcp/auth.ts
  44. 7 4
      packages/opencode/src/mcp/index.ts
  45. 2 2
      packages/opencode/src/permission/index.ts
  46. 1 5
      packages/opencode/src/permission/schema.ts
  47. 18 11
      packages/opencode/src/plugin/index.ts
  48. 3 2
      packages/opencode/src/project/bootstrap.ts
  49. 6 5
      packages/opencode/src/project/instance.ts
  50. 2 2
      packages/opencode/src/project/project.ts
  51. 1 2
      packages/opencode/src/project/schema.ts
  52. 2 2
      packages/opencode/src/project/vcs.ts
  53. 2 21
      packages/opencode/src/provider/auth.ts
  54. 11532 10994
      packages/opencode/src/provider/models-snapshot.js
  55. 5 3
      packages/opencode/src/provider/provider.ts
  56. 11 13
      packages/opencode/src/provider/schema.ts
  57. 4 1
      packages/opencode/src/provider/transform.ts
  58. 5 4
      packages/opencode/src/pty/index.ts
  59. 1 2
      packages/opencode/src/pty/schema.ts
  60. 2 2
      packages/opencode/src/question/index.ts
  61. 1 5
      packages/opencode/src/question/schema.ts
  62. 40 0
      packages/opencode/src/server/adapter.bun.ts
  63. 66 0
      packages/opencode/src/server/adapter.node.ts
  64. 21 0
      packages/opencode/src/server/adapter.ts
  65. 150 0
      packages/opencode/src/server/control/index.ts
  66. 0 0
      packages/opencode/src/server/instance/config.ts
  67. 0 0
      packages/opencode/src/server/instance/event.ts
  68. 40 17
      packages/opencode/src/server/instance/experimental.ts
  69. 0 0
      packages/opencode/src/server/instance/file.ts
  70. 36 18
      packages/opencode/src/server/instance/global.ts
  71. 27 82
      packages/opencode/src/server/instance/index.ts
  72. 0 0
      packages/opencode/src/server/instance/mcp.ts
  73. 6 10
      packages/opencode/src/server/instance/middleware.ts
  74. 0 0
      packages/opencode/src/server/instance/permission.ts
  75. 0 0
      packages/opencode/src/server/instance/project.ts
  76. 20 11
      packages/opencode/src/server/instance/provider.ts
  77. 0 0
      packages/opencode/src/server/instance/pty.ts
  78. 0 0
      packages/opencode/src/server/instance/question.ts
  79. 11 2
      packages/opencode/src/server/instance/session.ts
  80. 0 0
      packages/opencode/src/server/instance/tui.ts
  81. 0 0
      packages/opencode/src/server/instance/workspace.ts
  82. 82 23
      packages/opencode/src/server/middleware.ts
  83. 28 275
      packages/opencode/src/server/server.ts
  84. 55 0
      packages/opencode/src/server/ui/index.ts
  85. 2 2
      packages/opencode/src/session/compaction.ts
  86. 30 18
      packages/opencode/src/session/index.ts
  87. 2 2
      packages/opencode/src/session/instruction.ts
  88. 2 2
      packages/opencode/src/session/llm.ts
  89. 2 1
      packages/opencode/src/session/message-v2.ts
  90. 8 7
      packages/opencode/src/session/processor.ts
  91. 133 126
      packages/opencode/src/session/prompt.ts
  92. 2 2
      packages/opencode/src/session/revert.ts
  93. 2 2
      packages/opencode/src/session/run-state.ts
  94. 3 6
      packages/opencode/src/session/schema.ts
  95. 2 16
      packages/opencode/src/session/status.ts
  96. 2 2
      packages/opencode/src/session/summary.ts
  97. 45 37
      packages/opencode/src/session/system.ts
  98. 2 8
      packages/opencode/src/session/todo.ts
  99. 2 2
      packages/opencode/src/share/session.ts
  100. 2 25
      packages/opencode/src/share/share-next.ts

+ 37 - 25
bun.lock

@@ -319,7 +319,7 @@
         "@actions/core": "1.11.1",
         "@actions/github": "6.0.1",
         "@agentclientprotocol/sdk": "0.16.1",
-        "@ai-sdk/amazon-bedrock": "4.0.83",
+        "@ai-sdk/amazon-bedrock": "4.0.93",
         "@ai-sdk/anthropic": "3.0.67",
         "@ai-sdk/azure": "3.0.49",
         "@ai-sdk/cerebras": "2.0.41",
@@ -331,7 +331,7 @@
         "@ai-sdk/groq": "3.0.31",
         "@ai-sdk/mistral": "3.0.27",
         "@ai-sdk/openai": "3.0.48",
-        "@ai-sdk/openai-compatible": "2.0.37",
+        "@ai-sdk/openai-compatible": "2.0.41",
         "@ai-sdk/perplexity": "3.0.26",
         "@ai-sdk/provider": "3.0.8",
         "@ai-sdk/provider-utils": "4.0.23",
@@ -341,7 +341,6 @@
         "@aws-sdk/credential-providers": "3.993.0",
         "@clack/prompts": "1.0.0-alpha.1",
         "@effect/platform-node": "catalog:",
-        "@gitlab/gitlab-ai-provider": "3.6.0",
         "@gitlab/opencode-gitlab-auth": "1.3.3",
         "@hono/node-server": "1.19.11",
         "@hono/node-ws": "1.3.0",
@@ -357,7 +356,7 @@
         "@opencode-ai/script": "workspace:*",
         "@opencode-ai/sdk": "workspace:*",
         "@opencode-ai/util": "workspace:*",
-        "@openrouter/ai-sdk-provider": "2.4.2",
+        "@openrouter/ai-sdk-provider": "2.5.1",
         "@opentui/core": "0.1.97",
         "@opentui/solid": "0.1.97",
         "@parcel/watcher": "2.5.1",
@@ -413,7 +412,7 @@
       },
       "devDependencies": {
         "@babel/core": "7.28.4",
-        "@effect/language-service": "0.79.0",
+        "@effect/language-service": "0.84.2",
         "@octokit/webhooks-types": "7.6.1",
         "@opencode-ai/script": "workspace:*",
         "@parcel/watcher-darwin-arm64": "2.5.1",
@@ -450,6 +449,7 @@
       "version": "1.4.3",
       "dependencies": {
         "@opencode-ai/sdk": "workspace:*",
+        "effect": "catalog:",
         "zod": "catalog:",
       },
       "devDependencies": {
@@ -641,7 +641,7 @@
   },
   "catalog": {
     "@cloudflare/workers-types": "4.20251008.0",
-    "@effect/platform-node": "4.0.0-beta.43",
+    "@effect/platform-node": "4.0.0-beta.46",
     "@hono/zod-validator": "0.4.2",
     "@kobalte/core": "0.13.11",
     "@lydell/node-pty": "1.2.0-beta.10",
@@ -662,13 +662,13 @@
     "@types/node": "22.13.9",
     "@types/semver": "7.7.1",
     "@typescript/native-preview": "7.0.0-dev.20251207.1",
-    "ai": "6.0.149",
+    "ai": "6.0.158",
     "cross-spawn": "7.0.6",
     "diff": "8.0.2",
     "dompurify": "3.3.1",
     "drizzle-kit": "1.0.0-beta.19-d95b7a4",
     "drizzle-orm": "1.0.0-beta.19-d95b7a4",
-    "effect": "4.0.0-beta.43",
+    "effect": "4.0.0-beta.46",
     "fuzzysort": "3.1.0",
     "hono": "4.10.7",
     "hono-openapi": "1.1.2",
@@ -707,7 +707,7 @@
 
     "@agentclientprotocol/sdk": ["@agentclientprotocol/[email protected]", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-1ad+Sc/0sCtZGHthxxvgEUo5Wsbw16I+aF+YwdiLnPwkZG8KAGUEAPK6LM6Pf69lCyJPt1Aomk1d+8oE3C4ZEw=="],
 
-    "@ai-sdk/amazon-bedrock": ["@ai-sdk/[email protected].83", "", { "dependencies": { "@ai-sdk/anthropic": "3.0.64", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21", "@smithy/eventstream-codec": "^4.0.1", "@smithy/util-utf8": "^4.0.0", "aws4fetch": "^1.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-DoRpvIWGU/r83UeJAM9L93Lca8Kf/yP5fIhfEOltMPGP/PXrGe0BZaz0maLSRn8djJ6+HzWIsgu5ZI6bZqXEXg=="],
+    "@ai-sdk/amazon-bedrock": ["@ai-sdk/[email protected].93", "", { "dependencies": { "@ai-sdk/anthropic": "3.0.69", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@smithy/eventstream-codec": "^4.0.1", "@smithy/util-utf8": "^4.0.0", "aws4fetch": "^1.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-hcXDU8QDwpAzLVTuY932TQVlIij9+iaVTxc5mPGY6yb//JMAAC5hMVhg93IrxlrxWLvMgjezNgoZGwquR+SGnw=="],
 
     "@ai-sdk/anthropic": ["@ai-sdk/[email protected]", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-rwLi/Rsuj2pYniQXIrvClHvXDzgM4UQHHnvHTWEF14efnlKclG/1ghpNC+adsRujAbCTr6gRsSbDE2vEqriV7g=="],
 
@@ -1025,11 +1025,11 @@
 
     "@drizzle-team/brocli": ["@drizzle-team/[email protected]", "", {}, "sha512-hD3pekGiPg0WPCCGAZmusBBJsDqGUR66Y452YgQsZOnkdQ7ViEPKuyP4huUGEZQefp8g34RRodXYmJ2TbCH+tg=="],
 
-    "@effect/language-service": ["@effect/language-service@0.79.0", "", { "bin": { "effect-language-service": "cli.js" } }, "sha512-DEmIOsg1GjjP6s9HXH1oJrW+gDmzkhVv9WOZl6to5eNyyCrjz1S2PDqQ7aYrW/HuifhfwI5Bik1pK4pj7Z+lrg=="],
+    "@effect/language-service": ["@effect/language-service@0.84.2", "", { "bin": { "effect-language-service": "cli.js" } }, "sha512-l04qNxpiA8rY5yXWckRPJ7Mk5MNerXuNymSFf+IdflfI5i8jgL1bpBNLuP6ijg7wgjdHc/KmTnCj2kT0SCntuA=="],
 
-    "@effect/platform-node": ["@effect/[email protected]3", "", { "dependencies": { "@effect/platform-node-shared": "^4.0.0-beta.43", "mime": "^4.1.0", "undici": "^7.24.0" }, "peerDependencies": { "effect": "^4.0.0-beta.43", "ioredis": "^5.7.0" } }, "sha512-Uq6E1rjaIpjHauzjwoB2HzAg3battYt2Boy8XO50GoHiWCXKE6WapYZ0/AnaBx5v5qg2sOfqpuiLsUf9ZgxOkA=="],
+    "@effect/platform-node": ["@effect/[email protected]6", "", { "dependencies": { "@effect/platform-node-shared": "^4.0.0-beta.46", "mime": "^4.1.0", "undici": "^7.24.0" }, "peerDependencies": { "effect": "^4.0.0-beta.46", "ioredis": "^5.7.0" } }, "sha512-6AFRKjJO95dFl5lK/YnJi04uePjQDFi3+K1aXwcz/EfVlRwJ4+lg5O4vbievfKL/hnfcShVp3/eXnNS9tvlMZQ=="],
 
-    "@effect/platform-node-shared": ["@effect/[email protected]3", "", { "dependencies": { "@types/ws": "^8.18.1", "ws": "^8.19.0" }, "peerDependencies": { "effect": "^4.0.0-beta.43" } }, "sha512-A9q0GEb61pYcQ06Dr6gXj1nKlDI3KHsar1sk3qb1ZY+kVSR64tBAylI8zGon23KY+NPtTUj/sEIToB7jc3Qt5w=="],
+    "@effect/platform-node-shared": ["@effect/[email protected]6", "", { "dependencies": { "@types/ws": "^8.18.1", "ws": "^8.19.0" }, "peerDependencies": { "effect": "^4.0.0-beta.46" } }, "sha512-Yzci82XbZ1W3tuiownsJawrJZTGeTrTZKLD0uxdBWCBzlVyqDwoSwRwO5qh33DurJj9B7iS8MDf14fpGRBPNGQ=="],
 
     "@electron/asar": ["@electron/[email protected]", "", { "dependencies": { "commander": "^5.0.0", "glob": "^7.1.6", "minimatch": "^3.0.4" }, "bin": { "asar": "bin/asar.js" } }, "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA=="],
 
@@ -1161,8 +1161,6 @@
 
     "@gar/promise-retry": ["@gar/[email protected]", "", {}, "sha512-GmzA9ckNokPypTg10pgpeHNQe7ph+iIKKmhKu3Ob9ANkswreCx7R3cKmY781K8QK3AqVL3xVh9A42JvIAbkkSA=="],
 
-    "@gitlab/gitlab-ai-provider": ["@gitlab/[email protected]", "", { "dependencies": { "@anthropic-ai/sdk": "^0.71.0", "@anycable/core": "^0.9.2", "graphql-request": "^6.1.0", "isomorphic-ws": "^5.0.0", "openai": "^6.16.0", "socket.io-client": "^4.8.1", "vscode-jsonrpc": "^8.2.1", "zod": "^3.25.76" }, "peerDependencies": { "@ai-sdk/provider": ">=2.0.0", "@ai-sdk/provider-utils": ">=3.0.0" } }, "sha512-8LmcIQ86xkMtC7L4P1/QYVEC+yKMTRerfPeniaaQGalnzXKtX6iMHLjLPOL9Rxp55lOXi6ed0WrFuJzZx+fNRg=="],
-
     "@gitlab/opencode-gitlab-auth": ["@gitlab/[email protected]", "", { "dependencies": { "@fastify/rate-limit": "^10.2.0", "@opencode-ai/plugin": "*", "fastify": "^5.2.0", "open": "^10.0.0" } }, "sha512-FT+KsCmAJjtqWr1YAq0MywGgL9kaLQ4apmsoowAXrPqHtoYf2i/nY10/A+L06kNj22EATeEDRpbB1NWXMto/SA=="],
 
     "@graphql-typed-document-node/core": ["@graphql-typed-document-node/[email protected]", "", { "peerDependencies": { "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ=="],
@@ -1539,7 +1537,7 @@
 
     "@opencode-ai/web": ["@opencode-ai/web@workspace:packages/web"],
 
-    "@openrouter/ai-sdk-provider": ["@openrouter/ai-sdk-provider@2.4.2", "", { "peerDependencies": { "ai": "^6.0.0", "zod": "^3.25.0 || ^4.0.0" } }, "sha512-uRQZ4da77gru1I7/lNGJhKbqEIY7o/sPsLlbCM97VY9muGDjM/TaJzuwqIviqKTtXLzF0WDj5qBAi6FhxjvlSg=="],
+    "@openrouter/ai-sdk-provider": ["@openrouter/ai-sdk-provider@2.5.1", "", { "peerDependencies": { "ai": "^6.0.0", "zod": "^3.25.0 || ^4.0.0" } }, "sha512-r1fJL1Cb3gQDa2MpWH/sfx1BsEW0uzlRriJM6eihaKqbtKDmZoBisF32VcVaQYassighX7NGCkF68EsrZA43uQ=="],
 
     "@opentelemetry/api": ["@opentelemetry/[email protected]", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
 
@@ -2385,7 +2383,7 @@
 
     "agentkeepalive": ["[email protected]", "", { "dependencies": { "humanize-ms": "^1.2.1" } }, "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ=="],
 
-    "ai": ["[email protected]49", "", { "dependencies": { "@ai-sdk/gateway": "3.0.91", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-3asRb/m3ZGH7H4+VTuTgj8eQYJZ9IJUmV0ljLslY92mQp6Zj+NVn4SmFj0TBr2Y/wFBWC3xgn++47tSGOXxdbw=="],
+    "ai": ["[email protected]58", "", { "dependencies": { "@ai-sdk/gateway": "3.0.95", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-gLTp1UXFtMqKUi3XHs33K7UFglbvojkxF/aq337TxnLGOhHIW9+GyP2jwW4hYX87f1es+wId3VQoPRRu9zEStQ=="],
 
     "ai-gateway-provider": ["[email protected]", "", { "optionalDependencies": { "@ai-sdk/amazon-bedrock": "^4.0.62", "@ai-sdk/anthropic": "^3.0.46", "@ai-sdk/azure": "^3.0.31", "@ai-sdk/cerebras": "^2.0.34", "@ai-sdk/cohere": "^3.0.21", "@ai-sdk/deepgram": "^2.0.20", "@ai-sdk/deepseek": "^2.0.20", "@ai-sdk/elevenlabs": "^2.0.20", "@ai-sdk/fireworks": "^2.0.34", "@ai-sdk/google": "^3.0.30", "@ai-sdk/google-vertex": "^4.0.61", "@ai-sdk/groq": "^3.0.24", "@ai-sdk/mistral": "^3.0.20", "@ai-sdk/openai": "^3.0.30", "@ai-sdk/perplexity": "^3.0.19", "@ai-sdk/xai": "^3.0.57", "@openrouter/ai-sdk-provider": "^2.2.3" }, "peerDependencies": { "@ai-sdk/openai-compatible": "^2.0.0", "@ai-sdk/provider": "^3.0.0", "@ai-sdk/provider-utils": "^4.0.0", "ai": "^6.0.0" } }, "sha512-krGNnJSoO/gJ7Hbe5nQDlsBpDUGIBGtMQTRUaW7s1MylsfvLduba0TLWzQaGtOmNRkP0pGhtGlwsnS6FNQMlyw=="],
 
@@ -2889,7 +2887,7 @@
 
     "ee-first": ["[email protected]", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
 
-    "effect": ["[email protected]3", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.5.3", "find-my-way-ts": "^0.1.6", "ini": "^6.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^1.11.8", "multipasta": "^0.2.7", "toml": "^3.0.0", "uuid": "^13.0.0", "yaml": "^2.8.2" } }, "sha512-AJYyDimIwJOn87uUz/JzmgDc5GfjxJbXvEbTvNzMa+M3Uer344bLo/O5mMRkqc1vBleA+Ygs4+dbE3QsqOkKTQ=="],
+    "effect": ["[email protected]6", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.5.3", "find-my-way-ts": "^0.1.6", "ini": "^6.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^1.11.8", "multipasta": "^0.2.7", "toml": "^3.0.0", "uuid": "^13.0.0", "yaml": "^2.8.2" } }, "sha512-3f6gXvvUMtEueCRY0tU76Vq2Pej1SAwwE+s0Owd5nD53yS5n4RZhUA1rlCGFuSbQFA225pGy8vO72+lpvu7u5A=="],
 
     "ejs": ["[email protected]", "", { "dependencies": { "jake": "^10.8.5" }, "bin": { "ejs": "bin/cli.js" } }, "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA=="],
 
@@ -5005,7 +5003,11 @@
 
     "@actions/http-client/undici": ["[email protected]", "", {}, "sha512-sC+b0tB1whOCzbtlx20fx3WgCXwkW627p4EA9uM+/tNNPkSS+eSEld6pAs9nDv7WbY1UUljBMYPtu9BCOrCWKA=="],
 
-    "@ai-sdk/amazon-bedrock/@ai-sdk/provider-utils": ["@ai-sdk/[email protected]", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="],
+    "@ai-sdk/amazon-bedrock/@ai-sdk/anthropic": ["@ai-sdk/[email protected]", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-LshR7X3pFugY0o41G2VKTmg1XoGpSl7uoYWfzk6zjVZLhCfeFiwgpOga+eTV4XY1VVpZwKVqRnkDbIL7K2eH5g=="],
+
+    "@ai-sdk/amazon-bedrock/@smithy/eventstream-codec": ["@smithy/[email protected]", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.13.1", "@smithy/util-hex-encoding": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-FE3bZdEl62ojmy8x4FHqxq2+BuOHlcxiH5vaZ6aqHJr3AIZzwF5jfx8dEiU/X0a8RboyNDjmXjlbr8AdEyLgiA=="],
+
+    "@ai-sdk/amazon-bedrock/@smithy/util-utf8": ["@smithy/[email protected]", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="],
 
     "@ai-sdk/anthropic/@ai-sdk/provider-utils": ["@ai-sdk/[email protected]", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="],
 
@@ -5281,10 +5283,6 @@
 
     "@fastify/proxy-addr/ipaddr.js": ["[email protected]", "", {}, "sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg=="],
 
-    "@gitlab/gitlab-ai-provider/openai": ["[email protected]", "", { "peerDependencies": { "ws": "^8.18.0", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-xAYN1W3YsDXJWA5F277135YfkEk6H7D3D6vWwRhJ3OEkzRgcyK8z/P5P9Gyi/wB4N8kK9kM5ZjprfvyHagKmpw=="],
-
-    "@gitlab/gitlab-ai-provider/zod": ["[email protected]", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
-
     "@gitlab/opencode-gitlab-auth/open": ["[email protected]", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="],
 
     "@hey-api/openapi-ts/open": ["[email protected]", "", { "dependencies": { "default-browser": "^5.4.0", "define-lazy-prop": "^3.0.0", "is-in-ssh": "^1.0.0", "is-inside-container": "^1.0.0", "powershell-utils": "^0.1.0", "wsl-utils": "^0.3.0" } }, "sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw=="],
@@ -5509,6 +5507,10 @@
 
     "@solidjs/start/vite-plugin-solid": ["[email protected]", "", { "dependencies": { "@babel/core": "^7.23.3", "@types/babel__core": "^7.20.4", "babel-preset-solid": "^1.8.4", "merge-anything": "^5.1.7", "solid-refresh": "^0.6.3", "vitefu": "^1.0.4" }, "peerDependencies": { "@testing-library/jest-dom": "^5.16.6 || ^5.17.0 || ^6.*", "solid-js": "^1.7.2", "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["@testing-library/jest-dom"] }, "sha512-YMZCXsLw9kyuvQFEdwLP27fuTQJLmjNoHy90AOJnbRuJ6DwShUxKFo38gdFrWn9v11hnGicKCZEaeI/TFs6JKw=="],
 
+    "@standard-community/standard-json/effect": ["[email protected]", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.5.3", "find-my-way-ts": "^0.1.6", "ini": "^6.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^1.11.8", "multipasta": "^0.2.7", "toml": "^3.0.0", "uuid": "^13.0.0", "yaml": "^2.8.2" } }, "sha512-AJYyDimIwJOn87uUz/JzmgDc5GfjxJbXvEbTvNzMa+M3Uer344bLo/O5mMRkqc1vBleA+Ygs4+dbE3QsqOkKTQ=="],
+
+    "@standard-community/standard-openapi/effect": ["[email protected]", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.5.3", "find-my-way-ts": "^0.1.6", "ini": "^6.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^1.11.8", "multipasta": "^0.2.7", "toml": "^3.0.0", "uuid": "^13.0.0", "yaml": "^2.8.2" } }, "sha512-AJYyDimIwJOn87uUz/JzmgDc5GfjxJbXvEbTvNzMa+M3Uer344bLo/O5mMRkqc1vBleA+Ygs4+dbE3QsqOkKTQ=="],
+
     "@tailwindcss/oxide/detect-libc": ["[email protected]", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
 
     "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/[email protected]", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA=="],
@@ -5549,7 +5551,9 @@
 
     "accepts/mime-types": ["[email protected]", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
 
-    "ai/@ai-sdk/gateway": ["@ai-sdk/[email protected]", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@vercel/oidc": "3.1.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-J39Dh6Gyg6HjG3A7OFKnJMp3QyZ3Eex+XDiX8aFBdRwwZm3jGWaMhkCxQPH7yiQ9kRiErZwHXX/Oexx4SyGGGA=="],
+    "ai/@ai-sdk/gateway": ["@ai-sdk/[email protected]", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@vercel/oidc": "3.1.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ZmUNNbZl3V42xwQzPaNUi+s8eqR2lnrxf0bvB6YbLXpLjHYv0k2Y78t12cNOfY0bxGeuVVTLyk856uLuQIuXEQ=="],
+
+    "ai-gateway-provider/@ai-sdk/amazon-bedrock": ["@ai-sdk/[email protected]", "", { "dependencies": { "@ai-sdk/anthropic": "3.0.64", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21", "@smithy/eventstream-codec": "^4.0.1", "@smithy/util-utf8": "^4.0.0", "aws4fetch": "^1.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-DoRpvIWGU/r83UeJAM9L93Lca8Kf/yP5fIhfEOltMPGP/PXrGe0BZaz0maLSRn8djJ6+HzWIsgu5ZI6bZqXEXg=="],
 
     "ai-gateway-provider/@openrouter/ai-sdk-provider": ["@openrouter/[email protected]", "", { "peerDependencies": { "ai": "^6.0.0", "zod": "^3.25.0 || ^4.0.0" } }, "sha512-4fVteGkVedc7fGoA9+qJs4tpYwALezMq14m2Sjub3KmyRlksCbK+WJf67NPdGem8+NZrV2tAN42A1NU3+SiV3w=="],
 
@@ -5765,6 +5769,8 @@
 
     "opencode/@ai-sdk/anthropic": ["@ai-sdk/[email protected]", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-FFX4P5Fd6lcQJc2OLngZQkbbJHa0IDDZi087Edb8qRZx6h90krtM61ArbMUL8us/7ZUwojCXnyJ/wQ2Eflx2jQ=="],
 
+    "opencode/@ai-sdk/openai-compatible": ["@ai-sdk/[email protected]", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-kNAGINk71AlOXx10Dq/PXw4t/9XjdK8uxfpVElRwtSFMdeSiLVt58p9TPx4/FJD+hxZuVhvxYj9r42osxWq79g=="],
+
     "opencontrol/@modelcontextprotocol/sdk": ["@modelcontextprotocol/[email protected]", "", { "dependencies": { "content-type": "^1.0.5", "cors": "^2.8.5", "eventsource": "^3.0.2", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "pkce-challenge": "^4.1.0", "raw-body": "^3.0.0", "zod": "^3.23.8", "zod-to-json-schema": "^3.24.1" } }, "sha512-oxzMzYCkZHMntzuyerehK3fV6A2Kwh5BD6CGEJSVDU2QNEhfLOptf2X7esQgaHZXHZY0oHmMsOtIDLP71UJXgA=="],
 
     "opencontrol/@tsconfig/bun": ["@tsconfig/[email protected]", "", {}, "sha512-udGrGJBNQdXGVulehc1aWT73wkR9wdaGBtB6yL70RJsqwW/yJhIg6ZbRlPOfIUiFNrnBuYLBi9CSmMKfDC7dvA=="],
@@ -5947,8 +5953,6 @@
 
     "@actions/github/@octokit/plugin-rest-endpoint-methods/@octokit/types": ["@octokit/[email protected]", "", { "dependencies": { "@octokit/openapi-types": "^20.0.0" } }, "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw=="],
 
-    "@ai-sdk/amazon-bedrock/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/[email protected]", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
-
     "@ai-sdk/anthropic/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/[email protected]", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
 
     "@ai-sdk/azure/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/[email protected]", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
@@ -6435,12 +6439,18 @@
 
     "@solidjs/start/shiki/@shikijs/types": ["@shikijs/[email protected]", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.1", "@types/hast": "^3.0.4" } }, "sha512-VJjK0eIijTZf0QSTODEXCqinjBn0joAHQ+aPSBzrv4O2d/QSbsMw+ZeSRx03kV34Hy7NzUvV/7NqfYGRLrASmw=="],
 
+    "@standard-community/standard-json/effect/@standard-schema/spec": ["@standard-schema/[email protected]", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
+
+    "@standard-community/standard-openapi/effect/@standard-schema/spec": ["@standard-schema/[email protected]", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
+
     "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@tybys/wasm-util": ["@tybys/[email protected]", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
 
     "@vitest/expect/@vitest/utils/@vitest/pretty-format": ["@vitest/[email protected]", "", { "dependencies": { "tinyrainbow": "^2.0.0" } }, "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA=="],
 
     "accepts/mime-types/mime-db": ["[email protected]", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
 
+    "ai-gateway-provider/@ai-sdk/amazon-bedrock/@ai-sdk/provider-utils": ["@ai-sdk/[email protected]", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="],
+
     "ajv-keywords/ajv/json-schema-traverse": ["[email protected]", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
 
     "ansi-align/string-width/emoji-regex": ["[email protected]", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
@@ -6813,6 +6823,8 @@
 
     "@solidjs/start/shiki/@shikijs/engine-javascript/oniguruma-to-es": ["[email protected]", "", { "dependencies": { "emoji-regex-xs": "^1.0.0", "regex": "^5.1.1", "regex-recursion": "^5.1.1" } }, "sha512-bwALDxriqfKGfUufKGGepCzu9x7nJQuoRoAFp4AnwehhC2crqrDIAP/uN2qdlsAvSMpeRC3+Yzhqc7hLmle5+g=="],
 
+    "ai-gateway-provider/@ai-sdk/amazon-bedrock/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/[email protected]", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
+
     "ansi-align/string-width/strip-ansi/ansi-regex": ["[email protected]", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
 
     "app-builder-lib/@electron/get/fs-extra/universalify": ["[email protected]", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="],

+ 4 - 4
nix/hashes.json

@@ -1,8 +1,8 @@
 {
   "nodeModules": {
-    "x86_64-linux": "sha256-285KZ7rZLRoc6XqCZRHc25NE+mmpGh/BVeMpv8aPQtQ=",
-    "aarch64-linux": "sha256-qIwmY4TP4CI7R7G6A5OMYRrorVNXjkg25tTtVpIHm2o=",
-    "aarch64-darwin": "sha256-RwvnZQhdYZ0u7h7evyfxuPLHHX9eO/jXTAxIFc8B+IE=",
-    "x86_64-darwin": "sha256-vVj40al+TEeMpbe5XG2GmJEpN+eQAvtr9W0T98l5PBE="
+    "x86_64-linux": "sha256-fNRQYkucjXr1D61HJRScJpDa6+oBdyhgTBxCu+PE2kQ=",
+    "aarch64-linux": "sha256-V8J6kn2nSdXrplyqi6aIqNlHcVjSxvye+yC/YFO7PF4=",
+    "aarch64-darwin": "sha256-6cLmUJVUycGALCmslXuloVGBSlFOSHRjsWjx7KOW8rg=",
+    "x86_64-darwin": "sha256-kcOSO3NFIJh79ylLotG41ovWLQfH5kh1WYFghUu+4HE="
   }
 }

+ 3 - 3
package.json

@@ -26,7 +26,7 @@
       "packages/slack"
     ],
     "catalog": {
-      "@effect/platform-node": "4.0.0-beta.43",
+      "@effect/platform-node": "4.0.0-beta.46",
       "@types/bun": "1.3.11",
       "@types/cross-spawn": "6.0.6",
       "@octokit/rest": "22.0.0",
@@ -47,8 +47,8 @@
       "dompurify": "3.3.1",
       "drizzle-kit": "1.0.0-beta.19-d95b7a4",
       "drizzle-orm": "1.0.0-beta.19-d95b7a4",
-      "effect": "4.0.0-beta.43",
-      "ai": "6.0.149",
+      "effect": "4.0.0-beta.46",
+      "ai": "6.0.158",
       "cross-spawn": "7.0.6",
       "hono": "4.10.7",
       "hono-openapi": "1.1.2",

+ 2 - 1
packages/console/app/src/routes/download/index.css

@@ -316,7 +316,8 @@
 
   /* Download Hero Section */
   [data-component="download-hero"] {
-    display: grid;
+    /* display: grid; */
+    display: none;
     grid-template-columns: 260px 1fr;
     gap: 4rem;
     padding-bottom: 2rem;

+ 9 - 5
packages/opencode/package.json

@@ -39,11 +39,16 @@
       "bun": "./src/pty/pty.bun.ts",
       "node": "./src/pty/pty.node.ts",
       "default": "./src/pty/pty.bun.ts"
+    },
+    "#hono": {
+      "bun": "./src/server/adapter.bun.ts",
+      "node": "./src/server/adapter.node.ts",
+      "default": "./src/server/adapter.bun.ts"
     }
   },
   "devDependencies": {
     "@babel/core": "7.28.4",
-    "@effect/language-service": "0.79.0",
+    "@effect/language-service": "0.84.2",
     "@octokit/webhooks-types": "7.6.1",
     "@opencode-ai/script": "workspace:*",
     "@parcel/watcher-darwin-arm64": "2.5.1",
@@ -78,7 +83,7 @@
     "@actions/core": "1.11.1",
     "@actions/github": "6.0.1",
     "@agentclientprotocol/sdk": "0.16.1",
-    "@ai-sdk/amazon-bedrock": "4.0.83",
+    "@ai-sdk/amazon-bedrock": "4.0.93",
     "@ai-sdk/anthropic": "3.0.67",
     "@ai-sdk/azure": "3.0.49",
     "@ai-sdk/cerebras": "2.0.41",
@@ -90,7 +95,7 @@
     "@ai-sdk/groq": "3.0.31",
     "@ai-sdk/mistral": "3.0.27",
     "@ai-sdk/openai": "3.0.48",
-    "@ai-sdk/openai-compatible": "2.0.37",
+    "@ai-sdk/openai-compatible": "2.0.41",
     "@ai-sdk/perplexity": "3.0.26",
     "@ai-sdk/provider": "3.0.8",
     "@ai-sdk/provider-utils": "4.0.23",
@@ -100,7 +105,6 @@
     "@aws-sdk/credential-providers": "3.993.0",
     "@clack/prompts": "1.0.0-alpha.1",
     "@effect/platform-node": "catalog:",
-    "@gitlab/gitlab-ai-provider": "3.6.0",
     "@gitlab/opencode-gitlab-auth": "1.3.3",
     "@hono/node-server": "1.19.11",
     "@hono/node-ws": "1.3.0",
@@ -116,7 +120,7 @@
     "@opencode-ai/script": "workspace:*",
     "@opencode-ai/sdk": "workspace:*",
     "@opencode-ai/util": "workspace:*",
-    "@openrouter/ai-sdk-provider": "2.4.2",
+    "@openrouter/ai-sdk-provider": "2.5.1",
     "@opentui/core": "0.1.97",
     "@opentui/solid": "0.1.97",
     "@parcel/watcher": "2.5.1",

+ 42 - 10
packages/opencode/specs/effect-migration.md

@@ -23,7 +23,7 @@ export namespace Foo {
     readonly get: (id: FooID) => Effect.Effect<FooInfo, FooError>
   }
 
-  export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/Foo") {}
+  export class Service extends Context.Service<Service, Interface>()("@opencode/Foo") {}
 
   export const layer = Layer.effect(
     Service,
@@ -219,34 +219,34 @@ Fully migrated (single namespace, InstanceState where needed, flattened facade):
 - [x] `Instruction` — `session/instruction.ts`
 - [x] `Provider` — `provider/provider.ts`
 - [x] `Storage` — `storage/storage.ts`
+- [x] `ShareNext` — `share/share-next.ts`
 
 Still open:
 
-- [ ] `SessionTodo` — `session/todo.ts`
-- [ ] `ShareNext` — `share/share-next.ts`
+- [x] `SessionTodo` — `session/todo.ts`
 - [ ] `SyncEvent` — `sync/index.ts`
 - [ ] `Workspace` — `control-plane/workspace.ts`
 
 ## Tool interface → Effect
 
-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:
+`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:
 
-1. Migrate each tool to return Effects
-2. Update `Tool.define()` factory to work with Effects
-3. Update `SessionPrompt` to `yield*` tool results instead of `await`ing
+1. Migrate each tool body to return Effects
+2. Keep `Tool.define()` inputs Effect-native
+3. Update remaining callers to `yield*` tool initialization instead of `await`ing
 
 ### Tool migration details
 
-Until the tool interface itself returns `Effect`, use this transitional pattern for migrated tools:
+With `Tool.Info.init()` now effectful, use this transitional pattern for migrated tools that still need Promise-based boundaries internally:
 
 - `Tool.defineEffect(...)` should `yield*` the services the tool depends on and close over them in the returned tool definition.
-- 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.
+- 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()`.
 - If a tool starts requiring new services, wire them into `ToolRegistry.defaultLayer` so production callers resolve the same dependencies as tests.
 
 Tool tests should use the existing Effect helpers in `packages/opencode/test/lib/effect.ts`:
 
 - Use `testEffect(...)` / `it.live(...)` instead of creating fake local wrappers around effectful tools.
-- Yield the real tool export, then initialize it: `const info = yield* ReadTool`, `const tool = yield* Effect.promise(() => info.init())`.
+- Yield the real tool export, then initialize it: `const info = yield* ReadTool`, `const tool = yield* info.init()`.
 - Run tests inside a real instance with `provideTmpdirInstance(...)` or `provideInstance(tmpdirScoped(...))` so instance-scoped services resolve exactly as they do in production.
 
 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:
 - [ ] `util/flock.ts` — file-based distributed lock with heartbeat → Effect.repeat + addFinalizer
 - [ ] `util/process.ts` — child process spawn wrapper → return Effect instead of Promise
 - [ ] `util/lazy.ts` — replace uses in Effect code with Effect.cached; keep for sync-only code
+
+## Destroying the facades
+
+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.
+
+### Process
+
+For each service, the migration is roughly:
+
+1. **Find callers.** `grep -n "Namespace\.(methodA|methodB|...)"` across `src/` and `test/`. Skip the service file itself.
+2. **Migrate production callers.** For each effectful caller that does `Effect.tryPromise(() => Namespace.method(...))`:
+   - Add the service to the caller's layer R type (`Layer.Layer<Self, never, ... | Namespace.Service>`)
+   - Yield it at the top of the layer: `const ns = yield* Namespace.Service`
+   - Replace `Effect.tryPromise(() => Namespace.method(...))` with `yield* ns.method(...)` (or `ns.method(...).pipe(Effect.orElseSucceed(...))` for the common fallback case)
+   - Add `Layer.provide(Namespace.defaultLayer)` to the caller's own `defaultLayer` chain
+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.
+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`.
+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 }`.
+
+### Pitfalls
+
+- **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.
+- **`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.
+- **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.
+- **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.
+
+### Migration log
+
+- `SessionStatus` — migrated 2026-04-11. Replaced the last route and retry-policy callers with `AppRuntime.runPromise(SessionStatus.Service.use(...))` and removed the `makeRuntime(...)` facade.
+- `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.
+- `Storage` — migrated 2026-04-10. One production caller (`Session.diff`) and all storage.test.ts tests converted to effectful style. Facades and `makeRuntime` removed.

+ 2 - 17
packages/opencode/src/account/index.ts

@@ -1,4 +1,4 @@
-import { Cache, Clock, Duration, Effect, Layer, Option, Schema, SchemaGetter, ServiceMap } from "effect"
+import { Cache, Clock, Duration, Effect, Layer, Option, Schema, SchemaGetter, Context } from "effect"
 import {
   FetchHttpClient,
   HttpClient,
@@ -7,7 +7,6 @@ import {
   HttpClientResponse,
 } from "effect/unstable/http"
 
-import { makeRuntime } from "@/effect/run-service"
 import { withTransientReadRetry } from "@/util/effect-http-client"
 import { AccountRepo, type AccountRow } from "./repo"
 import { normalizeServerUrl } from "./url"
@@ -181,7 +180,7 @@ export namespace Account {
     readonly poll: (input: Login) => Effect.Effect<PollResult, AccountError>
   }
 
-  export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/Account") {}
+  export class Service extends Context.Service<Service, Interface>()("@opencode/Account") {}
 
   export const layer: Layer.Layer<Service, never, AccountRepo | HttpClient.HttpClient> = Layer.effect(
     Service,
@@ -454,18 +453,4 @@ export namespace Account {
   )
 
   export const defaultLayer = layer.pipe(Layer.provide(AccountRepo.layer), Layer.provide(FetchHttpClient.layer))
-
-  export const { runPromise } = makeRuntime(Service, defaultLayer)
-
-  export async function active(): Promise<Info | undefined> {
-    return Option.getOrUndefined(await runPromise((service) => service.active()))
-  }
-
-  export async function orgsByAccount(): Promise<readonly AccountOrgs[]> {
-    return runPromise((service) => service.orgsByAccount())
-  }
-
-  export async function switchOrg(accountID: AccountID, orgID: OrgID) {
-    return runPromise((service) => service.use(accountID, Option.some(orgID)))
-  }
 }

+ 2 - 2
packages/opencode/src/account/repo.ts

@@ -1,5 +1,5 @@
 import { eq } from "drizzle-orm"
-import { Effect, Layer, Option, Schema, ServiceMap } from "effect"
+import { Effect, Layer, Option, Schema, Context } from "effect"
 
 import { Database } from "@/storage/db"
 import { AccountStateTable, AccountTable } from "./account.sql"
@@ -38,7 +38,7 @@ export namespace AccountRepo {
   }
 }
 
-export class AccountRepo extends ServiceMap.Service<AccountRepo, AccountRepo.Service>()("@opencode/AccountRepo") {
+export class AccountRepo extends Context.Service<AccountRepo, AccountRepo.Service>()("@opencode/AccountRepo") {
   static readonly layer: Layer.Layer<AccountRepo> = Layer.effect(
     AccountRepo,
     Effect.gen(function* () {

+ 6 - 26
packages/opencode/src/account/schema.ts

@@ -1,42 +1,22 @@
 import { Schema } from "effect"
 import type * as HttpClientError from "effect/unstable/http/HttpClientError"
 
-import { withStatics } from "@/util/schema"
-
-export const AccountID = Schema.String.pipe(
-  Schema.brand("AccountID"),
-  withStatics((s) => ({ make: (id: string) => s.makeUnsafe(id) })),
-)
+export const AccountID = Schema.String.pipe(Schema.brand("AccountID"))
 export type AccountID = Schema.Schema.Type<typeof AccountID>
 
-export const OrgID = Schema.String.pipe(
-  Schema.brand("OrgID"),
-  withStatics((s) => ({ make: (id: string) => s.makeUnsafe(id) })),
-)
+export const OrgID = Schema.String.pipe(Schema.brand("OrgID"))
 export type OrgID = Schema.Schema.Type<typeof OrgID>
 
-export const AccessToken = Schema.String.pipe(
-  Schema.brand("AccessToken"),
-  withStatics((s) => ({ make: (token: string) => s.makeUnsafe(token) })),
-)
+export const AccessToken = Schema.String.pipe(Schema.brand("AccessToken"))
 export type AccessToken = Schema.Schema.Type<typeof AccessToken>
 
-export const RefreshToken = Schema.String.pipe(
-  Schema.brand("RefreshToken"),
-  withStatics((s) => ({ make: (token: string) => s.makeUnsafe(token) })),
-)
+export const RefreshToken = Schema.String.pipe(Schema.brand("RefreshToken"))
 export type RefreshToken = Schema.Schema.Type<typeof RefreshToken>
 
-export const DeviceCode = Schema.String.pipe(
-  Schema.brand("DeviceCode"),
-  withStatics((s) => ({ make: (code: string) => s.makeUnsafe(code) })),
-)
+export const DeviceCode = Schema.String.pipe(Schema.brand("DeviceCode"))
 export type DeviceCode = Schema.Schema.Type<typeof DeviceCode>
 
-export const UserCode = Schema.String.pipe(
-  Schema.brand("UserCode"),
-  withStatics((s) => ({ make: (code: string) => s.makeUnsafe(code) })),
-)
+export const UserCode = Schema.String.pipe(Schema.brand("UserCode"))
 export type UserCode = Schema.Schema.Type<typeof UserCode>
 
 export class Info extends Schema.Class<Info>("Account")({

+ 7 - 9
packages/opencode/src/agent/agent.ts

@@ -19,7 +19,7 @@ import { Global } from "@/global"
 import path from "path"
 import { Plugin } from "@/plugin"
 import { Skill } from "../skill"
-import { Effect, ServiceMap, Layer } from "effect"
+import { Effect, Context, Layer } from "effect"
 import { InstanceState } from "@/effect/instance-state"
 import { makeRuntime } from "@/effect/run-service"
 
@@ -67,7 +67,7 @@ export namespace Agent {
 
   type State = Omit<Interface, "generate">
 
-  export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/Agent") {}
+  export class Service extends Context.Service<Service, Interface>()("@opencode/Agent") {}
 
   export const layer = Layer.effect(
     Service,
@@ -398,13 +398,11 @@ export namespace Agent {
     }),
   )
 
-  export const defaultLayer = Layer.suspend(() =>
-    layer.pipe(
-      Layer.provide(Provider.defaultLayer),
-      Layer.provide(Auth.defaultLayer),
-      Layer.provide(Config.defaultLayer),
-      Layer.provide(Skill.defaultLayer),
-    ),
+  export const defaultLayer = layer.pipe(
+    Layer.provide(Provider.defaultLayer),
+    Layer.provide(Auth.defaultLayer),
+    Layer.provide(Config.defaultLayer),
+    Layer.provide(Skill.defaultLayer),
   )
 
   const { runPromise } = makeRuntime(Service, defaultLayer)

+ 2 - 2
packages/opencode/src/auth/index.ts

@@ -1,5 +1,5 @@
 import path from "path"
-import { Effect, Layer, Record, Result, Schema, ServiceMap } from "effect"
+import { Effect, Layer, Record, Result, Schema, Context } from "effect"
 import { makeRuntime } from "@/effect/run-service"
 import { zod } from "@/util/effect-zod"
 import { Global } from "../global"
@@ -49,7 +49,7 @@ export namespace Auth {
     readonly remove: (key: string) => Effect.Effect<void, AuthError>
   }
 
-  export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/Auth") {}
+  export class Service extends Context.Service<Service, Interface>()("@opencode/Auth") {}
 
   export const layer = Layer.effect(
     Service,

+ 4 - 3
packages/opencode/src/bus/index.ts

@@ -1,5 +1,6 @@
 import z from "zod"
-import { Effect, Exit, Layer, PubSub, Scope, ServiceMap, Stream } from "effect"
+import { Effect, Exit, Layer, PubSub, Scope, Context, Stream } from "effect"
+import { EffectLogger } from "@/effect/logger"
 import { Log } from "../util/log"
 import { BusEvent } from "./bus-event"
 import { GlobalBus } from "./global"
@@ -41,7 +42,7 @@ export namespace Bus {
     readonly subscribeAllCallback: (callback: (event: any) => unknown) => Effect.Effect<() => void>
   }
 
-  export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/Bus") {}
+  export class Service extends Context.Service<Service, Interface>()("@opencode/Bus") {}
 
   export const layer = Layer.effect(
     Service,
@@ -146,7 +147,7 @@ export namespace Bus {
 
           return () => {
             log.info("unsubscribing", { type })
-            Effect.runFork(Scope.close(scope, Exit.void))
+            Effect.runFork(Scope.close(scope, Exit.void).pipe(Effect.provide(EffectLogger.layer)))
           }
         })
       }

+ 6 - 5
packages/opencode/src/cli/cmd/account.ts

@@ -3,6 +3,7 @@ import { Duration, Effect, Match, Option } from "effect"
 import { UI } from "../ui"
 import { AccountID, Account, OrgID, PollExpired, type PollResult } from "@/account"
 import { type AccountError } from "@/account/schema"
+import { AppRuntime } from "@/effect/app-runtime"
 import * as Prompt from "../effect/prompt"
 import open from "open"
 
@@ -182,7 +183,7 @@ export const LoginCommand = cmd({
     }),
   async handler(args) {
     UI.empty()
-    await Account.runPromise((_svc) => loginEffect(args.url))
+    await AppRuntime.runPromise(loginEffect(args.url))
   },
 })
 
@@ -196,7 +197,7 @@ export const LogoutCommand = cmd({
     }),
   async handler(args) {
     UI.empty()
-    await Account.runPromise((_svc) => logoutEffect(args.email))
+    await AppRuntime.runPromise(logoutEffect(args.email))
   },
 })
 
@@ -205,7 +206,7 @@ export const SwitchCommand = cmd({
   describe: false,
   async handler() {
     UI.empty()
-    await Account.runPromise((_svc) => switchEffect())
+    await AppRuntime.runPromise(switchEffect())
   },
 })
 
@@ -214,7 +215,7 @@ export const OrgsCommand = cmd({
   describe: false,
   async handler() {
     UI.empty()
-    await Account.runPromise((_svc) => orgsEffect())
+    await AppRuntime.runPromise(orgsEffect())
   },
 })
 
@@ -223,7 +224,7 @@ export const OpenCommand = cmd({
   describe: false,
   async handler() {
     UI.empty()
-    await Account.runPromise((_svc) => openEffect())
+    await AppRuntime.runPromise(openEffect())
   },
 })
 

+ 10 - 7
packages/opencode/src/cli/cmd/debug/agent.ts

@@ -1,5 +1,6 @@
 import { EOL } from "os"
 import { basename } from "path"
+import { Effect } from "effect"
 import { Agent } from "../../../agent/agent"
 import { Provider } from "../../../provider/provider"
 import { Session } from "../../../session"
@@ -157,14 +158,16 @@ async function createToolContext(agent: Agent.Info) {
     agent: agent.name,
     abort: new AbortController().signal,
     messages: [],
-    metadata: () => {},
-    async ask(req: Omit<Permission.Request, "id" | "sessionID" | "tool">) {
-      for (const pattern of req.patterns) {
-        const rule = Permission.evaluate(req.permission, pattern, ruleset)
-        if (rule.action === "deny") {
-          throw new Permission.DeniedError({ ruleset })
+    metadata: () => Effect.void,
+    ask(req: Omit<Permission.Request, "id" | "sessionID" | "tool">) {
+      return Effect.sync(() => {
+        for (const pattern of req.patterns) {
+          const rule = Permission.evaluate(req.permission, pattern, ruleset)
+          if (rule.action === "deny") {
+            throw new Permission.DeniedError({ ruleset })
+          }
         }
-      }
+      })
     },
   }
 }

+ 8 - 4
packages/opencode/src/cli/cmd/github.ts

@@ -29,6 +29,7 @@ import { Provider } from "../../provider/provider"
 import { Bus } from "../../bus"
 import { MessageV2 } from "../../session/message-v2"
 import { SessionPrompt } from "@/session/prompt"
+import { AppRuntime } from "@/effect/app-runtime"
 import { Git } from "@/git"
 import { setTimeout as sleep } from "node:timers/promises"
 import { Process } from "@/util/process"
@@ -258,7 +259,9 @@ export const GithubInstallCommand = cmd({
             }
 
             // Get repo info
-            const info = (await Git.run(["remote", "get-url", "origin"], { cwd: Instance.worktree })).text().trim()
+            const info = await AppRuntime.runPromise(
+              Git.Service.use((git) => git.run(["remote", "get-url", "origin"], { cwd: Instance.worktree })),
+            ).then((x) => x.text().trim())
             const parsed = parseGitHubRemote(info)
             if (!parsed) {
               prompts.log.error(`Could not find git repository. Please run this command from a git repository.`)
@@ -497,20 +500,21 @@ export const GithubRunCommand = cmd({
           : "issue"
         : undefined
       const gitText = async (args: string[]) => {
-        const result = await Git.run(args, { cwd: Instance.worktree })
+        const result = await AppRuntime.runPromise(Git.Service.use((git) => git.run(args, { cwd: Instance.worktree })))
         if (result.exitCode !== 0) {
           throw new Process.RunFailedError(["git", ...args], result.exitCode, result.stdout, result.stderr)
         }
         return result.text().trim()
       }
       const gitRun = async (args: string[]) => {
-        const result = await Git.run(args, { cwd: Instance.worktree })
+        const result = await AppRuntime.runPromise(Git.Service.use((git) => git.run(args, { cwd: Instance.worktree })))
         if (result.exitCode !== 0) {
           throw new Process.RunFailedError(["git", ...args], result.exitCode, result.stdout, result.stderr)
         }
         return result
       }
-      const gitStatus = (args: string[]) => Git.run(args, { cwd: Instance.worktree })
+      const gitStatus = (args: string[]) =>
+        AppRuntime.runPromise(Git.Service.use((git) => git.run(args, { cwd: Instance.worktree })))
       const commitChanges = async (summary: string, actor?: string) => {
         const args = ["commit", "-m", summary]
         if (actor) args.push("-m", `Co-authored-by: ${actor} <${actor}@users.noreply.github.com>`)

+ 3 - 2
packages/opencode/src/cli/cmd/import.ts

@@ -10,6 +10,7 @@ import { Instance } from "../../project/instance"
 import { ShareNext } from "../../share/share-next"
 import { EOL } from "os"
 import { Filesystem } from "../../util/filesystem"
+import { AppRuntime } from "@/effect/app-runtime"
 
 /** Discriminated union returned by the ShareNext API (GET /api/shares/:id/data) */
 export type ShareData =
@@ -100,7 +101,7 @@ export const ImportCommand = cmd({
       if (isUrl) {
         const slug = parseShareUrl(args.file)
         if (!slug) {
-          const baseUrl = await ShareNext.url()
+          const baseUrl = await AppRuntime.runPromise(ShareNext.Service.use((svc) => svc.url()))
           process.stdout.write(`Invalid URL format. Expected: ${baseUrl}/share/<slug>`)
           process.stdout.write(EOL)
           return
@@ -108,7 +109,7 @@ export const ImportCommand = cmd({
 
         const parsed = new URL(args.file)
         const baseUrl = parsed.origin
-        const req = await ShareNext.request()
+        const req = await AppRuntime.runPromise(ShareNext.Service.use((svc) => svc.request()))
         const headers = shouldAttachShareAuthHeaders(args.file, req.baseUrl) ? req.headers : {}
 
         const dataPath = req.api.data(slug)

+ 18 - 7
packages/opencode/src/cli/cmd/pr.ts

@@ -1,5 +1,6 @@
 import { UI } from "../ui"
 import { cmd } from "./cmd"
+import { AppRuntime } from "@/effect/app-runtime"
 import { Git } from "@/git"
 import { Instance } from "@/project/instance"
 import { Process } from "@/util/process"
@@ -67,19 +68,29 @@ export const PrCommand = cmd({
               const remoteName = forkOwner
 
               // Check if remote already exists
-              const remotes = (await Git.run(["remote"], { cwd: Instance.worktree })).text().trim()
+              const remotes = await AppRuntime.runPromise(
+                Git.Service.use((git) => git.run(["remote"], { cwd: Instance.worktree })),
+              ).then((x) => x.text().trim())
               if (!remotes.split("\n").includes(remoteName)) {
-                await Git.run(["remote", "add", remoteName, `https://github.com/${forkOwner}/${forkName}.git`], {
-                  cwd: Instance.worktree,
-                })
+                await AppRuntime.runPromise(
+                  Git.Service.use((git) =>
+                    git.run(["remote", "add", remoteName, `https://github.com/${forkOwner}/${forkName}.git`], {
+                      cwd: Instance.worktree,
+                    }),
+                  ),
+                )
                 UI.println(`Added fork remote: ${remoteName}`)
               }
 
               // Set upstream to the fork so pushes go there
               const headRefName = prInfo.headRefName
-              await Git.run(["branch", `--set-upstream-to=${remoteName}/${headRefName}`, localBranchName], {
-                cwd: Instance.worktree,
-              })
+              await AppRuntime.runPromise(
+                Git.Service.use((git) =>
+                  git.run(["branch", `--set-upstream-to=${remoteName}/${headRefName}`, localBranchName], {
+                    cwd: Instance.worktree,
+                  }),
+                ),
+              )
             }
 
             // Check for opencode session link in PR body

+ 12 - 1
packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx

@@ -589,6 +589,13 @@ export function Prompt(props: PromptProps) {
   ])
 
   async function submit() {
+    // IME: double-defer may fire before onContentChange flushes the last
+    // composed character (e.g. Korean hangul) to the store, so read
+    // plainText directly and sync before any downstream reads.
+    if (input && !input.isDestroyed && input.plainText !== store.prompt.input) {
+      setStore("prompt", "input", input.plainText)
+      syncExtmarksWithPromptParts()
+    }
     if (props.disabled) return
     if (autocomplete?.visible) return
     if (!store.prompt.input) return
@@ -994,7 +1001,11 @@ export function Prompt(props: PromptProps) {
                     input.cursorOffset = input.plainText.length
                 }
               }}
-              onSubmit={submit}
+              onSubmit={() => {
+                // IME: double-defer so the last composed character (e.g. Korean
+                // hangul) is flushed to plainText before we read it for submission.
+                setTimeout(() => setTimeout(() => submit(), 0), 0)
+              }}
               onPaste={async (event: PasteEvent) => {
                 if (props.disabled) {
                   event.preventDefault()

+ 8 - 2
packages/opencode/src/cli/cmd/tui/thread.ts

@@ -137,12 +137,18 @@ export const TuiThreadCommand = cmd({
         ),
       })
       worker.onerror = (e) => {
-        Log.Default.error(e)
+        Log.Default.error("thread error", {
+          message: e.message,
+          filename: e.filename,
+          lineno: e.lineno,
+          colno: e.colno,
+          error: e.error,
+        })
       }
 
       const client = Rpc.client<typeof rpc>(worker)
       const error = (e: unknown) => {
-        Log.Default.error(e)
+        Log.Default.error("process error", { error: errorMessage(e) })
       }
       const reload = () => {
         client.call("reload", undefined).catch((err) => {

+ 2 - 1
packages/opencode/src/cli/cmd/uninstall.ts

@@ -1,6 +1,7 @@
 import type { Argv } from "yargs"
 import { UI } from "../ui"
 import * as prompts from "@clack/prompts"
+import { AppRuntime } from "@/effect/app-runtime"
 import { Installation } from "../../installation"
 import { Global } from "../../global"
 import fs from "fs/promises"
@@ -57,7 +58,7 @@ export const UninstallCommand = {
     UI.empty()
     prompts.intro("Uninstall OpenCode")
 
-    const method = await Installation.method()
+    const method = await AppRuntime.runPromise(Installation.Service.use((svc) => svc.method()))
     prompts.log.info(`Installation method: ${method}`)
 
     const targets = await collectRemovalTargets(args, method)

+ 8 - 3
packages/opencode/src/cli/cmd/upgrade.ts

@@ -1,6 +1,7 @@
 import type { Argv } from "yargs"
 import { UI } from "../ui"
 import * as prompts from "@clack/prompts"
+import { AppRuntime } from "@/effect/app-runtime"
 import { Installation } from "../../installation"
 
 export const UpgradeCommand = {
@@ -24,7 +25,7 @@ export const UpgradeCommand = {
     UI.println(UI.logo("  "))
     UI.empty()
     prompts.intro("Upgrade")
-    const detectedMethod = await Installation.method()
+    const detectedMethod = await AppRuntime.runPromise(Installation.Service.use((svc) => svc.method()))
     const method = (args.method as Installation.Method) ?? detectedMethod
     if (method === "unknown") {
       prompts.log.error(`opencode is installed to ${process.execPath} and may be managed by a package manager`)
@@ -42,7 +43,9 @@ export const UpgradeCommand = {
       }
     }
     prompts.log.info("Using method: " + method)
-    const target = args.target ? args.target.replace(/^v/, "") : await Installation.latest()
+    const target = args.target
+      ? args.target.replace(/^v/, "")
+      : await AppRuntime.runPromise(Installation.Service.use((svc) => svc.latest()))
 
     if (Installation.VERSION === target) {
       prompts.log.warn(`opencode upgrade skipped: ${target} is already installed`)
@@ -53,7 +56,9 @@ export const UpgradeCommand = {
     prompts.log.info(`From ${Installation.VERSION} → ${target}`)
     const spinner = prompts.spinner()
     spinner.start("Upgrading...")
-    const err = await Installation.upgrade(method, target).catch((err) => err)
+    const err = await AppRuntime.runPromise(Installation.Service.use((svc) => svc.upgrade(method, target))).catch(
+      (err) => err,
+    )
     if (err) {
       spinner.stop("Upgrade failed", 1)
       if (err instanceof Installation.UpgradeFailedError) {

+ 4 - 3
packages/opencode/src/cli/upgrade.ts

@@ -1,12 +1,13 @@
 import { Bus } from "@/bus"
 import { Config } from "@/config/config"
+import { AppRuntime } from "@/effect/app-runtime"
 import { Flag } from "@/flag/flag"
 import { Installation } from "@/installation"
 
 export async function upgrade() {
   const config = await Config.getGlobal()
-  const method = await Installation.method()
-  const latest = await Installation.latest(method).catch(() => {})
+  const method = await AppRuntime.runPromise(Installation.Service.use((svc) => svc.method()))
+  const latest = await AppRuntime.runPromise(Installation.Service.use((svc) => svc.latest(method))).catch(() => {})
   if (!latest) return
 
   if (Flag.OPENCODE_ALWAYS_NOTIFY_UPDATE) {
@@ -25,7 +26,7 @@ export async function upgrade() {
   }
 
   if (method === "unknown") return
-  await Installation.upgrade(method, latest)
+  await AppRuntime.runPromise(Installation.Service.use((svc) => svc.upgrade(method, latest)))
     .then(() => Bus.publish(Installation.Event.Updated, { version: latest }))
     .catch(() => {})
 }

+ 6 - 10
packages/opencode/src/command/index.ts

@@ -1,8 +1,9 @@
 import { BusEvent } from "@/bus/bus-event"
 import { InstanceState } from "@/effect/instance-state"
-import { makeRuntime } from "@/effect/run-service"
+import type { InstanceContext } from "@/project/instance"
 import { SessionID, MessageID } from "@/session/schema"
-import { Effect, Layer, ServiceMap } from "effect"
+import { Effect, Layer, Context } from "effect"
+import { EffectLogger } from "@/effect/logger"
 import z from "zod"
 import { Config } from "../config/config"
 import { MCP } from "../mcp"
@@ -70,7 +71,7 @@ export namespace Command {
     readonly list: () => Effect.Effect<Info[]>
   }
 
-  export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/Command") {}
+  export class Service extends Context.Service<Service, Interface>()("@opencode/Command") {}
 
   export const layer = Layer.effect(
     Service,
@@ -79,7 +80,7 @@ export namespace Command {
       const mcp = yield* MCP.Service
       const skill = yield* Skill.Service
 
-      const init = Effect.fn("Command.state")(function* (ctx) {
+      const init = Effect.fn("Command.state")(function* (ctx: InstanceContext) {
         const cfg = yield* config.get()
         const commands: Record<string, Info> = {}
 
@@ -140,6 +141,7 @@ export namespace Command {
                           .map((message) => (message.content.type === "text" ? message.content.text : ""))
                           .join("\n") || "",
                     ),
+                    Effect.provide(EffectLogger.layer),
                   ),
               )
             },
@@ -186,10 +188,4 @@ export namespace Command {
     Layer.provide(MCP.defaultLayer),
     Layer.provide(Skill.defaultLayer),
   )
-
-  const { runPromise } = makeRuntime(Service, defaultLayer)
-
-  export async function list() {
-    return runPromise((svc) => svc.list())
-  }
 }

+ 23 - 19
packages/opencode/src/config/config.ts

@@ -4,7 +4,6 @@ import { pathToFileURL } from "url"
 import os from "os"
 import { Process } from "../util/process"
 import z from "zod"
-import { ModelsDev } from "../provider/models"
 import { mergeDeep, pipe, unique } from "remeda"
 import { Global } from "../global"
 import fsNode from "fs/promises"
@@ -37,10 +36,11 @@ import type { ConsoleState } from "./console-state"
 import { AppFileSystem } from "@/filesystem"
 import { InstanceState } from "@/effect/instance-state"
 import { makeRuntime } from "@/effect/run-service"
-import { Duration, Effect, Layer, Option, ServiceMap } from "effect"
+import { Duration, Effect, Layer, Option, Context } from "effect"
 import { Flock } from "@/util/flock"
 import { isPathPluginSpec, parsePluginSpecifier, resolvePathPluginTarget } from "@/plugin/shared"
 import { Npm } from "@/npm"
+import { InstanceRef } from "@/effect/instance-ref"
 
 export namespace Config {
   const ModelId = z.string().meta({ $ref: "https://models.dev/model-schema.json#/$defs/Model" })
@@ -1126,7 +1126,7 @@ export namespace Config {
     readonly waitForDependencies: () => Effect.Effect<void>
   }
 
-  export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/Config") {}
+  export class Service extends Context.Service<Service, Interface>()("@opencode/Config") {}
 
   function globalConfigFile() {
     const candidates = ["opencode.jsonc", "opencode.json", "config.json"].map((file) =>
@@ -1327,27 +1327,31 @@ export namespace Config {
           const consoleManagedProviders = new Set<string>()
           let activeOrgName: string | undefined
 
-          const scope = (source: string): PluginScope => {
+          const scope = Effect.fnUntraced(function* (source: string) {
             if (source.startsWith("http://") || source.startsWith("https://")) return "global"
             if (source === "OPENCODE_CONFIG_CONTENT") return "local"
-            if (Instance.containsPath(source)) return "local"
+            if (yield* InstanceRef.use((ctx) => Effect.succeed(Instance.containsPath(source, ctx)))) return "local"
             return "global"
-          }
+          })
 
-          const track = (source: string, list: PluginSpec[] | undefined, kind?: PluginScope) => {
+          const track = Effect.fnUntraced(function* (
+            source: string,
+            list: PluginSpec[] | undefined,
+            kind?: PluginScope,
+          ) {
             if (!list?.length) return
-            const hit = kind ?? scope(source)
+            const hit = kind ?? (yield* scope(source))
             const plugins = deduplicatePluginOrigins([
               ...(result.plugin_origins ?? []),
               ...list.map((spec) => ({ spec, source, scope: hit })),
             ])
             result.plugin = plugins.map((item) => item.spec)
             result.plugin_origins = plugins
-          }
+          })
 
           const merge = (source: string, next: Info, kind?: PluginScope) => {
             result = mergeConfigConcatArrays(result, next)
-            track(source, next.plugin, kind)
+            return track(source, next.plugin, kind)
           }
 
           for (const [key, value] of Object.entries(auth)) {
@@ -1367,16 +1371,16 @@ export namespace Config {
                 dir: path.dirname(source),
                 source,
               })
-              merge(source, next, "global")
+              yield* merge(source, next, "global")
               log.debug("loaded remote config from well-known", { url })
             }
           }
 
           const global = yield* getGlobal()
-          merge(Global.Path.config, global, "global")
+          yield* merge(Global.Path.config, global, "global")
 
           if (Flag.OPENCODE_CONFIG) {
-            merge(Flag.OPENCODE_CONFIG, yield* loadFile(Flag.OPENCODE_CONFIG))
+            yield* merge(Flag.OPENCODE_CONFIG, yield* loadFile(Flag.OPENCODE_CONFIG))
             log.debug("loaded custom config", { path: Flag.OPENCODE_CONFIG })
           }
 
@@ -1384,7 +1388,7 @@ export namespace Config {
             for (const file of yield* Effect.promise(() =>
               ConfigPaths.projectFiles("opencode", ctx.directory, ctx.worktree),
             )) {
-              merge(file, yield* loadFile(file), "local")
+              yield* merge(file, yield* loadFile(file), "local")
             }
           }
 
@@ -1405,7 +1409,7 @@ export namespace Config {
               for (const file of ["opencode.json", "opencode.jsonc"]) {
                 const source = path.join(dir, file)
                 log.debug(`loading config from ${source}`)
-                merge(source, yield* loadFile(source))
+                yield* merge(source, yield* loadFile(source))
                 result.agent ??= {}
                 result.mode ??= {}
                 result.plugin ??= []
@@ -1424,7 +1428,7 @@ export namespace Config {
             result.agent = mergeDeep(result.agent, yield* Effect.promise(() => loadAgent(dir)))
             result.agent = mergeDeep(result.agent, yield* Effect.promise(() => loadMode(dir)))
             const list = yield* Effect.promise(() => loadPlugin(dir))
-            track(dir, list)
+            yield* track(dir, list)
           }
 
           if (process.env.OPENCODE_CONFIG_CONTENT) {
@@ -1433,7 +1437,7 @@ export namespace Config {
               dir: ctx.directory,
               source,
             })
-            merge(source, next, "local")
+            yield* merge(source, next, "local")
             log.debug("loaded custom config from OPENCODE_CONFIG_CONTENT")
           }
 
@@ -1462,7 +1466,7 @@ export namespace Config {
                 for (const providerID of Object.keys(next.provider ?? {})) {
                   consoleManagedProviders.add(providerID)
                 }
-                merge(source, next, "global")
+                yield* merge(source, next, "global")
               }
             }).pipe(
               Effect.catch((err) => {
@@ -1477,7 +1481,7 @@ export namespace Config {
           if (existsSync(managedDir)) {
             for (const file of ["opencode.json", "opencode.jsonc"]) {
               const source = path.join(managedDir, file)
-              merge(source, yield* loadFile(source), "global")
+              yield* merge(source, yield* loadFile(source), "global")
             }
           }
 

+ 1 - 2
packages/opencode/src/control-plane/schema.ts

@@ -10,8 +10,7 @@ export type WorkspaceID = typeof workspaceIdSchema.Type
 
 export const WorkspaceID = workspaceIdSchema.pipe(
   withStatics((schema: typeof workspaceIdSchema) => ({
-    make: (id: string) => schema.makeUnsafe(id),
-    ascending: (id?: string) => schema.makeUnsafe(Identifier.ascending("workspace", id)),
+    ascending: (id?: string) => schema.make(Identifier.ascending("workspace", id)),
     zod: Identifier.schema("workspace").pipe(z.custom<WorkspaceID>()),
   })),
 )

+ 2 - 2
packages/opencode/src/control-plane/workspace-context.ts

@@ -1,11 +1,11 @@
-import { Context } from "../util/context"
+import { LocalContext } from "../util/local-context"
 import type { WorkspaceID } from "../control-plane/schema"
 
 export interface WorkspaceContext {
   workspaceID: string
 }
 
-const context = Context.create<WorkspaceContext>("instance")
+const context = LocalContext.create<WorkspaceContext>("instance")
 
 export const WorkspaceContext = {
   async provide<R>(input: { workspaceID: WorkspaceID; fn: () => R }): Promise<R> {

+ 9 - 0
packages/opencode/src/effect/bootstrap-runtime.ts

@@ -0,0 +1,9 @@
+import { Layer, ManagedRuntime } from "effect"
+import { memoMap } from "./run-service"
+
+import { Format } from "@/format"
+import { ShareNext } from "@/share/share-next"
+
+export const BootstrapLayer = Layer.mergeAll(Format.defaultLayer, ShareNext.defaultLayer)
+
+export const BootstrapRuntime = ManagedRuntime.make(BootstrapLayer, { memoMap })

+ 13 - 0
packages/opencode/src/effect/cross-spawn-spawner.ts

@@ -402,6 +402,7 @@ export const make = Effect.gen(function* () {
 
           const fd = yield* setupFds(command, proc, extra)
           const out = setupOutput(command, proc, sout, serr)
+          let ref = true
           return makeHandle({
             pid: ProcessId(proc.pid!),
             stdin: yield* setupStdin(command, proc, sin),
@@ -432,6 +433,18 @@ export const make = Effect.gen(function* () {
                 orElse: () => send("SIGKILL").pipe(Effect.andThen(Deferred.await(signal)), Effect.asVoid),
               })
             },
+            unref: Effect.sync(() => {
+              if (ref) {
+                proc.unref()
+                ref = false
+              }
+              return Effect.sync(() => {
+                if (!ref) {
+                  proc.ref()
+                  ref = true
+                }
+              })
+            }),
           })
         }
         case "PipedCommand": {

+ 3 - 3
packages/opencode/src/effect/instance-ref.ts

@@ -1,10 +1,10 @@
-import { ServiceMap } from "effect"
+import { Context } from "effect"
 import type { InstanceContext } from "@/project/instance"
 
-export const InstanceRef = ServiceMap.Reference<InstanceContext | undefined>("~opencode/InstanceRef", {
+export const InstanceRef = Context.Reference<InstanceContext | undefined>("~opencode/InstanceRef", {
   defaultValue: () => undefined,
 })
 
-export const WorkspaceRef = ServiceMap.Reference<string | undefined>("~opencode/WorkspaceRef", {
+export const WorkspaceRef = Context.Reference<string | undefined>("~opencode/WorkspaceRef", {
   defaultValue: () => undefined,
 })

+ 8 - 5
packages/opencode/src/effect/instance-state.ts

@@ -1,6 +1,7 @@
-import { Effect, Fiber, ScopedCache, Scope, ServiceMap } from "effect"
+import { Effect, Fiber, ScopedCache, Scope, Context } from "effect"
+import { EffectLogger } from "@/effect/logger"
 import { Instance, type InstanceContext } from "@/project/instance"
-import { Context } from "@/util/context"
+import { LocalContext } from "@/util/local-context"
 import { InstanceRef, WorkspaceRef } from "./instance-ref"
 import { registerDisposer } from "./instance-registry"
 import { WorkspaceContext } from "@/control-plane/workspace-context"
@@ -17,10 +18,10 @@ export namespace InstanceState {
     try {
       return Instance.bind(fn)
     } catch (err) {
-      if (!(err instanceof Context.NotFound)) throw err
+      if (!(err instanceof LocalContext.NotFound)) throw err
     }
     const fiber = Fiber.getCurrent()
-    const ctx = fiber ? ServiceMap.getReferenceUnsafe(fiber.services, InstanceRef) : undefined
+    const ctx = fiber ? Context.getReferenceUnsafe(fiber.context, InstanceRef) : undefined
     if (!ctx) return fn
     return ((...args: any[]) => Instance.restore(ctx, () => fn(...args))) as F
   }
@@ -47,7 +48,9 @@ export namespace InstanceState {
           }),
       })
 
-      const off = registerDisposer((directory) => Effect.runPromise(ScopedCache.invalidate(cache, directory)))
+      const off = registerDisposer((directory) =>
+        Effect.runPromise(ScopedCache.invalidate(cache, directory).pipe(Effect.provide(EffectLogger.layer))),
+      )
       yield* Effect.addFinalizer(() => Effect.sync(off))
 
       return {

+ 67 - 0
packages/opencode/src/effect/logger.ts

@@ -0,0 +1,67 @@
+import { Cause, Effect, Logger, References } from "effect"
+import { Log } from "@/util/log"
+
+export namespace EffectLogger {
+  type Fields = Record<string, unknown>
+
+  export interface Handle {
+    readonly debug: (msg?: unknown, extra?: Fields) => Effect.Effect<void>
+    readonly info: (msg?: unknown, extra?: Fields) => Effect.Effect<void>
+    readonly warn: (msg?: unknown, extra?: Fields) => Effect.Effect<void>
+    readonly error: (msg?: unknown, extra?: Fields) => Effect.Effect<void>
+    readonly with: (extra: Fields) => Handle
+  }
+
+  const clean = (input?: Fields): Fields =>
+    Object.fromEntries(Object.entries(input ?? {}).filter((entry) => entry[1] !== undefined && entry[1] !== null))
+
+  const text = (input: unknown): string => {
+    if (Array.isArray(input)) return input.map((item) => String(item)).join(" ")
+    return input === undefined ? "" : String(input)
+  }
+
+  const call = (run: (msg?: unknown) => Effect.Effect<void>, base: Fields, msg?: unknown, extra?: Fields) => {
+    const ann = clean({ ...base, ...extra })
+    const fx = run(msg)
+    return Object.keys(ann).length ? Effect.annotateLogs(fx, ann) : fx
+  }
+
+  export const logger = Logger.make((opts) => {
+    const extra = clean(opts.fiber.getRef(References.CurrentLogAnnotations))
+    const now = opts.date.getTime()
+    for (const [key, start] of opts.fiber.getRef(References.CurrentLogSpans)) {
+      extra[`logSpan.${key}`] = `${now - start}ms`
+    }
+    if (opts.cause.reasons.length > 0) {
+      extra.cause = Cause.pretty(opts.cause)
+    }
+
+    const svc = typeof extra.service === "string" ? extra.service : undefined
+    if (svc) delete extra.service
+    const log = svc ? Log.create({ service: svc }) : Log.Default
+    const msg = text(opts.message)
+
+    switch (opts.logLevel) {
+      case "Trace":
+      case "Debug":
+        return log.debug(msg, extra)
+      case "Warn":
+        return log.warn(msg, extra)
+      case "Error":
+      case "Fatal":
+        return log.error(msg, extra)
+      default:
+        return log.info(msg, extra)
+    }
+  })
+
+  export const layer = Logger.layer([logger], { mergeWithExisting: false })
+
+  export const create = (base: Fields = {}): Handle => ({
+    debug: (msg, extra) => call((item) => Effect.logDebug(item), base, msg, extra),
+    info: (msg, extra) => call((item) => Effect.logInfo(item), base, msg, extra),
+    warn: (msg, extra) => call((item) => Effect.logWarning(item), base, msg, extra),
+    error: (msg, extra) => call((item) => Effect.logError(item), base, msg, extra),
+    with: (extra) => create({ ...base, ...extra }),
+  })
+}

+ 32 - 25
packages/opencode/src/effect/oltp.ts

@@ -1,34 +1,41 @@
-import { Layer } from "effect"
+import { Duration, Layer } from "effect"
 import { FetchHttpClient } from "effect/unstable/http"
 import { Otlp } from "effect/unstable/observability"
+import { EffectLogger } from "@/effect/logger"
 import { Flag } from "@/flag/flag"
 import { CHANNEL, VERSION } from "@/installation/meta"
 
 export namespace Observability {
-  export const enabled = !!Flag.OTEL_EXPORTER_OTLP_ENDPOINT
+  const base = Flag.OTEL_EXPORTER_OTLP_ENDPOINT
+  export const enabled = !!base
 
-  export const layer = !Flag.OTEL_EXPORTER_OTLP_ENDPOINT
-    ? Layer.empty
-    : Otlp.layerJson({
-        baseUrl: Flag.OTEL_EXPORTER_OTLP_ENDPOINT,
-        loggerMergeWithExisting: false,
-        resource: {
-          serviceName: "opencode",
-          serviceVersion: VERSION,
-          attributes: {
-            "deployment.environment.name": CHANNEL === "local" ? "local" : CHANNEL,
-            "opencode.client": Flag.OPENCODE_CLIENT,
-          },
+  const resource = {
+    serviceName: "opencode",
+    serviceVersion: VERSION,
+    attributes: {
+      "deployment.environment.name": CHANNEL === "local" ? "local" : CHANNEL,
+      "opencode.client": Flag.OPENCODE_CLIENT,
+    },
+  }
+
+  const headers = Flag.OTEL_EXPORTER_OTLP_HEADERS
+    ? Flag.OTEL_EXPORTER_OTLP_HEADERS.split(",").reduce(
+        (acc, x) => {
+          const [key, value] = x.split("=")
+          acc[key] = value
+          return acc
         },
-        headers: Flag.OTEL_EXPORTER_OTLP_HEADERS
-          ? Flag.OTEL_EXPORTER_OTLP_HEADERS.split(",").reduce(
-              (acc, x) => {
-                const [key, value] = x.split("=")
-                acc[key] = value
-                return acc
-              },
-              {} as Record<string, string>,
-            )
-          : undefined,
-      }).pipe(Layer.provide(FetchHttpClient.layer))
+        {} as Record<string, string>,
+      )
+    : undefined
+
+  export const layer = !base
+    ? EffectLogger.layer
+    : Otlp.layerJson({
+        baseUrl: base,
+        loggerExportInterval: Duration.seconds(1),
+        loggerMergeWithExisting: true,
+        resource,
+        headers,
+      }).pipe(Layer.provide(EffectLogger.layer), Layer.provide(FetchHttpClient.layer))
 }

+ 4 - 4
packages/opencode/src/effect/run-service.ts

@@ -1,7 +1,7 @@
 import { Effect, Layer, ManagedRuntime } from "effect"
-import * as ServiceMap from "effect/ServiceMap"
+import * as Context from "effect/Context"
 import { Instance } from "@/project/instance"
-import { Context } from "@/util/context"
+import { LocalContext } from "@/util/local-context"
 import { InstanceRef, WorkspaceRef } from "./instance-ref"
 import { Observability } from "./oltp"
 import { WorkspaceContext } from "@/control-plane/workspace-context"
@@ -14,12 +14,12 @@ export function attach<A, E, R>(effect: Effect.Effect<A, E, R>): Effect.Effect<A
     const workspaceID = WorkspaceContext.workspaceID
     return effect.pipe(Effect.provideService(InstanceRef, ctx), Effect.provideService(WorkspaceRef, workspaceID))
   } catch (err) {
-    if (!(err instanceof Context.NotFound)) throw err
+    if (!(err instanceof LocalContext.NotFound)) throw err
   }
   return effect
 }
 
-export function makeRuntime<I, S, E>(service: ServiceMap.Service<I, S>, layer: Layer.Layer<I, E>) {
+export function makeRuntime<I, S, E>(service: Context.Service<I, S>, layer: Layer.Layer<I, E>) {
   let rt: ManagedRuntime.ManagedRuntime<I, E> | undefined
   const getRuntime = () => (rt ??= ManagedRuntime.make(Layer.merge(layer, Observability.layer), { memoMap }))
 

+ 2 - 2
packages/opencode/src/file/index.ts

@@ -3,7 +3,7 @@ import { InstanceState } from "@/effect/instance-state"
 import { makeRuntime } from "@/effect/run-service"
 import { AppFileSystem } from "@/filesystem"
 import { Git } from "@/git"
-import { Effect, Layer, ServiceMap } from "effect"
+import { Effect, Layer, Context } from "effect"
 import { formatPatch, structuredPatch } from "diff"
 import fuzzysort from "fuzzysort"
 import ignore from "ignore"
@@ -337,7 +337,7 @@ export namespace File {
     }) => Effect.Effect<string[]>
   }
 
-  export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/File") {}
+  export class Service extends Context.Service<Service, Interface>()("@opencode/File") {}
 
   export const layer = Layer.effect(
     Service,

+ 2 - 2
packages/opencode/src/file/ripgrep.ts

@@ -3,7 +3,7 @@ import path from "path"
 import { Global } from "../global"
 import fs from "fs/promises"
 import z from "zod"
-import { Effect, Layer, ServiceMap } from "effect"
+import { Effect, Layer, Context } from "effect"
 import * as Stream from "effect/Stream"
 import { ChildProcess } from "effect/unstable/process"
 import { ChildProcessSpawner } from "effect/unstable/process/ChildProcessSpawner"
@@ -291,7 +291,7 @@ export namespace Ripgrep {
     }) => Stream.Stream<string, PlatformError>
   }
 
-  export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/Ripgrep") {}
+  export class Service extends Context.Service<Service, Interface>()("@opencode/Ripgrep") {}
 
   export const layer: Layer.Layer<Service, never, ChildProcessSpawner | AppFileSystem.Service> = Layer.effect(
     Service,

+ 6 - 6
packages/opencode/src/file/time.ts

@@ -1,4 +1,4 @@
-import { DateTime, Effect, Layer, Option, Semaphore, ServiceMap } from "effect"
+import { DateTime, Effect, Layer, Option, Semaphore, Context } from "effect"
 import { InstanceState } from "@/effect/instance-state"
 import { makeRuntime } from "@/effect/run-service"
 import { AppFileSystem } from "@/filesystem"
@@ -34,10 +34,10 @@ export namespace FileTime {
     readonly read: (sessionID: SessionID, file: string) => Effect.Effect<void>
     readonly get: (sessionID: SessionID, file: string) => Effect.Effect<Date | undefined>
     readonly assert: (sessionID: SessionID, filepath: string) => Effect.Effect<void>
-    readonly withLock: <T>(filepath: string, fn: () => Promise<T>) => Effect.Effect<T>
+    readonly withLock: <T>(filepath: string, fn: () => Effect.Effect<T>) => Effect.Effect<T>
   }
 
-  export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/FileTime") {}
+  export class Service extends Context.Service<Service, Interface>()("@opencode/FileTime") {}
 
   export const layer = Layer.effect(
     Service,
@@ -103,8 +103,8 @@ export namespace FileTime {
         )
       })
 
-      const withLock = Effect.fn("FileTime.withLock")(function* <T>(filepath: string, fn: () => Promise<T>) {
-        return yield* Effect.promise(fn).pipe((yield* getLock(filepath)).withPermits(1))
+      const withLock = Effect.fn("FileTime.withLock")(function* <T>(filepath: string, fn: () => Effect.Effect<T>) {
+        return yield* fn().pipe((yield* getLock(filepath)).withPermits(1))
       })
 
       return Service.of({ read, get, assert, withLock })
@@ -128,6 +128,6 @@ export namespace FileTime {
   }
 
   export async function withLock<T>(filepath: string, fn: () => Promise<T>): Promise<T> {
-    return runPromise((s) => s.withLock(filepath, fn))
+    return runPromise((s) => s.withLock(filepath, () => Effect.promise(fn)))
   }
 }

+ 2 - 2
packages/opencode/src/file/watcher.ts

@@ -1,4 +1,4 @@
-import { Cause, Effect, Layer, Scope, ServiceMap } from "effect"
+import { Cause, Effect, Layer, Scope, Context } from "effect"
 // @ts-ignore
 import { createWrapper } from "@parcel/watcher/wrapper"
 import type ParcelWatcher from "@parcel/watcher"
@@ -65,7 +65,7 @@ export namespace FileWatcher {
     readonly init: () => Effect.Effect<void>
   }
 
-  export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/FileWatcher") {}
+  export class Service extends Context.Service<Service, Interface>()("@opencode/FileWatcher") {}
 
   export const layer = Layer.effect(
     Service,

+ 2 - 2
packages/opencode/src/filesystem/index.ts

@@ -3,7 +3,7 @@ import { dirname, join, relative, resolve as pathResolve } from "path"
 import { realpathSync } from "fs"
 import * as NFS from "fs/promises"
 import { lookup } from "mime-types"
-import { Effect, FileSystem, Layer, Schema, ServiceMap } from "effect"
+import { Effect, FileSystem, Layer, Schema, Context } from "effect"
 import type { PlatformError } from "effect/PlatformError"
 import { Glob } from "../util/glob"
 
@@ -36,7 +36,7 @@ export namespace AppFileSystem {
     readonly globMatch: (pattern: string, filepath: string) => boolean
   }
 
-  export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/FileSystem") {}
+  export class Service extends Context.Service<Service, Interface>()("@opencode/FileSystem") {}
 
   export const layer = Layer.effect(
     Service,

+ 2 - 17
packages/opencode/src/format/index.ts

@@ -1,8 +1,7 @@
-import { Effect, Layer, ServiceMap } from "effect"
+import { Effect, Layer, Context } from "effect"
 import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
 import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner"
 import { InstanceState } from "@/effect/instance-state"
-import { makeRuntime } from "@/effect/run-service"
 import path from "path"
 import { mergeDeep } from "remeda"
 import z from "zod"
@@ -31,7 +30,7 @@ export namespace Format {
     readonly file: (filepath: string) => Effect.Effect<void>
   }
 
-  export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/Format") {}
+  export class Service extends Context.Service<Service, Interface>()("@opencode/Format") {}
 
   export const layer = Layer.effect(
     Service,
@@ -193,18 +192,4 @@ export namespace Format {
     Layer.provide(Config.defaultLayer),
     Layer.provide(CrossSpawnSpawner.defaultLayer),
   )
-
-  const { runPromise } = makeRuntime(Service, defaultLayer)
-
-  export async function init() {
-    return runPromise((s) => s.init())
-  }
-
-  export async function status() {
-    return runPromise((s) => s.status())
-  }
-
-  export async function file(filepath: string) {
-    return runPromise((s) => s.file(filepath))
-  }
 }

+ 2 - 13
packages/opencode/src/git/index.ts

@@ -1,7 +1,6 @@
 import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner"
-import { Effect, Layer, ServiceMap, Stream } from "effect"
+import { Effect, Layer, Context, Stream } from "effect"
 import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
-import { makeRuntime } from "@/effect/run-service"
 
 export namespace Git {
   const cfg = [
@@ -80,7 +79,7 @@ export namespace Git {
     return "modified"
   }
 
-  export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/Git") {}
+  export class Service extends Context.Service<Service, Interface>()("@opencode/Git") {}
 
   export const layer = Layer.effect(
     Service,
@@ -258,14 +257,4 @@ export namespace Git {
   )
 
   export const defaultLayer = layer.pipe(Layer.provide(CrossSpawnSpawner.defaultLayer))
-
-  const { runPromise } = makeRuntime(Service, defaultLayer)
-
-  export async function run(args: string[], opts: Options) {
-    return runPromise((git) => git.run(args, opts))
-  }
-
-  export async function defaultBranch(cwd: string) {
-    return runPromise((git) => git.defaultBranch(cwd))
-  }
 }

+ 2 - 17
packages/opencode/src/installation/index.ts

@@ -1,7 +1,6 @@
-import { Effect, Layer, Schema, ServiceMap, Stream } from "effect"
+import { Effect, Layer, Schema, Context, Stream } from "effect"
 import { FetchHttpClient, HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http"
 import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner"
-import { makeRuntime } from "@/effect/run-service"
 import { withTransientReadRetry } from "@/util/effect-http-client"
 import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
 import path from "path"
@@ -91,7 +90,7 @@ export namespace Installation {
     readonly upgrade: (method: Method, target: string) => Effect.Effect<void, UpgradeFailedError>
   }
 
-  export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/Installation") {}
+  export class Service extends Context.Service<Service, Interface>()("@opencode/Installation") {}
 
   export const layer: Layer.Layer<Service, never, HttpClient.HttpClient | ChildProcessSpawner.ChildProcessSpawner> =
     Layer.effect(
@@ -338,18 +337,4 @@ export namespace Installation {
     Layer.provide(FetchHttpClient.layer),
     Layer.provide(CrossSpawnSpawner.defaultLayer),
   )
-
-  const { runPromise } = makeRuntime(Service, defaultLayer)
-
-  export async function method(): Promise<Method> {
-    return runPromise((svc) => svc.method())
-  }
-
-  export async function latest(installMethod?: Method): Promise<string> {
-    return runPromise((svc) => svc.latest(installMethod))
-  }
-
-  export async function upgrade(m: Method, target: string): Promise<void> {
-    return runPromise((svc) => svc.upgrade(m, target))
-  }
 }

+ 13 - 2
packages/opencode/src/lsp/index.ts

@@ -11,7 +11,7 @@ import { Instance } from "../project/instance"
 import { Flag } from "@/flag/flag"
 import { Process } from "../util/process"
 import { spawn as lspspawn } from "./launch"
-import { Effect, Layer, ServiceMap } from "effect"
+import { Effect, Layer, Context } from "effect"
 import { InstanceState } from "@/effect/instance-state"
 import { makeRuntime } from "@/effect/run-service"
 
@@ -156,7 +156,7 @@ export namespace LSP {
     readonly outgoingCalls: (input: LocInput) => Effect.Effect<any[]>
   }
 
-  export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/LSP") {}
+  export class Service extends Context.Service<Service, Interface>()("@opencode/LSP") {}
 
   export const layer = Layer.effect(
     Service,
@@ -540,6 +540,8 @@ export namespace LSP {
   export const outgoingCalls = async (input: LocInput) => runPromise((svc) => svc.outgoingCalls(input))
 
   export namespace Diagnostic {
+    const MAX_PER_FILE = 20
+
     export function pretty(diagnostic: LSPClient.Diagnostic) {
       const severityMap = {
         1: "ERROR",
@@ -554,5 +556,14 @@ export namespace LSP {
 
       return `${severity} [${line}:${col}] ${diagnostic.message}`
     }
+
+    export function report(file: string, issues: LSPClient.Diagnostic[]) {
+      const errors = issues.filter((item) => item.severity === 1)
+      if (errors.length === 0) return ""
+      const limited = errors.slice(0, MAX_PER_FILE)
+      const more = errors.length - MAX_PER_FILE
+      const suffix = more > 0 ? `\n... and ${more} more` : ""
+      return `<diagnostics file="${file}">\n${limited.map(pretty).join("\n")}${suffix}\n</diagnostics>`
+    }
   }
 }

+ 2 - 2
packages/opencode/src/mcp/auth.ts

@@ -1,7 +1,7 @@
 import path from "path"
 import z from "zod"
 import { Global } from "../global"
-import { Effect, Layer, ServiceMap } from "effect"
+import { Effect, Layer, Context } from "effect"
 import { AppFileSystem } from "@/filesystem"
 import { makeRuntime } from "@/effect/run-service"
 
@@ -49,7 +49,7 @@ export namespace McpAuth {
     readonly isTokenExpired: (mcpName: string) => Effect.Effect<boolean | null>
   }
 
-  export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/McpAuth") {}
+  export class Service extends Context.Service<Service, Interface>()("@opencode/McpAuth") {}
 
   export const layer = Layer.effect(
     Service,

+ 7 - 4
packages/opencode/src/mcp/index.ts

@@ -24,7 +24,8 @@ import { BusEvent } from "../bus/bus-event"
 import { Bus } from "@/bus"
 import { TuiEvent } from "@/cli/cmd/tui/event"
 import open from "open"
-import { Effect, Exit, Layer, Option, ServiceMap, Stream } from "effect"
+import { Effect, Exit, Layer, Option, Context, Stream } from "effect"
+import { EffectLogger } from "@/effect/logger"
 import { InstanceState } from "@/effect/instance-state"
 import { makeRuntime } from "@/effect/run-service"
 import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
@@ -239,7 +240,7 @@ export namespace MCP {
     readonly getAuthStatus: (mcpName: string) => Effect.Effect<AuthStatus>
   }
 
-  export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/MCP") {}
+  export class Service extends Context.Service<Service, Interface>()("@opencode/MCP") {}
 
   export const layer = Layer.effect(
     Service,
@@ -469,12 +470,14 @@ export namespace MCP {
           log.info("tools list changed notification received", { server: name })
           if (s.clients[name] !== client || s.status[name]?.status !== "connected") return
 
-          const listed = await Effect.runPromise(defs(name, client, timeout))
+          const listed = await Effect.runPromise(defs(name, client, timeout).pipe(Effect.provide(EffectLogger.layer)))
           if (!listed) return
           if (s.clients[name] !== client || s.status[name]?.status !== "connected") return
 
           s.defs[name] = listed
-          await Effect.runPromise(bus.publish(ToolsChanged, { server: name }).pipe(Effect.ignore))
+          await Effect.runPromise(
+            bus.publish(ToolsChanged, { server: name }).pipe(Effect.ignore, Effect.provide(EffectLogger.layer)),
+          )
         })
       }
 

+ 2 - 2
packages/opencode/src/permission/index.ts

@@ -10,7 +10,7 @@ import { PermissionTable } from "@/session/session.sql"
 import { Database, eq } from "@/storage/db"
 import { Log } from "@/util/log"
 import { Wildcard } from "@/util/wildcard"
-import { Deferred, Effect, Layer, Schema, ServiceMap } from "effect"
+import { Deferred, Effect, Layer, Schema, Context } from "effect"
 import os from "os"
 import z from "zod"
 import { evaluate as evalRule } from "./evaluate"
@@ -135,7 +135,7 @@ export namespace Permission {
     return evalRule(permission, pattern, ...rulesets)
   }
 
-  export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/Permission") {}
+  export class Service extends Context.Service<Service, Interface>()("@opencode/Permission") {}
 
   export const layer = Layer.effect(
     Service,

+ 1 - 5
packages/opencode/src/permission/schema.ts

@@ -5,12 +5,8 @@ import { Identifier } from "@/id/id"
 import { Newtype } from "@/util/schema"
 
 export class PermissionID extends Newtype<PermissionID>()("PermissionID", Schema.String) {
-  static make(id: string): PermissionID {
-    return this.makeUnsafe(id)
-  }
-
   static ascending(id?: string): PermissionID {
-    return this.makeUnsafe(Identifier.ascending("permission", id))
+    return this.make(Identifier.ascending("permission", id))
   }
 
   static readonly zod = Identifier.schema("permission") as unknown as z.ZodType<PermissionID>

+ 18 - 11
packages/opencode/src/plugin/index.ts

@@ -11,7 +11,8 @@ import { CopilotAuthPlugin } from "./github-copilot/copilot"
 import { gitlabAuthPlugin as GitlabAuthPlugin } from "opencode-gitlab-auth"
 import { PoeAuthPlugin } from "opencode-poe-auth"
 import { CloudflareAIGatewayAuthPlugin, CloudflareWorkersAuthPlugin } from "./cloudflare"
-import { Effect, Layer, ServiceMap, Stream } from "effect"
+import { Effect, Layer, Context, Stream } from "effect"
+import { EffectLogger } from "@/effect/logger"
 import { InstanceState } from "@/effect/instance-state"
 import { makeRuntime } from "@/effect/run-service"
 import { errorMessage } from "@/util/error"
@@ -44,7 +45,7 @@ export namespace Plugin {
     readonly init: () => Effect.Effect<void>
   }
 
-  export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/Plugin") {}
+  export class Service extends Context.Service<Service, Interface>()("@opencode/Plugin") {}
 
   // Built-in plugins that are directly imported (not installed from npm)
   const INTERNAL_PLUGINS: PluginInstance[] = [
@@ -83,7 +84,11 @@ export namespace Plugin {
   }
 
   function publishPluginError(bus: Bus.Interface, message: string) {
-    Effect.runFork(bus.publish(Session.Event.Error, { error: new NamedError.Unknown({ message }).toObject() }))
+    Effect.runFork(
+      bus
+        .publish(Session.Event.Error, { error: new NamedError.Unknown({ message }).toObject() })
+        .pipe(Effect.provide(EffectLogger.layer)),
+    )
   }
 
   async function applyPlugin(load: PluginLoader.Loaded, input: PluginInput, hooks: Hooks[]) {
@@ -119,7 +124,7 @@ export namespace Plugin {
                   Authorization: `Basic ${Buffer.from(`${Flag.OPENCODE_SERVER_USERNAME ?? "opencode"}:${Flag.OPENCODE_SERVER_PASSWORD}`).toString("base64")}`,
                 }
               : undefined,
-            fetch: async (...args) => Server.Default().app.fetch(...args),
+            fetch: async (...args) => (await Server.Default()).app.fetch(...args),
           })
           const cfg = yield* config.get()
           const input: PluginInput = {
@@ -205,13 +210,15 @@ export namespace Plugin {
                 return message
               },
             }).pipe(
-              Effect.catch((message) =>
-                bus.publish(Session.Event.Error, {
-                  error: new NamedError.Unknown({
-                    message: `Failed to load plugin ${load.spec}: ${message}`,
-                  }).toObject(),
-                }),
-              ),
+              Effect.catch(() => {
+                // TODO: make proper events for this
+                // bus.publish(Session.Event.Error, {
+                //   error: new NamedError.Unknown({
+                //     message: `Failed to load plugin ${load.spec}: ${message}`,
+                //   }).toObject(),
+                // })
+                return Effect.void
+              }),
             )
           }
 

+ 3 - 2
packages/opencode/src/project/bootstrap.ts

@@ -10,13 +10,14 @@ import { Bus } from "../bus"
 import { Command } from "../command"
 import { Instance } from "./instance"
 import { Log } from "@/util/log"
+import { BootstrapRuntime } from "@/effect/bootstrap-runtime"
 import { ShareNext } from "@/share/share-next"
 
 export async function InstanceBootstrap() {
   Log.Default.info("bootstrapping", { directory: Instance.directory })
   await Plugin.init()
-  ShareNext.init()
-  Format.init()
+  void BootstrapRuntime.runPromise(ShareNext.Service.use((svc) => svc.init()))
+  void BootstrapRuntime.runPromise(Format.Service.use((svc) => svc.init()))
   await LSP.init()
   File.init()
   FileWatcher.init()

+ 6 - 5
packages/opencode/src/project/instance.ts

@@ -3,7 +3,7 @@ import { disposeInstance } from "@/effect/instance-registry"
 import { Filesystem } from "@/util/filesystem"
 import { iife } from "@/util/iife"
 import { Log } from "@/util/log"
-import { Context } from "../util/context"
+import { LocalContext } from "../util/local-context"
 import { Project } from "./project"
 import { WorkspaceContext } from "@/control-plane/workspace-context"
 import { State } from "./state"
@@ -14,7 +14,7 @@ export interface InstanceContext {
   project: Project.Info
 }
 
-const context = Context.create<InstanceContext>("instance")
+const context = LocalContext.create<InstanceContext>("instance")
 const cache = new Map<string, Promise<InstanceContext>>()
 
 const disposal = {
@@ -90,12 +90,13 @@ export const Instance = {
    * Returns true if path is inside Instance.directory OR Instance.worktree.
    * Paths within the worktree but outside the working directory should not trigger external_directory permission.
    */
-  containsPath(filepath: string) {
-    if (Filesystem.contains(Instance.directory, filepath)) return true
+  containsPath(filepath: string, ctx?: InstanceContext) {
+    const instance = ctx ?? Instance
+    if (Filesystem.contains(instance.directory, filepath)) return true
     // Non-git projects set worktree to "/" which would match ANY absolute path.
     // Skip worktree check in this case to preserve external_directory permissions.
     if (Instance.worktree === "/") return false
-    return Filesystem.contains(Instance.worktree, filepath)
+    return Filesystem.contains(instance.worktree, filepath)
   },
   /**
    * Captures the current instance ALS context and returns a wrapper that

+ 2 - 2
packages/opencode/src/project/project.ts

@@ -8,7 +8,7 @@ import { BusEvent } from "@/bus/bus-event"
 import { GlobalBus } from "@/bus/global"
 import { which } from "../util/which"
 import { ProjectID } from "./schema"
-import { Effect, Layer, Path, Scope, ServiceMap, Stream } from "effect"
+import { Effect, Layer, Path, Scope, Context, Stream } from "effect"
 import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
 import { NodeFileSystem, NodePath } from "@effect/platform-node"
 import { makeRuntime } from "@/effect/run-service"
@@ -100,7 +100,7 @@ export namespace Project {
     readonly removeSandbox: (id: ProjectID, directory: string) => Effect.Effect<void>
   }
 
-  export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/Project") {}
+  export class Service extends Context.Service<Service, Interface>()("@opencode/Project") {}
 
   type GitResult = { code: number; text: string; stderr: string }
 

+ 1 - 2
packages/opencode/src/project/schema.ts

@@ -9,8 +9,7 @@ export type ProjectID = typeof projectIdSchema.Type
 
 export const ProjectID = projectIdSchema.pipe(
   withStatics((schema: typeof projectIdSchema) => ({
-    global: schema.makeUnsafe("global"),
-    make: (id: string) => schema.makeUnsafe(id),
+    global: schema.make("global"),
     zod: z.string().pipe(z.custom<ProjectID>()),
   })),
 )

+ 2 - 2
packages/opencode/src/project/vcs.ts

@@ -1,4 +1,4 @@
-import { Effect, Layer, ServiceMap, Stream } from "effect"
+import { Effect, Layer, Context, Stream } from "effect"
 import { formatPatch, structuredPatch } from "diff"
 import path from "path"
 import { Bus } from "@/bus"
@@ -151,7 +151,7 @@ export namespace Vcs {
     root: Git.Base | undefined
   }
 
-  export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/Vcs") {}
+  export class Service extends Context.Service<Service, Interface>()("@opencode/Vcs") {}
 
   export const layer: Layer.Layer<Service, never, AppFileSystem.Service | Git.Service | Bus.Service> = Layer.effect(
     Service,

+ 2 - 21
packages/opencode/src/provider/auth.ts

@@ -2,10 +2,9 @@ import type { AuthOAuthResult, Hooks } from "@opencode-ai/plugin"
 import { NamedError } from "@opencode-ai/util/error"
 import { Auth } from "@/auth"
 import { InstanceState } from "@/effect/instance-state"
-import { makeRuntime } from "@/effect/run-service"
 import { Plugin } from "../plugin"
 import { ProviderID } from "./schema"
-import { Array as Arr, Effect, Layer, Record, Result, ServiceMap } from "effect"
+import { Array as Arr, Effect, Layer, Record, Result, Context } from "effect"
 import z from "zod"
 
 export namespace ProviderAuth {
@@ -109,7 +108,7 @@ export namespace ProviderAuth {
     pending: Map<ProviderID, AuthOAuthResult>
   }
 
-  export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/ProviderAuth") {}
+  export class Service extends Context.Service<Service, Interface>()("@opencode/ProviderAuth") {}
 
   export const layer: Layer.Layer<Service, never, Auth.Service | Plugin.Service> = Layer.effect(
     Service,
@@ -232,22 +231,4 @@ export namespace ProviderAuth {
   export const defaultLayer = Layer.suspend(() =>
     layer.pipe(Layer.provide(Auth.defaultLayer), Layer.provide(Plugin.defaultLayer)),
   )
-
-  const { runPromise } = makeRuntime(Service, defaultLayer)
-
-  export async function methods() {
-    return runPromise((svc) => svc.methods())
-  }
-
-  export async function authorize(input: {
-    providerID: ProviderID
-    method: number
-    inputs?: Record<string, string>
-  }): Promise<Authorization | undefined> {
-    return runPromise((svc) => svc.authorize(input))
-  }
-
-  export async function callback(input: { providerID: ProviderID; method: number; code?: string }) {
-    return runPromise((svc) => svc.callback(input))
-  }
 }

Diferenças do arquivo suprimidas por serem muito extensas
+ 11532 - 10994
packages/opencode/src/provider/models-snapshot.js


+ 5 - 3
packages/opencode/src/provider/provider.ts

@@ -19,7 +19,8 @@ import { iife } from "@/util/iife"
 import { Global } from "../global"
 import path from "path"
 import { Filesystem } from "../util/filesystem"
-import { Effect, Layer, ServiceMap } from "effect"
+import { Effect, Layer, Context } from "effect"
+import { EffectLogger } from "@/effect/logger"
 import { InstanceState } from "@/effect/instance-state"
 import { makeRuntime } from "@/effect/run-service"
 
@@ -924,7 +925,7 @@ export namespace Provider {
     varsLoaders: Record<string, CustomVarsLoader>
   }
 
-  export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/Provider") {}
+  export class Service extends Context.Service<Service, Interface>()("@opencode/Provider") {}
 
   function cost(c: ModelsDev.Model["cost"]): Model["cost"] {
     const result: Model["cost"] = {
@@ -1215,7 +1216,8 @@ export namespace Provider {
 
             const options = yield* Effect.promise(() =>
               plugin.auth!.loader!(
-                () => Effect.runPromise(auth.get(providerID).pipe(Effect.orDie)) as any,
+                () =>
+                  Effect.runPromise(auth.get(providerID).pipe(Effect.orDie, Effect.provide(EffectLogger.layer))) as any,
                 database[plugin.auth!.provider],
               ),
             )

+ 11 - 13
packages/opencode/src/provider/schema.ts

@@ -9,20 +9,19 @@ export type ProviderID = typeof providerIdSchema.Type
 
 export const ProviderID = providerIdSchema.pipe(
   withStatics((schema: typeof providerIdSchema) => ({
-    make: (id: string) => schema.makeUnsafe(id),
     zod: z.string().pipe(z.custom<ProviderID>()),
     // Well-known providers
-    opencode: schema.makeUnsafe("opencode"),
-    anthropic: schema.makeUnsafe("anthropic"),
-    openai: schema.makeUnsafe("openai"),
-    google: schema.makeUnsafe("google"),
-    googleVertex: schema.makeUnsafe("google-vertex"),
-    githubCopilot: schema.makeUnsafe("github-copilot"),
-    amazonBedrock: schema.makeUnsafe("amazon-bedrock"),
-    azure: schema.makeUnsafe("azure"),
-    openrouter: schema.makeUnsafe("openrouter"),
-    mistral: schema.makeUnsafe("mistral"),
-    gitlab: schema.makeUnsafe("gitlab"),
+    opencode: schema.make("opencode"),
+    anthropic: schema.make("anthropic"),
+    openai: schema.make("openai"),
+    google: schema.make("google"),
+    googleVertex: schema.make("google-vertex"),
+    githubCopilot: schema.make("github-copilot"),
+    amazonBedrock: schema.make("amazon-bedrock"),
+    azure: schema.make("azure"),
+    openrouter: schema.make("openrouter"),
+    mistral: schema.make("mistral"),
+    gitlab: schema.make("gitlab"),
   })),
 )
 
@@ -32,7 +31,6 @@ export type ModelID = typeof modelIdSchema.Type
 
 export const ModelID = modelIdSchema.pipe(
   withStatics((schema: typeof modelIdSchema) => ({
-    make: (id: string) => schema.makeUnsafe(id),
     zod: z.string().pipe(z.custom<ModelID>()),
   })),
 )

+ 4 - 1
packages/opencode/src/provider/transform.ts

@@ -774,7 +774,10 @@ export namespace ProviderTransform {
       result["chat_template_args"] = { enable_thinking: true }
     }
 
-    if (["zai", "zhipuai"].includes(input.model.providerID) && input.model.api.npm === "@ai-sdk/openai-compatible") {
+    if (
+      ["zai", "zhipuai"].some((id) => input.model.providerID.includes(id)) &&
+      input.model.api.npm === "@ai-sdk/openai-compatible"
+    ) {
       result["thinking"] = {
         type: "enabled",
         clear_thinking: false,

+ 5 - 4
packages/opencode/src/pty/index.ts

@@ -10,7 +10,8 @@ import { lazy } from "@opencode-ai/util/lazy"
 import { Shell } from "@/shell/shell"
 import { Plugin } from "@/plugin"
 import { PtyID } from "./schema"
-import { Effect, Layer, ServiceMap } from "effect"
+import { Effect, Layer, Context } from "effect"
+import { EffectLogger } from "@/effect/logger"
 
 export namespace Pty {
   const log = Log.create({ service: "pty" })
@@ -112,7 +113,7 @@ export namespace Pty {
     ) => Effect.Effect<{ onMessage: (message: string | ArrayBuffer) => void; onClose: () => void } | undefined>
   }
 
-  export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/Pty") {}
+  export class Service extends Context.Service<Service, Interface>()("@opencode/Pty") {}
 
   export const layer = Layer.effect(
     Service,
@@ -256,8 +257,8 @@ export namespace Pty {
             if (session.info.status === "exited") return
             log.info("session exited", { id, exitCode })
             session.info.status = "exited"
-            Effect.runFork(bus.publish(Event.Exited, { id, exitCode }))
-            Effect.runFork(remove(id))
+            Effect.runFork(bus.publish(Event.Exited, { id, exitCode }).pipe(Effect.provide(EffectLogger.layer)))
+            Effect.runFork(remove(id).pipe(Effect.provide(EffectLogger.layer)))
           }),
         )
         yield* bus.publish(Event.Created, { info })

+ 1 - 2
packages/opencode/src/pty/schema.ts

@@ -10,8 +10,7 @@ export type PtyID = typeof ptyIdSchema.Type
 
 export const PtyID = ptyIdSchema.pipe(
   withStatics((schema: typeof ptyIdSchema) => ({
-    make: (id: string) => schema.makeUnsafe(id),
-    ascending: (id?: string) => schema.makeUnsafe(Identifier.ascending("pty", id)),
+    ascending: (id?: string) => schema.make(Identifier.ascending("pty", id)),
     zod: Identifier.schema("pty").pipe(z.custom<PtyID>()),
   })),
 )

+ 2 - 2
packages/opencode/src/question/index.ts

@@ -1,4 +1,4 @@
-import { Deferred, Effect, Layer, Schema, ServiceMap } from "effect"
+import { Deferred, Effect, Layer, Schema, Context } from "effect"
 import { Bus } from "@/bus"
 import { BusEvent } from "@/bus/bus-event"
 import { InstanceState } from "@/effect/instance-state"
@@ -104,7 +104,7 @@ export namespace Question {
     readonly list: () => Effect.Effect<Request[]>
   }
 
-  export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/Question") {}
+  export class Service extends Context.Service<Service, Interface>()("@opencode/Question") {}
 
   export const layer = Layer.effect(
     Service,

+ 1 - 5
packages/opencode/src/question/schema.ts

@@ -5,12 +5,8 @@ import { Identifier } from "@/id/id"
 import { Newtype } from "@/util/schema"
 
 export class QuestionID extends Newtype<QuestionID>()("QuestionID", Schema.String) {
-  static make(id: string): QuestionID {
-    return this.makeUnsafe(id)
-  }
-
   static ascending(id?: string): QuestionID {
-    return this.makeUnsafe(Identifier.ascending("question", id))
+    return this.make(Identifier.ascending("question", id))
   }
 
   static readonly zod = Identifier.schema("question") as unknown as z.ZodType<QuestionID>

+ 40 - 0
packages/opencode/src/server/adapter.bun.ts

@@ -0,0 +1,40 @@
+import type { Hono } from "hono"
+import { createBunWebSocket } from "hono/bun"
+import type { Adapter } from "./adapter"
+
+export const adapter: Adapter = {
+  create(app: Hono) {
+    const ws = createBunWebSocket()
+    return {
+      upgradeWebSocket: ws.upgradeWebSocket,
+      async listen(opts) {
+        const args = {
+          fetch: app.fetch,
+          hostname: opts.hostname,
+          idleTimeout: 0,
+          websocket: ws.websocket,
+        } as const
+        const start = (port: number) => {
+          try {
+            return Bun.serve({ ...args, port })
+          } catch {
+            return
+          }
+        }
+        const server = opts.port === 0 ? (start(4096) ?? start(0)) : start(opts.port)
+        if (!server) {
+          throw new Error(`Failed to start server on port ${opts.port}`)
+        }
+        if (!server.port) {
+          throw new Error(`Failed to resolve server address for port ${opts.port}`)
+        }
+        return {
+          port: server.port,
+          stop(close?: boolean) {
+            return Promise.resolve(server.stop(close))
+          },
+        }
+      },
+    }
+  },
+}

+ 66 - 0
packages/opencode/src/server/adapter.node.ts

@@ -0,0 +1,66 @@
+import { createAdaptorServer, type ServerType } from "@hono/node-server"
+import { createNodeWebSocket } from "@hono/node-ws"
+import type { Hono } from "hono"
+import type { Adapter } from "./adapter"
+
+export const adapter: Adapter = {
+  create(app: Hono) {
+    const ws = createNodeWebSocket({ app })
+    return {
+      upgradeWebSocket: ws.upgradeWebSocket,
+      async listen(opts) {
+        const start = (port: number) =>
+          new Promise<ServerType>((resolve, reject) => {
+            const server = createAdaptorServer({ fetch: app.fetch })
+            ws.injectWebSocket(server)
+            const fail = (err: Error) => {
+              cleanup()
+              reject(err)
+            }
+            const ready = () => {
+              cleanup()
+              resolve(server)
+            }
+            const cleanup = () => {
+              server.off("error", fail)
+              server.off("listening", ready)
+            }
+            server.once("error", fail)
+            server.once("listening", ready)
+            server.listen(port, opts.hostname)
+          })
+
+        const server = opts.port === 0 ? await start(4096).catch(() => start(0)) : await start(opts.port)
+        const addr = server.address()
+        if (!addr || typeof addr === "string") {
+          throw new Error(`Failed to resolve server address for port ${opts.port}`)
+        }
+
+        let closing: Promise<void> | undefined
+        return {
+          port: addr.port,
+          stop(close?: boolean) {
+            closing ??= new Promise((resolve, reject) => {
+              server.close((err) => {
+                if (err) {
+                  reject(err)
+                  return
+                }
+                resolve()
+              })
+              if (close) {
+                if ("closeAllConnections" in server && typeof server.closeAllConnections === "function") {
+                  server.closeAllConnections()
+                }
+                if ("closeIdleConnections" in server && typeof server.closeIdleConnections === "function") {
+                  server.closeIdleConnections()
+                }
+              }
+            })
+            return closing
+          },
+        }
+      },
+    }
+  },
+}

+ 21 - 0
packages/opencode/src/server/adapter.ts

@@ -0,0 +1,21 @@
+import type { Hono } from "hono"
+import type { UpgradeWebSocket } from "hono/ws"
+
+export type Opts = {
+  port: number
+  hostname: string
+}
+
+export type Listener = {
+  port: number
+  stop: (close?: boolean) => Promise<void>
+}
+
+export interface Runtime {
+  upgradeWebSocket: UpgradeWebSocket
+  listen(opts: Opts): Promise<Listener>
+}
+
+export interface Adapter {
+  create(app: Hono): Runtime
+}

+ 150 - 0
packages/opencode/src/server/control/index.ts

@@ -0,0 +1,150 @@
+import { Auth } from "@/auth"
+import { Log } from "@/util/log"
+import { ProviderID } from "@/provider/schema"
+import { Hono } from "hono"
+import { describeRoute, resolver, validator, openAPIRouteHandler } from "hono-openapi"
+import z from "zod"
+import { errors } from "../error"
+import { GlobalRoutes } from "../instance/global"
+
+export function ControlPlaneRoutes(): Hono {
+  const app = new Hono()
+  return app
+    .route("/global", GlobalRoutes())
+    .put(
+      "/auth/:providerID",
+      describeRoute({
+        summary: "Set auth credentials",
+        description: "Set authentication credentials",
+        operationId: "auth.set",
+        responses: {
+          200: {
+            description: "Successfully set authentication credentials",
+            content: {
+              "application/json": {
+                schema: resolver(z.boolean()),
+              },
+            },
+          },
+          ...errors(400),
+        },
+      }),
+      validator(
+        "param",
+        z.object({
+          providerID: ProviderID.zod,
+        }),
+      ),
+      validator("json", Auth.Info.zod),
+      async (c) => {
+        const providerID = c.req.valid("param").providerID
+        const info = c.req.valid("json")
+        await Auth.set(providerID, info)
+        return c.json(true)
+      },
+    )
+    .delete(
+      "/auth/:providerID",
+      describeRoute({
+        summary: "Remove auth credentials",
+        description: "Remove authentication credentials",
+        operationId: "auth.remove",
+        responses: {
+          200: {
+            description: "Successfully removed authentication credentials",
+            content: {
+              "application/json": {
+                schema: resolver(z.boolean()),
+              },
+            },
+          },
+          ...errors(400),
+        },
+      }),
+      validator(
+        "param",
+        z.object({
+          providerID: ProviderID.zod,
+        }),
+      ),
+      async (c) => {
+        const providerID = c.req.valid("param").providerID
+        await Auth.remove(providerID)
+        return c.json(true)
+      },
+    )
+    .get(
+      "/doc",
+      openAPIRouteHandler(app, {
+        documentation: {
+          info: {
+            title: "opencode",
+            version: "0.0.3",
+            description: "opencode api",
+          },
+          openapi: "3.1.1",
+        },
+      }),
+    )
+    .use(
+      validator(
+        "query",
+        z.object({
+          directory: z.string().optional(),
+          workspace: z.string().optional(),
+        }),
+      ),
+    )
+    .post(
+      "/log",
+      describeRoute({
+        summary: "Write log",
+        description: "Write a log entry to the server logs with specified level and metadata.",
+        operationId: "app.log",
+        responses: {
+          200: {
+            description: "Log entry written successfully",
+            content: {
+              "application/json": {
+                schema: resolver(z.boolean()),
+              },
+            },
+          },
+          ...errors(400),
+        },
+      }),
+      validator(
+        "json",
+        z.object({
+          service: z.string().meta({ description: "Service name for the log entry" }),
+          level: z.enum(["debug", "info", "error", "warn"]).meta({ description: "Log level" }),
+          message: z.string().meta({ description: "Log message" }),
+          extra: z
+            .record(z.string(), z.any())
+            .optional()
+            .meta({ description: "Additional metadata for the log entry" }),
+        }),
+      ),
+      async (c) => {
+        const { service, level, message, extra } = c.req.valid("json")
+        const logger = Log.create({ service })
+
+        switch (level) {
+          case "debug":
+            logger.debug(message, extra)
+            break
+          case "info":
+            logger.info(message, extra)
+            break
+          case "error":
+            logger.error(message, extra)
+            break
+          case "warn":
+            logger.warn(message, extra)
+            break
+        }
+
+        return c.json(true)
+      },
+    )
+}

+ 0 - 0
packages/opencode/src/server/routes/config.ts → packages/opencode/src/server/instance/config.ts


+ 0 - 0
packages/opencode/src/server/routes/event.ts → packages/opencode/src/server/instance/event.ts


+ 40 - 17
packages/opencode/src/server/routes/experimental.ts → packages/opencode/src/server/instance/experimental.ts

@@ -11,9 +11,11 @@ import { Session } from "../../session"
 import { Config } from "../../config/config"
 import { ConsoleState } from "../../config/console-state"
 import { Account, AccountID, OrgID } from "../../account"
+import { AppRuntime } from "../../effect/app-runtime"
 import { zodToJsonSchema } from "zod-to-json-schema"
 import { errors } from "../error"
 import { lazy } from "../../util/lazy"
+import { Effect, Option } from "effect"
 import { WorkspaceRoutes } from "./workspace"
 import { Agent } from "@/agent/agent"
 
@@ -55,11 +57,20 @@ export const ExperimentalRoutes = lazy(() =>
         },
       }),
       async (c) => {
-        const [consoleState, groups] = await Promise.all([Config.getConsoleState(), Account.orgsByAccount()])
-        return c.json({
-          ...consoleState,
-          switchableOrgCount: groups.reduce((count, group) => count + group.orgs.length, 0),
-        })
+        const result = await AppRuntime.runPromise(
+          Effect.gen(function* () {
+            const config = yield* Config.Service
+            const account = yield* Account.Service
+            const [state, groups] = yield* Effect.all([config.getConsoleState(), account.orgsByAccount()], {
+              concurrency: "unbounded",
+            })
+            return {
+              ...state,
+              switchableOrgCount: groups.reduce((count, group) => count + group.orgs.length, 0),
+            }
+          }),
+        )
+        return c.json(result)
       },
     )
     .get(
@@ -80,17 +91,24 @@ export const ExperimentalRoutes = lazy(() =>
         },
       }),
       async (c) => {
-        const [groups, active] = await Promise.all([Account.orgsByAccount(), Account.active()])
-
-        const orgs = groups.flatMap((group) =>
-          group.orgs.map((org) => ({
-            accountID: group.account.id,
-            accountEmail: group.account.email,
-            accountUrl: group.account.url,
-            orgID: org.id,
-            orgName: org.name,
-            active: !!active && active.id === group.account.id && active.active_org_id === org.id,
-          })),
+        const orgs = await AppRuntime.runPromise(
+          Effect.gen(function* () {
+            const account = yield* Account.Service
+            const [groups, active] = yield* Effect.all([account.orgsByAccount(), account.active()], {
+              concurrency: "unbounded",
+            })
+            const info = Option.getOrUndefined(active)
+            return groups.flatMap((group) =>
+              group.orgs.map((org) => ({
+                accountID: group.account.id,
+                accountEmail: group.account.email,
+                accountUrl: group.account.url,
+                orgID: org.id,
+                orgName: org.name,
+                active: !!info && info.id === group.account.id && info.active_org_id === org.id,
+              })),
+            )
+          }),
         )
         return c.json({ orgs })
       },
@@ -115,7 +133,12 @@ export const ExperimentalRoutes = lazy(() =>
       validator("json", ConsoleSwitchBody),
       async (c) => {
         const body = c.req.valid("json")
-        await Account.switchOrg(AccountID.make(body.accountID), OrgID.make(body.orgID))
+        await AppRuntime.runPromise(
+          Effect.gen(function* () {
+            const account = yield* Account.Service
+            yield* account.use(AccountID.make(body.accountID), Option.some(OrgID.make(body.orgID)))
+          }),
+        )
         return c.json(true)
       },
     )

+ 0 - 0
packages/opencode/src/server/routes/file.ts → packages/opencode/src/server/instance/file.ts


+ 36 - 18
packages/opencode/src/server/routes/global.ts → packages/opencode/src/server/instance/global.ts

@@ -1,10 +1,12 @@
 import { Hono, type Context } from "hono"
 import { describeRoute, resolver, validator } from "hono-openapi"
 import { streamSSE } from "hono/streaming"
+import { Effect } from "effect"
 import z from "zod"
 import { BusEvent } from "@/bus/bus-event"
 import { SyncEvent } from "@/sync"
 import { GlobalBus } from "@/bus/global"
+import { AppRuntime } from "@/effect/app-runtime"
 import { AsyncQueue } from "@/util/queue"
 import { Instance } from "../../project/instance"
 import { Installation } from "@/installation"
@@ -290,25 +292,41 @@ export const GlobalRoutes = lazy(() =>
         }),
       ),
       async (c) => {
-        const method = await Installation.method()
-        if (method === "unknown") {
-          return c.json({ success: false, error: "Unknown installation method" }, 400)
-        }
-        const target = c.req.valid("json").target || (await Installation.latest(method))
-        const result = await Installation.upgrade(method, target)
-          .then(() => ({ success: true as const, version: target }))
-          .catch((e) => ({ success: false as const, error: e instanceof Error ? e.message : String(e) }))
-        if (result.success) {
-          GlobalBus.emit("event", {
-            directory: "global",
-            payload: {
-              type: Installation.Event.Updated.type,
-              properties: { version: target },
-            },
-          })
-          return c.json(result)
+        const result = await AppRuntime.runPromise(
+          Installation.Service.use((svc) =>
+            Effect.gen(function* () {
+              const method = yield* svc.method()
+              if (method === "unknown") {
+                return { success: false as const, status: 400 as const, error: "Unknown installation method" }
+              }
+
+              const target = c.req.valid("json").target || (yield* svc.latest(method))
+              const result = yield* Effect.catch(
+                svc.upgrade(method, target).pipe(Effect.as({ success: true as const, version: target })),
+                (err) =>
+                  Effect.succeed({
+                    success: false as const,
+                    status: 500 as const,
+                    error: err instanceof Error ? err.message : String(err),
+                  }),
+              )
+              if (!result.success) return result
+              return { ...result, status: 200 as const }
+            }),
+          ),
+        )
+        if (!result.success) {
+          return c.json({ success: false, error: result.error }, result.status)
         }
-        return c.json(result, 500)
+        const target = result.version
+        GlobalBus.emit("event", {
+          directory: "global",
+          payload: {
+            type: Installation.Event.Updated.type,
+            properties: { version: target },
+          },
+        })
+        return c.json({ success: true, version: target })
       },
     ),
 )

+ 27 - 82
packages/opencode/src/server/instance.ts → packages/opencode/src/server/instance/index.ts

@@ -1,52 +1,33 @@
 import { describeRoute, resolver, validator } from "hono-openapi"
 import { Hono } from "hono"
-import { proxy } from "hono/proxy"
 import type { UpgradeWebSocket } from "hono/ws"
 import z from "zod"
-import { createHash } from "node:crypto"
-import * as fs from "node:fs/promises"
-import { Log } from "../util/log"
-import { Format } from "../format"
-import { TuiRoutes } from "./routes/tui"
-import { Instance } from "../project/instance"
-import { Vcs } from "../project/vcs"
-import { Agent } from "../agent/agent"
-import { Skill } from "../skill"
-import { Global } from "../global"
-import { LSP } from "../lsp"
-import { Command } from "../command"
-import { Flag } from "../flag/flag"
-import { QuestionRoutes } from "./routes/question"
-import { PermissionRoutes } from "./routes/permission"
-import { Snapshot } from "@/snapshot"
-import { ProjectRoutes } from "./routes/project"
-import { SessionRoutes } from "./routes/session"
-import { PtyRoutes } from "./routes/pty"
-import { McpRoutes } from "./routes/mcp"
-import { FileRoutes } from "./routes/file"
-import { ConfigRoutes } from "./routes/config"
-import { ExperimentalRoutes } from "./routes/experimental"
-import { ProviderRoutes } from "./routes/provider"
-import { EventRoutes } from "./routes/event"
-import { errorHandler } from "./middleware"
-import { getMimeType } from "hono/utils/mime"
+import { Format } from "../../format"
+import { TuiRoutes } from "./tui"
+import { Instance } from "../../project/instance"
+import { Vcs } from "../../project/vcs"
+import { Agent } from "../../agent/agent"
+import { Skill } from "../../skill"
+import { Global } from "../../global"
+import { LSP } from "../../lsp"
+import { Command } from "../../command"
+import { QuestionRoutes } from "./question"
+import { PermissionRoutes } from "./permission"
+import { ProjectRoutes } from "./project"
+import { SessionRoutes } from "./session"
+import { PtyRoutes } from "./pty"
+import { McpRoutes } from "./mcp"
+import { FileRoutes } from "./file"
+import { ConfigRoutes } from "./config"
+import { ExperimentalRoutes } from "./experimental"
+import { ProviderRoutes } from "./provider"
+import { EventRoutes } from "./event"
+import { WorkspaceRouterMiddleware } from "./middleware"
+import { AppRuntime } from "@/effect/app-runtime"
 
-const log = Log.create({ service: "server" })
-
-const embeddedUIPromise = Flag.OPENCODE_DISABLE_EMBEDDED_WEB_UI
-  ? Promise.resolve(null)
-  : // @ts-expect-error - generated file at build time
-    import("opencode-web-ui.gen.ts").then((module) => module.default as Record<string, string>).catch(() => null)
-
-const DEFAULT_CSP =
-  "default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; media-src 'self' data:; connect-src 'self' data:"
-
-const csp = (hash = "") =>
-  `default-src 'self'; script-src 'self' 'wasm-unsafe-eval'${hash ? ` 'sha256-${hash}'` : ""}; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; media-src 'self' data:; connect-src 'self' data:`
-
-export const InstanceRoutes = (upgrade: UpgradeWebSocket, app: Hono = new Hono()) =>
-  app
-    .onError(errorHandler(log))
+export const InstanceRoutes = (upgrade: UpgradeWebSocket): Hono =>
+  new Hono()
+    .use(WorkspaceRouterMiddleware(upgrade))
     .route("/project", ProjectRoutes())
     .route("/pty", PtyRoutes(upgrade))
     .route("/config", ConfigRoutes())
@@ -190,7 +171,7 @@ export const InstanceRoutes = (upgrade: UpgradeWebSocket, app: Hono = new Hono()
         },
       }),
       async (c) => {
-        const commands = await Command.list()
+        const commands = await AppRuntime.runPromise(Command.Service.use((svc) => svc.list()))
         return c.json(commands)
       },
     )
@@ -277,42 +258,6 @@ export const InstanceRoutes = (upgrade: UpgradeWebSocket, app: Hono = new Hono()
         },
       }),
       async (c) => {
-        return c.json(await Format.status())
+        return c.json(await AppRuntime.runPromise(Format.Service.use((svc) => svc.status())))
       },
     )
-    .all("/*", async (c) => {
-      const embeddedWebUI = await embeddedUIPromise
-      const path = c.req.path
-
-      if (embeddedWebUI) {
-        const match = embeddedWebUI[path.replace(/^\//, "")] ?? embeddedWebUI["index.html"] ?? null
-        if (!match) return c.json({ error: "Not Found" }, 404)
-
-        if (await fs.exists(match)) {
-          const mime = getMimeType(match) ?? "text/plain"
-          c.header("Content-Type", mime)
-          if (mime.startsWith("text/html")) {
-            c.header("Content-Security-Policy", DEFAULT_CSP)
-          }
-          return c.body(new Uint8Array(await fs.readFile(match)))
-        } else {
-          return c.json({ error: "Not Found" }, 404)
-        }
-      } else {
-        const response = await proxy(`https://app.opencode.ai${path}`, {
-          ...c.req,
-          headers: {
-            ...c.req.raw.headers,
-            host: "app.opencode.ai",
-          },
-        })
-        const match = response.headers.get("content-type")?.includes("text/html")
-          ? (await response.clone().text()).match(
-              /<script\b(?![^>]*\bsrc\s*=)[^>]*\bid=(['"])oc-theme-preload-script\1[^>]*>([\s\S]*?)<\/script>/i,
-            )
-          : undefined
-        const hash = match ? createHash("sha256").update(match[2]).digest("base64") : ""
-        response.headers.set("Content-Security-Policy", csp(hash))
-        return response
-      }
-    })

+ 0 - 0
packages/opencode/src/server/routes/mcp.ts → packages/opencode/src/server/instance/mcp.ts


+ 6 - 10
packages/opencode/src/server/router.ts → packages/opencode/src/server/instance/middleware.ts

@@ -3,12 +3,10 @@ import type { UpgradeWebSocket } from "hono/ws"
 import { getAdaptor } from "@/control-plane/adaptors"
 import { WorkspaceID } from "@/control-plane/schema"
 import { Workspace } from "@/control-plane/workspace"
-import { ServerProxy } from "./proxy"
-import { lazy } from "@/util/lazy"
+import { ServerProxy } from "../proxy"
 import { Filesystem } from "@/util/filesystem"
 import { Instance } from "@/project/instance"
 import { InstanceBootstrap } from "@/project/bootstrap"
-import { InstanceRoutes } from "./instance"
 import { Session } from "@/session"
 import { SessionID } from "@/session/schema"
 import { WorkspaceContext } from "@/control-plane/workspace-context"
@@ -47,9 +45,7 @@ async function getSessionWorkspace(url: URL) {
 }
 
 export function WorkspaceRouterMiddleware(upgrade: UpgradeWebSocket): MiddlewareHandler {
-  const routes = lazy(() => InstanceRoutes(upgrade))
-
-  return async (c) => {
+  return async (c, next) => {
     const raw = c.req.query("directory") || c.req.header("x-opencode-directory") || process.cwd()
     const directory = Filesystem.resolve(
       (() => {
@@ -72,7 +68,7 @@ export function WorkspaceRouterMiddleware(upgrade: UpgradeWebSocket): Middleware
         directory,
         init: InstanceBootstrap,
         async fn() {
-          return routes().fetch(c.req.raw, c.env)
+          return next()
         },
       })
     }
@@ -87,7 +83,7 @@ export function WorkspaceRouterMiddleware(upgrade: UpgradeWebSocket): Middleware
       // The lets the `DELETE /session/:id` endpoint through and we've
       // made sure that it will run without an instance
       if (url.pathname.match(/\/session\/[^/]+$/) && c.req.method === "DELETE") {
-        return routes().fetch(c.req.raw, c.env)
+        return next()
       }
 
       return new Response(`Workspace not found: ${workspaceID}`, {
@@ -109,7 +105,7 @@ export function WorkspaceRouterMiddleware(upgrade: UpgradeWebSocket): Middleware
             directory: target.directory,
             init: InstanceBootstrap,
             async fn() {
-              return routes().fetch(c.req.raw, c.env)
+              return next()
             },
           }),
       })
@@ -118,7 +114,7 @@ export function WorkspaceRouterMiddleware(upgrade: UpgradeWebSocket): Middleware
     if (local(c.req.method, url.pathname)) {
       // No instance provided because we are serving cached data; there
       // is no instance to work with
-      return routes().fetch(c.req.raw, c.env)
+      return next()
     }
 
     if (c.req.header("upgrade")?.toLowerCase() === "websocket") {

+ 0 - 0
packages/opencode/src/server/routes/permission.ts → packages/opencode/src/server/instance/permission.ts


+ 0 - 0
packages/opencode/src/server/routes/project.ts → packages/opencode/src/server/instance/project.ts


+ 20 - 11
packages/opencode/src/server/routes/provider.ts → packages/opencode/src/server/instance/provider.ts

@@ -6,6 +6,7 @@ import { Provider } from "../../provider/provider"
 import { ModelsDev } from "../../provider/models"
 import { ProviderAuth } from "../../provider/auth"
 import { ProviderID } from "../../provider/schema"
+import { AppRuntime } from "../../effect/app-runtime"
 import { mapValues } from "remeda"
 import { errors } from "../error"
 import { lazy } from "../../util/lazy"
@@ -81,7 +82,7 @@ export const ProviderRoutes = lazy(() =>
         },
       }),
       async (c) => {
-        return c.json(await ProviderAuth.methods())
+        return c.json(await AppRuntime.runPromise(ProviderAuth.Service.use((svc) => svc.methods())))
       },
     )
     .post(
@@ -118,11 +119,15 @@ export const ProviderRoutes = lazy(() =>
       async (c) => {
         const providerID = c.req.valid("param").providerID
         const { method, inputs } = c.req.valid("json")
-        const result = await ProviderAuth.authorize({
-          providerID,
-          method,
-          inputs,
-        })
+        const result = await AppRuntime.runPromise(
+          ProviderAuth.Service.use((svc) =>
+            svc.authorize({
+              providerID,
+              method,
+              inputs,
+            }),
+          ),
+        )
         return c.json(result)
       },
     )
@@ -160,11 +165,15 @@ export const ProviderRoutes = lazy(() =>
       async (c) => {
         const providerID = c.req.valid("param").providerID
         const { method, code } = c.req.valid("json")
-        await ProviderAuth.callback({
-          providerID,
-          method,
-          code,
-        })
+        await AppRuntime.runPromise(
+          ProviderAuth.Service.use((svc) =>
+            svc.callback({
+              providerID,
+              method,
+              code,
+            }),
+          ),
+        )
         return c.json(true)
       },
     ),

+ 0 - 0
packages/opencode/src/server/routes/pty.ts → packages/opencode/src/server/instance/pty.ts


+ 0 - 0
packages/opencode/src/server/routes/question.ts → packages/opencode/src/server/instance/question.ts


+ 11 - 2
packages/opencode/src/server/routes/session.ts → packages/opencode/src/server/instance/session.ts

@@ -13,6 +13,7 @@ import { SessionShare } from "@/share/session"
 import { SessionStatus } from "@/session/status"
 import { SessionSummary } from "@/session/summary"
 import { Todo } from "../../session/todo"
+import { AppRuntime } from "../../effect/app-runtime"
 import { Agent } from "../../agent/agent"
 import { Snapshot } from "@/snapshot"
 import { Command } from "../../command"
@@ -93,7 +94,7 @@ export const SessionRoutes = lazy(() =>
         },
       }),
       async (c) => {
-        const result = await SessionStatus.list()
+        const result = await AppRuntime.runPromise(SessionStatus.Service.use((svc) => svc.list()))
         return c.json(Object.fromEntries(result))
       },
     )
@@ -185,7 +186,7 @@ export const SessionRoutes = lazy(() =>
       ),
       async (c) => {
         const sessionID = c.req.valid("param").sessionID
-        const todos = await Todo.get(sessionID)
+        const todos = await AppRuntime.runPromise(Todo.Service.use((svc) => svc.get(sessionID)))
         return c.json(todos)
       },
     )
@@ -272,6 +273,7 @@ export const SessionRoutes = lazy(() =>
         "json",
         z.object({
           title: z.string().optional(),
+          permission: Permission.Ruleset.optional(),
           time: z
             .object({
               archived: z.number().optional(),
@@ -282,10 +284,17 @@ export const SessionRoutes = lazy(() =>
       async (c) => {
         const sessionID = c.req.valid("param").sessionID
         const updates = c.req.valid("json")
+        const current = await Session.get(sessionID)
 
         if (updates.title !== undefined) {
           await Session.setTitle({ sessionID, title: updates.title })
         }
+        if (updates.permission !== undefined) {
+          await Session.setPermission({
+            sessionID,
+            permission: Permission.merge(current.permission ?? [], updates.permission),
+          })
+        }
         if (updates.time?.archived !== undefined) {
           await Session.setArchived({ sessionID, time: updates.time.archived })
         }

+ 0 - 0
packages/opencode/src/server/routes/tui.ts → packages/opencode/src/server/instance/tui.ts


+ 0 - 0
packages/opencode/src/server/routes/workspace.ts → packages/opencode/src/server/instance/workspace.ts


+ 82 - 23
packages/opencode/src/server/middleware.ts

@@ -3,31 +3,90 @@ import { NamedError } from "@opencode-ai/util/error"
 import { NotFoundError } from "../storage/db"
 import { Session } from "../session"
 import type { ContentfulStatusCode } from "hono/utils/http-status"
-import type { ErrorHandler } from "hono"
+import type { ErrorHandler, MiddlewareHandler } from "hono"
 import { HTTPException } from "hono/http-exception"
-import type { Log } from "../util/log"
+import { Log } from "../util/log"
+import { Flag } from "@/flag/flag"
+import { basicAuth } from "hono/basic-auth"
+import { cors } from "hono/cors"
+import { compress } from "hono/compress"
 
-export function errorHandler(log: Log.Logger): ErrorHandler {
-  return (err, c) => {
-    log.error("failed", {
-      error: err,
-    })
-    if (err instanceof NamedError) {
-      let status: ContentfulStatusCode
-      if (err instanceof NotFoundError) status = 404
-      else if (err instanceof Provider.ModelNotFoundError) status = 400
-      else if (err.name === "ProviderAuthValidationFailed") status = 400
-      else if (err.name.startsWith("Worktree")) status = 400
-      else status = 500
-      return c.json(err.toObject(), { status })
-    }
-    if (err instanceof Session.BusyError) {
-      return c.json(new NamedError.Unknown({ message: err.message }).toObject(), { status: 400 })
-    }
-    if (err instanceof HTTPException) return err.getResponse()
-    const message = err instanceof Error && err.stack ? err.stack : err.toString()
-    return c.json(new NamedError.Unknown({ message }).toObject(), {
-      status: 500,
+const log = Log.create({ service: "server" })
+
+export const ErrorMiddleware: ErrorHandler = (err, c) => {
+  log.error("failed", {
+    error: err,
+  })
+  if (err instanceof NamedError) {
+    let status: ContentfulStatusCode
+    if (err instanceof NotFoundError) status = 404
+    else if (err instanceof Provider.ModelNotFoundError) status = 400
+    else if (err.name === "ProviderAuthValidationFailed") status = 400
+    else if (err.name.startsWith("Worktree")) status = 400
+    else status = 500
+    return c.json(err.toObject(), { status })
+  }
+  if (err instanceof Session.BusyError) {
+    return c.json(new NamedError.Unknown({ message: err.message }).toObject(), { status: 400 })
+  }
+  if (err instanceof HTTPException) return err.getResponse()
+  const message = err instanceof Error && err.stack ? err.stack : err.toString()
+  return c.json(new NamedError.Unknown({ message }).toObject(), {
+    status: 500,
+  })
+}
+
+export const AuthMiddleware: MiddlewareHandler = (c, next) => {
+  // Allow CORS preflight requests to succeed without auth.
+  // Browser clients sending Authorization headers will preflight with OPTIONS.
+  if (c.req.method === "OPTIONS") return next()
+  const password = Flag.OPENCODE_SERVER_PASSWORD
+  if (!password) return next()
+  const username = Flag.OPENCODE_SERVER_USERNAME ?? "opencode"
+
+  if (c.req.query("auth_token")) c.req.raw.headers.set("authorization", `Basic ${c.req.query("auth_token")}`)
+
+  return basicAuth({ username, password })(c, next)
+}
+
+export const LoggerMiddleware: MiddlewareHandler = async (c, next) => {
+  const skip = c.req.path === "/log"
+  if (!skip) {
+    log.info("request", {
+      method: c.req.method,
+      path: c.req.path,
     })
   }
+  const timer = log.time("request", {
+    method: c.req.method,
+    path: c.req.path,
+  })
+  await next()
+  if (!skip) timer.stop()
+}
+
+export function CorsMiddleware(opts?: { cors?: string[] }): MiddlewareHandler {
+  return cors({
+    maxAge: 86_400,
+    origin(input) {
+      if (!input) return
+
+      if (input.startsWith("http://localhost:")) return input
+      if (input.startsWith("http://127.0.0.1:")) return input
+      if (input === "tauri://localhost" || input === "http://tauri.localhost" || input === "https://tauri.localhost")
+        return input
+
+      if (/^https:\/\/([a-z0-9-]+\.)*opencode\.ai$/.test(input)) return input
+      if (opts?.cors?.includes(input)) return input
+    },
+  })
+}
+
+const zipped = compress()
+export const CompressionMiddleware: MiddlewareHandler = (c, next) => {
+  const path = c.req.path
+  const method = c.req.method
+  if (path === "/event" || path === "/global/event" || path === "/global/sync-event") return next()
+  if (method === "POST" && /\/session\/[^/]+\/(message|prompt_async)$/.test(path)) return next()
+  return zipped(c, next)
 }

+ 28 - 275
packages/opencode/src/server/server.ts

@@ -1,24 +1,14 @@
-import { Log } from "../util/log"
-import { describeRoute, generateSpecs, validator, resolver, openAPIRouteHandler } from "hono-openapi"
+import { generateSpecs } from "hono-openapi"
 import { Hono } from "hono"
-import { compress } from "hono/compress"
-import { createNodeWebSocket } from "@hono/node-ws"
-import { cors } from "hono/cors"
-import { basicAuth } from "hono/basic-auth"
-import type { UpgradeWebSocket } from "hono/ws"
-import z from "zod"
-import { Auth } from "../auth"
-import { Flag } from "../flag/flag"
-import { ProviderID } from "../provider/schema"
-import { WorkspaceRouterMiddleware } from "./router"
-import { errors } from "./error"
-import { GlobalRoutes } from "./routes/global"
+import { adapter } from "#hono"
 import { MDNS } from "./mdns"
 import { lazy } from "@/util/lazy"
-import { errorHandler } from "./middleware"
+import { AuthMiddleware, CompressionMiddleware, CorsMiddleware, ErrorMiddleware, LoggerMiddleware } from "./middleware"
 import { InstanceRoutes } from "./instance"
 import { initProjectors } from "./projectors"
-import { createAdaptorServer, type ServerType } from "@hono/node-server"
+import { Log } from "@/util/log"
+import { ControlPlaneRoutes } from "./control"
+import { UIRoutes } from "./ui"
 
 // @ts-ignore This global is needed to prevent ai-sdk from logging warnings to stdout https://github.com/vercel/ai/blob/2dc67e0ef538307f21368db32d5a12345d98831b/packages/ai/src/logger/log-warnings.ts#L85
 globalThis.AI_SDK_LOG_WARNINGS = false
@@ -26,6 +16,8 @@ globalThis.AI_SDK_LOG_WARNINGS = false
 initProjectors()
 
 export namespace Server {
+  const log = Log.create({ service: "server" })
+
   export type Listener = {
     hostname: string
     port: number
@@ -33,231 +25,31 @@ export namespace Server {
     stop: (close?: boolean) => Promise<void>
   }
 
-  const log = Log.create({ service: "server" })
-  const zipped = compress()
-
-  const skipCompress = (path: string, method: string) => {
-    if (path === "/event" || path === "/global/event" || path === "/global/sync-event") return true
-    if (method === "POST" && /\/session\/[^/]+\/(message|prompt_async)$/.test(path)) return true
-    return false
-  }
-
   export const Default = lazy(() => create({}))
 
-  export function ControlPlaneRoutes(upgrade: UpgradeWebSocket, app = new Hono(), opts?: { cors?: string[] }): Hono {
-    return app
-      .onError(errorHandler(log))
-      .use((c, next) => {
-        // Allow CORS preflight requests to succeed without auth.
-        // Browser clients sending Authorization headers will preflight with OPTIONS.
-        if (c.req.method === "OPTIONS") return next()
-        const password = Flag.OPENCODE_SERVER_PASSWORD
-        if (!password) return next()
-        const username = Flag.OPENCODE_SERVER_USERNAME ?? "opencode"
-
-        if (c.req.query("auth_token")) c.req.raw.headers.set("authorization", `Basic ${c.req.query("auth_token")}`)
-
-        return basicAuth({ username, password })(c, next)
-      })
-      .use(async (c, next) => {
-        const skip = c.req.path === "/log"
-        if (!skip) {
-          log.info("request", {
-            method: c.req.method,
-            path: c.req.path,
-          })
-        }
-        const timer = log.time("request", {
-          method: c.req.method,
-          path: c.req.path,
-        })
-        await next()
-        if (!skip) timer.stop()
-      })
-      .use(
-        cors({
-          maxAge: 86_400,
-          origin(input) {
-            if (!input) return
-
-            if (input.startsWith("http://localhost:")) return input
-            if (input.startsWith("http://127.0.0.1:")) return input
-            if (
-              input === "tauri://localhost" ||
-              input === "http://tauri.localhost" ||
-              input === "https://tauri.localhost"
-            )
-              return input
-
-            if (/^https:\/\/([a-z0-9-]+\.)*opencode\.ai$/.test(input)) return input
-            if (opts?.cors?.includes(input)) return input
-          },
-        }),
-      )
-      .use((c, next) => {
-        if (skipCompress(c.req.path, c.req.method)) return next()
-        return zipped(c, next)
-      })
-      .route("/global", GlobalRoutes())
-      .put(
-        "/auth/:providerID",
-        describeRoute({
-          summary: "Set auth credentials",
-          description: "Set authentication credentials",
-          operationId: "auth.set",
-          responses: {
-            200: {
-              description: "Successfully set authentication credentials",
-              content: {
-                "application/json": {
-                  schema: resolver(z.boolean()),
-                },
-              },
-            },
-            ...errors(400),
-          },
-        }),
-        validator(
-          "param",
-          z.object({
-            providerID: ProviderID.zod,
-          }),
-        ),
-        validator("json", Auth.Info.zod),
-        async (c) => {
-          const providerID = c.req.valid("param").providerID
-          const info = c.req.valid("json")
-          await Auth.set(providerID, info)
-          return c.json(true)
-        },
-      )
-      .delete(
-        "/auth/:providerID",
-        describeRoute({
-          summary: "Remove auth credentials",
-          description: "Remove authentication credentials",
-          operationId: "auth.remove",
-          responses: {
-            200: {
-              description: "Successfully removed authentication credentials",
-              content: {
-                "application/json": {
-                  schema: resolver(z.boolean()),
-                },
-              },
-            },
-            ...errors(400),
-          },
-        }),
-        validator(
-          "param",
-          z.object({
-            providerID: ProviderID.zod,
-          }),
-        ),
-        async (c) => {
-          const providerID = c.req.valid("param").providerID
-          await Auth.remove(providerID)
-          return c.json(true)
-        },
-      )
-      .get(
-        "/doc",
-        openAPIRouteHandler(app, {
-          documentation: {
-            info: {
-              title: "opencode",
-              version: "0.0.3",
-              description: "opencode api",
-            },
-            openapi: "3.1.1",
-          },
-        }),
-      )
-      .use(
-        validator(
-          "query",
-          z.object({
-            directory: z.string().optional(),
-            workspace: z.string().optional(),
-          }),
-        ),
-      )
-      .post(
-        "/log",
-        describeRoute({
-          summary: "Write log",
-          description: "Write a log entry to the server logs with specified level and metadata.",
-          operationId: "app.log",
-          responses: {
-            200: {
-              description: "Log entry written successfully",
-              content: {
-                "application/json": {
-                  schema: resolver(z.boolean()),
-                },
-              },
-            },
-            ...errors(400),
-          },
-        }),
-        validator(
-          "json",
-          z.object({
-            service: z.string().meta({ description: "Service name for the log entry" }),
-            level: z.enum(["debug", "info", "error", "warn"]).meta({ description: "Log level" }),
-            message: z.string().meta({ description: "Log message" }),
-            extra: z
-              .record(z.string(), z.any())
-              .optional()
-              .meta({ description: "Additional metadata for the log entry" }),
-          }),
-        ),
-        async (c) => {
-          const { service, level, message, extra } = c.req.valid("json")
-          const logger = Log.create({ service })
-
-          switch (level) {
-            case "debug":
-              logger.debug(message, extra)
-              break
-            case "info":
-              logger.info(message, extra)
-              break
-            case "error":
-              logger.error(message, extra)
-              break
-            case "warn":
-              logger.warn(message, extra)
-              break
-          }
-
-          return c.json(true)
-        },
-      )
-      .use(WorkspaceRouterMiddleware(upgrade))
-  }
-
   function create(opts: { cors?: string[] }) {
     const app = new Hono()
-    const ws = createNodeWebSocket({ app })
+    const runtime = adapter.create(app)
     return {
-      app: ControlPlaneRoutes(ws.upgradeWebSocket, app, opts),
-      ws,
+      app: app
+        .onError(ErrorMiddleware)
+        .use(AuthMiddleware)
+        .use(LoggerMiddleware)
+        .use(CompressionMiddleware)
+        .use(CorsMiddleware(opts))
+        .route("/", ControlPlaneRoutes())
+        .route("/", InstanceRoutes(runtime.upgradeWebSocket))
+        .route("/", UIRoutes()),
+      runtime,
     }
   }
 
-  export function createApp(opts: { cors?: string[] }) {
-    return create(opts).app
-  }
-
   export async function openapi() {
     // Build a fresh app with all routes registered directly so
     // hono-openapi can see describeRoute metadata (`.route()` wraps
     // handlers when the sub-app has a custom errorHandler, which
     // strips the metadata symbol).
-    const { app, ws } = create({})
-    InstanceRoutes(ws.upgradeWebSocket, app)
+    const { app } = create({})
     const result = await generateSpecs(app, {
       documentation: {
         info: {
@@ -281,46 +73,21 @@ export namespace Server {
     cors?: string[]
   }): Promise<Listener> {
     const built = create(opts)
-    const start = (port: number) =>
-      new Promise<ServerType>((resolve, reject) => {
-        const server = createAdaptorServer({ fetch: built.app.fetch })
-        built.ws.injectWebSocket(server)
-        const fail = (err: Error) => {
-          cleanup()
-          reject(err)
-        }
-        const ready = () => {
-          cleanup()
-          resolve(server)
-        }
-        const cleanup = () => {
-          server.off("error", fail)
-          server.off("listening", ready)
-        }
-        server.once("error", fail)
-        server.once("listening", ready)
-        server.listen(port, opts.hostname)
-      })
-
-    const server = opts.port === 0 ? await start(4096).catch(() => start(0)) : await start(opts.port)
-    const addr = server.address()
-    if (!addr || typeof addr === "string") {
-      throw new Error(`Failed to resolve server address for port ${opts.port}`)
-    }
+    const server = await built.runtime.listen(opts)
 
     const next = new URL("http://localhost")
     next.hostname = opts.hostname
-    next.port = String(addr.port)
+    next.port = String(server.port)
     url = next
 
     const mdns =
       opts.mdns &&
-      addr.port &&
+      server.port &&
       opts.hostname !== "127.0.0.1" &&
       opts.hostname !== "localhost" &&
       opts.hostname !== "::1"
     if (mdns) {
-      MDNS.publish(addr.port, opts.mdnsDomain)
+      MDNS.publish(server.port, opts.mdnsDomain)
     } else if (opts.mdns) {
       log.warn("mDNS enabled but hostname is loopback; skipping mDNS publish")
     }
@@ -328,27 +95,13 @@ export namespace Server {
     let closing: Promise<void> | undefined
     return {
       hostname: opts.hostname,
-      port: addr.port,
+      port: server.port,
       url: next,
       stop(close?: boolean) {
-        closing ??= new Promise((resolve, reject) => {
+        closing ??= (async () => {
           if (mdns) MDNS.unpublish()
-          server.close((err) => {
-            if (err) {
-              reject(err)
-              return
-            }
-            resolve()
-          })
-          if (close) {
-            if ("closeAllConnections" in server && typeof server.closeAllConnections === "function") {
-              server.closeAllConnections()
-            }
-            if ("closeIdleConnections" in server && typeof server.closeIdleConnections === "function") {
-              server.closeIdleConnections()
-            }
-          }
-        })
+          await server.stop(close)
+        })()
         return closing
       },
     }

+ 55 - 0
packages/opencode/src/server/ui/index.ts

@@ -0,0 +1,55 @@
+import { Flag } from "@/flag/flag"
+import { Hono } from "hono"
+import { proxy } from "hono/proxy"
+import { getMimeType } from "hono/utils/mime"
+import { createHash } from "node:crypto"
+import fs from "node:fs/promises"
+
+const embeddedUIPromise = Flag.OPENCODE_DISABLE_EMBEDDED_WEB_UI
+  ? Promise.resolve(null)
+  : // @ts-expect-error - generated file at build time
+    import("opencode-web-ui.gen.ts").then((module) => module.default as Record<string, string>).catch(() => null)
+
+const DEFAULT_CSP =
+  "default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; media-src 'self' data:; connect-src 'self' data:"
+
+const csp = (hash = "") =>
+  `default-src 'self'; script-src 'self' 'wasm-unsafe-eval'${hash ? ` 'sha256-${hash}'` : ""}; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; media-src 'self' data:; connect-src 'self' data:`
+
+export const UIRoutes = (): Hono =>
+  new Hono().all("/*", async (c) => {
+    const embeddedWebUI = await embeddedUIPromise
+    const path = c.req.path
+
+    if (embeddedWebUI) {
+      const match = embeddedWebUI[path.replace(/^\//, "")] ?? embeddedWebUI["index.html"] ?? null
+      if (!match) return c.json({ error: "Not Found" }, 404)
+
+      if (await fs.exists(match)) {
+        const mime = getMimeType(match) ?? "text/plain"
+        c.header("Content-Type", mime)
+        if (mime.startsWith("text/html")) {
+          c.header("Content-Security-Policy", DEFAULT_CSP)
+        }
+        return c.body(new Uint8Array(await fs.readFile(match)))
+      } else {
+        return c.json({ error: "Not Found" }, 404)
+      }
+    } else {
+      const response = await proxy(`https://app.opencode.ai${path}`, {
+        ...c.req,
+        headers: {
+          ...c.req.raw.headers,
+          host: "app.opencode.ai",
+        },
+      })
+      const match = response.headers.get("content-type")?.includes("text/html")
+        ? (await response.clone().text()).match(
+            /<script\b(?![^>]*\bsrc\s*=)[^>]*\bid=(['"])oc-theme-preload-script\1[^>]*>([\s\S]*?)<\/script>/i,
+          )
+        : undefined
+      const hash = match ? createHash("sha256").update(match[2]).digest("base64") : ""
+      response.headers.set("Content-Security-Policy", csp(hash))
+      return response
+    }
+  })

+ 2 - 2
packages/opencode/src/session/compaction.ts

@@ -15,7 +15,7 @@ import { Plugin } from "@/plugin"
 import { Config } from "@/config/config"
 import { NotFoundError } from "@/storage/db"
 import { ModelID, ProviderID } from "@/provider/schema"
-import { Effect, Layer, ServiceMap } from "effect"
+import { Effect, Layer, Context } from "effect"
 import { makeRuntime } from "@/effect/run-service"
 import { InstanceState } from "@/effect/instance-state"
 import { isOverflow as overflow } from "./overflow"
@@ -58,7 +58,7 @@ export namespace SessionCompaction {
     }) => Effect.Effect<void>
   }
 
-  export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/SessionCompaction") {}
+  export class Service extends Context.Service<Service, Interface>()("@opencode/SessionCompaction") {}
 
   export const layer: Layer.Layer<
     Service,

+ 30 - 18
packages/opencode/src/session/index.ts

@@ -29,7 +29,7 @@ import type { Provider } from "@/provider/provider"
 import { Permission } from "@/permission"
 import { Global } from "@/global"
 import type { LanguageModelV2Usage } from "@ai-sdk/provider"
-import { Effect, Layer, ServiceMap } from "effect"
+import { Effect, Layer, Option, Context } from "effect"
 import { makeRuntime } from "@/effect/run-service"
 
 export namespace Session {
@@ -352,19 +352,25 @@ export namespace Session {
       field: string
       delta: string
     }) => Effect.Effect<void>
+    /** Finds the first message matching the predicate, searching newest-first. */
+    readonly findMessage: (
+      sessionID: SessionID,
+      predicate: (msg: MessageV2.WithParts) => boolean,
+    ) => Effect.Effect<Option.Option<MessageV2.WithParts>>
   }
 
-  export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/Session") {}
+  export class Service extends Context.Service<Service, Interface>()("@opencode/Session") {}
 
   type Patch = z.infer<typeof Event.Updated.schema>["info"]
 
   const db = <T>(fn: (d: Parameters<typeof Database.use>[0] extends (trx: infer D) => any ? D : never) => T) =>
     Effect.sync(() => Database.use(fn))
 
-  export const layer: Layer.Layer<Service, never, Bus.Service> = Layer.effect(
+  export const layer: Layer.Layer<Service, never, Bus.Service | Storage.Service> = Layer.effect(
     Service,
     Effect.gen(function* () {
       const bus = yield* Bus.Service
+      const storage = yield* Storage.Service
 
       const createNext = Effect.fn("Session.createNext")(function* (input: {
         id?: SessionID
@@ -585,9 +591,9 @@ export namespace Session {
       })
 
       const diff = Effect.fn("Session.diff")(function* (sessionID: SessionID) {
-        return yield* Effect.tryPromise(() => Storage.read<Snapshot.FileDiff[]>(["session_diff", sessionID])).pipe(
-          Effect.orElseSucceed((): Snapshot.FileDiff[] => []),
-        )
+        return yield* storage
+          .read<Snapshot.FileDiff[]>(["session_diff", sessionID])
+          .pipe(Effect.orElseSucceed((): Snapshot.FileDiff[] => []))
       })
 
       const messages = Effect.fn("Session.messages")(function* (input: { sessionID: SessionID; limit?: number }) {
@@ -635,6 +641,17 @@ export namespace Session {
         yield* bus.publish(MessageV2.Event.PartDelta, input)
       })
 
+      /** Finds the first message matching the predicate, searching newest-first. */
+      const findMessage = Effect.fn("Session.findMessage")(function* (
+        sessionID: SessionID,
+        predicate: (msg: MessageV2.WithParts) => boolean,
+      ) {
+        for (const item of MessageV2.stream(sessionID)) {
+          if (predicate(item)) return Option.some(item)
+        }
+        return Option.none<MessageV2.WithParts>()
+      })
+
       return Service.of({
         create,
         fork,
@@ -656,11 +673,12 @@ export namespace Session {
         updatePart,
         getPart,
         updatePartDelta,
+        findMessage,
       })
     }),
   )
 
-  export const defaultLayer = layer.pipe(Layer.provide(Bus.layer))
+  export const defaultLayer = layer.pipe(Layer.provide(Bus.layer), Layer.provide(Storage.defaultLayer))
 
   const { runPromise } = makeRuntime(Service, defaultLayer)
 
@@ -690,6 +708,11 @@ export namespace Session {
     runPromise((svc) => svc.setArchived(input)),
   )
 
+  export const setPermission = fn(
+    z.object({ sessionID: SessionID.zod, permission: Permission.Ruleset }),
+    (input) => runPromise((svc) => svc.setPermission(input)),
+  )
+
   export const setRevert = fn(
     z.object({ sessionID: SessionID.zod, revert: Info.shape.revert, summary: Info.shape.summary }),
     (input) =>
@@ -832,15 +855,4 @@ export namespace Session {
     MessageV2.Part.parse(part)
     return runPromise((svc) => svc.updatePart(part))
   }
-
-  export const updatePartDelta = fn(
-    z.object({
-      sessionID: SessionID.zod,
-      messageID: MessageID.zod,
-      partID: PartID.zod,
-      field: z.string(),
-      delta: z.string(),
-    }),
-    (input) => runPromise((svc) => svc.updatePartDelta(input)),
-  )
 }

+ 2 - 2
packages/opencode/src/session/instruction.ts

@@ -1,6 +1,6 @@
 import os from "os"
 import path from "path"
-import { Effect, Layer, ServiceMap } from "effect"
+import { Effect, Layer, Context } from "effect"
 import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/unstable/http"
 import { Config } from "@/config/config"
 import { InstanceState } from "@/effect/instance-state"
@@ -64,7 +64,7 @@ export namespace Instruction {
     ) => Effect.Effect<{ filepath: string; content: string }[], AppFileSystem.Error>
   }
 
-  export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/Instruction") {}
+  export class Service extends Context.Service<Service, Interface>()("@opencode/Instruction") {}
 
   export const layer: Layer.Layer<Service, never, AppFileSystem.Service | Config.Service | HttpClient.HttpClient> =
     Layer.effect(

+ 2 - 2
packages/opencode/src/session/llm.ts

@@ -1,6 +1,6 @@
 import { Provider } from "@/provider/provider"
 import { Log } from "@/util/log"
-import { Cause, Effect, Layer, Record, ServiceMap } from "effect"
+import { Cause, Effect, Layer, Record, Context } from "effect"
 import * as Queue from "effect/Queue"
 import * as Stream from "effect/Stream"
 import { streamText, wrapLanguageModel, type ModelMessage, type Tool, tool, jsonSchema } from "ai"
@@ -51,7 +51,7 @@ export namespace LLM {
     readonly stream: (input: StreamInput) => Stream.Stream<Event, unknown>
   }
 
-  export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/LLM") {}
+  export class Service extends Context.Service<Service, Interface>()("@opencode/LLM") {}
 
   export const layer = Layer.effect(
     Service,

+ 2 - 1
packages/opencode/src/session/message-v2.ts

@@ -15,6 +15,7 @@ import type { SystemError } from "bun"
 import type { Provider } from "@/provider/provider"
 import { ModelID, ProviderID } from "@/provider/schema"
 import { Effect } from "effect"
+import { EffectLogger } from "@/effect/logger"
 
 /** Error shape thrown by Bun's fetch() when gzip/br decompression fails mid-stream */
 interface FetchDecompressionError extends Error {
@@ -839,7 +840,7 @@ export namespace MessageV2 {
     model: Provider.Model,
     options?: { stripMedia?: boolean },
   ): Promise<ModelMessage[]> {
-    return Effect.runPromise(toModelMessagesEffect(input, model, options))
+    return Effect.runPromise(toModelMessagesEffect(input, model, options).pipe(Effect.provide(EffectLogger.layer)))
   }
 
   export function page(input: { sessionID: SessionID; limit: number; before?: string }) {

+ 8 - 7
packages/opencode/src/session/processor.ts

@@ -1,4 +1,4 @@
-import { Cause, Deferred, Effect, Layer, ServiceMap } from "effect"
+import { Cause, Deferred, Effect, Layer, Context } from "effect"
 import * as Stream from "effect/Stream"
 import { Agent } from "@/agent/agent"
 import { Bus } from "@/bus"
@@ -6,7 +6,7 @@ import { Config } from "@/config/config"
 import { Permission } from "@/permission"
 import { Plugin } from "@/plugin"
 import { Snapshot } from "@/snapshot"
-import { Log } from "@/util/log"
+import { EffectLogger } from "@/effect/logger"
 import { Session } from "."
 import { LLM } from "./llm"
 import { MessageV2 } from "./message-v2"
@@ -23,7 +23,7 @@ import { isRecord } from "@/util/record"
 
 export namespace SessionProcessor {
   const DOOM_LOOP_THRESHOLD = 3
-  const log = Log.create({ service: "session.processor" })
+  const log = EffectLogger.create({ service: "session.processor" })
 
   export type Result = "compact" | "stop" | "continue"
 
@@ -76,7 +76,7 @@ export namespace SessionProcessor {
 
   type StreamEvent = Event
 
-  export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/SessionProcessor") {}
+  export class Service extends Context.Service<Service, Interface>()("@opencode/SessionProcessor") {}
 
   export const layer: Layer.Layer<
     Service,
@@ -121,6 +121,7 @@ export namespace SessionProcessor {
           reasoningMap: {},
         }
         let aborted = false
+        const slog = log.with({ sessionID: input.sessionID, messageID: input.assistantMessage.id })
 
         const parse = (e: unknown) =>
           MessageV2.fromError(e, {
@@ -448,7 +449,7 @@ export namespace SessionProcessor {
               return
 
             default:
-              log.info("unhandled", { ...value })
+              yield* slog.info("unhandled", { event: value.type, value })
               return
           }
         })
@@ -514,7 +515,7 @@ export namespace SessionProcessor {
         })
 
         const halt = Effect.fn("SessionProcessor.halt")(function* (e: unknown) {
-          log.error("process", { error: e, stack: e instanceof Error ? e.stack : undefined })
+          yield* slog.error("process", { error: errorMessage(e), stack: e instanceof Error ? e.stack : undefined })
           const error = parse(e)
           if (MessageV2.ContextOverflowError.isInstance(error)) {
             ctx.needsCompaction = true
@@ -530,7 +531,7 @@ export namespace SessionProcessor {
         })
 
         const process = Effect.fn("SessionProcessor.process")(function* (streamInput: LLM.StreamInput) {
-          log.info("process")
+          yield* slog.info("process")
           ctx.needsCompaction = false
           ctx.shouldBreak = (yield* config.get()).experimental?.continue_loop_on_deny !== true
 

+ 133 - 126
packages/opencode/src/session/prompt.ts

@@ -43,7 +43,8 @@ import { AppFileSystem } from "@/filesystem"
 import { Truncate } from "@/tool/truncate"
 import { decodeDataUrl } from "@/util/data-url"
 import { Process } from "@/util/process"
-import { Cause, Effect, Exit, Layer, Option, Scope, ServiceMap } from "effect"
+import { Cause, Effect, Exit, Layer, Option, Scope, Context } from "effect"
+import { EffectLogger } from "@/effect/logger"
 import { InstanceState } from "@/effect/instance-state"
 import { makeRuntime } from "@/effect/run-service"
 import { TaskTool, type TaskPromptOps } from "@/tool/task"
@@ -64,6 +65,7 @@ const STRUCTURED_OUTPUT_SYSTEM_PROMPT = `IMPORTANT: The user has requested struc
 
 export namespace SessionPrompt {
   const log = Log.create({ service: "session.prompt" })
+  const elog = EffectLogger.create({ service: "session.prompt" })
 
   export interface Interface {
     readonly cancel: (sessionID: SessionID) => Effect.Effect<void>
@@ -74,7 +76,7 @@ export namespace SessionPrompt {
     readonly resolvePromptParts: (template: string) => Effect.Effect<PromptInput["parts"]>
   }
 
-  export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/SessionPrompt") {}
+  export class Service extends Context.Service<Service, Interface>()("@opencode/SessionPrompt") {}
 
   export const layer = Layer.effect(
     Service,
@@ -100,9 +102,17 @@ export namespace SessionPrompt {
       const instruction = yield* Instruction.Service
       const state = yield* SessionRunState.Service
       const revert = yield* SessionRevert.Service
+      const sys = yield* SystemPrompt.Service
+      const llm = yield* LLM.Service
+
+      const run = {
+        promise: <A, E>(effect: Effect.Effect<A, E>) =>
+          Effect.runPromise(effect.pipe(Effect.provide(EffectLogger.layer))),
+        fork: <A, E>(effect: Effect.Effect<A, E>) => Effect.runFork(effect.pipe(Effect.provide(EffectLogger.layer))),
+      }
 
       const cancel = Effect.fn("SessionPrompt.cancel")(function* (sessionID: SessionID) {
-        log.info("cancel", { sessionID })
+        yield* elog.info("cancel", { sessionID })
         yield* state.cancel(sessionID)
       })
 
@@ -172,21 +182,24 @@ export namespace SessionPrompt {
         const msgs = onlySubtasks
           ? [{ role: "user" as const, content: subtasks.map((p) => p.prompt).join("\n") }]
           : yield* MessageV2.toModelMessagesEffect(context, mdl)
-        const text = yield* Effect.promise(async (signal) => {
-          const result = await LLM.stream({
+        const text = yield* llm
+          .stream({
             agent: ag,
             user: firstInfo,
             system: [],
             small: true,
             tools: {},
             model: mdl,
-            abort: signal,
             sessionID: input.session.id,
             retries: 2,
             messages: [{ role: "user", content: "Generate a title for this conversation:\n" }, ...msgs],
           })
-          return result.text
-        })
+          .pipe(
+            Stream.filter((e): e is Extract<LLM.Event, { type: "text-delta" }> => e.type === "text-delta"),
+            Stream.map((e) => e.text),
+            Stream.mkString,
+            Effect.orDie,
+          )
         const cleaned = text
           .replace(/<think>[\s\S]*?<\/think>\s*/g, "")
           .split("\n")
@@ -196,11 +209,7 @@ export namespace SessionPrompt {
         const t = cleaned.length > 100 ? cleaned.substring(0, 97) + "..." : cleaned
         yield* sessions
           .setTitle({ sessionID: input.session.id, title: t })
-          .pipe(
-            Effect.catchCause((cause) =>
-              Effect.sync(() => log.error("failed to generate title", { error: Cause.squash(cause) })),
-            ),
-          )
+          .pipe(Effect.catchCause((cause) => elog.error("failed to generate title", { error: Cause.squash(cause) })))
       })
 
       const insertReminders = Effect.fn("SessionPrompt.insertReminders")(function* (input: {
@@ -360,30 +369,28 @@ NOTE: At any point in time through this workflow you should feel free to ask the
           agent: input.agent.name,
           messages: input.messages,
           metadata: (val) =>
-            Effect.runPromise(
-              input.processor.updateToolCall(options.toolCallId, (match) => {
-                if (!["running", "pending"].includes(match.state.status)) return match
-                return {
-                  ...match,
-                  state: {
-                    title: val.title,
-                    metadata: val.metadata,
-                    status: "running",
-                    input: args,
-                    time: { start: Date.now() },
-                  },
-                }
-              }),
-            ),
+            input.processor.updateToolCall(options.toolCallId, (match) => {
+              if (!["running", "pending"].includes(match.state.status)) return match
+              return {
+                ...match,
+                state: {
+                  title: val.title,
+                  metadata: val.metadata,
+                  status: "running",
+                  input: args,
+                  time: { start: Date.now() },
+                },
+              }
+            }),
           ask: (req) =>
-            Effect.runPromise(
-              permission.ask({
+            permission
+              .ask({
                 ...req,
                 sessionID: input.session.id,
                 tool: { messageID: input.processor.message.id, callID: options.toolCallId },
                 ruleset: Permission.merge(input.agent.permission, input.session.permission ?? []),
-              }),
-            ),
+              })
+              .pipe(Effect.orDie),
         })
 
         for (const item of yield* registry.tools({
@@ -397,7 +404,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the
             description: item.description,
             inputSchema: jsonSchema(schema as any),
             execute(args, options) {
-              return Effect.runPromise(
+              return run.promise(
                 Effect.gen(function* () {
                   const ctx = context(args, options)
                   yield* plugin.trigger(
@@ -405,7 +412,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the
                     { tool: item.id, sessionID: ctx.sessionID, callID: ctx.callID },
                     { args },
                   )
-                  const result = yield* Effect.promise(() => item.execute(args, ctx))
+                  const result = yield* item.execute(args, ctx)
                   const output = {
                     ...result,
                     attachments: result.attachments?.map((attachment) => ({
@@ -438,7 +445,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the
           const transformed = ProviderTransform.schema(input.model, schema)
           item.inputSchema = jsonSchema(transformed)
           item.execute = (args, opts) =>
-            Effect.runPromise(
+            run.promise(
               Effect.gen(function* () {
                 const ctx = context(args, opts)
                 yield* plugin.trigger(
@@ -446,7 +453,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the
                   { tool: key, sessionID: ctx.sessionID, callID: opts.toolCallId },
                   { args },
                 )
-                yield* Effect.promise(() => ctx.ask({ permission: key, metadata: {}, patterns: ["*"], always: ["*"] }))
+                yield* ctx.ask({ permission: key, metadata: {}, patterns: ["*"], always: ["*"] })
                 const result: Awaited<ReturnType<NonNullable<typeof execute>>> = yield* Effect.promise(() =>
                   execute(args, opts),
                 )
@@ -578,63 +585,61 @@ NOTE: At any point in time through this workflow you should feel free to ask the
         }
 
         let error: Error | undefined
-        const result = yield* Effect.promise((signal) =>
-          taskTool
-            .execute(taskArgs, {
-              agent: task.agent,
-              messageID: assistantMessage.id,
-              sessionID,
-              abort: signal,
-              callID: part.callID,
-              extra: { bypassAgentCheck: true, promptOps },
-              messages: msgs,
-              metadata(val: { title?: string; metadata?: Record<string, any> }) {
-                return Effect.runPromise(
-                  Effect.gen(function* () {
-                    part = yield* sessions.updatePart({
-                      ...part,
-                      type: "tool",
-                      state: { ...part.state, ...val },
-                    } satisfies MessageV2.ToolPart)
-                  }),
-                )
-              },
-              ask(req: any) {
-                return Effect.runPromise(
-                  permission.ask({
-                    ...req,
-                    sessionID,
-                    ruleset: Permission.merge(taskAgent.permission, session.permission ?? []),
-                  }),
-                )
-              },
-            })
-            .catch((e) => {
-              error = e instanceof Error ? e : new Error(String(e))
-              log.error("subtask execution failed", { error, agent: task.agent, description: task.description })
-              return undefined
-            }),
-        ).pipe(
-          Effect.onInterrupt(() =>
-            Effect.gen(function* () {
-              assistantMessage.finish = "tool-calls"
-              assistantMessage.time.completed = Date.now()
-              yield* sessions.updateMessage(assistantMessage)
-              if (part.state.status === "running") {
-                yield* sessions.updatePart({
+        const taskAbort = new AbortController()
+        const result = yield* taskTool
+          .execute(taskArgs, {
+            agent: task.agent,
+            messageID: assistantMessage.id,
+            sessionID,
+            abort: taskAbort.signal,
+            callID: part.callID,
+            extra: { bypassAgentCheck: true, promptOps },
+            messages: msgs,
+            metadata: (val: { title?: string; metadata?: Record<string, any> }) =>
+              Effect.gen(function* () {
+                part = yield* sessions.updatePart({
                   ...part,
-                  state: {
-                    status: "error",
-                    error: "Cancelled",
-                    time: { start: part.state.time.start, end: Date.now() },
-                    metadata: part.state.metadata,
-                    input: part.state.input,
-                  },
+                  type: "tool",
+                  state: { ...part.state, ...val },
                 } satisfies MessageV2.ToolPart)
-              }
+              }),
+            ask: (req: any) =>
+              permission
+                .ask({
+                  ...req,
+                  sessionID,
+                  ruleset: Permission.merge(taskAgent.permission, session.permission ?? []),
+                })
+                .pipe(Effect.orDie),
+          })
+          .pipe(
+            Effect.catchCause((cause) => {
+              const defect = Cause.squash(cause)
+              error = defect instanceof Error ? defect : new Error(String(defect))
+              log.error("subtask execution failed", { error, agent: task.agent, description: task.description })
+              return Effect.void
             }),
-          ),
-        )
+            Effect.onInterrupt(() =>
+              Effect.gen(function* () {
+                taskAbort.abort()
+                assistantMessage.finish = "tool-calls"
+                assistantMessage.time.completed = Date.now()
+                yield* sessions.updateMessage(assistantMessage)
+                if (part.state.status === "running") {
+                  yield* sessions.updatePart({
+                    ...part,
+                    state: {
+                      status: "error",
+                      error: "Cancelled",
+                      time: { start: part.state.time.start, end: Date.now() },
+                      metadata: part.state.metadata,
+                      input: part.state.input,
+                    },
+                  } satisfies MessageV2.ToolPart)
+                }
+              }),
+            ),
+          )
 
         const attachments = result?.attachments?.map((attachment) => ({
           ...attachment,
@@ -857,7 +862,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the
               output += chunk
               if (part.state.status === "running") {
                 part.state.metadata = { output, description: "" }
-                void Effect.runFork(sessions.updatePart(part))
+                void run.fork(sessions.updatePart(part))
               }
             }),
           )
@@ -902,12 +907,8 @@ NOTE: At any point in time through this workflow you should feel free to ask the
       })
 
       const lastModel = Effect.fnUntraced(function* (sessionID: SessionID) {
-        const model = yield* Effect.promise(async () => {
-          for await (const item of MessageV2.stream(sessionID)) {
-            if (item.info.role === "user" && item.info.model) return item.info.model
-          }
-        })
-        if (model) return model
+        const match = yield* sessions.findMessage(sessionID, (m) => m.info.role === "user" && !!m.info.model)
+        if (Option.isSome(match) && match.value.info.role === "user") return match.value.info.model
         return yield* provider.defaultModel()
       })
 
@@ -1039,19 +1040,21 @@ NOTE: At any point in time through this workflow you should feel free to ask the
                 if (yield* fsys.isDir(filepath)) part.mime = "application/x-directory"
 
                 const { read } = yield* registry.named()
-                const execRead = (args: Parameters<typeof read.execute>[0], extra?: Tool.Context["extra"]) =>
-                  Effect.promise((signal: AbortSignal) =>
-                    read.execute(args, {
+                const execRead = (args: Parameters<typeof read.execute>[0], extra?: Tool.Context["extra"]) => {
+                  const controller = new AbortController()
+                  return read
+                    .execute(args, {
                       sessionID: input.sessionID,
-                      abort: signal,
+                      abort: controller.signal,
                       agent: input.agent!,
                       messageID: info.id,
                       extra: { bypassCwdCheck: true, ...extra },
                       messages: [],
-                      metadata: async () => {},
-                      ask: async () => {},
-                    }),
-                  )
+                      metadata: () => Effect.void,
+                      ask: () => Effect.void,
+                    })
+                    .pipe(Effect.onInterrupt(() => Effect.sync(() => controller.abort())))
+                }
 
                 if (part.mime === "text/plain") {
                   let offset: number | undefined
@@ -1288,27 +1291,25 @@ NOTE: At any point in time through this workflow you should feel free to ask the
         },
       )
 
-      const lastAssistant = (sessionID: SessionID) =>
-        Effect.promise(async () => {
-          let latest: MessageV2.WithParts | undefined
-          for await (const item of MessageV2.stream(sessionID)) {
-            latest ??= item
-            if (item.info.role !== "user") return item
-          }
-          if (latest) return latest
-          throw new Error("Impossible")
-        })
+      const lastAssistant = Effect.fnUntraced(function* (sessionID: SessionID) {
+        const match = yield* sessions.findMessage(sessionID, (m) => m.info.role !== "user")
+        if (Option.isSome(match)) return match.value
+        const msgs = yield* sessions.messages({ sessionID, limit: 1 })
+        if (msgs.length > 0) return msgs[0]
+        throw new Error("Impossible")
+      })
 
       const runLoop: (sessionID: SessionID) => Effect.Effect<MessageV2.WithParts> = Effect.fn("SessionPrompt.run")(
         function* (sessionID: SessionID) {
           const ctx = yield* InstanceState.context
+          const slog = elog.with({ sessionID })
           let structured: unknown | undefined
           let step = 0
           const session = yield* sessions.get(sessionID)
 
           while (true) {
             yield* status.set(sessionID, { type: "busy" })
-            log.info("loop", { step, sessionID })
+            yield* slog.info("loop", { step })
 
             let msgs = yield* MessageV2.filterCompactedEffect(sessionID)
 
@@ -1344,7 +1345,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the
               !hasToolCalls &&
               lastUser.id < lastAssistant.id
             ) {
-              log.info("exiting loop", { sessionID })
+              yield* slog.info("exiting loop")
               break
             }
 
@@ -1466,8 +1467,8 @@ NOTE: At any point in time through this workflow you should feel free to ask the
               yield* plugin.trigger("experimental.chat.messages.transform", {}, { messages: msgs })
 
               const [skills, env, instructions, modelMsgs] = yield* Effect.all([
-                Effect.promise(() => SystemPrompt.skills(agent)),
-                Effect.promise(() => SystemPrompt.environment(model)),
+                sys.skills(agent),
+                Effect.sync(() => sys.environment(model)),
                 instruction.system().pipe(Effect.orDie),
                 MessageV2.toModelMessagesEffect(msgs, model),
               ])
@@ -1540,7 +1541,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the
       )
 
       const command = Effect.fn("SessionPrompt.command")(function* (input: CommandInput) {
-        log.info("command", input)
+        yield* elog.info("command", { sessionID: input.sessionID, command: input.command, agent: input.agent })
         const cmd = yield* commands.get(input.command)
         if (!cmd) {
           const available = (yield* commands.list()).map((c) => c.name)
@@ -1656,9 +1657,9 @@ NOTE: At any point in time through this workflow you should feel free to ask the
       })
 
       const promptOps: TaskPromptOps = {
-        cancel: (sessionID) => Effect.runFork(cancel(sessionID)),
-        resolvePromptParts: (template) => Effect.runPromise(resolvePromptParts(template)),
-        prompt: (input) => Effect.runPromise(prompt(input)),
+        cancel: (sessionID) => run.fork(cancel(sessionID)),
+        resolvePromptParts: (template) => resolvePromptParts(template),
+        prompt: (input) => prompt(input),
       }
 
       return Service.of({
@@ -1691,9 +1692,15 @@ NOTE: At any point in time through this workflow you should feel free to ask the
       Layer.provide(Plugin.defaultLayer),
       Layer.provide(Session.defaultLayer),
       Layer.provide(SessionRevert.defaultLayer),
-      Layer.provide(Agent.defaultLayer),
-      Layer.provide(Bus.layer),
-      Layer.provide(CrossSpawnSpawner.defaultLayer),
+      Layer.provide(
+        Layer.mergeAll(
+          Agent.defaultLayer,
+          SystemPrompt.defaultLayer,
+          LLM.defaultLayer,
+          Bus.layer,
+          CrossSpawnSpawner.defaultLayer,
+        ),
+      ),
     ),
   )
   const { runPromise } = makeRuntime(Service, defaultLayer)

+ 2 - 2
packages/opencode/src/session/revert.ts

@@ -1,5 +1,5 @@
 import z from "zod"
-import { Effect, Layer, ServiceMap } from "effect"
+import { Effect, Layer, Context } from "effect"
 import { makeRuntime } from "@/effect/run-service"
 import { Bus } from "../bus"
 import { Snapshot } from "../snapshot"
@@ -29,7 +29,7 @@ export namespace SessionRevert {
     readonly cleanup: (session: Session.Info) => Effect.Effect<void>
   }
 
-  export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/SessionRevert") {}
+  export class Service extends Context.Service<Service, Interface>()("@opencode/SessionRevert") {}
 
   export const layer = Layer.effect(
     Service,

+ 2 - 2
packages/opencode/src/session/run-state.ts

@@ -1,7 +1,7 @@
 import { InstanceState } from "@/effect/instance-state"
 import { Runner } from "@/effect/runner"
 import { makeRuntime } from "@/effect/run-service"
-import { Effect, Layer, Scope, ServiceMap } from "effect"
+import { Effect, Layer, Scope, Context } from "effect"
 import { Session } from "."
 import { MessageV2 } from "./message-v2"
 import { SessionID } from "./schema"
@@ -23,7 +23,7 @@ export namespace SessionRunState {
     ) => Effect.Effect<MessageV2.WithParts>
   }
 
-  export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/SessionRunState") {}
+  export class Service extends Context.Service<Service, Interface>()("@opencode/SessionRunState") {}
 
   export const layer = Layer.effect(
     Service,

+ 3 - 6
packages/opencode/src/session/schema.ts

@@ -7,8 +7,7 @@ import { withStatics } from "@/util/schema"
 export const SessionID = Schema.String.pipe(
   Schema.brand("SessionID"),
   withStatics((s) => ({
-    make: (id: string) => s.makeUnsafe(id),
-    descending: (id?: string) => s.makeUnsafe(Identifier.descending("session", id)),
+    descending: (id?: string) => s.make(Identifier.descending("session", id)),
     zod: Identifier.schema("session").pipe(z.custom<Schema.Schema.Type<typeof s>>()),
   })),
 )
@@ -18,8 +17,7 @@ export type SessionID = Schema.Schema.Type<typeof SessionID>
 export const MessageID = Schema.String.pipe(
   Schema.brand("MessageID"),
   withStatics((s) => ({
-    make: (id: string) => s.makeUnsafe(id),
-    ascending: (id?: string) => s.makeUnsafe(Identifier.ascending("message", id)),
+    ascending: (id?: string) => s.make(Identifier.ascending("message", id)),
     zod: Identifier.schema("message").pipe(z.custom<Schema.Schema.Type<typeof s>>()),
   })),
 )
@@ -29,8 +27,7 @@ export type MessageID = Schema.Schema.Type<typeof MessageID>
 export const PartID = Schema.String.pipe(
   Schema.brand("PartID"),
   withStatics((s) => ({
-    make: (id: string) => s.makeUnsafe(id),
-    ascending: (id?: string) => s.makeUnsafe(Identifier.ascending("part", id)),
+    ascending: (id?: string) => s.make(Identifier.ascending("part", id)),
     zod: Identifier.schema("part").pipe(z.custom<Schema.Schema.Type<typeof s>>()),
   })),
 )

+ 2 - 16
packages/opencode/src/session/status.ts

@@ -1,9 +1,8 @@
 import { BusEvent } from "@/bus/bus-event"
 import { Bus } from "@/bus"
 import { InstanceState } from "@/effect/instance-state"
-import { makeRuntime } from "@/effect/run-service"
 import { SessionID } from "./schema"
-import { Effect, Layer, ServiceMap } from "effect"
+import { Effect, Layer, Context } from "effect"
 import z from "zod"
 
 export namespace SessionStatus {
@@ -50,7 +49,7 @@ export namespace SessionStatus {
     readonly set: (sessionID: SessionID, status: Info) => Effect.Effect<void>
   }
 
-  export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/SessionStatus") {}
+  export class Service extends Context.Service<Service, Interface>()("@opencode/SessionStatus") {}
 
   export const layer = Layer.effect(
     Service,
@@ -86,17 +85,4 @@ export namespace SessionStatus {
   )
 
   export const defaultLayer = layer.pipe(Layer.provide(Bus.layer))
-  const { runPromise } = makeRuntime(Service, defaultLayer)
-
-  export async function get(sessionID: SessionID) {
-    return runPromise((svc) => svc.get(sessionID))
-  }
-
-  export async function list() {
-    return runPromise((svc) => svc.list())
-  }
-
-  export async function set(sessionID: SessionID, status: Info) {
-    return runPromise((svc) => svc.set(sessionID, status))
-  }
 }

+ 2 - 2
packages/opencode/src/session/summary.ts

@@ -1,5 +1,5 @@
 import z from "zod"
-import { Effect, Layer, ServiceMap } from "effect"
+import { Effect, Layer, Context } from "effect"
 import { makeRuntime } from "@/effect/run-service"
 import { Bus } from "@/bus"
 import { Snapshot } from "@/snapshot"
@@ -71,7 +71,7 @@ export namespace SessionSummary {
     readonly computeDiff: (input: { messages: MessageV2.WithParts[] }) => Effect.Effect<Snapshot.FileDiff[]>
   }
 
-  export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/SessionSummary") {}
+  export class Service extends Context.Service<Service, Interface>()("@opencode/SessionSummary") {}
 
   export const layer = Layer.effect(
     Service,

+ 45 - 37
packages/opencode/src/session/system.ts

@@ -1,4 +1,4 @@
-import { Ripgrep } from "../file/ripgrep"
+import { Context, Effect, Layer } from "effect"
 
 import { Instance } from "../project/instance"
 
@@ -33,44 +33,52 @@ export namespace SystemPrompt {
     return [PROMPT_DEFAULT]
   }
 
-  export async function environment(model: Provider.Model) {
-    const project = Instance.project
-    return [
-      [
-        `You are powered by the model named ${model.api.id}. The exact model ID is ${model.providerID}/${model.api.id}`,
-        `Here is some useful information about the environment you are running in:`,
-        `<env>`,
-        `  Working directory: ${Instance.directory}`,
-        `  Workspace root folder: ${Instance.worktree}`,
-        `  Is directory a git repo: ${project.vcs === "git" ? "yes" : "no"}`,
-        `  Platform: ${process.platform}`,
-        `  Today's date: ${new Date().toDateString()}`,
-        `</env>`,
-        `<directories>`,
-        `  ${
-          project.vcs === "git" && false
-            ? await Ripgrep.tree({
-                cwd: Instance.directory,
-                limit: 50,
-              })
-            : ""
-        }`,
-        `</directories>`,
-      ].join("\n"),
-    ]
+  export interface Interface {
+    readonly environment: (model: Provider.Model) => string[]
+    readonly skills: (agent: Agent.Info) => Effect.Effect<string | undefined>
   }
 
-  export async function skills(agent: Agent.Info) {
-    if (Permission.disabled(["skill"], agent.permission).has("skill")) return
+  export class Service extends Context.Service<Service, Interface>()("@opencode/SystemPrompt") {}
 
-    const list = await Skill.available(agent)
+  export const layer = Layer.effect(
+    Service,
+    Effect.gen(function* () {
+      const skill = yield* Skill.Service
 
-    return [
-      "Skills provide specialized instructions and workflows for specific tasks.",
-      "Use the skill tool to load a skill when a task matches its description.",
-      // the agents seem to ingest the information about skills a bit better if we present a more verbose
-      // version of them here and a less verbose version in tool description, rather than vice versa.
-      Skill.fmt(list, { verbose: true }),
-    ].join("\n")
-  }
+      return Service.of({
+        environment(model) {
+          const project = Instance.project
+          return [
+            [
+              `You are powered by the model named ${model.api.id}. The exact model ID is ${model.providerID}/${model.api.id}`,
+              `Here is some useful information about the environment you are running in:`,
+              `<env>`,
+              `  Working directory: ${Instance.directory}`,
+              `  Workspace root folder: ${Instance.worktree}`,
+              `  Is directory a git repo: ${project.vcs === "git" ? "yes" : "no"}`,
+              `  Platform: ${process.platform}`,
+              `  Today's date: ${new Date().toDateString()}`,
+              `</env>`,
+            ].join("\n"),
+          ]
+        },
+
+        skills: Effect.fn("SystemPrompt.skills")(function* (agent: Agent.Info) {
+          if (Permission.disabled(["skill"], agent.permission).has("skill")) return
+
+          const list = yield* skill.available(agent)
+
+          return [
+            "Skills provide specialized instructions and workflows for specific tasks.",
+            "Use the skill tool to load a skill when a task matches its description.",
+            // the agents seem to ingest the information about skills a bit better if we present a more verbose
+            // version of them here and a less verbose version in tool description, rather than vice versa.
+            Skill.fmt(list, { verbose: true }),
+          ].join("\n")
+        }),
+      })
+    }),
+  )
+
+  export const defaultLayer = layer.pipe(Layer.provide(Skill.defaultLayer))
 }

+ 2 - 8
packages/opencode/src/session/todo.ts

@@ -1,8 +1,7 @@
 import { BusEvent } from "@/bus/bus-event"
 import { Bus } from "@/bus"
-import { makeRuntime } from "@/effect/run-service"
 import { SessionID } from "./schema"
-import { Effect, Layer, ServiceMap } from "effect"
+import { Effect, Layer, Context } from "effect"
 import z from "zod"
 import { Database, eq, asc } from "../storage/db"
 import { TodoTable } from "./session.sql"
@@ -32,7 +31,7 @@ export namespace Todo {
     readonly get: (sessionID: SessionID) => Effect.Effect<Info[]>
   }
 
-  export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/SessionTodo") {}
+  export class Service extends Context.Service<Service, Interface>()("@opencode/SessionTodo") {}
 
   export const layer = Layer.effect(
     Service,
@@ -83,9 +82,4 @@ export namespace Todo {
   )
 
   export const defaultLayer = layer.pipe(Layer.provide(Bus.layer))
-  const { runPromise } = makeRuntime(Service, defaultLayer)
-
-  export async function get(sessionID: SessionID) {
-    return runPromise((svc) => svc.get(sessionID))
-  }
 }

+ 2 - 2
packages/opencode/src/share/session.ts

@@ -3,7 +3,7 @@ import { Session } from "@/session"
 import { SessionID } from "@/session/schema"
 import { SyncEvent } from "@/sync"
 import { fn } from "@/util/fn"
-import { Effect, Layer, Scope, ServiceMap } from "effect"
+import { Effect, Layer, Scope, Context } from "effect"
 import { Config } from "../config/config"
 import { Flag } from "../flag/flag"
 import { ShareNext } from "./share-next"
@@ -15,7 +15,7 @@ export namespace SessionShare {
     readonly unshare: (sessionID: SessionID) => Effect.Effect<void, unknown>
   }
 
-  export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/SessionShare") {}
+  export class Service extends Context.Service<Service, Interface>()("@opencode/SessionShare") {}
 
   export const layer = Layer.effect(
     Service,

+ 2 - 25
packages/opencode/src/share/share-next.ts

@@ -1,10 +1,9 @@
 import type * as SDK from "@opencode-ai/sdk/v2"
-import { Effect, Exit, Layer, Option, Schema, Scope, ServiceMap, Stream } from "effect"
+import { Effect, Exit, Layer, Option, Schema, Scope, Context, Stream } from "effect"
 import { FetchHttpClient, HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http"
 import { Account } from "@/account"
 import { Bus } from "@/bus"
 import { InstanceState } from "@/effect/instance-state"
-import { makeRuntime } from "@/effect/run-service"
 import { Provider } from "@/provider/provider"
 import { ModelID, ProviderID } from "@/provider/schema"
 import { Session } from "@/session"
@@ -74,7 +73,7 @@ export namespace ShareNext {
     readonly remove: (sessionID: SessionID) => Effect.Effect<void, unknown>
   }
 
-  export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/ShareNext") {}
+  export class Service extends Context.Service<Service, Interface>()("@opencode/ShareNext") {}
 
   const db = <T>(fn: (d: Parameters<typeof Database.use>[0] extends (trx: infer D) => any ? D : never) => T) =>
     Effect.sync(() => Database.use(fn))
@@ -348,26 +347,4 @@ export namespace ShareNext {
     Layer.provide(Provider.defaultLayer),
     Layer.provide(Session.defaultLayer),
   )
-
-  const { runPromise } = makeRuntime(Service, defaultLayer)
-
-  export async function init() {
-    return runPromise((svc) => svc.init())
-  }
-
-  export async function url() {
-    return runPromise((svc) => svc.url())
-  }
-
-  export async function request(): Promise<Req> {
-    return runPromise((svc) => svc.request())
-  }
-
-  export async function create(sessionID: SessionID) {
-    return runPromise((svc) => svc.create(sessionID))
-  }
-
-  export async function remove(sessionID: SessionID) {
-    return runPromise((svc) => svc.remove(sessionID))
-  }
 }

Alguns arquivos não foram mostrados porque muitos arquivos mudaram nesse diff