1
0
opencode-agent[bot] 2 сар өмнө
parent
commit
b0a929440b
87 өөрчлөгдсөн 2301 нэмэгдсэн , 1599 устгасан
  1. 35 107
      packages/core/migration/20260603001617_session_message_projection_indexes/snapshot.json
  2. 43 130
      packages/core/migration/20260603160727_jittery_ezekiel_stane/snapshot.json
  3. 3 1
      packages/core/src/config/plugin/skill.ts
  4. 6 2
      packages/core/src/database/migration/20260603001617_session_message_projection_indexes.ts
  5. 11 4
      packages/core/src/database/migration/20260603040000_session_message_projection_order.ts
  6. 3 1
      packages/core/src/database/migration/20260603160727_jittery_ezekiel_stane.ts
  7. 17 15
      packages/core/src/effect/keyed-mutex.ts
  8. 389 346
      packages/core/src/event.ts
  9. 19 9
      packages/core/src/file-mutation.ts
  10. 5 1
      packages/core/src/filesystem.ts
  11. 7 1
      packages/core/src/location-mutation.ts
  12. 4 1
      packages/core/src/location-search.ts
  13. 5 5
      packages/core/src/opencode.ts
  14. 23 5
      packages/core/src/patch.ts
  15. 67 63
      packages/core/src/permission.ts
  16. 19 9
      packages/core/src/ripgrep.ts
  17. 2 1
      packages/core/src/schema.ts
  18. 11 3
      packages/core/src/session.ts
  19. 2 1
      packages/core/src/session/input.ts
  20. 4 2
      packages/core/src/session/projector.ts
  21. 50 14
      packages/core/src/session/runner/publish-llm-event.ts
  22. 13 4
      packages/core/src/session/runner/to-llm-message.ts
  23. 1 1
      packages/core/src/session/schema.ts
  24. 4 5
      packages/core/src/skill/discovery.ts
  25. 48 21
      packages/core/src/tool-output-store.ts
  26. 107 82
      packages/core/src/tool-registry.ts
  27. 72 27
      packages/core/src/tool/apply-patch.ts
  28. 11 3
      packages/core/src/tool/edit.ts
  29. 21 6
      packages/core/src/tool/glob.ts
  30. 25 10
      packages/core/src/tool/grep.ts
  31. 5 1
      packages/core/src/tool/read.ts
  32. 3 1
      packages/core/src/tool/webfetch.ts
  33. 52 38
      packages/core/src/tool/websearch.ts
  34. 9 1
      packages/core/test/agent.test.ts
  35. 4 1
      packages/core/test/database-migration.test.ts
  36. 6 2
      packages/core/test/effect/keyed-mutex.test.ts
  37. 90 61
      packages/core/test/event.test.ts
  38. 51 9
      packages/core/test/file-mutation.test.ts
  39. 14 11
      packages/core/test/location-search.test.ts
  40. 22 11
      packages/core/test/patch.test.ts
  41. 7 1
      packages/core/test/permission.test.ts
  42. 3 1
      packages/core/test/session-create.test.ts
  43. 33 16
      packages/core/test/session-projector.test.ts
  44. 3 1
      packages/core/test/session-prompt.test.ts
  45. 159 142
      packages/core/test/session-runner-message.test.ts
  46. 3 1
      packages/core/test/session-runner-tool-registry.test.ts
  47. 5 1
      packages/core/test/session-runner.test.ts
  48. 106 17
      packages/core/test/session-tool-progress.test.ts
  49. 4 7
      packages/core/test/skill-discovery.test.ts
  50. 65 17
      packages/core/test/tool-apply-patch.test.ts
  51. 10 5
      packages/core/test/tool-edit.test.ts
  52. 24 3
      packages/core/test/tool-glob.test.ts
  53. 25 7
      packages/core/test/tool-grep.test.ts
  54. 30 10
      packages/core/test/tool-output-store.test.ts
  55. 6 1
      packages/core/test/tool-read.test.ts
  56. 6 1
      packages/core/test/tool-skill.test.ts
  57. 34 8
      packages/core/test/tool-websearch.test.ts
  58. 16 12
      packages/llm/example/tutorial.ts
  59. 11 4
      packages/llm/src/protocols/anthropic-messages.ts
  60. 9 3
      packages/llm/src/protocols/bedrock-converse.ts
  61. 5 2
      packages/llm/src/protocols/gemini.ts
  62. 3 1
      packages/llm/src/protocols/openai-chat.ts
  63. 6 2
      packages/llm/src/protocols/openai-responses.ts
  64. 2 1
      packages/llm/src/protocols/shared.ts
  65. 2 1
      packages/llm/src/schema/messages.ts
  66. 10 6
      packages/llm/src/tool-runtime.ts
  67. 11 2
      packages/llm/src/tool.ts
  68. 5 1
      packages/llm/test/lib/tool-runtime.ts
  69. 3 1
      packages/llm/test/llm.test.ts
  70. 17 13
      packages/llm/test/provider/anthropic-messages.test.ts
  71. 4 1
      packages/llm/test/provider/bedrock-converse.test.ts
  72. 15 3
      packages/llm/test/provider/openai-chat.test.ts
  73. 5 1
      packages/llm/test/provider/openai-responses.test.ts
  74. 42 12
      packages/llm/test/tool-runtime.test.ts
  75. 3 1
      packages/llm/test/tool.types.ts
  76. 29 8
      packages/opencode/src/cli/cmd/tui/context/sync-v2.tsx
  77. 2 1
      packages/opencode/src/cli/cmd/tui/feature-plugins/system/session-v2.tsx
  78. 20 12
      packages/opencode/src/session/llm/native-runtime.ts
  79. 26 16
      packages/opencode/src/session/processor.ts
  80. 5 1
      packages/opencode/test/cli/tui/sync-v2.test.tsx
  81. 1 1
      packages/opencode/test/server/httpapi-exercise/backend.ts
  82. 9 7
      packages/opencode/test/server/httpapi-session.test.ts
  83. 11 13
      packages/opencode/test/session/llm-native-recorded.test.ts
  84. 4 2
      packages/opencode/test/session/processor-effect.test.ts
  85. 60 60
      packages/sdk/js/src/v2/gen/types.gen.ts
  86. 155 155
      packages/sdk/openapi.json
  87. 4 1
      specs/v2/provider-model.md

+ 35 - 107
packages/core/migration/20260603001617_session_message_projection_indexes/snapshot.json

@@ -1205,13 +1205,9 @@
       "table": "session_share"
     },
     {
-      "columns": [
-        "project_id"
-      ],
+      "columns": ["project_id"],
       "tableTo": "project",
-      "columnsTo": [
-        "id"
-      ],
+      "columnsTo": ["id"],
       "onUpdate": "NO ACTION",
       "onDelete": "CASCADE",
       "nameExplicit": false,
@@ -1220,13 +1216,9 @@
       "table": "workspace"
     },
     {
-      "columns": [
-        "active_account_id"
-      ],
+      "columns": ["active_account_id"],
       "tableTo": "account",
-      "columnsTo": [
-        "id"
-      ],
+      "columnsTo": ["id"],
       "onUpdate": "NO ACTION",
       "onDelete": "SET NULL",
       "nameExplicit": false,
@@ -1235,13 +1227,9 @@
       "table": "account_state"
     },
     {
-      "columns": [
-        "aggregate_id"
-      ],
+      "columns": ["aggregate_id"],
       "tableTo": "event_sequence",
-      "columnsTo": [
-        "aggregate_id"
-      ],
+      "columnsTo": ["aggregate_id"],
       "onUpdate": "NO ACTION",
       "onDelete": "CASCADE",
       "nameExplicit": false,
@@ -1250,13 +1238,9 @@
       "table": "event"
     },
     {
-      "columns": [
-        "project_id"
-      ],
+      "columns": ["project_id"],
       "tableTo": "project",
-      "columnsTo": [
-        "id"
-      ],
+      "columnsTo": ["id"],
       "onUpdate": "NO ACTION",
       "onDelete": "CASCADE",
       "nameExplicit": false,
@@ -1265,13 +1249,9 @@
       "table": "permission"
     },
     {
-      "columns": [
-        "session_id"
-      ],
+      "columns": ["session_id"],
       "tableTo": "session",
-      "columnsTo": [
-        "id"
-      ],
+      "columnsTo": ["id"],
       "onUpdate": "NO ACTION",
       "onDelete": "CASCADE",
       "nameExplicit": false,
@@ -1280,13 +1260,9 @@
       "table": "message"
     },
     {
-      "columns": [
-        "message_id"
-      ],
+      "columns": ["message_id"],
       "tableTo": "message",
-      "columnsTo": [
-        "id"
-      ],
+      "columnsTo": ["id"],
       "onUpdate": "NO ACTION",
       "onDelete": "CASCADE",
       "nameExplicit": false,
@@ -1295,13 +1271,9 @@
       "table": "part"
     },
     {
-      "columns": [
-        "session_id"
-      ],
+      "columns": ["session_id"],
       "tableTo": "session",
-      "columnsTo": [
-        "id"
-      ],
+      "columnsTo": ["id"],
       "onUpdate": "NO ACTION",
       "onDelete": "CASCADE",
       "nameExplicit": false,
@@ -1310,13 +1282,9 @@
       "table": "session_message"
     },
     {
-      "columns": [
-        "project_id"
-      ],
+      "columns": ["project_id"],
       "tableTo": "project",
-      "columnsTo": [
-        "id"
-      ],
+      "columnsTo": ["id"],
       "onUpdate": "NO ACTION",
       "onDelete": "CASCADE",
       "nameExplicit": false,
@@ -1325,13 +1293,9 @@
       "table": "session"
     },
     {
-      "columns": [
-        "session_id"
-      ],
+      "columns": ["session_id"],
       "tableTo": "session",
-      "columnsTo": [
-        "id"
-      ],
+      "columnsTo": ["id"],
       "onUpdate": "NO ACTION",
       "onDelete": "CASCADE",
       "nameExplicit": false,
@@ -1340,13 +1304,9 @@
       "table": "todo"
     },
     {
-      "columns": [
-        "session_id"
-      ],
+      "columns": ["session_id"],
       "tableTo": "session",
-      "columnsTo": [
-        "id"
-      ],
+      "columnsTo": ["id"],
       "onUpdate": "NO ACTION",
       "onDelete": "CASCADE",
       "nameExplicit": false,
@@ -1355,137 +1315,105 @@
       "table": "session_share"
     },
     {
-      "columns": [
-        "email",
-        "url"
-      ],
+      "columns": ["email", "url"],
       "nameExplicit": false,
       "name": "control_account_pk",
       "entityType": "pks",
       "table": "control_account"
     },
     {
-      "columns": [
-        "session_id",
-        "position"
-      ],
+      "columns": ["session_id", "position"],
       "nameExplicit": false,
       "name": "todo_pk",
       "entityType": "pks",
       "table": "todo"
     },
     {
-      "columns": [
-        "id"
-      ],
+      "columns": ["id"],
       "nameExplicit": false,
       "name": "workspace_pk",
       "table": "workspace",
       "entityType": "pks"
     },
     {
-      "columns": [
-        "name"
-      ],
+      "columns": ["name"],
       "nameExplicit": false,
       "name": "data_migration_pk",
       "table": "data_migration",
       "entityType": "pks"
     },
     {
-      "columns": [
-        "id"
-      ],
+      "columns": ["id"],
       "nameExplicit": false,
       "name": "account_state_pk",
       "table": "account_state",
       "entityType": "pks"
     },
     {
-      "columns": [
-        "id"
-      ],
+      "columns": ["id"],
       "nameExplicit": false,
       "name": "account_pk",
       "table": "account",
       "entityType": "pks"
     },
     {
-      "columns": [
-        "aggregate_id"
-      ],
+      "columns": ["aggregate_id"],
       "nameExplicit": false,
       "name": "event_sequence_pk",
       "table": "event_sequence",
       "entityType": "pks"
     },
     {
-      "columns": [
-        "id"
-      ],
+      "columns": ["id"],
       "nameExplicit": false,
       "name": "event_pk",
       "table": "event",
       "entityType": "pks"
     },
     {
-      "columns": [
-        "id"
-      ],
+      "columns": ["id"],
       "nameExplicit": false,
       "name": "permission_pk",
       "table": "permission",
       "entityType": "pks"
     },
     {
-      "columns": [
-        "id"
-      ],
+      "columns": ["id"],
       "nameExplicit": false,
       "name": "project_pk",
       "table": "project",
       "entityType": "pks"
     },
     {
-      "columns": [
-        "id"
-      ],
+      "columns": ["id"],
       "nameExplicit": false,
       "name": "message_pk",
       "table": "message",
       "entityType": "pks"
     },
     {
-      "columns": [
-        "id"
-      ],
+      "columns": ["id"],
       "nameExplicit": false,
       "name": "part_pk",
       "table": "part",
       "entityType": "pks"
     },
     {
-      "columns": [
-        "id"
-      ],
+      "columns": ["id"],
       "nameExplicit": false,
       "name": "session_message_pk",
       "table": "session_message",
       "entityType": "pks"
     },
     {
-      "columns": [
-        "id"
-      ],
+      "columns": ["id"],
       "nameExplicit": false,
       "name": "session_pk",
       "table": "session",
       "entityType": "pks"
     },
     {
-      "columns": [
-        "session_id"
-      ],
+      "columns": ["session_id"],
       "nameExplicit": false,
       "name": "session_share_pk",
       "table": "session_share",

+ 43 - 130
packages/core/migration/20260603160727_jittery_ezekiel_stane/snapshot.json

@@ -1333,13 +1333,9 @@
       "table": "session_share"
     },
     {
-      "columns": [
-        "project_id"
-      ],
+      "columns": ["project_id"],
       "tableTo": "project",
-      "columnsTo": [
-        "id"
-      ],
+      "columnsTo": ["id"],
       "onUpdate": "NO ACTION",
       "onDelete": "CASCADE",
       "nameExplicit": false,
@@ -1348,13 +1344,9 @@
       "table": "workspace"
     },
     {
-      "columns": [
-        "active_account_id"
-      ],
+      "columns": ["active_account_id"],
       "tableTo": "account",
-      "columnsTo": [
-        "id"
-      ],
+      "columnsTo": ["id"],
       "onUpdate": "NO ACTION",
       "onDelete": "SET NULL",
       "nameExplicit": false,
@@ -1363,13 +1355,9 @@
       "table": "account_state"
     },
     {
-      "columns": [
-        "aggregate_id"
-      ],
+      "columns": ["aggregate_id"],
       "tableTo": "event_sequence",
-      "columnsTo": [
-        "aggregate_id"
-      ],
+      "columnsTo": ["aggregate_id"],
       "onUpdate": "NO ACTION",
       "onDelete": "CASCADE",
       "nameExplicit": false,
@@ -1378,13 +1366,9 @@
       "table": "event"
     },
     {
-      "columns": [
-        "project_id"
-      ],
+      "columns": ["project_id"],
       "tableTo": "project",
-      "columnsTo": [
-        "id"
-      ],
+      "columnsTo": ["id"],
       "onUpdate": "NO ACTION",
       "onDelete": "CASCADE",
       "nameExplicit": false,
@@ -1393,13 +1377,9 @@
       "table": "permission"
     },
     {
-      "columns": [
-        "project_id"
-      ],
+      "columns": ["project_id"],
       "tableTo": "project",
-      "columnsTo": [
-        "id"
-      ],
+      "columnsTo": ["id"],
       "onUpdate": "NO ACTION",
       "onDelete": "CASCADE",
       "nameExplicit": false,
@@ -1408,13 +1388,9 @@
       "table": "project_directory"
     },
     {
-      "columns": [
-        "session_id"
-      ],
+      "columns": ["session_id"],
       "tableTo": "session",
-      "columnsTo": [
-        "id"
-      ],
+      "columnsTo": ["id"],
       "onUpdate": "NO ACTION",
       "onDelete": "CASCADE",
       "nameExplicit": false,
@@ -1423,13 +1399,9 @@
       "table": "message"
     },
     {
-      "columns": [
-        "message_id"
-      ],
+      "columns": ["message_id"],
       "tableTo": "message",
-      "columnsTo": [
-        "id"
-      ],
+      "columnsTo": ["id"],
       "onUpdate": "NO ACTION",
       "onDelete": "CASCADE",
       "nameExplicit": false,
@@ -1438,13 +1410,9 @@
       "table": "part"
     },
     {
-      "columns": [
-        "session_id"
-      ],
+      "columns": ["session_id"],
       "tableTo": "session",
-      "columnsTo": [
-        "id"
-      ],
+      "columnsTo": ["id"],
       "onUpdate": "NO ACTION",
       "onDelete": "CASCADE",
       "nameExplicit": false,
@@ -1453,13 +1421,9 @@
       "table": "session_input"
     },
     {
-      "columns": [
-        "session_id"
-      ],
+      "columns": ["session_id"],
       "tableTo": "session",
-      "columnsTo": [
-        "id"
-      ],
+      "columnsTo": ["id"],
       "onUpdate": "NO ACTION",
       "onDelete": "CASCADE",
       "nameExplicit": false,
@@ -1468,13 +1432,9 @@
       "table": "session_message"
     },
     {
-      "columns": [
-        "project_id"
-      ],
+      "columns": ["project_id"],
       "tableTo": "project",
-      "columnsTo": [
-        "id"
-      ],
+      "columnsTo": ["id"],
       "onUpdate": "NO ACTION",
       "onDelete": "CASCADE",
       "nameExplicit": false,
@@ -1483,13 +1443,9 @@
       "table": "session"
     },
     {
-      "columns": [
-        "session_id"
-      ],
+      "columns": ["session_id"],
       "tableTo": "session",
-      "columnsTo": [
-        "id"
-      ],
+      "columnsTo": ["id"],
       "onUpdate": "NO ACTION",
       "onDelete": "CASCADE",
       "nameExplicit": false,
@@ -1498,13 +1454,9 @@
       "table": "todo"
     },
     {
-      "columns": [
-        "session_id"
-      ],
+      "columns": ["session_id"],
       "tableTo": "session",
-      "columnsTo": [
-        "id"
-      ],
+      "columnsTo": ["id"],
       "onUpdate": "NO ACTION",
       "onDelete": "CASCADE",
       "nameExplicit": false,
@@ -1513,156 +1465,119 @@
       "table": "session_share"
     },
     {
-      "columns": [
-        "email",
-        "url"
-      ],
+      "columns": ["email", "url"],
       "nameExplicit": false,
       "name": "control_account_pk",
       "entityType": "pks",
       "table": "control_account"
     },
     {
-      "columns": [
-        "project_id",
-        "directory"
-      ],
+      "columns": ["project_id", "directory"],
       "nameExplicit": false,
       "name": "project_directory_pk",
       "entityType": "pks",
       "table": "project_directory"
     },
     {
-      "columns": [
-        "session_id",
-        "position"
-      ],
+      "columns": ["session_id", "position"],
       "nameExplicit": false,
       "name": "todo_pk",
       "entityType": "pks",
       "table": "todo"
     },
     {
-      "columns": [
-        "id"
-      ],
+      "columns": ["id"],
       "nameExplicit": false,
       "name": "workspace_pk",
       "table": "workspace",
       "entityType": "pks"
     },
     {
-      "columns": [
-        "name"
-      ],
+      "columns": ["name"],
       "nameExplicit": false,
       "name": "data_migration_pk",
       "table": "data_migration",
       "entityType": "pks"
     },
     {
-      "columns": [
-        "id"
-      ],
+      "columns": ["id"],
       "nameExplicit": false,
       "name": "account_state_pk",
       "table": "account_state",
       "entityType": "pks"
     },
     {
-      "columns": [
-        "id"
-      ],
+      "columns": ["id"],
       "nameExplicit": false,
       "name": "account_pk",
       "table": "account",
       "entityType": "pks"
     },
     {
-      "columns": [
-        "aggregate_id"
-      ],
+      "columns": ["aggregate_id"],
       "nameExplicit": false,
       "name": "event_sequence_pk",
       "table": "event_sequence",
       "entityType": "pks"
     },
     {
-      "columns": [
-        "id"
-      ],
+      "columns": ["id"],
       "nameExplicit": false,
       "name": "event_pk",
       "table": "event",
       "entityType": "pks"
     },
     {
-      "columns": [
-        "id"
-      ],
+      "columns": ["id"],
       "nameExplicit": false,
       "name": "permission_pk",
       "table": "permission",
       "entityType": "pks"
     },
     {
-      "columns": [
-        "id"
-      ],
+      "columns": ["id"],
       "nameExplicit": false,
       "name": "project_pk",
       "table": "project",
       "entityType": "pks"
     },
     {
-      "columns": [
-        "id"
-      ],
+      "columns": ["id"],
       "nameExplicit": false,
       "name": "message_pk",
       "table": "message",
       "entityType": "pks"
     },
     {
-      "columns": [
-        "id"
-      ],
+      "columns": ["id"],
       "nameExplicit": false,
       "name": "part_pk",
       "table": "part",
       "entityType": "pks"
     },
     {
-      "columns": [
-        "seq"
-      ],
+      "columns": ["seq"],
       "nameExplicit": false,
       "name": "session_input_pk",
       "table": "session_input",
       "entityType": "pks"
     },
     {
-      "columns": [
-        "id"
-      ],
+      "columns": ["id"],
       "nameExplicit": false,
       "name": "session_message_pk",
       "table": "session_message",
       "entityType": "pks"
     },
     {
-      "columns": [
-        "id"
-      ],
+      "columns": ["id"],
       "nameExplicit": false,
       "name": "session_pk",
       "table": "session",
       "entityType": "pks"
     },
     {
-      "columns": [
-        "session_id"
-      ],
+      "columns": ["session_id"],
       "nameExplicit": false,
       "name": "session_share_pk",
       "table": "session_share",
@@ -1943,9 +1858,7 @@
       "table": "todo"
     },
     {
-      "columns": [
-        "id"
-      ],
+      "columns": ["id"],
       "nameExplicit": false,
       "name": "session_input_id_unique",
       "entityType": "uniques",
@@ -1953,4 +1866,4 @@
     }
   ],
   "renames": []
-}
+}

+ 3 - 1
packages/core/src/config/plugin/skill.ts

@@ -19,7 +19,9 @@ export const Plugin = PluginV2.define({
     const transform = yield* skill.transform()
     const entries = yield* config.entries()
     const items = entries.flatMap((entry) =>
-      entry.type === "document" ? (entry.info.skills ?? []) : [path.join(entry.path, "skill"), path.join(entry.path, "skills")],
+      entry.type === "document"
+        ? (entry.info.skills ?? [])
+        : [path.join(entry.path, "skill"), path.join(entry.path, "skills")],
     )
 
     yield* transform((editor) => {

+ 6 - 2
packages/core/src/database/migration/20260603001617_session_message_projection_indexes.ts

@@ -8,8 +8,12 @@ export default {
       yield* tx.run(`DROP INDEX IF EXISTS \`session_message_session_idx\`;`)
       yield* tx.run(`DROP INDEX IF EXISTS \`session_message_session_type_idx\`;`)
       yield* tx.run(`CREATE INDEX \`event_aggregate_seq_idx\` ON \`event\` (\`aggregate_id\`,\`seq\`);`)
-      yield* tx.run(`CREATE INDEX \`session_message_session_time_created_id_idx\` ON \`session_message\` (\`session_id\`,\`time_created\`,\`id\`);`)
-      yield* tx.run(`CREATE INDEX \`session_message_session_type_time_created_id_idx\` ON \`session_message\` (\`session_id\`,\`type\`,\`time_created\`,\`id\`);`)
+      yield* tx.run(
+        `CREATE INDEX \`session_message_session_time_created_id_idx\` ON \`session_message\` (\`session_id\`,\`time_created\`,\`id\`);`,
+      )
+      yield* tx.run(
+        `CREATE INDEX \`session_message_session_type_time_created_id_idx\` ON \`session_message\` (\`session_id\`,\`type\`,\`time_created\`,\`id\`);`,
+      )
     })
   },
 } satisfies DatabaseMigration.Migration

+ 11 - 4
packages/core/src/database/migration/20260603040000_session_message_projection_order.ts

@@ -6,13 +6,20 @@ export default {
   up(tx) {
     return Effect.gen(function* () {
       yield* tx.run(`ALTER TABLE \`session_message\` ADD COLUMN \`seq\` integer NOT NULL DEFAULT 0;`)
-      yield* tx.run(`UPDATE \`session_message\` SET \`seq\` = COALESCE((SELECT \`seq\` + 1 FROM \`event\` WHERE \`event\`.\`id\` = \`session_message\`.\`id\`), 0);`)
-      const unmatched = yield* tx.get<{ count: number }>(`SELECT COUNT(*) AS \`count\` FROM \`session_message\` WHERE \`seq\` = 0;`)
-      if ((unmatched?.count ?? 0) > 0) return yield* Effect.die("Cannot migrate session_message projections without matching durable events")
+      yield* tx.run(
+        `UPDATE \`session_message\` SET \`seq\` = COALESCE((SELECT \`seq\` + 1 FROM \`event\` WHERE \`event\`.\`id\` = \`session_message\`.\`id\`), 0);`,
+      )
+      const unmatched = yield* tx.get<{ count: number }>(
+        `SELECT COUNT(*) AS \`count\` FROM \`session_message\` WHERE \`seq\` = 0;`,
+      )
+      if ((unmatched?.count ?? 0) > 0)
+        return yield* Effect.die("Cannot migrate session_message projections without matching durable events")
       yield* tx.run(`UPDATE \`session_message\` SET \`seq\` = \`seq\` - 1;`)
       yield* tx.run(`DROP INDEX IF EXISTS \`session_message_session_type_time_created_id_idx\`;`)
       yield* tx.run(`CREATE INDEX \`session_message_session_seq_idx\` ON \`session_message\` (\`session_id\`,\`seq\`);`)
-      yield* tx.run(`CREATE INDEX \`session_message_session_type_seq_idx\` ON \`session_message\` (\`session_id\`,\`type\`,\`seq\`);`)
+      yield* tx.run(
+        `CREATE INDEX \`session_message_session_type_seq_idx\` ON \`session_message\` (\`session_id\`,\`type\`,\`seq\`);`,
+      )
     })
   },
 } satisfies DatabaseMigration.Migration

+ 3 - 1
packages/core/src/database/migration/20260603160727_jittery_ezekiel_stane.ts

@@ -6,7 +6,9 @@ export default {
   up(tx) {
     return Effect.gen(function* () {
       yield* tx.run(`DROP INDEX IF EXISTS \`session_input_session_pending_seq_idx\`;`)
-      yield* tx.run(`CREATE INDEX IF NOT EXISTS \`event_aggregate_type_seq_idx\` ON \`event\` (\`aggregate_id\`,\`type\`,\`seq\`);`)
+      yield* tx.run(
+        `CREATE INDEX IF NOT EXISTS \`event_aggregate_type_seq_idx\` ON \`event\` (\`aggregate_id\`,\`type\`,\`seq\`);`,
+      )
       yield* tx.run(
         `CREATE INDEX IF NOT EXISTS \`session_input_session_pending_delivery_seq_idx\` ON \`session_input\` (\`session_id\`,\`promoted_seq\`,\`delivery\`,\`seq\`);`,
       )

+ 17 - 15
packages/core/src/effect/keyed-mutex.ts

@@ -20,21 +20,23 @@ export interface KeyedMutex<in Key> {
 export const makeUnsafe = <Key>(): KeyedMutex<Key> => {
   const locks = new Map<Key, { readonly semaphore: Semaphore.Semaphore; users: number }>()
 
-  const withLock = (key: Key) => <A, E, R>(effect: Effect.Effect<A, E, R>) =>
-    Effect.suspend(() => {
-      const current = locks.get(key)
-      const entry = current ?? { semaphore: Semaphore.makeUnsafe(1), users: 0 }
-      if (!current) locks.set(key, entry)
-      entry.users++
-      return entry.semaphore.withPermit(effect).pipe(
-        Effect.ensuring(
-          Effect.sync(() => {
-            entry.users--
-            if (entry.users === 0) locks.delete(key)
-          }),
-        ),
-      )
-    })
+  const withLock =
+    (key: Key) =>
+    <A, E, R>(effect: Effect.Effect<A, E, R>) =>
+      Effect.suspend(() => {
+        const current = locks.get(key)
+        const entry = current ?? { semaphore: Semaphore.makeUnsafe(1), users: 0 }
+        if (!current) locks.set(key, entry)
+        entry.users++
+        return entry.semaphore.withPermit(effect).pipe(
+          Effect.ensuring(
+            Effect.sync(() => {
+              entry.users--
+              if (entry.users === 0) locks.delete(key)
+            }),
+          ),
+        )
+      })
 
   return { size: Effect.sync(() => locks.size), withLock }
 }

+ 389 - 346
packages/core/src/event.ts

@@ -146,7 +146,10 @@ export interface Interface {
   ) => Effect.Effect<Payload<D>>
   readonly subscribe: <D extends Definition>(definition: D) => Stream.Stream<Payload<D>>
   readonly all: () => Stream.Stream<Payload>
-  readonly aggregateEvents: (input: { readonly aggregateID: string; readonly after?: Cursor }) => Stream.Stream<CursorEvent>
+  readonly aggregateEvents: (input: {
+    readonly aggregateID: string
+    readonly after?: Cursor
+  }) => Stream.Stream<CursorEvent>
   readonly sync: (handler: Sync) => Effect.Effect<Unsubscribe>
   readonly listen: (listener: Listener) => Effect.Effect<Unsubscribe>
   readonly beforeCommit: (guard: CommitGuard) => Effect.Effect<void>
@@ -169,394 +172,434 @@ export interface LayerOptions {
   readonly beforeAggregateRead?: (aggregateID: string) => Effect.Effect<void>
 }
 
-export const layerWith = (options?: LayerOptions) => Layer.effect(
-  Service,
-  Effect.gen(function* () {
-    const all = yield* PubSub.unbounded<Payload>()
-    const synchronized = new Map<string, Set<PubSub.PubSub<void>>>()
-    const typed = new Map<string, PubSub.PubSub<Payload>>()
-    const projectors = new Map<string, AnyProjector[]>()
-    const commitGuards = new Array<CommitGuard>()
-    const listeners = new Array<Listener>()
-    const syncHandlers = new Array<Sync>()
-    const { db } = yield* Database.Service
-
-    const getOrCreate = (definition: Definition) =>
-      Effect.gen(function* () {
-        const existing = typed.get(definition.type)
-        if (existing) return existing
-        const pubsub = yield* PubSub.unbounded<Payload>()
-        typed.set(definition.type, pubsub)
-        return pubsub
-      })
+export const layerWith = (options?: LayerOptions) =>
+  Layer.effect(
+    Service,
+    Effect.gen(function* () {
+      const all = yield* PubSub.unbounded<Payload>()
+      const synchronized = new Map<string, Set<PubSub.PubSub<void>>>()
+      const typed = new Map<string, PubSub.PubSub<Payload>>()
+      const projectors = new Map<string, AnyProjector[]>()
+      const commitGuards = new Array<CommitGuard>()
+      const listeners = new Array<Listener>()
+      const syncHandlers = new Array<Sync>()
+      const { db } = yield* Database.Service
+
+      const getOrCreate = (definition: Definition) =>
+        Effect.gen(function* () {
+          const existing = typed.get(definition.type)
+          if (existing) return existing
+          const pubsub = yield* PubSub.unbounded<Payload>()
+          typed.set(definition.type, pubsub)
+          return pubsub
+        })
 
-    yield* Effect.addFinalizer(() =>
-      Effect.gen(function* () {
-        yield* PubSub.shutdown(all)
-        yield* Effect.forEach(synchronized.values(), (pubsubs) =>
-          Effect.forEach(pubsubs, PubSub.shutdown, { discard: true }),
-        { discard: true })
-        yield* Effect.forEach(typed.values(), PubSub.shutdown, { discard: true })
-      }),
-    )
+      yield* Effect.addFinalizer(() =>
+        Effect.gen(function* () {
+          yield* PubSub.shutdown(all)
+          yield* Effect.forEach(
+            synchronized.values(),
+            (pubsubs) => Effect.forEach(pubsubs, PubSub.shutdown, { discard: true }),
+            { discard: true },
+          )
+          yield* Effect.forEach(typed.values(), PubSub.shutdown, { discard: true })
+        }),
+      )
 
-    function commitSyncEvent(
-      event: Payload,
-      input?: { readonly seq: number; readonly aggregateID: string; readonly ownerID?: string; readonly strictOwner?: boolean },
-    ) {
-      return Effect.gen(function* () {
-        const definition = registry.get(event.type)
-        const sync = definition?.sync
-        if (sync) {
-          if (event.version !== sync.version) {
-            yield* Effect.die(
-              new InvalidSyncEventError({
-                type: event.type,
-                message: `Expected event version ${sync.version}, got ${event.version}`,
-              }),
-            )
-          }
-          const aggregateID = (event.data as Record<string, unknown>)[sync.aggregate]
-          if (typeof aggregateID !== "string") {
-            yield* Effect.die(
-              new InvalidSyncEventError({
-                type: event.type,
-                message: `Expected string aggregate field ${sync.aggregate}`,
-              }),
-            )
-          } else {
-            if (input && input.aggregateID !== aggregateID) {
+      function commitSyncEvent(
+        event: Payload,
+        input?: {
+          readonly seq: number
+          readonly aggregateID: string
+          readonly ownerID?: string
+          readonly strictOwner?: boolean
+        },
+      ) {
+        return Effect.gen(function* () {
+          const definition = registry.get(event.type)
+          const sync = definition?.sync
+          if (sync) {
+            if (event.version !== sync.version) {
               yield* Effect.die(
                 new InvalidSyncEventError({
                   type: event.type,
-                  message: `Aggregate mismatch: expected ${input.aggregateID}, got ${aggregateID}`,
+                  message: `Expected event version ${sync.version}, got ${event.version}`,
                 }),
               )
             }
-            const list = projectors.get(event.type) ?? []
-            return yield* Effect.uninterruptible(
-              Effect.gen(function* () {
-                const committed = yield* db
-                  .transaction(
-                    () =>
-                      Effect.gen(function* () {
-                        const row = yield* db
-                          .select({ seq: EventSequenceTable.seq, ownerID: EventSequenceTable.owner_id })
-                          .from(EventSequenceTable)
-                          .where(eq(EventSequenceTable.aggregate_id, aggregateID))
-                          .get()
-                          .pipe(Effect.orDie)
-                        const latest = row?.seq ?? -1
-                        if (input && input.seq <= latest) return
-                        if (input && row?.ownerID && row.ownerID !== input.ownerID) {
-                          if (input.strictOwner) {
+            const aggregateID = (event.data as Record<string, unknown>)[sync.aggregate]
+            if (typeof aggregateID !== "string") {
+              yield* Effect.die(
+                new InvalidSyncEventError({
+                  type: event.type,
+                  message: `Expected string aggregate field ${sync.aggregate}`,
+                }),
+              )
+            } else {
+              if (input && input.aggregateID !== aggregateID) {
+                yield* Effect.die(
+                  new InvalidSyncEventError({
+                    type: event.type,
+                    message: `Aggregate mismatch: expected ${input.aggregateID}, got ${aggregateID}`,
+                  }),
+                )
+              }
+              const list = projectors.get(event.type) ?? []
+              return yield* Effect.uninterruptible(
+                Effect.gen(function* () {
+                  const committed = yield* db
+                    .transaction(
+                      () =>
+                        Effect.gen(function* () {
+                          const row = yield* db
+                            .select({ seq: EventSequenceTable.seq, ownerID: EventSequenceTable.owner_id })
+                            .from(EventSequenceTable)
+                            .where(eq(EventSequenceTable.aggregate_id, aggregateID))
+                            .get()
+                            .pipe(Effect.orDie)
+                          const latest = row?.seq ?? -1
+                          if (input && input.seq <= latest) return
+                          if (input && row?.ownerID && row.ownerID !== input.ownerID) {
+                            if (input.strictOwner) {
+                              yield* Effect.die(
+                                new InvalidSyncEventError({
+                                  type: event.type,
+                                  message: `Replay owner mismatch for aggregate ${aggregateID}: expected ${row.ownerID}, got ${input.ownerID ?? "none"}`,
+                                }),
+                              )
+                            }
+                            return
+                          }
+                          const seq = input?.seq ?? latest + 1
+                          if (input && seq !== latest + 1) {
                             yield* Effect.die(
                               new InvalidSyncEventError({
                                 type: event.type,
-                                message: `Replay owner mismatch for aggregate ${aggregateID}: expected ${row.ownerID}, got ${input.ownerID ?? "none"}`,
+                                message: `Sequence mismatch for aggregate ${aggregateID}: expected ${latest + 1}, got ${seq}`,
                               }),
                             )
                           }
-                          return
-                        }
-                        const seq = input?.seq ?? latest + 1
-                        if (input && seq !== latest + 1) {
-                          yield* Effect.die(
-                            new InvalidSyncEventError({
-                              type: event.type,
-                              message: `Sequence mismatch for aggregate ${aggregateID}: expected ${latest + 1}, got ${seq}`,
-                            }),
-                          )
-                        }
-                        for (const guard of commitGuards) {
-                          yield* guard(event)
-                        }
-                        for (const projector of list) {
-                          yield* projector({ ...event, seq } as Payload)
-                        }
-                        const encoded = syncRegistry.get(versionedType(definition.type, sync.version))!.encode(event.data)
-                        yield* db
-                          .insert(EventSequenceTable)
-                          .values([{ aggregate_id: aggregateID, seq, owner_id: input?.ownerID }])
-                          .onConflictDoUpdate({
-                            target: EventSequenceTable.aggregate_id,
-                            set: { seq, ...(input?.ownerID && row?.ownerID == null ? { owner_id: input.ownerID } : {}) },
-                          })
-                          .run()
-                          .pipe(Effect.orDie)
-                        yield* db
-                          .insert(EventTable)
-                          .values([
-                            {
-                              id: event.id,
-                              aggregate_id: aggregateID,
-                              seq,
-                              type: versionedType(definition.type, sync.version),
-                              data: encoded as Record<string, unknown>,
-                            },
-                          ])
-                          .run()
-                          .pipe(Effect.orDie)
-                        return { aggregateID, seq }
-                      }),
-                    { behavior: "immediate" },
-                  )
-                  .pipe(Effect.orDie)
-                if (committed) {
-                  yield* Effect.forEach(
-                    synchronized.get(committed.aggregateID) ?? [],
-                    (pubsub) => PubSub.publish(pubsub, undefined),
-                    { discard: true },
-                  )
-                }
-                return committed
-              }),
-            )
+                          for (const guard of commitGuards) {
+                            yield* guard(event)
+                          }
+                          for (const projector of list) {
+                            yield* projector({ ...event, seq } as Payload)
+                          }
+                          const encoded = syncRegistry
+                            .get(versionedType(definition.type, sync.version))!
+                            .encode(event.data)
+                          yield* db
+                            .insert(EventSequenceTable)
+                            .values([{ aggregate_id: aggregateID, seq, owner_id: input?.ownerID }])
+                            .onConflictDoUpdate({
+                              target: EventSequenceTable.aggregate_id,
+                              set: {
+                                seq,
+                                ...(input?.ownerID && row?.ownerID == null ? { owner_id: input.ownerID } : {}),
+                              },
+                            })
+                            .run()
+                            .pipe(Effect.orDie)
+                          yield* db
+                            .insert(EventTable)
+                            .values([
+                              {
+                                id: event.id,
+                                aggregate_id: aggregateID,
+                                seq,
+                                type: versionedType(definition.type, sync.version),
+                                data: encoded as Record<string, unknown>,
+                              },
+                            ])
+                            .run()
+                            .pipe(Effect.orDie)
+                          return { aggregateID, seq }
+                        }),
+                      { behavior: "immediate" },
+                    )
+                    .pipe(Effect.orDie)
+                  if (committed) {
+                    yield* Effect.forEach(
+                      synchronized.get(committed.aggregateID) ?? [],
+                      (pubsub) => PubSub.publish(pubsub, undefined),
+                      { discard: true },
+                    )
+                  }
+                  return committed
+                }),
+              )
+            }
           }
-        }
-      })
-    }
-
-    function publishEvent<D extends Definition>(event: Payload<D>) {
-      return Effect.gen(function* () {
-        const durable = registry.get(event.type)?.sync !== undefined
-        if (durable) {
-          for (const sync of syncHandlers) {
-            yield* sync(event as Payload)
+        })
+      }
+
+      function publishEvent<D extends Definition>(event: Payload<D>) {
+        return Effect.gen(function* () {
+          const durable = registry.get(event.type)?.sync !== undefined
+          if (durable) {
+            for (const sync of syncHandlers) {
+              yield* sync(event as Payload)
+            }
+            const committed = yield* commitSyncEvent(event as Payload)
+            if (committed) event = { ...event, seq: committed.seq }
           }
-          const committed = yield* commitSyncEvent(event as Payload)
-          if (committed) event = { ...event, seq: committed.seq }
-        }
-        for (const listener of listeners) {
-          yield* listener(event as Payload)
-        }
-        const pubsub = typed.get(event.type)
-        if (pubsub) yield* PubSub.publish(pubsub, event as Payload)
-        yield* PubSub.publish(all, event as Payload)
-        return event
-      })
-    }
-
-    function publish<D extends Definition>(definition: D, data: Data<D>, options?: PublishOptions) {
-      return Effect.gen(function* () {
-        const serviceLocation = Option.getOrUndefined(yield* Effect.serviceOption(Location.Service))
-        const location =
-          options?.location ??
-          (serviceLocation
-            ? { directory: serviceLocation.directory, workspaceID: serviceLocation.workspaceID }
-            : undefined)
-        return yield* publishEvent({
-          id: options?.id ?? ID.create(),
-          ...(options?.metadata ? { metadata: options.metadata } : {}),
-          type: definition.type,
-          ...(definition.sync === undefined ? {} : { version: definition.sync.version }),
-          ...(location ? { location } : {}),
-          data,
-        } as Payload<D>)
-      })
-    }
+          for (const listener of listeners) {
+            yield* listener(event as Payload)
+          }
+          const pubsub = typed.get(event.type)
+          if (pubsub) yield* PubSub.publish(pubsub, event as Payload)
+          yield* PubSub.publish(all, event as Payload)
+          return event
+        })
+      }
 
-    function replay(event: SerializedEvent, options?: { readonly publish?: boolean; readonly ownerID?: string; readonly strictOwner?: boolean }) {
-      return Effect.gen(function* () {
-        const definition = syncRegistry.get(event.type)
-        if (!definition) {
-          yield* Effect.die(
-            new InvalidSyncEventError({ type: event.type, message: `Unknown sync event type ${event.type}` }),
-          )
-        } else {
-          const payload = {
-            id: event.id,
+      function publish<D extends Definition>(definition: D, data: Data<D>, options?: PublishOptions) {
+        return Effect.gen(function* () {
+          const serviceLocation = Option.getOrUndefined(yield* Effect.serviceOption(Location.Service))
+          const location =
+            options?.location ??
+            (serviceLocation
+              ? { directory: serviceLocation.directory, workspaceID: serviceLocation.workspaceID }
+              : undefined)
+          return yield* publishEvent({
+            id: options?.id ?? ID.create(),
+            ...(options?.metadata ? { metadata: options.metadata } : {}),
             type: definition.type,
-            version: definition.sync.version,
-            data: definition.decode(event.data),
-          } as Payload
-          const committed = yield* commitSyncEvent(payload, { seq: event.seq, aggregateID: event.aggregateID, ownerID: options?.ownerID, strictOwner: options?.strictOwner })
-          if (committed && options?.publish) {
-            const published = { ...payload, seq: committed.seq }
-            for (const listener of listeners) {
-              yield* listener(published)
+            ...(definition.sync === undefined ? {} : { version: definition.sync.version }),
+            ...(location ? { location } : {}),
+            data,
+          } as Payload<D>)
+        })
+      }
+
+      function replay(
+        event: SerializedEvent,
+        options?: { readonly publish?: boolean; readonly ownerID?: string; readonly strictOwner?: boolean },
+      ) {
+        return Effect.gen(function* () {
+          const definition = syncRegistry.get(event.type)
+          if (!definition) {
+            yield* Effect.die(
+              new InvalidSyncEventError({ type: event.type, message: `Unknown sync event type ${event.type}` }),
+            )
+          } else {
+            const payload = {
+              id: event.id,
+              type: definition.type,
+              version: definition.sync.version,
+              data: definition.decode(event.data),
+            } as Payload
+            const committed = yield* commitSyncEvent(payload, {
+              seq: event.seq,
+              aggregateID: event.aggregateID,
+              ownerID: options?.ownerID,
+              strictOwner: options?.strictOwner,
+            })
+            if (committed && options?.publish) {
+              const published = { ...payload, seq: committed.seq }
+              for (const listener of listeners) {
+                yield* listener(published)
+              }
+              const pubsub = typed.get(payload.type)
+              if (pubsub) yield* PubSub.publish(pubsub, published)
+              yield* PubSub.publish(all, published)
             }
-            const pubsub = typed.get(payload.type)
-            if (pubsub) yield* PubSub.publish(pubsub, published)
-            yield* PubSub.publish(all, published)
           }
-        }
-      })
-    }
-
-    function replayAll(events: SerializedEvent[], options?: { readonly publish?: boolean; readonly ownerID?: string; readonly strictOwner?: boolean }) {
-      return Effect.gen(function* () {
-        const source = events[0]?.aggregateID
-        if (!source) return undefined
-        if (events.some((event) => event.aggregateID !== source)) {
-          yield* Effect.die(
-            new InvalidSyncEventError({
-              type: events[0]?.type ?? "unknown",
-              message: "Replay events must belong to the same aggregate",
-            }),
-          )
-        }
-        const start = events[0]?.seq ?? 0
-        for (const [index, event] of events.entries()) {
-          const seq = start + index
-          if (event.seq !== seq) {
+        })
+      }
+
+      function replayAll(
+        events: SerializedEvent[],
+        options?: { readonly publish?: boolean; readonly ownerID?: string; readonly strictOwner?: boolean },
+      ) {
+        return Effect.gen(function* () {
+          const source = events[0]?.aggregateID
+          if (!source) return undefined
+          if (events.some((event) => event.aggregateID !== source)) {
             yield* Effect.die(
               new InvalidSyncEventError({
-                type: event.type,
-                message: `Replay sequence mismatch at index ${index}: expected ${seq}, got ${event.seq}`,
+                type: events[0]?.type ?? "unknown",
+                message: "Replay events must belong to the same aggregate",
               }),
             )
           }
-        }
-        for (const event of events) {
-          yield* replay(event, options)
-        }
-        return source
-      })
-    }
+          const start = events[0]?.seq ?? 0
+          for (const [index, event] of events.entries()) {
+            const seq = start + index
+            if (event.seq !== seq) {
+              yield* Effect.die(
+                new InvalidSyncEventError({
+                  type: event.type,
+                  message: `Replay sequence mismatch at index ${index}: expected ${seq}, got ${event.seq}`,
+                }),
+              )
+            }
+          }
+          for (const event of events) {
+            yield* replay(event, options)
+          }
+          return source
+        })
+      }
 
-    function remove(aggregateID: string) {
-      return db
-        .transaction(() =>
-          Effect.gen(function* () {
-            yield* db.delete(EventSequenceTable).where(eq(EventSequenceTable.aggregate_id, aggregateID)).run()
-            yield* db.delete(EventTable).where(eq(EventTable.aggregate_id, aggregateID)).run()
-          }),
+      function remove(aggregateID: string) {
+        return db
+          .transaction(() =>
+            Effect.gen(function* () {
+              yield* db.delete(EventSequenceTable).where(eq(EventSequenceTable.aggregate_id, aggregateID)).run()
+              yield* db.delete(EventTable).where(eq(EventTable.aggregate_id, aggregateID)).run()
+            }),
+          )
+          .pipe(Effect.orDie)
+      }
+
+      function claim(aggregateID: string, ownerID: string) {
+        return db
+          .update(EventSequenceTable)
+          .set({ owner_id: ownerID })
+          .where(eq(EventSequenceTable.aggregate_id, aggregateID))
+          .run()
+          .pipe(Effect.orDie)
+      }
+
+      const subscribe = <D extends Definition>(definition: D): Stream.Stream<Payload<D>> =>
+        Stream.unwrap(getOrCreate(definition).pipe(Effect.map((pubsub) => Stream.fromPubSub(pubsub)))).pipe(
+          Stream.map((event) => event as Payload<D>),
         )
-        .pipe(Effect.orDie)
-    }
-
-    function claim(aggregateID: string, ownerID: string) {
-      return db
-        .update(EventSequenceTable)
-        .set({ owner_id: ownerID })
-        .where(eq(EventSequenceTable.aggregate_id, aggregateID))
-        .run()
-        .pipe(Effect.orDie)
-    }
-
-    const subscribe = <D extends Definition>(definition: D): Stream.Stream<Payload<D>> =>
-      Stream.unwrap(getOrCreate(definition).pipe(Effect.map((pubsub) => Stream.fromPubSub(pubsub)))).pipe(
-        Stream.map((event) => event as Payload<D>),
-      )
 
-    const streamAll = (): Stream.Stream<Payload> => Stream.fromPubSub(all)
+      const streamAll = (): Stream.Stream<Payload> => Stream.fromPubSub(all)
 
-    const decodeSerializedEvent = (event: SerializedEvent): CursorEvent => {
-      const definition = syncRegistry.get(event.type)
-      if (!definition) {
-        throw new InvalidSyncEventError({ type: event.type, message: `Unknown sync event type ${event.type}` })
-      }
-      return {
-        cursor: Cursor.make(event.seq),
-        event: {
-          id: event.id,
-          type: definition.type,
-          version: definition.sync.version,
-          seq: event.seq,
-          data: definition.decode(event.data),
-        },
+      const decodeSerializedEvent = (event: SerializedEvent): CursorEvent => {
+        const definition = syncRegistry.get(event.type)
+        if (!definition) {
+          throw new InvalidSyncEventError({ type: event.type, message: `Unknown sync event type ${event.type}` })
+        }
+        return {
+          cursor: Cursor.make(event.seq),
+          event: {
+            id: event.id,
+            type: definition.type,
+            version: definition.sync.version,
+            seq: event.seq,
+            data: definition.decode(event.data),
+          },
+        }
       }
-    }
-
-    const readAfter = (aggregateID: string, after: number) =>
-      (options?.beforeAggregateRead?.(aggregateID) ?? Effect.void).pipe(
-        Effect.andThen(
-          db
-            .select()
-            .from(EventTable)
-            .where(and(eq(EventTable.aggregate_id, aggregateID), gt(EventTable.seq, after)))
-            .orderBy(asc(EventTable.seq))
-            .all(),
-        ),
-        Effect.orDie,
-        Effect.map((rows) =>
-          rows.map((event) =>
-            decodeSerializedEvent({
-              id: event.id,
-              aggregateID: event.aggregate_id,
-              seq: event.seq,
-              type: event.type,
-              data: event.data,
-            }),
-          ),
-        ),
-      )
 
-    const subscribeSynchronized = (aggregateID: string) =>
-      Effect.gen(function* () {
-        const pubsub = yield* PubSub.sliding<void>(1)
-        const subscription = yield* PubSub.subscribe(pubsub)
-        yield* Effect.acquireRelease(
-          Effect.sync(() => {
-            const pubsubs = synchronized.get(aggregateID) ?? new Set()
-            pubsubs.add(pubsub)
-            synchronized.set(aggregateID, pubsubs)
-          }),
-          () =>
-            Effect.sync(() => {
-              const pubsubs = synchronized.get(aggregateID)
-              pubsubs?.delete(pubsub)
-              if (pubsubs?.size === 0) synchronized.delete(aggregateID)
-            }).pipe(Effect.andThen(PubSub.shutdown(pubsub))),
+      const readAfter = (aggregateID: string, after: number) =>
+        (options?.beforeAggregateRead?.(aggregateID) ?? Effect.void).pipe(
+          Effect.andThen(
+            db
+              .select()
+              .from(EventTable)
+              .where(and(eq(EventTable.aggregate_id, aggregateID), gt(EventTable.seq, after)))
+              .orderBy(asc(EventTable.seq))
+              .all(),
+          ),
+          Effect.orDie,
+          Effect.map((rows) =>
+            rows.map((event) =>
+              decodeSerializedEvent({
+                id: event.id,
+                aggregateID: event.aggregate_id,
+                seq: event.seq,
+                type: event.type,
+                data: event.data,
+              }),
+            ),
+          ),
         )
-        return subscription
-      })
 
-    const streamEvents = (input: { readonly aggregateID: string; readonly after?: Cursor }): Stream.Stream<CursorEvent> =>
-      Stream.unwrap(
+      const subscribeSynchronized = (aggregateID: string) =>
         Effect.gen(function* () {
-          const synchronized = yield* subscribeSynchronized(input.aggregateID)
-          let cursor = input.after ?? -1
-          const read = Effect.suspend(() => readAfter(input.aggregateID, cursor)).pipe(
-            Effect.tap((events) =>
+          const pubsub = yield* PubSub.sliding<void>(1)
+          const subscription = yield* PubSub.subscribe(pubsub)
+          yield* Effect.acquireRelease(
+            Effect.sync(() => {
+              const pubsubs = synchronized.get(aggregateID) ?? new Set()
+              pubsubs.add(pubsub)
+              synchronized.set(aggregateID, pubsubs)
+            }),
+            () =>
               Effect.sync(() => {
-                cursor = events.at(-1)?.cursor ?? cursor
-              }),
-            ),
+                const pubsubs = synchronized.get(aggregateID)
+                pubsubs?.delete(pubsub)
+                if (pubsubs?.size === 0) synchronized.delete(aggregateID)
+              }).pipe(Effect.andThen(PubSub.shutdown(pubsub))),
           )
-          const historical = yield* read
-          const live = Stream.fromSubscription(synchronized).pipe(
-            Stream.mapEffect(() => read),
-            Stream.flattenIterable,
-          )
-          return Stream.concat(Stream.fromIterable(historical), live)
-        }),
-      )
+          return subscription
+        })
+
+      const streamEvents = (input: {
+        readonly aggregateID: string
+        readonly after?: Cursor
+      }): Stream.Stream<CursorEvent> =>
+        Stream.unwrap(
+          Effect.gen(function* () {
+            const synchronized = yield* subscribeSynchronized(input.aggregateID)
+            let cursor = input.after ?? -1
+            const read = Effect.suspend(() => readAfter(input.aggregateID, cursor)).pipe(
+              Effect.tap((events) =>
+                Effect.sync(() => {
+                  cursor = events.at(-1)?.cursor ?? cursor
+                }),
+              ),
+            )
+            const historical = yield* read
+            const live = Stream.fromSubscription(synchronized).pipe(
+              Stream.mapEffect(() => read),
+              Stream.flattenIterable,
+            )
+            return Stream.concat(Stream.fromIterable(historical), live)
+          }),
+        )
 
-    const listen = (listener: Listener): Effect.Effect<Unsubscribe> =>
-      Effect.sync(() => {
-        listeners.push(listener)
-        return Effect.sync(() => {
-          const index = listeners.indexOf(listener)
-          if (index >= 0) listeners.splice(index, 1)
+      const listen = (listener: Listener): Effect.Effect<Unsubscribe> =>
+        Effect.sync(() => {
+          listeners.push(listener)
+          return Effect.sync(() => {
+            const index = listeners.indexOf(listener)
+            if (index >= 0) listeners.splice(index, 1)
+          })
         })
-      })
 
-    const sync = (handler: Sync): Effect.Effect<Unsubscribe> =>
-      Effect.sync(() => {
-        syncHandlers.push(handler)
-        return Effect.sync(() => {
-          const index = syncHandlers.indexOf(handler)
-          if (index >= 0) syncHandlers.splice(index, 1)
+      const sync = (handler: Sync): Effect.Effect<Unsubscribe> =>
+        Effect.sync(() => {
+          syncHandlers.push(handler)
+          return Effect.sync(() => {
+            const index = syncHandlers.indexOf(handler)
+            if (index >= 0) syncHandlers.splice(index, 1)
+          })
         })
-      })
 
-    const beforeCommit = (guard: CommitGuard): Effect.Effect<void> =>
-      Effect.sync(() => {
-        commitGuards.push(guard)
-      })
+      const beforeCommit = (guard: CommitGuard): Effect.Effect<void> =>
+        Effect.sync(() => {
+          commitGuards.push(guard)
+        })
 
-    const project = <D extends Definition>(definition: D, projector: Projector<D>): Effect.Effect<void> =>
-      Effect.sync(() => {
-        const list = projectors.get(definition.type) ?? []
-        list.push((event) => projector(event as Payload<D>))
-        projectors.set(definition.type, list)
-      })
+      const project = <D extends Definition>(definition: D, projector: Projector<D>): Effect.Effect<void> =>
+        Effect.sync(() => {
+          const list = projectors.get(definition.type) ?? []
+          list.push((event) => projector(event as Payload<D>))
+          projectors.set(definition.type, list)
+        })
 
-    return Service.of({ publish, subscribe, all: streamAll, aggregateEvents: streamEvents, sync, listen, beforeCommit, project, replay, replayAll, remove, claim })
-  }),
-)
+      return Service.of({
+        publish,
+        subscribe,
+        all: streamAll,
+        aggregateEvents: streamEvents,
+        sync,
+        listen,
+        beforeCommit,
+        project,
+        replay,
+        replayAll,
+        remove,
+        claim,
+      })
+    }),
+  )
 
 export const layer = layerWith()
 

+ 19 - 9
packages/core/src/file-mutation.ts

@@ -52,17 +52,23 @@ export interface RemoveResult {
 
 export interface Interface {
   /** Create only while the planned target remains absent. */
-  readonly create: (input: WriteInput) => Effect.Effect<WriteResult, TargetExistsError | LocationMutation.RevalidationError | FSUtil.Error>
+  readonly create: (
+    input: WriteInput,
+  ) => Effect.Effect<WriteResult, TargetExistsError | LocationMutation.RevalidationError | FSUtil.Error>
   /** Write after immediately revalidating the planned target. */
   readonly write: (input: WriteInput) => Effect.Effect<WriteResult, LocationMutation.RevalidationError | FSUtil.Error>
   /** Write text while retaining an existing UTF-8 BOM and emitting at most one BOM. */
-  readonly writeTextPreservingBom: (input: TextWriteInput) => Effect.Effect<WriteResult, LocationMutation.RevalidationError | FSUtil.Error>
+  readonly writeTextPreservingBom: (
+    input: TextWriteInput,
+  ) => Effect.Effect<WriteResult, LocationMutation.RevalidationError | FSUtil.Error>
   /** Commit only if an existing target still has the expected bytes. */
   readonly writeIfUnchanged: (
     input: ConditionalWriteInput,
   ) => Effect.Effect<WriteResult, StaleContentError | LocationMutation.RevalidationError | FSUtil.Error>
   /** Remove after immediately revalidating the planned target. */
-  readonly remove: (input: RemoveInput) => Effect.Effect<RemoveResult, LocationMutation.RevalidationError | FSUtil.Error>
+  readonly remove: (
+    input: RemoveInput,
+  ) => Effect.Effect<RemoveResult, LocationMutation.RevalidationError | FSUtil.Error>
 }
 
 export class Service extends Context.Service<Service, Interface>()("@opencode/v2/FileMutation") {}
@@ -92,12 +98,15 @@ export const layer = Layer.effect(
     const fs = yield* FSUtil.Service
     const mutation = yield* LocationMutation.Service
     const locks = KeyedMutex.makeUnsafe<string>()
-    const withTargetLock = (target: string) => <A, E, R>(effect: Effect.Effect<A, E, R>) =>
-      locks.withLock(target)(Effect.uninterruptible(effect))
+    const withTargetLock =
+      (target: string) =>
+      <A, E, R>(effect: Effect.Effect<A, E, R>) =>
+        locks.withLock(target)(Effect.uninterruptible(effect))
 
-    const withValidatedTarget = (plan: LocationMutation.Plan) => <A, E, R>(
-      commit: (target: LocationMutation.Target) => Effect.Effect<A, E, R>,
-    ) => withTargetLock(plan.target.canonical)(mutation.revalidate(plan).pipe(Effect.flatMap(commit)))
+    const withValidatedTarget =
+      (plan: LocationMutation.Plan) =>
+      <A, E, R>(commit: (target: LocationMutation.Target) => Effect.Effect<A, E, R>) =>
+        withTargetLock(plan.target.canonical)(mutation.revalidate(plan).pipe(Effect.flatMap(commit)))
 
     const writeResult = (target: LocationMutation.Target, existed = target.exists): WriteResult => ({
       operation: "write",
@@ -138,7 +147,8 @@ export const layer = Layer.effect(
         Effect.gen(function* () {
           if (target.exists) return yield* new TargetExistsError({ path: target.canonical })
           yield* fs.ensureDir(dirname(target.canonical))
-          if (typeof input.content === "string") yield* fs.writeFileString(target.canonical, input.content, { flag: "wx" })
+          if (typeof input.content === "string")
+            yield* fs.writeFileString(target.canonical, input.content, { flag: "wx" })
           else yield* fs.writeFile(target.canonical, input.content, { flag: "wx" })
           return writeResult(target, false)
         }),

+ 5 - 1
packages/core/src/filesystem.ts

@@ -447,7 +447,11 @@ export const layer = Layer.effect(
       const canonical = yield* fs.realPath(target.absolute).pipe(Effect.orDie)
       if (canonical !== target.real) return yield* Effect.die(new Error("Search root changed after approval"))
       const info = yield* fs.stat(canonical).pipe(Effect.orDie)
-      if (info.type !== (target.type === "file" ? "File" : "Directory") || info.dev !== target.dev || Option.getOrUndefined(info.ino) !== target.ino)
+      if (
+        info.type !== (target.type === "file" ? "File" : "Directory") ||
+        info.dev !== target.dev ||
+        Option.getOrUndefined(info.ino) !== target.ino
+      )
         return yield* Effect.die(new Error("Search root identity changed after approval"))
       return target
     })

+ 7 - 1
packages/core/src/location-mutation.ts

@@ -241,7 +241,13 @@ export const layer = Layer.effect(
       const directory =
         boundary.exists && boundary.type === "Directory" ? boundary.canonical : boundary.authority.canonical
       const resource = slash(path.join(directory, "*"))
-      return { action: "external_directory" as const, directory, resource, save: resource, authority: boundary.authority }
+      return {
+        action: "external_directory" as const,
+        directory,
+        resource,
+        save: resource,
+        authority: boundary.authority,
+      }
     })
 
     const resolve = Effect.fn("LocationMutation.resolve")(function* (input: ResolveInput) {

+ 4 - 1
packages/core/src/location-search.ts

@@ -83,7 +83,10 @@ export class GrepResult extends Schema.Class<GrepResult>("LocationSearch.GrepRes
 
 export interface Interface {
   readonly files: (input: FilesInput, root?: FileSystem.RootTarget) => Effect.Effect<FilesResult, Ripgrep.Error>
-  readonly grep: (input: GrepInput, root?: FileSystem.RootTarget) => Effect.Effect<GrepResult, Ripgrep.Error | Ripgrep.InvalidPatternError>
+  readonly grep: (
+    input: GrepInput,
+    root?: FileSystem.RootTarget,
+  ) => Effect.Effect<GrepResult, Ripgrep.Error | Ripgrep.InvalidPatternError>
 }
 
 export class Service extends Context.Service<Service, Interface>()("@opencode/v2/LocationSearch") {}

+ 5 - 5
packages/core/src/opencode.ts

@@ -30,10 +30,10 @@ const DefaultSessions = SessionV2.layer.pipe(
 
 // TODO: Accept explicit storage so tests and embeddings can select disposable or application-owned persistence.
 export const layer = Layer.effect(
-    Service,
-    Effect.gen(function* () {
-      return Service.of({ sessions: yield* SessionV2.Service })
-    }),
-  ).pipe(Layer.provide(DefaultSessions))
+  Service,
+  Effect.gen(function* () {
+    return Service.of({ sessions: yield* SessionV2.Service })
+  }),
+).pipe(Layer.provide(DefaultSessions))
 
 // TODO: Add OpenCode.create(...) as the Promise facade over the same embedded API semantics.

+ 23 - 5
packages/core/src/patch.ts

@@ -3,7 +3,12 @@ export * as Patch from "./patch"
 export type Hunk =
   | { readonly type: "add"; readonly path: string; readonly contents: string }
   | { readonly type: "delete"; readonly path: string }
-  | { readonly type: "update"; readonly path: string; readonly movePath?: string; readonly chunks: ReadonlyArray<UpdateFileChunk> }
+  | {
+      readonly type: "update"
+      readonly path: string
+      readonly movePath?: string
+      readonly chunks: ReadonlyArray<UpdateFileChunk>
+    }
 
 export interface UpdateFileChunk {
   readonly oldLines: ReadonlyArray<string>
@@ -166,7 +171,12 @@ function seek(lines: ReadonlyArray<string>, pattern: ReadonlyArray<string>, star
   return -1
 }
 
-function matches(lines: ReadonlyArray<string>, pattern: ReadonlyArray<string>, offset: number, compare: (left: string, right: string) => boolean) {
+function matches(
+  lines: ReadonlyArray<string>,
+  pattern: ReadonlyArray<string>,
+  offset: number,
+  compare: (left: string, right: string) => boolean,
+) {
   return pattern.every((line, index) => compare(lines[offset + index]!, line))
 }
 
@@ -174,6 +184,14 @@ const exact = (left: string, right: string) => left === right
 const rstrip = (left: string, right: string) => left.trimEnd() === right.trimEnd()
 const trim = (left: string, right: string) => left.trim() === right.trim()
 const normalized = (left: string, right: string) => normalize(left.trim()) === normalize(right.trim())
-const normalize = (value: string) => value.replace(/[‘’‚‛]/g, "'").replace(/[“”„‟]/g, '"').replace(/[‐‑‒–—―]/g, "-").replace(/…/g, "...").replace(/ /g, " ")
-const splitBom = (text: string) => text.startsWith("\uFEFF") ? { bom: true, text: text.slice(1) } : { bom: false, text }
-const stripHeredoc = (input: string) => input.match(/^(?:cat\s+)?<<['"]?(\w+)['"]?\s*\n([\s\S]*?)\n\1\s*$/)?.[2] ?? input
+const normalize = (value: string) =>
+  value
+    .replace(/[‘’‚‛]/g, "'")
+    .replace(/[“”„‟]/g, '"')
+    .replace(/[‐‑‒–—―]/g, "-")
+    .replace(/…/g, "...")
+    .replace(/ /g, " ")
+const splitBom = (text: string) =>
+  text.startsWith("\uFEFF") ? { bom: true, text: text.slice(1) } : { bom: false, text }
+const stripHeredoc = (input: string) =>
+  input.match(/^(?:cat\s+)?<<['"]?(\w+)['"]?\s*\n([\s\S]*?)\n\1\s*$/)?.[2] ?? input

+ 67 - 63
packages/core/src/permission.ts

@@ -200,9 +200,9 @@ export const layer = Layer.effect(
           const item = { request, deferred }
           if (pending.has(request.id)) return yield* EffectRuntime.die(`Duplicate pending permission ID: ${request.id}`)
           pending.set(request.id, item)
-          yield* events.publish(Event.Asked, request).pipe(
-            EffectRuntime.onError(() => EffectRuntime.sync(() => pending.delete(request.id))),
-          )
+          yield* events
+            .publish(Event.Asked, request)
+            .pipe(EffectRuntime.onError(() => EffectRuntime.sync(() => pending.delete(request.id))))
           return item
         }),
       )
@@ -236,69 +236,73 @@ export const layer = Layer.effect(
       ),
     )
 
-    const reply = EffectRuntime.fn("PermissionV2.reply")((input: ReplyInput) => EffectRuntime.uninterruptible(EffectRuntime.gen(function* () {
-      const existing = pending.get(input.requestID)
-      if (!existing) return yield* new NotFoundError({ requestID: input.requestID })
-      yield* events.publish(Event.Replied, {
-        sessionID: existing.request.sessionID,
-        requestID: existing.request.id,
-        reply: input.reply,
-      })
-
-      if (input.reply === "reject") {
-        yield* Deferred.fail(
-          existing.deferred,
-          input.message ? new CorrectedError({ feedback: input.message }) : new RejectedError(),
-        )
-        pending.delete(input.requestID)
-        for (const [id, item] of pending) {
-          if (item.request.sessionID !== existing.request.sessionID) continue
+    const reply = EffectRuntime.fn("PermissionV2.reply")((input: ReplyInput) =>
+      EffectRuntime.uninterruptible(
+        EffectRuntime.gen(function* () {
+          const existing = pending.get(input.requestID)
+          if (!existing) return yield* new NotFoundError({ requestID: input.requestID })
           yield* events.publish(Event.Replied, {
-            sessionID: item.request.sessionID,
-            requestID: item.request.id,
-            reply: "reject",
+            sessionID: existing.request.sessionID,
+            requestID: existing.request.id,
+            reply: input.reply,
           })
-          yield* Deferred.fail(item.deferred, new RejectedError())
-          pending.delete(id)
-        }
-        return
-      }
 
-      if (input.reply === "always" && existing.request.save?.length) {
-        yield* saved.add({
-          projectID: location.project.id,
-          action: existing.request.action,
-          resources: existing.request.save,
-        })
-      }
-      yield* Deferred.succeed(existing.deferred, undefined)
-      pending.delete(input.requestID)
-      if (input.reply !== "always" || !existing.request.save?.length) return
-
-      const rememberedRules = yield* savedRules()
-      for (const [id, item] of pending) {
-        const input = { ...item.request }
-        const rules = yield* configured(item.request.sessionID).pipe(
-          EffectRuntime.catchTag("Session.NotFoundError", () => EffectRuntime.succeed(undefined)),
-        )
-        if (!rules) continue
-        if (denied(input, rules)) continue
-        const effective = [...rules, ...rememberedRules]
-        if (
-          !item.request.resources.every(
-            (resource) => evaluate(item.request.action, resource, effective).effect === "allow",
-          )
-        )
-          continue
-        yield* events.publish(Event.Replied, {
-          sessionID: item.request.sessionID,
-          requestID: item.request.id,
-          reply: "always",
-        })
-        yield* Deferred.succeed(item.deferred, undefined)
-        pending.delete(id)
-      }
-    })))
+          if (input.reply === "reject") {
+            yield* Deferred.fail(
+              existing.deferred,
+              input.message ? new CorrectedError({ feedback: input.message }) : new RejectedError(),
+            )
+            pending.delete(input.requestID)
+            for (const [id, item] of pending) {
+              if (item.request.sessionID !== existing.request.sessionID) continue
+              yield* events.publish(Event.Replied, {
+                sessionID: item.request.sessionID,
+                requestID: item.request.id,
+                reply: "reject",
+              })
+              yield* Deferred.fail(item.deferred, new RejectedError())
+              pending.delete(id)
+            }
+            return
+          }
+
+          if (input.reply === "always" && existing.request.save?.length) {
+            yield* saved.add({
+              projectID: location.project.id,
+              action: existing.request.action,
+              resources: existing.request.save,
+            })
+          }
+          yield* Deferred.succeed(existing.deferred, undefined)
+          pending.delete(input.requestID)
+          if (input.reply !== "always" || !existing.request.save?.length) return
+
+          const rememberedRules = yield* savedRules()
+          for (const [id, item] of pending) {
+            const input = { ...item.request }
+            const rules = yield* configured(item.request.sessionID).pipe(
+              EffectRuntime.catchTag("Session.NotFoundError", () => EffectRuntime.succeed(undefined)),
+            )
+            if (!rules) continue
+            if (denied(input, rules)) continue
+            const effective = [...rules, ...rememberedRules]
+            if (
+              !item.request.resources.every(
+                (resource) => evaluate(item.request.action, resource, effective).effect === "allow",
+              )
+            )
+              continue
+            yield* events.publish(Event.Replied, {
+              sessionID: item.request.sessionID,
+              requestID: item.request.id,
+              reply: "always",
+            })
+            yield* Deferred.succeed(item.deferred, undefined)
+            pending.delete(id)
+          }
+        }),
+      ),
+    )
 
     const list = EffectRuntime.fn("PermissionV2.list")(function* () {
       return Array.from(pending.values(), (item) => item.request)

+ 19 - 9
packages/core/src/ripgrep.ts

@@ -34,7 +34,7 @@ const RawMatch = Schema.Struct({
   }),
 })
 
-export type Match = typeof RawMatch.Type["data"]
+export type Match = (typeof RawMatch.Type)["data"]
 
 export class Error extends Schema.TaggedErrorClass<Error>()("Ripgrep.Error", {
   message: Schema.String,
@@ -77,7 +77,8 @@ export class Service extends Context.Service<Service, Interface>()("@opencode/v2
 
 const failure = (message: string, cause?: unknown) => new Error({ message, cause })
 
-const isInvalidPattern = (stderr: string) => stderr.includes("regex parse error") || stderr.includes("error parsing regex")
+const isInvalidPattern = (stderr: string) =>
+  stderr.includes("regex parse error") || stderr.includes("error parsing regex")
 
 export const layer = Layer.effect(
   Service,
@@ -126,7 +127,13 @@ export const layer = Layer.effect(
         }),
       )
       const abortable = input.signal ? program.pipe(Effect.raceFirst(waitForAbort(input.signal))) : program
-      return abortable.pipe(Effect.mapError((cause) => cause instanceof Error || cause instanceof InvalidPatternError ? cause : failure("ripgrep execution failed", cause)))
+      return abortable.pipe(
+        Effect.mapError((cause) =>
+          cause instanceof Error || cause instanceof InvalidPatternError
+            ? cause
+            : failure("ripgrep execution failed", cause),
+        ),
+      )
     }
 
     return Service.of({
@@ -143,9 +150,7 @@ export const layer = Layer.effect(
             ".",
           ],
           parse: (line) => Effect.succeed(line.replace(/^\.\//, "")),
-        }).pipe(
-          Effect.catchTag("Ripgrep.InvalidPatternError", (cause) => Effect.fail(failure(cause.message, cause))),
-        ),
+        }).pipe(Effect.catchTag("Ripgrep.InvalidPatternError", (cause) => Effect.fail(failure(cause.message, cause)))),
       grep: (input) =>
         run<Match>({
           ...input,
@@ -167,11 +172,16 @@ export const layer = Layer.effect(
               : Effect.try({
                   try: () => JSON.parse(line) as unknown,
                   catch: (cause) => failure("Invalid ripgrep JSON output", cause),
-                })).pipe(
+                })
+            ).pipe(
               Effect.flatMap((json) => {
-                if (!json || typeof json !== "object" || !("type" in json) || json.type !== "match") return Effect.succeed(undefined)
+                if (!json || typeof json !== "object" || !("type" in json) || json.type !== "match")
+                  return Effect.succeed(undefined)
                 return Schema.decodeUnknownEffect(RawMatch)(json).pipe(
-                  Effect.map((match) => ({ ...match.data, submatches: match.data.submatches.slice(0, MAX_SUBMATCHES) })),
+                  Effect.map((match) => ({
+                    ...match.data,
+                    submatches: match.data.submatches.slice(0, MAX_SUBMATCHES),
+                  })),
                   Effect.mapError((cause) => failure("Invalid ripgrep match output", cause)),
                 )
               }),

+ 2 - 1
packages/core/src/schema.ts

@@ -6,7 +6,8 @@ export type ExternalID = {
   readonly key: string
 }
 
-export const externalID = (prefix: string, input: ExternalID) => `${prefix}_${Hash.sha256(JSON.stringify([input.namespace, input.key]))}`
+export const externalID = (prefix: string, input: ExternalID) =>
+  `${prefix}_${Hash.sha256(JSON.stringify([input.namespace, input.key]))}`
 
 /**
  * Integer greater than zero.

+ 11 - 3
packages/core/src/session.ts

@@ -132,8 +132,14 @@ export interface Interface {
     sessionID: SessionSchema.ID
     after?: EventV2.Cursor
   }) => Stream.Stream<EventV2.CursorEvent<SessionEvent.DurableEvent>, NotFoundError>
-  readonly switchAgent: (input: { sessionID: SessionSchema.ID; agent: string }) => Effect.Effect<void, OperationUnavailableError>
-  readonly switchModel: (input: { sessionID: SessionSchema.ID; model: ModelV2.Ref }) => Effect.Effect<void, OperationUnavailableError>
+  readonly switchAgent: (input: {
+    sessionID: SessionSchema.ID
+    agent: string
+  }) => Effect.Effect<void, OperationUnavailableError>
+  readonly switchModel: (input: {
+    sessionID: SessionSchema.ID
+    model: ModelV2.Ref
+  }) => Effect.Effect<void, OperationUnavailableError>
   readonly prompt: (input: {
     id?: SessionMessage.ID
     sessionID: SessionSchema.ID
@@ -318,7 +324,9 @@ export const layer = Layer.effect(
           ? yield* db
               .select({ seq: SessionMessageTable.seq })
               .from(SessionMessageTable)
-              .where(and(eq(SessionMessageTable.session_id, input.sessionID), eq(SessionMessageTable.id, input.cursor.id)))
+              .where(
+                and(eq(SessionMessageTable.session_id, input.sessionID), eq(SessionMessageTable.id, input.cursor.id)),
+              )
               .get()
               .pipe(Effect.orDie)
           : undefined

+ 2 - 1
packages/core/src/session/input.ts

@@ -126,7 +126,8 @@ export const equivalent = (
 ) => input.delivery === expected.delivery && matchesPrompt(input, expected)
 
 const matchesPrompt = (input: Admitted, expected: { readonly sessionID: SessionSchema.ID; readonly prompt: Prompt }) =>
-  input.sessionID === expected.sessionID && JSON.stringify(encodePrompt(input.prompt)) === JSON.stringify(encodePrompt(expected.prompt))
+  input.sessionID === expected.sessionID &&
+  JSON.stringify(encodePrompt(input.prompt)) === JSON.stringify(encodePrompt(expected.prompt))
 
 export const guardReservedID = Effect.fn("SessionInput.guardReservedID")(function* (
   db: DatabaseService,

+ 4 - 2
packages/core/src/session/projector.ts

@@ -318,14 +318,16 @@ export const layer = Layer.effectDiscard(
       }),
     )
     yield* events.project(SessionEvent.AgentSwitched, (event) =>
-      db.update(SessionTable)
+      db
+        .update(SessionTable)
         .set({ agent: event.data.agent, time_updated: DateTime.toEpochMillis(event.data.timestamp) })
         .where(eq(SessionTable.id, event.data.sessionID))
         .run()
         .pipe(Effect.orDie, Effect.andThen(run(db, event))),
     )
     yield* events.project(SessionEvent.ModelSwitched, (event) =>
-      db.update(SessionTable)
+      db
+        .update(SessionTable)
         .set({ model: event.data.model, time_updated: DateTime.toEpochMillis(event.data.timestamp) })
         .where(eq(SessionTable.id, event.data.sessionID))
         .run()

+ 50 - 14
packages/core/src/session/runner/publish-llm-event.ts

@@ -1,4 +1,11 @@
-import { ToolOutput as LLMToolOutput, type LLMEvent, type ProviderMetadata, type ToolOutput as LLMToolOutputType, type ToolResultValue, type Usage } from "@opencode-ai/llm"
+import {
+  ToolOutput as LLMToolOutput,
+  type LLMEvent,
+  type ProviderMetadata,
+  type ToolOutput as LLMToolOutputType,
+  type ToolResultValue,
+  type Usage,
+} from "@opencode-ai/llm"
 import { DateTime, Effect } from "effect"
 import { EventV2 } from "../../event"
 import { ModelV2 } from "../../model"
@@ -52,7 +59,15 @@ const settledOutput = (value: LLMToolOutputType | undefined, result: ToolResultV
 export const createLLMEventPublisher = (events: EventV2.Interface, input: Input) => {
   const tools = new Map<
     string,
-    { readonly assistantMessageID: EventV2.ID; readonly name: string; inputEnded: boolean; called: boolean; settled: boolean; providerExecuted: boolean; providerMetadata?: ProviderMetadata }
+    {
+      readonly assistantMessageID: EventV2.ID
+      readonly name: string
+      inputEnded: boolean
+      called: boolean
+      settled: boolean
+      providerExecuted: boolean
+      providerMetadata?: ProviderMetadata
+    }
   >()
   const timestamp = DateTime.now
   let assistantMessageID: EventV2.ID | undefined
@@ -60,14 +75,19 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input)
 
   const startAssistant = Effect.fnUntraced(function* () {
     if (assistantMessageID !== undefined) return assistantMessageID
-    assistantMessageID = (yield* events.publish(SessionEvent.Step.Started, { ...input, timestamp: yield* timestamp })).id
+    assistantMessageID = (yield* events.publish(SessionEvent.Step.Started, { ...input, timestamp: yield* timestamp }))
+      .id
     return assistantMessageID
   })
-  const currentAssistantMessageID = () => assistantMessageID === undefined
-    ? Effect.die("Tool event before assistant step start")
-    : Effect.succeed(assistantMessageID)
+  const currentAssistantMessageID = () =>
+    assistantMessageID === undefined
+      ? Effect.die("Tool event before assistant step start")
+      : Effect.succeed(assistantMessageID)
 
-  const fragments = (name: string, ended: (id: string, value: string, providerMetadata?: ProviderMetadata) => Effect.Effect<void>) => {
+  const fragments = (
+    name: string,
+    ended: (id: string, value: string, providerMetadata?: ProviderMetadata) => Effect.Effect<void>,
+  ) => {
     const chunks = new Map<string, string[]>()
     const start = (id: string) =>
       Effect.suspend(() => {
@@ -139,7 +159,14 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input)
   const startToolInput = Effect.fnUntraced(function* (event: { readonly id: string; readonly name: string }) {
     if (tools.has(event.id)) return yield* Effect.die(`Duplicate tool input start: ${event.id}`)
     const assistantMessageID = yield* currentAssistantMessageID()
-    tools.set(event.id, { assistantMessageID, name: event.name, inputEnded: false, called: false, settled: false, providerExecuted: false })
+    tools.set(event.id, {
+      assistantMessageID,
+      name: event.name,
+      inputEnded: false,
+      called: false,
+      settled: false,
+      providerExecuted: false,
+    })
     yield* toolInput.start(event.id)
     yield* events.publish(SessionEvent.Tool.Input.Started, {
       sessionID: input.sessionID,
@@ -153,7 +180,8 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input)
   const endToolInput = Effect.fnUntraced(function* (event: { readonly id: string; readonly name: string }) {
     const tool = tools.get(event.id)
     if (!tool) return yield* Effect.die(`Tool input end before start: ${event.id}`)
-    if (tool.name !== event.name) return yield* Effect.die(`Tool input name changed for ${event.id}: ${tool.name} -> ${event.name}`)
+    if (tool.name !== event.name)
+      return yield* Effect.die(`Tool input name changed for ${event.id}: ${tool.name} -> ${event.name}`)
     if (tool.inputEnded) return yield* Effect.die(`Duplicate tool input end: ${event.id}`)
     yield* toolInput.end(event.id)
   })
@@ -190,7 +218,11 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input)
         return
       case "text-start":
         yield* text.start(event.id)
-        yield* events.publish(SessionEvent.Text.Started, { sessionID: input.sessionID, timestamp: yield* timestamp, textID: event.id })
+        yield* events.publish(SessionEvent.Text.Started, {
+          sessionID: input.sessionID,
+          timestamp: yield* timestamp,
+          textID: event.id,
+        })
         return
       case "text-delta":
         yield* text.append(event.id, event.text)
@@ -231,7 +263,8 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input)
       case "tool-input-delta": {
         const tool = tools.get(event.id)
         if (!tool) return yield* Effect.die(`Tool input delta before start: ${event.id}`)
-        if (tool.name !== event.name) return yield* Effect.die(`Tool input name changed for ${event.id}: ${tool.name} -> ${event.name}`)
+        if (tool.name !== event.name)
+          return yield* Effect.die(`Tool input name changed for ${event.id}: ${tool.name} -> ${event.name}`)
         if (tool.inputEnded) return yield* Effect.die(`Tool input delta after end: ${event.id}`)
         yield* toolInput.append(event.id, event.text)
         yield* events.publish(SessionEvent.Tool.Input.Delta, {
@@ -250,7 +283,8 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input)
         if (!tools.has(event.id)) yield* startToolInput(event)
         const tool = tools.get(event.id)!
         if (!tool.inputEnded) yield* endToolInput(event)
-        if (tool.name !== event.name) return yield* Effect.die(`Tool call name changed for ${event.id}: ${tool.name} -> ${event.name}`)
+        if (tool.name !== event.name)
+          return yield* Effect.die(`Tool call name changed for ${event.id}: ${tool.name} -> ${event.name}`)
         if (tool.called) return yield* Effect.die(`Duplicate tool call: ${event.id}`)
         tool.called = true
         tool.providerExecuted = event.providerExecuted === true
@@ -272,7 +306,8 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input)
       case "tool-result": {
         const tool = tools.get(event.id)
         if (!tool?.called) return yield* Effect.die(`Tool result before call: ${event.id}`)
-        if (tool.name !== event.name) return yield* Effect.die(`Tool result name changed for ${event.id}: ${tool.name} -> ${event.name}`)
+        if (tool.name !== event.name)
+          return yield* Effect.die(`Tool result name changed for ${event.id}: ${tool.name} -> ${event.name}`)
         if (tool.settled) {
           if (event.result.type === "error") return
           return yield* Effect.die(`Duplicate tool result: ${event.id}`)
@@ -309,7 +344,8 @@ export const createLLMEventPublisher = (events: EventV2.Interface, input: Input)
       case "tool-error": {
         const tool = tools.get(event.id)
         if (!tool?.called) return yield* Effect.die(`Tool error before call: ${event.id}`)
-        if (tool.name !== event.name) return yield* Effect.die(`Tool error name changed for ${event.id}: ${tool.name} -> ${event.name}`)
+        if (tool.name !== event.name)
+          return yield* Effect.die(`Tool error name changed for ${event.id}: ${tool.name} -> ${event.name}`)
         if (tool.settled) return yield* Effect.die(`Duplicate tool error: ${event.id}`)
         tool.settled = true
         yield* events.publish(SessionEvent.Tool.Failed, {

+ 13 - 4
packages/core/src/session/runner/to-llm-message.ts

@@ -1,4 +1,12 @@
-import { Message, ToolCallPart, ToolOutput, ToolResultPart, type ContentPart, type Model, type ProviderMetadata } from "@opencode-ai/llm"
+import {
+  Message,
+  ToolCallPart,
+  ToolOutput,
+  ToolResultPart,
+  type ContentPart,
+  type Model,
+  type ProviderMetadata,
+} from "@opencode-ai/llm"
 import { SessionMessage } from "../message"
 import type { FileAttachment } from "../prompt"
 
@@ -61,7 +69,8 @@ const toolResult = (tool: SessionMessage.AssistantTool, providerMetadata: Provid
 }
 
 const assistant = (message: SessionMessage.Assistant, model: Model) => {
-  const sameModel = String(message.model.providerID) === String(model.provider) && String(message.model.id) === String(model.id)
+  const sameModel =
+    String(message.model.providerID) === String(model.provider) && String(message.model.id) === String(model.id)
   const content = message.content.flatMap((item): ContentPart[] => {
     if (item.type === "text") return [{ type: "text", text: item.text }]
     if (item.type === "reasoning")
@@ -71,12 +80,12 @@ const assistant = (message: SessionMessage.Assistant, model: Model) => {
           ? [{ type: "text", text: item.text }]
           : []
     const call = toolCall(item, sameModel ? item.provider?.metadata : undefined)
-    const result = toolResult(item, sameModel ? item.provider?.resultMetadata ?? item.provider?.metadata : undefined)
+    const result = toolResult(item, sameModel ? (item.provider?.resultMetadata ?? item.provider?.metadata) : undefined)
     return item.provider?.executed === true && result ? [call, result] : [call]
   })
   const results = message.content
     .filter((item): item is SessionMessage.AssistantTool => item.type === "tool" && item.provider?.executed !== true)
-    .map((item) => toolResult(item, sameModel ? item.provider?.resultMetadata ?? item.provider?.metadata : undefined))
+    .map((item) => toolResult(item, sameModel ? (item.provider?.resultMetadata ?? item.provider?.metadata) : undefined))
     .filter((message) => message !== undefined)
     .map(Message.tool)
   return [Message.make({ id: message.id, role: "assistant", content, metadata: message.metadata }), ...results]

+ 1 - 1
packages/core/src/session/schema.ts

@@ -15,7 +15,7 @@ export const ID = Schema.String.check(Schema.isStartsWith("ses")).pipe(
     const create = () => schema.make("ses_" + Identifier.descending())
     return {
       create,
-      descending: (id?: string) => id === undefined ? create() : schema.make(id),
+      descending: (id?: string) => (id === undefined ? create() : schema.make(id)),
       fromExternal: (input: ExternalID) => schema.make(externalID("ses", input)),
     }
   }),

+ 4 - 5
packages/core/src/skill/discovery.ts

@@ -151,11 +151,10 @@ export const layer = Layer.effect(
           }),
           ({ skill, root, files }) =>
             Effect.gen(function* () {
-              yield* Effect.forEach(
-                files,
-                (file) => download(file.url, file.destination),
-                { concurrency: fileConcurrency, discard: true },
-              )
+              yield* Effect.forEach(files, (file) => download(file.url, file.destination), {
+                concurrency: fileConcurrency,
+                discard: true,
+              })
               return (yield* fs.exists(path.join(root, "SKILL.md")).pipe(Effect.orDie)) ||
                 (yield* fs.exists(path.join(root, `${skill.name}.md`)).pipe(Effect.orDie))
                 ? [AbsolutePath.make(root)]

+ 48 - 21
packages/core/src/tool-output-store.ts

@@ -33,14 +33,20 @@ export class Page extends Schema.Class<Page>("ToolOutputStore.Page")({
   next: NonNegativeInt.pipe(Schema.optional),
 }) {}
 
-export class AccessDeniedError extends Schema.TaggedErrorClass<AccessDeniedError>()("ToolOutputStore.AccessDeniedError", {
-  uri: Schema.String,
-  sessionID: SessionSchema.ID,
-}) {}
+export class AccessDeniedError extends Schema.TaggedErrorClass<AccessDeniedError>()(
+  "ToolOutputStore.AccessDeniedError",
+  {
+    uri: Schema.String,
+    sessionID: SessionSchema.ID,
+  },
+) {}
 
-export class InvalidResourceError extends Schema.TaggedErrorClass<InvalidResourceError>()("ToolOutputStore.InvalidResourceError", {
-  uri: Schema.String,
-}) {}
+export class InvalidResourceError extends Schema.TaggedErrorClass<InvalidResourceError>()(
+  "ToolOutputStore.InvalidResourceError",
+  {
+    uri: Schema.String,
+  },
+) {}
 
 export class ResourceNotFoundError extends Schema.TaggedErrorClass<ResourceNotFoundError>()(
   "ToolOutputStore.ResourceNotFoundError",
@@ -88,7 +94,9 @@ export interface Interface {
   readonly limits: () => Effect.Effect<{ readonly maxLines: number; readonly maxBytes: number }>
   readonly write: (input: WriteInput) => Effect.Effect<Resource>
   readonly truncate: (input: TruncateInput) => Effect.Effect<TruncateResult>
-  readonly read: (input: ReadInput) => Effect.Effect<Page, AccessDeniedError | InvalidResourceError | ResourceNotFoundError>
+  readonly read: (
+    input: ReadInput,
+  ) => Effect.Effect<Page, AccessDeniedError | InvalidResourceError | ResourceNotFoundError>
   readonly cleanup: () => Effect.Effect<void>
 }
 
@@ -153,11 +161,17 @@ const preview = (text: string, maxLines: number, maxBytes: number) => {
   const sampled =
     lines.length <= maxLines
       ? text
-      : [lines.slice(0, headLines).join("\n"), ...(tailLines > 0 ? [lines.slice(lines.length - tailLines).join("\n")] : [])].join("\n")
+      : [
+          lines.slice(0, headLines).join("\n"),
+          ...(tailLines > 0 ? [lines.slice(lines.length - tailLines).join("\n")] : []),
+        ].join("\n")
   if (Buffer.byteLength(sampled, "utf-8") <= maxBytes) {
     return lines.length <= maxLines
       ? { head: sampled, tail: "" }
-      : { head: lines.slice(0, headLines).join("\n"), tail: tailLines > 0 ? lines.slice(lines.length - tailLines).join("\n") : "" }
+      : {
+          head: lines.slice(0, headLines).join("\n"),
+          tail: tailLines > 0 ? lines.slice(lines.length - tailLines).join("\n") : "",
+        }
   }
   const headBytes = Math.ceil(maxBytes / 2)
   const tailBytes = Math.floor(maxBytes / 2)
@@ -192,7 +206,7 @@ export const layer = Layer.effect(
       const entries = yield* config.value.entries().pipe(Effect.catch(() => Effect.succeed([] as Config.Entry[])))
       const configured = Object.assign(
         {},
-        ...entries.flatMap((entry) => entry.type === "document" ? [entry.info.tool_output ?? {}] : []),
+        ...entries.flatMap((entry) => (entry.type === "document" ? [entry.info.tool_output ?? {}] : [])),
       )
       return { maxLines: configured.max_lines ?? MAX_LINES, maxBytes: configured.max_bytes ?? MAX_BYTES }
     })
@@ -218,7 +232,12 @@ export const layer = Layer.effect(
         Effect.onError(() => fs.remove(contentPath(id)).pipe(Effect.catch(() => Effect.void))),
         Effect.orDie,
       )
-      return new Resource({ uri: resourceUri, mime: record.mime, ...(record.name === undefined ? {} : { name: record.name }), size })
+      return new Resource({
+        uri: resourceUri,
+        mime: record.mime,
+        ...(record.name === undefined ? {} : { name: record.name }),
+        size,
+      })
     })
 
     const truncate = Effect.fn("ToolOutputStore.truncate")(function* (input: TruncateInput) {
@@ -281,12 +300,14 @@ export const layer = Layer.effect(
     const cleanup = Effect.fn("ToolOutputStore.cleanup")(function* () {
       const entries = yield* fs.readDirectory(directory).pipe(Effect.catch(() => Effect.succeed([])))
       const cutoff = Date.now() - Duration.toMillis(RETENTION)
-      const ids = new Set(entries.flatMap((entry) => {
-        const match = entry.match(/^([0-9a-f]{12}[0-9A-Za-z]{14})\.(?:json|txt)$/)
-        return match ? [match[1]] : []
-      }))
+      const ids = new Set(
+        entries.flatMap((entry) => {
+          const match = entry.match(/^([0-9a-f]{12}[0-9A-Za-z]{14})\.(?:json|txt)$/)
+          return match ? [match[1]] : []
+        }),
+      )
       const removeIfPresent = (target: string) =>
-        fs.existsSafe(target).pipe(Effect.flatMap((exists) => exists ? fs.remove(target) : Effect.void))
+        fs.existsSafe(target).pipe(Effect.flatMap((exists) => (exists ? fs.remove(target) : Effect.void)))
       const removePair = (id: string) =>
         Effect.gen(function* () {
           yield* removeIfPresent(contentPath(id))
@@ -298,13 +319,19 @@ export const layer = Layer.effect(
         if (!text) {
           if (!contentExists) continue
           const info = yield* fs.stat(contentPath(id)).pipe(Effect.catch(() => Effect.void))
-          const modified = info ? info.mtime.pipe(Option.map((date) => date.getTime()), Option.getOrElse(() => 0)) : 0
+          const modified = info
+            ? info.mtime.pipe(
+                Option.map((date) => date.getTime()),
+                Option.getOrElse(() => 0),
+              )
+            : 0
           if (modified < cutoff) yield* removePair(id)
           continue
         }
-        const record = yield* Effect.try({ try: () => JSON.parse(text), catch: () => new globalThis.Error("Invalid metadata") }).pipe(
-          Effect.catch(() => Effect.succeed(undefined)),
-        )
+        const record = yield* Effect.try({
+          try: () => JSON.parse(text),
+          catch: () => new globalThis.Error("Invalid metadata"),
+        }).pipe(Effect.catch(() => Effect.succeed(undefined)))
         const info = contentExists ? yield* fs.stat(contentPath(id)).pipe(Effect.catch(() => Effect.void)) : undefined
         if (
           !contentExists ||

+ 107 - 82
packages/core/src/tool-registry.ts

@@ -1,6 +1,16 @@
 export * as ToolRegistry from "./tool-registry"
 
-import { Tool, ToolFailure, ToolOutput, ToolResultValue as ToolResult, type Tool as TypedTool, type ToolCall, type ToolResultValue, type ToolSchema, type ToolSettlement } from "@opencode-ai/llm"
+import {
+  Tool,
+  ToolFailure,
+  ToolOutput,
+  ToolResultValue as ToolResult,
+  type Tool as TypedTool,
+  type ToolCall,
+  type ToolResultValue,
+  type ToolSchema,
+  type ToolSettlement,
+} from "@opencode-ai/llm"
 import { Context, Effect, Layer, Schema, Scope } from "effect"
 import { castDraft, enableMapSet } from "immer"
 import { PermissionV2 } from "./permission"
@@ -35,10 +45,15 @@ export type AuthorizeInput<Parameters = unknown> = Invocation & {
   readonly parameters: Parameters
 }
 
-export type Entry<Parameters extends ToolSchema<any> = ToolSchema<any>, Success extends ToolSchema<any> = ToolSchema<any>> = {
+export type Entry<
+  Parameters extends ToolSchema<any> = ToolSchema<any>,
+  Success extends ToolSchema<any> = ToolSchema<any>,
+> = {
   readonly tool: TypedTool<Parameters, Success>
   readonly authorize?: (input: AuthorizeInput<Schema.Schema.Type<Parameters>>) => Effect.Effect<void, ToolFailure>
-  readonly execute?: (input: AuthorizeInput<Schema.Schema.Type<Parameters>>) => Effect.Effect<Schema.Schema.Type<Success>, ToolFailure>
+  readonly execute?: (
+    input: AuthorizeInput<Schema.Schema.Type<Parameters>>,
+  ) => Effect.Effect<Schema.Schema.Type<Success>, ToolFailure>
 }
 
 type Data = {
@@ -48,7 +63,10 @@ type Data = {
 export type Editor = {
   readonly list: () => ReadonlyArray<readonly [string, Entry]>
   readonly get: (name: string) => Entry | undefined
-  readonly set: <Parameters extends ToolSchema<any>, Success extends ToolSchema<any>>(name: string, entry: Entry<Parameters, Success>) => void
+  readonly set: <Parameters extends ToolSchema<any>, Success extends ToolSchema<any>>(
+    name: string,
+    entry: Entry<Parameters, Success>,
+  ) => void
   readonly remove: (name: string) => void
 }
 
@@ -65,84 +83,91 @@ export class Service extends Context.Service<Service, Interface>()("@opencode/v2
 enableMapSet()
 
 export const layer = Layer.effect(
-    Service,
-    Effect.gen(function* () {
-      const permission = yield* PermissionV2.Service
-      const state = State.create<Data, Editor>({
-        initial: () => ({ entries: new Map() }),
-        editor: (draft) => ({
-          list: () => Array.from(draft.entries.entries()) as Array<[string, Entry]>,
-          get: (name) => draft.entries.get(name) as Entry | undefined,
-          set: (name, entry) => {
-            draft.entries.set(name, castDraft(entry) as typeof draft.entries extends Map<string, infer Value> ? Value : never)
-          },
-          remove: (name) => {
-            draft.entries.delete(name)
-          },
-        }),
-      })
-
-      const definitions = Effect.fn("ToolRegistry.definitions")(function* () {
-        return Tool.toDefinitions(Object.fromEntries(Array.from(state.get().entries, ([name, entry]) => [name, entry.tool])))
-      })
-
-      const invocation = (input: ExecuteInput): Invocation => ({
-        ...input,
-        // Source needs the durable owning assistant message ID, which the registry does not receive yet.
-        assertPermission: (request) => permission.assert({ ...request, sessionID: input.sessionID }),
-      })
-
-      const settle = Effect.fn("ToolRegistry.settle")(function* (input: ExecuteInput) {
-        const entry = state.get().entries.get(input.call.name)
-        if (!entry) return { result: { type: "error" as const, value: `Unknown tool: ${input.call.name}` } }
-        if (!entry.execute && !entry.tool.execute)
-          return { result: { type: "error" as const, value: `Tool has no execute handler: ${input.call.name}` } }
-
-        return yield* entry.tool._decode(input.call.input).pipe(
-          Effect.mapError((error) => new ToolFailure({ message: `Invalid tool input: ${error.message}` })),
-          Effect.flatMap((parameters) => {
-            const context = { ...invocation(input), parameters }
-            const execute = entry.execute?.(context) ??
-              entry.tool.execute!(parameters, { id: input.call.id, name: input.call.name })
-            return (entry.authorize === undefined ? execute : entry.authorize(context).pipe(Effect.andThen(execute))).pipe(
-              Effect.flatMap((value) =>
-                entry.tool._encode(value).pipe(
-                  Effect.mapError(
-                    (error) =>
-                      new ToolFailure({
-                        message: `Tool returned an invalid value for its success schema: ${error.message}`,
-                      }),
-                  ),
+  Service,
+  Effect.gen(function* () {
+    const permission = yield* PermissionV2.Service
+    const state = State.create<Data, Editor>({
+      initial: () => ({ entries: new Map() }),
+      editor: (draft) => ({
+        list: () => Array.from(draft.entries.entries()) as Array<[string, Entry]>,
+        get: (name) => draft.entries.get(name) as Entry | undefined,
+        set: (name, entry) => {
+          draft.entries.set(
+            name,
+            castDraft(entry) as typeof draft.entries extends Map<string, infer Value> ? Value : never,
+          )
+        },
+        remove: (name) => {
+          draft.entries.delete(name)
+        },
+      }),
+    })
+
+    const definitions = Effect.fn("ToolRegistry.definitions")(function* () {
+      return Tool.toDefinitions(
+        Object.fromEntries(Array.from(state.get().entries, ([name, entry]) => [name, entry.tool])),
+      )
+    })
+
+    const invocation = (input: ExecuteInput): Invocation => ({
+      ...input,
+      // Source needs the durable owning assistant message ID, which the registry does not receive yet.
+      assertPermission: (request) => permission.assert({ ...request, sessionID: input.sessionID }),
+    })
+
+    const settle = Effect.fn("ToolRegistry.settle")(function* (input: ExecuteInput) {
+      const entry = state.get().entries.get(input.call.name)
+      if (!entry) return { result: { type: "error" as const, value: `Unknown tool: ${input.call.name}` } }
+      if (!entry.execute && !entry.tool.execute)
+        return { result: { type: "error" as const, value: `Tool has no execute handler: ${input.call.name}` } }
+
+      return yield* entry.tool._decode(input.call.input).pipe(
+        Effect.mapError((error) => new ToolFailure({ message: `Invalid tool input: ${error.message}` })),
+        Effect.flatMap((parameters) => {
+          const context = { ...invocation(input), parameters }
+          const execute =
+            entry.execute?.(context) ?? entry.tool.execute!(parameters, { id: input.call.id, name: input.call.name })
+          return (
+            entry.authorize === undefined ? execute : entry.authorize(context).pipe(Effect.andThen(execute))
+          ).pipe(
+            Effect.flatMap((value) =>
+              entry.tool._encode(value).pipe(
+                Effect.mapError(
+                  (error) =>
+                    new ToolFailure({
+                      message: `Tool returned an invalid value for its success schema: ${error.message}`,
+                    }),
                 ),
               ),
-              Effect.map((value): ToolSettlement => {
-                if (entry.tool._legacyResult && ToolResult.is(value))
-                  return { result: value, output: ToolOutput.fromResultValue(value) }
-                const output = entry.tool._project(parameters, input.call.id, value)
-                const result = ToolOutput.toResultValue(output)
-                return result.type === "error" ? { result } : { result, output }
-              }),
-            )
-          }),
-          Effect.catchTag("LLM.ToolFailure", (failure) =>
-            Effect.succeed({ result: { type: "error" as const, value: failure.message } }),
-          ),
-        )
-      })
-
-      const execute = Effect.fn("ToolRegistry.execute")(function* (input: ExecuteInput) {
-        return (yield* settle(input)).result
-      })
-
-      return Service.of({
-        transform: state.transform,
-        contribute: Effect.fn("ToolRegistry.contribute")(function* (update) {
-          const transform = yield* state.transform()
-          yield* transform(update)
+            ),
+            Effect.map((value): ToolSettlement => {
+              if (entry.tool._legacyResult && ToolResult.is(value))
+                return { result: value, output: ToolOutput.fromResultValue(value) }
+              const output = entry.tool._project(parameters, input.call.id, value)
+              const result = ToolOutput.toResultValue(output)
+              return result.type === "error" ? { result } : { result, output }
+            }),
+          )
         }),
-        definitions,
-        execute,
-        settle,
-      })
-    }),
-  )
+        Effect.catchTag("LLM.ToolFailure", (failure) =>
+          Effect.succeed({ result: { type: "error" as const, value: failure.message } }),
+        ),
+      )
+    })
+
+    const execute = Effect.fn("ToolRegistry.execute")(function* (input: ExecuteInput) {
+      return (yield* settle(input)).result
+    })
+
+    return Service.of({
+      transform: state.transform,
+      contribute: Effect.fn("ToolRegistry.contribute")(function* (update) {
+        const transform = yield* state.transform()
+        yield* transform(update)
+      }),
+      definitions,
+      execute,
+      settle,
+    })
+  }),
+)

+ 72 - 27
packages/core/src/tool/apply-patch.ts

@@ -11,7 +11,9 @@ import { ToolRegistry } from "../tool-registry"
 export const name = "apply_patch"
 
 export const Parameters = Schema.Struct({
-  patchText: Schema.String.annotate({ description: "The full patch text describing add, update, and delete operations" }),
+  patchText: Schema.String.annotate({
+    description: "The full patch text describing add, update, and delete operations",
+  }),
 })
 
 export const Applied = Schema.Struct({
@@ -24,7 +26,12 @@ export const Success = Schema.Struct({ applied: Schema.Array(Applied) })
 export type Success = typeof Success.Type
 
 export const toModelOutput = (output: Success) =>
-  ["Applied patch sequentially:", ...output.applied.map((item) => `${item.type === "add" ? "A" : item.type === "delete" ? "D" : "M"} ${item.resource}`)].join("\n")
+  [
+    "Applied patch sequentially:",
+    ...output.applied.map(
+      (item) => `${item.type === "add" ? "A" : item.type === "delete" ? "D" : "M"} ${item.resource}`,
+    ),
+  ].join("\n")
 
 const definition = Tool.make({
   description:
@@ -36,9 +43,23 @@ const definition = Tool.make({
 
 type Planned = { readonly hunk: Patch.Hunk; readonly plan: LocationMutation.Plan }
 type Prepared =
-  | { readonly type: "add"; readonly hunk: Extract<Patch.Hunk, { readonly type: "add" }>; readonly plan: LocationMutation.Plan }
-  | { readonly type: "delete"; readonly hunk: Extract<Patch.Hunk, { readonly type: "delete" }>; readonly plan: LocationMutation.Plan }
-  | { readonly type: "update"; readonly hunk: Extract<Patch.Hunk, { readonly type: "update" }>; readonly plan: LocationMutation.Plan; readonly source: Uint8Array; readonly content: string }
+  | {
+      readonly type: "add"
+      readonly hunk: Extract<Patch.Hunk, { readonly type: "add" }>
+      readonly plan: LocationMutation.Plan
+    }
+  | {
+      readonly type: "delete"
+      readonly hunk: Extract<Patch.Hunk, { readonly type: "delete" }>
+      readonly plan: LocationMutation.Plan
+    }
+  | {
+      readonly type: "update"
+      readonly hunk: Extract<Patch.Hunk, { readonly type: "update" }>
+      readonly plan: LocationMutation.Plan
+      readonly source: Uint8Array
+      readonly content: string
+    }
 
 export const layer = Layer.effectDiscard(
   Effect.gen(function* () {
@@ -53,9 +74,10 @@ export const layer = Layer.effectDiscard(
         execute: ({ parameters, assertPermission }) => {
           const applied: Array<typeof Applied.Type> = []
           const fail = (path: string, cause: unknown) => {
-            const prefix = applied.length === 0
-              ? `Unable to apply patch at ${path}`
-              : `Patch partially applied before failing at ${path}. Applied: ${applied.map((item) => item.resource).join(", ")}`
+            const prefix =
+              applied.length === 0
+                ? `Unable to apply patch at ${path}`
+                : `Patch partially applied before failing at ${path}. Applied: ${applied.map((item) => item.resource).join(", ")}`
             return new ToolFailure({ message: prefix, error: cause })
           }
           return Effect.gen(function* () {
@@ -69,7 +91,8 @@ export const layer = Layer.effectDiscard(
             if (move) return yield* new ToolFailure({ message: "apply_patch moves are not supported yet" })
 
             const planned: Planned[] = []
-            for (const hunk of hunks) planned.push({ hunk, plan: yield* mutation.resolve({ path: hunk.path, kind: "file" }) })
+            for (const hunk of hunks)
+              planned.push({ hunk, plan: yield* mutation.resolve({ path: hunk.path, kind: "file" }) })
             const externalDirectories = new Map<string, LocationMutation.ExternalDirectoryAuthorization>()
             for (const { plan } of planned) {
               const external = plan.target.externalDirectory
@@ -78,7 +101,11 @@ export const layer = Layer.effectDiscard(
             for (const external of externalDirectories.values()) {
               yield* assertPermission(LocationMutation.externalDirectoryPermission(external))
             }
-            yield* assertPermission({ action: "edit", resources: [...new Set(planned.map(({ plan }) => plan.target.resource))], save: ["*"] })
+            yield* assertPermission({
+              action: "edit",
+              resources: [...new Set(planned.map(({ plan }) => plan.target.resource))],
+              save: ["*"],
+            })
 
             const prepared: Prepared[] = []
             for (const { hunk, plan } of planned) {
@@ -89,33 +116,51 @@ export const layer = Layer.effectDiscard(
                 continue
               }
               const target = yield* mutation.revalidate(plan)
-              if (!target.exists || target.type !== "File") return yield* fail(hunk.path, new Error("Target file does not exist"))
+              if (!target.exists || target.type !== "File")
+                return yield* fail(hunk.path, new Error("Target file does not exist"))
               if (hunk.type === "delete") {
                 prepared.push({ type: hunk.type, hunk, plan })
                 continue
               }
               const source = yield* fs.readFile(target.canonical)
-              const update = Patch.derive(hunk.path, hunk.chunks, new TextDecoder("utf-8", { ignoreBOM: true }).decode(source))
+              const update = Patch.derive(
+                hunk.path,
+                hunk.chunks,
+                new TextDecoder("utf-8", { ignoreBOM: true }).decode(source),
+              )
               prepared.push({ type: hunk.type, hunk, plan, source, content: Patch.joinBom(update.content, update.bom) })
             }
 
             yield* Effect.uninterruptible(
-              Effect.forEach(prepared, (change) =>
-                Effect.gen(function* () {
-                  if (change.type === "add") {
-                    const result = yield* files.create({ plan: change.plan, content: change.hunk.contents.endsWith("\n") || change.hunk.contents === "" ? change.hunk.contents : `${change.hunk.contents}\n` })
-                    applied.push({ type: change.type, resource: result.resource, target: result.target })
-                    return
-                  }
-                  if (change.type === "delete") {
-                    const result = yield* files.remove({ plan: change.plan })
+              Effect.forEach(
+                prepared,
+                (change) =>
+                  Effect.gen(function* () {
+                    if (change.type === "add") {
+                      const result = yield* files.create({
+                        plan: change.plan,
+                        content:
+                          change.hunk.contents.endsWith("\n") || change.hunk.contents === ""
+                            ? change.hunk.contents
+                            : `${change.hunk.contents}\n`,
+                      })
+                      applied.push({ type: change.type, resource: result.resource, target: result.target })
+                      return
+                    }
+                    if (change.type === "delete") {
+                      const result = yield* files.remove({ plan: change.plan })
+                      applied.push({ type: change.type, resource: result.resource, target: result.target })
+                      return
+                    }
+                    const result = yield* files.writeIfUnchanged({
+                      plan: change.plan,
+                      expected: change.source,
+                      content: change.content,
+                    })
                     applied.push({ type: change.type, resource: result.resource, target: result.target })
-                    return
-                  }
-                  const result = yield* files.writeIfUnchanged({ plan: change.plan, expected: change.source, content: change.content })
-                  applied.push({ type: change.type, resource: result.resource, target: result.target })
-                }).pipe(Effect.catchCause((cause) => Effect.fail(fail(change.hunk.path, Cause.squash(cause))))),
-              { discard: true }),
+                  }).pipe(Effect.catchCause((cause) => Effect.fail(fail(change.hunk.path, Cause.squash(cause))))),
+                { discard: true },
+              ),
             )
             return { applied }
           }).pipe(

+ 11 - 3
packages/core/src/tool/edit.ts

@@ -112,7 +112,9 @@ export const layer = Layer.effectDiscard(
                 const error = Cause.squash(cause)
                 return Effect.fail(
                   error instanceof FileMutation.StaleContentError
-                    ? new ToolFailure({ message: "File changed after permission approval. Read it again before editing." })
+                    ? new ToolFailure({
+                        message: "File changed after permission approval. Read it again before editing.",
+                      })
                     : new ToolFailure({ message: `Unable to edit ${parameters.path}`, error }),
                 )
               }),
@@ -123,7 +125,9 @@ export const layer = Layer.effectDiscard(
               return yield* new ToolFailure({ message: "No changes to apply: oldString and newString are identical." })
             }
             if (parameters.oldString === "") {
-              return yield* new ToolFailure({ message: "oldString must not be empty. Use write to create or overwrite a file." })
+              return yield* new ToolFailure({
+                message: "oldString must not be empty. Use write to create or overwrite a file.",
+              })
             }
 
             const plan = yield* unableToEdit(mutation.resolve({ path: parameters.path, kind: "file" }))
@@ -158,7 +162,11 @@ export const layer = Layer.effectDiscard(
                 : source.text.replace(oldString, newString)
             const next = splitBom(replaced)
             const result = yield* unableToEdit(
-              files.writeIfUnchanged({ plan, expected: source.content, content: joinBom(next.text, source.bom || next.bom) }),
+              files.writeIfUnchanged({
+                plan,
+                expected: source.content,
+                content: joinBom(next.text, source.bom || next.bom),
+              }),
             )
             return { ...result, replacements } satisfies Success
           })

+ 21 - 6
packages/core/src/tool/glob.ts

@@ -10,9 +10,15 @@ export const name = "glob"
 
 export const Parameters = Schema.Struct({
   pattern: LocationSearch.FilesInput.fields.pattern.annotate({ description: "Glob pattern to match files against" }),
-  path: LocationSearch.FilesInput.fields.path.annotate({ description: "Relative directory to search. Defaults to the active Location." }),
-  reference: LocationSearch.FilesInput.fields.reference.annotate({ description: "Named project reference to search instead of the active Location" }),
-  limit: LocationSearch.FilesInput.fields.limit.annotate({ description: `Maximum results to return (default: ${LocationSearch.DEFAULT_RESULT_LIMIT})` }),
+  path: LocationSearch.FilesInput.fields.path.annotate({
+    description: "Relative directory to search. Defaults to the active Location.",
+  }),
+  reference: LocationSearch.FilesInput.fields.reference.annotate({
+    description: "Named project reference to search instead of the active Location",
+  }),
+  limit: LocationSearch.FilesInput.fields.limit.annotate({
+    description: `Maximum results to return (default: ${LocationSearch.DEFAULT_RESULT_LIMIT})`,
+  }),
 })
 
 type ModelOutput = typeof LocationSearch.FilesResult.Encoded
@@ -21,14 +27,18 @@ type ModelOutput = typeof LocationSearch.FilesResult.Encoded
 export const toModelOutput = (output: ModelOutput) => {
   const lines = output.items.length === 0 ? ["No files found"] : output.items.map((item) => item.resource)
   if (output.truncated) {
-    lines.push("", `(Results are truncated: showing first ${output.items.length} results. Consider using a more specific path or pattern.)`)
+    lines.push(
+      "",
+      `(Results are truncated: showing first ${output.items.length} results. Consider using a more specific path or pattern.)`,
+    )
   }
   if (output.partial) lines.push("", "(Results may be incomplete because some discovered files could not be read.)")
   return lines.join("\n")
 }
 
 const definition = Tool.make({
-  description: "Find files by glob pattern within the active Location or a named project reference. Returns concise relative file resources. Use a relative path to narrow the search and limit to bound the result count.",
+  description:
+    "Find files by glob pattern within the active Location or a named project reference. Returns concise relative file resources. Use a relative path to narrow the search and limit to bound the result count.",
   parameters: Parameters,
   success: LocationSearch.FilesResult,
   toModelOutput: ({ output }) => [toolText({ type: "text", text: toModelOutput(output) })],
@@ -66,7 +76,12 @@ export const layer = Layer.effectDiscard(
             return yield* search.files(parameters, root)
           }).pipe(
             Effect.catchCause((cause) =>
-              Effect.fail(new ToolFailure({ message: `Unable to find files matching ${parameters.pattern}`, error: Cause.squash(cause) })),
+              Effect.fail(
+                new ToolFailure({
+                  message: `Unable to find files matching ${parameters.pattern}`,
+                  error: Cause.squash(cause),
+                }),
+              ),
             ),
           ),
       }),

+ 25 - 10
packages/core/src/tool/grep.ts

@@ -10,11 +10,21 @@ import { ToolRegistry } from "../tool-registry"
 export const name = "grep"
 
 export const Parameters = Schema.Struct({
-  pattern: LocationSearch.GrepInput.fields.pattern.annotate({ description: "Regex pattern to search for in file contents" }),
-  path: LocationSearch.GrepInput.fields.path.annotate({ description: "Relative file or directory to search. Defaults to the active Location." }),
-  reference: LocationSearch.GrepInput.fields.reference.annotate({ description: "Named project reference to search instead of the active Location" }),
-  include: LocationSearch.GrepInput.fields.include.annotate({ description: 'File glob to include in the search (for example, "*.js" or "*.{ts,tsx}")' }),
-  limit: LocationSearch.GrepInput.fields.limit.annotate({ description: `Maximum matches to return (default: ${LocationSearch.DEFAULT_RESULT_LIMIT})` }),
+  pattern: LocationSearch.GrepInput.fields.pattern.annotate({
+    description: "Regex pattern to search for in file contents",
+  }),
+  path: LocationSearch.GrepInput.fields.path.annotate({
+    description: "Relative file or directory to search. Defaults to the active Location.",
+  }),
+  reference: LocationSearch.GrepInput.fields.reference.annotate({
+    description: "Named project reference to search instead of the active Location",
+  }),
+  include: LocationSearch.GrepInput.fields.include.annotate({
+    description: 'File glob to include in the search (for example, "*.js" or "*.{ts,tsx}")',
+  }),
+  limit: LocationSearch.GrepInput.fields.limit.annotate({
+    description: `Maximum matches to return (default: ${LocationSearch.DEFAULT_RESULT_LIMIT})`,
+  }),
 })
 
 type Success = typeof LocationSearch.GrepResult.Encoded
@@ -32,14 +42,18 @@ export const toModelOutput = (output: Success) => {
     lines.push(`  Line ${match.line}: ${match.lines}${match.linePreviewTruncated ? "..." : ""}`)
   }
   if (output.truncated) {
-    lines.push("", `(Results are truncated: showing first ${output.items.length} matches. Consider using a more specific path or pattern.)`)
+    lines.push(
+      "",
+      `(Results are truncated: showing first ${output.items.length} matches. Consider using a more specific path or pattern.)`,
+    )
   }
   if (output.partial) lines.push("", "(Some paths were inaccessible and skipped)")
   return lines.join("\n")
 }
 
 const definition = Tool.make({
-  description: "Search file contents by regular expression within the active Location or a named project reference. Use a relative path to narrow the search, include to filter files by glob, and limit to bound the match count. Returns concise relative file resources, line numbers, and bounded line previews.",
+  description:
+    "Search file contents by regular expression within the active Location or a named project reference. Use a relative path to narrow the search, include to filter files by glob, and limit to bound the match count. Returns concise relative file resources, line numbers, and bounded line previews.",
   parameters: Parameters,
   success: LocationSearch.GrepResult,
   toModelOutput: ({ output }) => [toolText({ type: "text", text: toModelOutput(output) })],
@@ -79,9 +93,10 @@ export const layer = Layer.effectDiscard(
           }).pipe(
             Effect.catchCause((cause) => {
               const error = Cause.squash(cause)
-              const message = error instanceof Ripgrep.InvalidPatternError
-                ? `Invalid grep pattern ${JSON.stringify(parameters.pattern)}: ${error.message}`
-                : `Unable to grep for ${parameters.pattern}`
+              const message =
+                error instanceof Ripgrep.InvalidPatternError
+                  ? `Invalid grep pattern ${JSON.stringify(parameters.pattern)}: ${error.message}`
+                  : `Unable to grep for ${parameters.pattern}`
               return Effect.fail(new ToolFailure({ message, error }))
             }),
           ),

+ 5 - 1
packages/core/src/tool/read.ts

@@ -70,7 +70,11 @@ export const layer = Layer.effectDiscard(
             const final = yield* filesystem.resolveReadPath(input)
             if (final.type !== "file" || final.target.resource !== target.resource || final.target.real !== target.real)
               return yield* Effect.die(new Error("File changed after permission approval"))
-            if (final.target.size > FileSystem.MAX_READ_BYTES || input.offset !== undefined || input.limit !== undefined)
+            if (
+              final.target.size > FileSystem.MAX_READ_BYTES ||
+              input.offset !== undefined ||
+              input.limit !== undefined
+            )
               return yield* filesystem.readTextPageResolved(final.target, { offset: input.offset, limit: input.limit })
             return yield* filesystem.readResolved(final.target, FileSystem.MAX_READ_BYTES)
           }).pipe(

+ 3 - 1
packages/core/src/tool/webfetch.ts

@@ -150,7 +150,9 @@ export const layer = Layer.effectDiscard(
 
             const { body, contentType } = yield* Effect.gen(function* () {
               const response = yield* execute(http, parameters.url, parameters.format).pipe(
-                Effect.catchIf(isCloudflareChallenge, () => execute(http, parameters.url, parameters.format, "opencode")),
+                Effect.catchIf(isCloudflareChallenge, () =>
+                  execute(http, parameters.url, parameters.format, "opencode"),
+                ),
               )
               const contentType = response.headers["content-type"] || ""
               const mime = mimeFrom(contentType)

+ 52 - 38
packages/core/src/tool/websearch.ts

@@ -34,7 +34,9 @@ The current year is ${new Date().getFullYear()}. Use this year when searching fo
 
 export const Parameters = Schema.Struct({
   query: Schema.String.annotate({ description: "Websearch query" }),
-  numResults: Schema.optional(PositiveInt.check(Schema.isLessThanOrEqualTo(MAX_NUM_RESULTS))).annotate({ description: `Number of search results to return (default: 8, maximum: ${MAX_NUM_RESULTS})` }),
+  numResults: Schema.optional(PositiveInt.check(Schema.isLessThanOrEqualTo(MAX_NUM_RESULTS))).annotate({
+    description: `Number of search results to return (default: 8, maximum: ${MAX_NUM_RESULTS})`,
+  }),
   livecrawl: Schema.optional(Schema.Literals(["fallback", "preferred"])).annotate({
     description:
       "Live crawl mode - 'fallback': use live crawling as backup if cached unavailable, 'preferred': prioritize live crawling (default: 'fallback')",
@@ -42,9 +44,11 @@ export const Parameters = Schema.Struct({
   type: Schema.optional(Schema.Literals(["auto", "fast", "deep"])).annotate({
     description: "Search type - 'auto': balanced search (default), 'fast': quick results, 'deep': comprehensive search",
   }),
-  contextMaxCharacters: Schema.optional(PositiveInt.check(Schema.isLessThanOrEqualTo(MAX_CONTEXT_CHARACTERS))).annotate({
-    description: `Maximum characters for context string optimized for models (default: 10000, maximum: ${MAX_CONTEXT_CHARACTERS})`,
-  }),
+  contextMaxCharacters: Schema.optional(PositiveInt.check(Schema.isLessThanOrEqualTo(MAX_CONTEXT_CHARACTERS))).annotate(
+    {
+      description: `Maximum characters for context string optimized for models (default: 10000, maximum: ${MAX_CONTEXT_CHARACTERS})`,
+    },
+  ),
 })
 
 export const Provider = Schema.Literals(["exa", "parallel"])
@@ -63,13 +67,11 @@ export class ConfigService extends Context.Service<ConfigService, Config>()("@op
 /** Isolates the retained product environment contract from the generic tool implementation. */
 export const defaultConfigLayer = Layer.sync(ConfigService, () =>
   ConfigService.of({
-    provider: process.env.OPENCODE_WEBSEARCH_PROVIDER === "exa" || process.env.OPENCODE_WEBSEARCH_PROVIDER === "parallel"
-      ? process.env.OPENCODE_WEBSEARCH_PROVIDER
-      : undefined,
-    enableExa:
-      truthy("OPENCODE_EXPERIMENTAL") ||
-      truthy("OPENCODE_ENABLE_EXA") ||
-      truthy("OPENCODE_EXPERIMENTAL_EXA"),
+    provider:
+      process.env.OPENCODE_WEBSEARCH_PROVIDER === "exa" || process.env.OPENCODE_WEBSEARCH_PROVIDER === "parallel"
+        ? process.env.OPENCODE_WEBSEARCH_PROVIDER
+        : undefined,
+    enableExa: truthy("OPENCODE_EXPERIMENTAL") || truthy("OPENCODE_ENABLE_EXA") || truthy("OPENCODE_EXPERIMENTAL_EXA"),
     enableParallel: truthy("OPENCODE_ENABLE_PARALLEL") || truthy("OPENCODE_EXPERIMENTAL_PARALLEL"),
     exaApiKey: process.env.EXA_API_KEY,
     parallelApiKey: process.env.PARALLEL_API_KEY,
@@ -162,9 +164,15 @@ const callMcp = <F extends Schema.Struct.Fields>(
     return yield* Effect.gen(function* () {
       const response = yield* HttpClient.filterStatusOk(http).execute(request)
       const body = yield* response.text
-      if (Buffer.byteLength(body, "utf8") > MAX_RESPONSE_BYTES) return yield* Effect.die(new Error(`${tool} response exceeded ${MAX_RESPONSE_BYTES} bytes`))
+      if (Buffer.byteLength(body, "utf8") > MAX_RESPONSE_BYTES)
+        return yield* Effect.die(new Error(`${tool} response exceeded ${MAX_RESPONSE_BYTES} bytes`))
       return yield* parseResponse(body)
-    }).pipe(Effect.timeoutOrElse({ duration: Duration.seconds(25), orElse: () => Effect.die(new Error(`${tool} request timed out`)) }))
+    }).pipe(
+      Effect.timeoutOrElse({
+        duration: Duration.seconds(25),
+        orElse: () => Effect.die(new Error(`${tool} request timed out`)),
+      }),
+    )
   })
 
 const Success = Schema.Struct({
@@ -201,30 +209,31 @@ export const layer = Layer.effectDiscard(
               metadata: { ...parameters, provider },
             })
 
-            const text = provider === "exa"
-              ? yield* callMcp(http, exaUrl(config.exaApiKey), "web_search_exa", ExaArgs, {
-                  query: parameters.query,
-                  type: parameters.type || "auto",
-                  numResults: parameters.numResults || 8,
-                  livecrawl: parameters.livecrawl || "fallback",
-                  contextMaxCharacters: parameters.contextMaxCharacters,
-                })
-              : yield* callMcp(
-                  http,
-                  PARALLEL_URL,
-                  "web_search",
-                  ParallelArgs,
-                  {
-                    objective: parameters.query,
-                    search_queries: [parameters.query],
-                    session_id: sessionID,
-                    // V2 invocation context does not safely expose the model yet.
-                  },
-                  {
-                    "User-Agent": `opencode/${InstallationVersion}`,
-                    ...(config.parallelApiKey ? { Authorization: `Bearer ${config.parallelApiKey}` } : {}),
-                  },
-                )
+            const text =
+              provider === "exa"
+                ? yield* callMcp(http, exaUrl(config.exaApiKey), "web_search_exa", ExaArgs, {
+                    query: parameters.query,
+                    type: parameters.type || "auto",
+                    numResults: parameters.numResults || 8,
+                    livecrawl: parameters.livecrawl || "fallback",
+                    contextMaxCharacters: parameters.contextMaxCharacters,
+                  })
+                : yield* callMcp(
+                    http,
+                    PARALLEL_URL,
+                    "web_search",
+                    ParallelArgs,
+                    {
+                      objective: parameters.query,
+                      search_queries: [parameters.query],
+                      session_id: sessionID,
+                      // V2 invocation context does not safely expose the model yet.
+                    },
+                    {
+                      "User-Agent": `opencode/${InstallationVersion}`,
+                      ...(config.parallelApiKey ? { Authorization: `Bearer ${config.parallelApiKey}` } : {}),
+                    },
+                  )
             const truncated = yield* resources.truncate({ sessionID, toolCallID: call.id, content: text ?? NO_RESULTS })
             return {
               provider,
@@ -234,7 +243,12 @@ export const layer = Layer.effectDiscard(
             }
           }).pipe(
             Effect.catchCause((cause) =>
-              Effect.fail(new ToolFailure({ message: `Unable to search the web for ${parameters.query}`, error: Cause.squash(cause) })),
+              Effect.fail(
+                new ToolFailure({
+                  message: `Unable to search the web for ${parameters.query}`,
+                  error: Cause.squash(cause),
+                }),
+              ),
             ),
           )
         },

+ 9 - 1
packages/core/test/agent.test.ts

@@ -114,7 +114,15 @@ describe("AgentV2", () => {
       )
 
       const agents = yield* agent.all()
-      expect(agents.map((item) => String(item.id)).sort()).toEqual(["build", "compaction", "explore", "general", "plan", "summary", "title"])
+      expect(agents.map((item) => String(item.id)).sort()).toEqual([
+        "build",
+        "compaction",
+        "explore",
+        "general",
+        "plan",
+        "summary",
+        "title",
+      ])
       for (const item of agents) {
         expect(item.permissions.some((rule) => rule.action === "bash" && rule.effect !== "deny")).toBe(false)
       }

+ 4 - 1
packages/core/test/database-migration.test.ts

@@ -34,7 +34,10 @@ describe("DatabaseMigration", () => {
     const layers = [Database.layerFromPath(filename), Database.layerFromPath(filename)]
 
     await Effect.runPromise(
-      Effect.all(layers.map((layer) => Effect.scoped(Layer.build(layer))), { concurrency: "unbounded" }),
+      Effect.all(
+        layers.map((layer) => Effect.scoped(Layer.build(layer))),
+        { concurrency: "unbounded" },
+      ),
     )
   })
   if (process.platform === "linux") {

+ 6 - 2
packages/core/test/effect/keyed-mutex.test.ts

@@ -12,7 +12,9 @@ describe("KeyedMutex", () => {
       const secondStarted = yield* Deferred.make<void>()
 
       const first = yield* mutex
-        .withLock("shared")(Deferred.succeed(firstStarted, undefined).pipe(Effect.andThen(Deferred.await(releaseFirst))))
+        .withLock("shared")(
+          Deferred.succeed(firstStarted, undefined).pipe(Effect.andThen(Deferred.await(releaseFirst))),
+        )
         .pipe(Effect.forkChild)
       yield* Deferred.await(firstStarted)
       const second = yield* mutex.withLock("shared")(Deferred.succeed(secondStarted, undefined)).pipe(Effect.forkChild)
@@ -53,7 +55,9 @@ describe("KeyedMutex", () => {
       const releaseFirst = yield* Deferred.make<void>()
 
       const first = yield* mutex
-        .withLock("shared")(Deferred.succeed(firstStarted, undefined).pipe(Effect.andThen(Deferred.await(releaseFirst))))
+        .withLock("shared")(
+          Deferred.succeed(firstStarted, undefined).pipe(Effect.andThen(Deferred.await(releaseFirst))),
+        )
         .pipe(Effect.forkChild)
       yield* Deferred.await(firstStarted)
       const interrupted = yield* mutex.withLock("shared")(Effect.void).pipe(Effect.forkChild)

+ 90 - 61
packages/core/test/event.test.ts

@@ -13,7 +13,9 @@ import { testEffect } from "./lib/effect"
 
 const locationLayer = Layer.succeed(
   Location.Service,
-  Location.Service.of(location({ directory: AbsolutePath.make("project"), workspaceID: WorkspaceV2.ID.make("wrk_test") })),
+  Location.Service.of(
+    location({ directory: AbsolutePath.make("project"), workspaceID: WorkspaceV2.ID.make("wrk_test") }),
+  ),
 )
 const eventLayer = Layer.mergeAll(EventV2.defaultLayer, Database.defaultLayer)
 const it = testEffect(eventLayer.pipe(Layer.provideMerge(locationLayer)))
@@ -89,7 +91,9 @@ describe("EventV2", () => {
       expect(EventV2.ID.fromExternal(input)).toBe(EventV2.ID.fromExternal(input))
       expect(EventV2.ID.fromExternal(input)).toMatch(/^evt_[a-f0-9]{64}$/)
       expect(EventV2.ID.fromExternal({ ...input, namespace: "another-app" })).not.toBe(EventV2.ID.fromExternal(input))
-      expect(EventV2.ID.fromExternal({ namespace: "a:b", key: "c" })).not.toBe(EventV2.ID.fromExternal({ namespace: "a", key: "b:c" }))
+      expect(EventV2.ID.fromExternal({ namespace: "a:b", key: "c" })).not.toBe(
+        EventV2.ID.fromExternal({ namespace: "a", key: "b:c" }),
+      )
     }),
   )
 
@@ -105,7 +109,10 @@ describe("EventV2", () => {
       expect(event.type).toBe("test.message")
       expect(event).not.toHaveProperty("version")
       expect(event.data).toEqual({ text: "hello" })
-      expect(event.location).toEqual({ directory: AbsolutePath.make("project"), workspaceID: WorkspaceV2.ID.make("wrk_test") })
+      expect(event.location).toEqual({
+        directory: AbsolutePath.make("project"),
+        workspaceID: WorkspaceV2.ID.make("wrk_test"),
+      })
     }),
   )
 
@@ -292,7 +299,9 @@ describe("EventV2", () => {
       const aggregateID = EventV2.ID.create()
       yield* events.publish(SyncMessage, { id: aggregateID, text: "zero" })
       yield* events.publish(SyncMessage, { id: aggregateID, text: "one" })
-      const fiber = yield* events.aggregateEvents({ aggregateID, after: EventV2.Cursor.make(0) }).pipe(Stream.take(2), Stream.runCollect, Effect.forkScoped)
+      const fiber = yield* events
+        .aggregateEvents({ aggregateID, after: EventV2.Cursor.make(0) })
+        .pipe(Stream.take(2), Stream.runCollect, Effect.forkScoped)
       yield* Effect.yieldNow
 
       yield* events.publish(SyncMessage, { id: aggregateID, text: "two" })
@@ -309,11 +318,18 @@ describe("EventV2", () => {
       const events = yield* EventV2.Service
       const aggregateID = EventV2.ID.create()
       yield* events.publish(SyncMessage, { id: aggregateID, text: "zero" })
-      const fiber = yield* events.aggregateEvents({ aggregateID }).pipe(Stream.take(2), Stream.runCollect, Effect.forkScoped)
+      const fiber = yield* events
+        .aggregateEvents({ aggregateID })
+        .pipe(Stream.take(2), Stream.runCollect, Effect.forkScoped)
 
       yield* events.publish(SyncMessage, { id: aggregateID, text: "one" })
 
-      expect(Array.from(yield* Fiber.join(fiber)).map((event) => [event.cursor, (event.event.data as { text: string }).text])).toEqual([
+      expect(
+        Array.from(yield* Fiber.join(fiber)).map((event) => [
+          event.cursor,
+          (event.event.data as { text: string }).text,
+        ]),
+      ).toEqual([
         [EventV2.Cursor.make(0), "zero"],
         [EventV2.Cursor.make(1), "one"],
       ])
@@ -336,7 +352,9 @@ describe("EventV2", () => {
       yield* Effect.gen(function* () {
         const events = yield* EventV2.Service
         const aggregateID = EventV2.ID.create()
-        const fiber = yield* events.aggregateEvents({ aggregateID }).pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped)
+        const fiber = yield* events
+          .aggregateEvents({ aggregateID })
+          .pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped)
         yield* Deferred.await(readStarted)
 
         pause = false
@@ -355,7 +373,9 @@ describe("EventV2", () => {
       const events = yield* EventV2.Service
       const aggregateID = EventV2.ID.create()
       const count = 64
-      const fiber = yield* events.aggregateEvents({ aggregateID }).pipe(Stream.take(count), Stream.runCollect, Effect.forkScoped)
+      const fiber = yield* events
+        .aggregateEvents({ aggregateID })
+        .pipe(Stream.take(count), Stream.runCollect, Effect.forkScoped)
       yield* Effect.yieldNow
 
       for (let index = 0; index < count; index++) {
@@ -363,7 +383,10 @@ describe("EventV2", () => {
       }
 
       expect(Array.from(yield* Fiber.join(fiber)).map((event) => [event.cursor, event.event.data])).toEqual(
-        Array.from({ length: count }, (_, index) => [EventV2.Cursor.make(index), { id: aggregateID, text: String(index) }]),
+        Array.from({ length: count }, (_, index) => [
+          EventV2.Cursor.make(index),
+          { id: aggregateID, text: String(index) },
+        ]),
       )
     }),
   )
@@ -372,7 +395,9 @@ describe("EventV2", () => {
     Effect.gen(function* () {
       const events = yield* EventV2.Service
       const aggregateID = EventV2.ID.create()
-      const fiber = yield* events.aggregateEvents({ aggregateID }).pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped)
+      const fiber = yield* events
+        .aggregateEvents({ aggregateID })
+        .pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped)
       yield* Effect.yieldNow
 
       yield* events.publish(Message, { text: "live only" })
@@ -450,47 +475,49 @@ describe("EventV2", () => {
     }),
   )
 
-  it.effect("replay rejects an envelope aggregate that differs from its payload without mutating the payload aggregate", () =>
-    Effect.gen(function* () {
-      const events = yield* EventV2.Service
-      const { db } = yield* Database.Service
-      const envelopeAggregateID = EventV2.ID.create()
-      const payloadAggregateID = EventV2.ID.create()
-      const received = new Array<EventV2.Payload>()
-      yield* events.publish(SyncMessage, { id: payloadAggregateID, text: "seed" })
-      yield* events.project(SyncMessage, (event) =>
-        Effect.sync(() => {
-          received.push(event)
-        }),
-      )
-
-      const exit = yield* events
-        .replay({
-          id: EventV2.ID.create(),
-          type: EventV2.versionedType(SyncMessage.type, 1),
-          seq: 1,
-          aggregateID: envelopeAggregateID,
-          data: { id: payloadAggregateID, text: "replayed" },
-        })
-        .pipe(Effect.exit)
-      const rows = yield* db
-        .select()
-        .from(EventTable)
-        .where(eq(EventTable.aggregate_id, payloadAggregateID))
-        .all()
-        .pipe(Effect.orDie)
-      const sequence = yield* db
-        .select({ seq: EventSequenceTable.seq })
-        .from(EventSequenceTable)
-        .where(eq(EventSequenceTable.aggregate_id, payloadAggregateID))
-        .get()
-        .pipe(Effect.orDie)
-
-      expect(String(exit)).toContain("Aggregate mismatch")
-      expect(received).toHaveLength(0)
-      expect(rows).toHaveLength(1)
-      expect(sequence).toEqual({ seq: 0 })
-    }),
+  it.effect(
+    "replay rejects an envelope aggregate that differs from its payload without mutating the payload aggregate",
+    () =>
+      Effect.gen(function* () {
+        const events = yield* EventV2.Service
+        const { db } = yield* Database.Service
+        const envelopeAggregateID = EventV2.ID.create()
+        const payloadAggregateID = EventV2.ID.create()
+        const received = new Array<EventV2.Payload>()
+        yield* events.publish(SyncMessage, { id: payloadAggregateID, text: "seed" })
+        yield* events.project(SyncMessage, (event) =>
+          Effect.sync(() => {
+            received.push(event)
+          }),
+        )
+
+        const exit = yield* events
+          .replay({
+            id: EventV2.ID.create(),
+            type: EventV2.versionedType(SyncMessage.type, 1),
+            seq: 1,
+            aggregateID: envelopeAggregateID,
+            data: { id: payloadAggregateID, text: "replayed" },
+          })
+          .pipe(Effect.exit)
+        const rows = yield* db
+          .select()
+          .from(EventTable)
+          .where(eq(EventTable.aggregate_id, payloadAggregateID))
+          .all()
+          .pipe(Effect.orDie)
+        const sequence = yield* db
+          .select({ seq: EventSequenceTable.seq })
+          .from(EventSequenceTable)
+          .where(eq(EventSequenceTable.aggregate_id, payloadAggregateID))
+          .get()
+          .pipe(Effect.orDie)
+
+        expect(String(exit)).toContain("Aggregate mismatch")
+        expect(received).toHaveLength(0)
+        expect(rows).toHaveLength(1)
+        expect(sequence).toEqual({ seq: 0 })
+      }),
   )
 
   it.effect("replay defects on sequence mismatch", () =>
@@ -750,16 +777,18 @@ describe("EventV2", () => {
         { ownerID: "owner-1" },
       )
 
-      const exit = yield* events.replay(
-        {
-          id: EventV2.ID.create(),
-          type: EventV2.versionedType(SyncMessage.type, 1),
-          seq: 1,
-          aggregateID,
-          data: { id: aggregateID, text: "conflict" },
-        },
-        { ownerID: "owner-2", strictOwner: true },
-      ).pipe(Effect.exit)
+      const exit = yield* events
+        .replay(
+          {
+            id: EventV2.ID.create(),
+            type: EventV2.versionedType(SyncMessage.type, 1),
+            seq: 1,
+            aggregateID,
+            data: { id: aggregateID, text: "conflict" },
+          },
+          { ownerID: "owner-2", strictOwner: true },
+        )
+        .pipe(Effect.exit)
 
       expect(String(exit)).toContain("Replay owner mismatch")
     }),

+ 51 - 9
packages/core/test/file-mutation.test.ts

@@ -89,7 +89,9 @@ describe("FileMutation", () => {
         const plan = yield* (yield* LocationMutation.Service).resolve({ path: "appeared.txt" })
         yield* Effect.promise(() => fs.writeFile(targetPath, "winner"))
 
-        expect(yield* (yield* FileMutation.Service).create({ plan, content: "replacement" }).pipe(Effect.flip)).toMatchObject({
+        expect(
+          yield* (yield* FileMutation.Service).create({ plan, content: "replacement" }).pipe(Effect.flip),
+        ).toMatchObject({
           _tag: "LocationMutation.RevalidationError",
         })
         expect(yield* Effect.promise(() => fs.readFile(targetPath, "utf8"))).toBe("winner")
@@ -105,8 +107,20 @@ describe("FileMutation", () => {
         const plan = yield* (yield* LocationMutation.Service).resolve({ path: "remove.txt" })
         const result = yield* (yield* FileMutation.Service).remove({ plan })
 
-        expect(result).toEqual({ operation: "remove", target: plan.target.canonical, resource: "remove.txt", existed: true })
-        expect(yield* Effect.promise(() => fs.stat(targetPath).then(() => true, () => false))).toBe(false)
+        expect(result).toEqual({
+          operation: "remove",
+          target: plan.target.canonical,
+          resource: "remove.txt",
+          existed: true,
+        })
+        expect(
+          yield* Effect.promise(() =>
+            fs.stat(targetPath).then(
+              () => true,
+              () => false,
+            ),
+          ),
+        ).toBe(false)
       }).pipe(provide(directory)),
     ),
   )
@@ -119,7 +133,12 @@ describe("FileMutation", () => {
           const plan = yield* (yield* LocationMutation.Service).resolve({ path: targetPath })
           const result = yield* (yield* FileMutation.Service).write({ plan, content: "external" })
 
-          expect(result).toEqual({ operation: "write", target: plan.target.canonical, resource: plan.target.resource, existed: false })
+          expect(result).toEqual({
+            operation: "write",
+            target: plan.target.canonical,
+            resource: plan.target.resource,
+            existed: false,
+          })
           expect(yield* Effect.promise(() => fs.readFile(targetPath, "utf8"))).toBe("external")
         }).pipe(provide(directory)),
       ),
@@ -135,8 +154,20 @@ describe("FileMutation", () => {
           const plan = yield* (yield* LocationMutation.Service).resolve({ path: targetPath })
           const result = yield* (yield* FileMutation.Service).remove({ plan })
 
-          expect(result).toEqual({ operation: "remove", target: plan.target.canonical, resource: plan.target.resource, existed: true })
-          expect(yield* Effect.promise(() => fs.stat(targetPath).then(() => true, () => false))).toBe(false)
+          expect(result).toEqual({
+            operation: "remove",
+            target: plan.target.canonical,
+            resource: plan.target.resource,
+            existed: true,
+          })
+          expect(
+            yield* Effect.promise(() =>
+              fs.stat(targetPath).then(
+                () => true,
+                () => false,
+              ),
+            ),
+          ).toBe(false)
         }).pipe(provide(directory)),
       ),
     ),
@@ -155,10 +186,19 @@ describe("FileMutation", () => {
             await fs.symlink(outside, parent)
           })
 
-          expect(yield* (yield* FileMutation.Service).write({ plan, content: "escape" }).pipe(Effect.flip)).toMatchObject({
+          expect(
+            yield* (yield* FileMutation.Service).write({ plan, content: "escape" }).pipe(Effect.flip),
+          ).toMatchObject({
             _tag: "LocationMutation.RevalidationError",
           })
-          expect(yield* Effect.promise(() => fs.stat(path.join(outside, "new.txt")).then(() => true, () => false))).toBe(false)
+          expect(
+            yield* Effect.promise(() =>
+              fs.stat(path.join(outside, "new.txt")).then(
+                () => true,
+                () => false,
+              ),
+            ),
+          ).toBe(false)
         }).pipe(provide(directory)),
       ),
     ),
@@ -233,7 +273,9 @@ describe("FileMutation", () => {
           const expected = new TextEncoder().encode("initial")
           const first = yield* files.writeIfUnchanged({ plan, expected, content: "first" }).pipe(Effect.forkChild)
           yield* Deferred.await(firstStarted)
-          const second = yield* files.writeIfUnchanged({ plan, expected, content: "second" }).pipe(Effect.flip, Effect.forkChild)
+          const second = yield* files
+            .writeIfUnchanged({ plan, expected, content: "second" })
+            .pipe(Effect.flip, Effect.forkChild)
 
           yield* Deferred.succeed(releaseFirst, undefined)
           yield* Fiber.join(first)

+ 14 - 11
packages/core/test/location-search.test.ts

@@ -27,11 +27,11 @@ function provide(directory: string, projectReferences = inertReferences) {
   )
   const filesystem = FileSystem.layer.pipe(Layer.provide(dependencies))
   const search = LocationSearch.layer.pipe(
-      Layer.provide(filesystem),
-      Layer.provide(Ripgrep.layer.pipe(Layer.provide(dependencies))),
-      Layer.provide(FSUtil.defaultLayer),
-      Layer.provide(dependencies),
-    )
+    Layer.provide(filesystem),
+    Layer.provide(Ripgrep.layer.pipe(Layer.provide(dependencies))),
+    Layer.provide(FSUtil.defaultLayer),
+    Layer.provide(dependencies),
+  )
   return Effect.provide(Layer.merge(filesystem, search))
 }
 
@@ -138,10 +138,9 @@ describe("LocationSearch", () => {
           RelativePath.make("visible.txt"),
         ])
         expect((yield* search.files({ pattern: ".env" })).items).toEqual([])
-        expect((yield* search.grep({ pattern: "needle", include: "*" })).items.map((item) => item.path).sort()).toEqual([
-          RelativePath.make("nested/visible.txt"),
-          RelativePath.make("visible.txt"),
-        ])
+        expect((yield* search.grep({ pattern: "needle", include: "*" })).items.map((item) => item.path).sort()).toEqual(
+          [RelativePath.make("nested/visible.txt"), RelativePath.make("visible.txt")],
+        )
       }).pipe(provide(directory)),
     ),
   )
@@ -191,7 +190,9 @@ describe("LocationSearch", () => {
   it.live("rejects oversized ripgrep JSON records before durable projection", () =>
     withTmp((directory) =>
       Effect.gen(function* () {
-        yield* Effect.promise(() => fs.writeFile(path.join(directory, "huge.txt"), `needle ${"x".repeat(Ripgrep.MAX_RECORD_BYTES)}\n`))
+        yield* Effect.promise(() =>
+          fs.writeFile(path.join(directory, "huge.txt"), `needle ${"x".repeat(Ripgrep.MAX_RECORD_BYTES)}\n`),
+        )
         const exit = yield* (yield* LocationSearch.Service).grep({ pattern: "needle" }).pipe(Effect.exit)
 
         expect(Exit.isFailure(exit)).toBe(true)
@@ -243,7 +244,9 @@ describe("LocationSearch", () => {
           await fs.symlink(outside, source)
         })
 
-        expect(Exit.isFailure(yield* (yield* LocationSearch.Service).files({ pattern: "*" }, approved).pipe(Effect.exit))).toBe(true)
+        expect(
+          Exit.isFailure(yield* (yield* LocationSearch.Service).files({ pattern: "*" }, approved).pipe(Effect.exit)),
+        ).toBe(true)
         yield* Effect.promise(() => fs.rm(outside, { recursive: true, force: true }))
       }).pipe(provide(directory)),
     ),

+ 22 - 11
packages/core/test/patch.test.ts

@@ -4,10 +4,17 @@ import { Patch } from "@opencode-ai/core/patch"
 describe("Patch", () => {
   test("parses add, update, and delete hunks", () => {
     expect(
-      Patch.parse("*** Begin Patch\n*** Add File: add.txt\n+added\n*** Update File: update.txt\n@@ section\n-old\n+new\n*** Delete File: delete.txt\n*** End Patch"),
+      Patch.parse(
+        "*** Begin Patch\n*** Add File: add.txt\n+added\n*** Update File: update.txt\n@@ section\n-old\n+new\n*** Delete File: delete.txt\n*** End Patch",
+      ),
     ).toEqual([
       { type: "add", path: "add.txt", contents: "added" },
-      { type: "update", path: "update.txt", chunks: [{ oldLines: ["old"], newLines: ["new"], changeContext: "section", endOfFile: undefined }], movePath: undefined },
+      {
+        type: "update",
+        path: "update.txt",
+        chunks: [{ oldLines: ["old"], newLines: ["new"], changeContext: "section", endOfFile: undefined }],
+        movePath: undefined,
+      },
       { type: "delete", path: "delete.txt" },
     ])
   })
@@ -19,11 +26,7 @@ describe("Patch", () => {
   })
 
   test("derives fuzzy line updates while preserving BOM", () => {
-    const update = Patch.derive(
-      "update.txt",
-      [{ oldLines: ["  old   "], newLines: ["new"] }],
-      "\uFEFFold\n",
-    )
+    const update = Patch.derive("update.txt", [{ oldLines: ["  old   "], newLines: ["new"] }], "\uFEFFold\n")
     expect(update).toEqual({ content: "new\n", bom: true })
     expect(Patch.joinBom(update.content, update.bom)).toBe("\uFEFFnew\n")
   })
@@ -39,7 +42,9 @@ describe("Patch", () => {
   })
 
   test("parses the EOF marker inside update chunks", () => {
-    expect(Patch.parse("*** Begin Patch\n*** Update File: update.txt\n@@\n-last\n+end\n*** End of File\n*** End Patch")).toEqual([
+    expect(
+      Patch.parse("*** Begin Patch\n*** Update File: update.txt\n@@\n-last\n+end\n*** End of File\n*** End Patch"),
+    ).toEqual([
       {
         type: "update",
         path: "update.txt",
@@ -50,8 +55,14 @@ describe("Patch", () => {
   })
 
   test("rejects malformed hunk bodies", () => {
-    expect(() => Patch.parse("*** Begin Patch\n*** Add File: add.txt\nmissing plus\n*** End Patch")).toThrow("Invalid add file line")
-    expect(() => Patch.parse("*** Begin Patch\n*** Update File: update.txt\n*** End Patch")).toThrow("expected at least one @@ chunk")
-    expect(() => Patch.parse("*** Begin Patch\n*** Delete File: delete.txt\nunexpected body\n*** End Patch")).toThrow("Invalid patch line")
+    expect(() => Patch.parse("*** Begin Patch\n*** Add File: add.txt\nmissing plus\n*** End Patch")).toThrow(
+      "Invalid add file line",
+    )
+    expect(() => Patch.parse("*** Begin Patch\n*** Update File: update.txt\n*** End Patch")).toThrow(
+      "expected at least one @@ chunk",
+    )
+    expect(() => Patch.parse("*** Begin Patch\n*** Delete File: delete.txt\nunexpected body\n*** End Patch")).toThrow(
+      "Invalid patch line",
+    )
   })
 })

+ 7 - 1
packages/core/test/permission.test.ts

@@ -25,7 +25,13 @@ const current = Layer.succeed(
 )
 const events = EventV2.layer.pipe(Layer.provide(database))
 const store = SessionStore.layer.pipe(Layer.provide(database))
-const sessions = SessionV2.layer.pipe(Layer.provide(events), Layer.provide(database), Layer.provide(store), Layer.provide(Project.defaultLayer), Layer.provide(SessionExecution.noopLayer))
+const sessions = SessionV2.layer.pipe(
+  Layer.provide(events),
+  Layer.provide(database),
+  Layer.provide(store),
+  Layer.provide(Project.defaultLayer),
+  Layer.provide(SessionExecution.noopLayer),
+)
 const saved = PermissionSaved.layer.pipe(Layer.provide(database))
 const layer = PermissionV2.locationLayer.pipe(
   Layer.provideMerge(database),

+ 3 - 1
packages/core/test/session-create.test.ts

@@ -234,7 +234,9 @@ describe("SessionV2.create", () => {
     Effect.gen(function* () {
       const session = yield* SessionV2.Service
       const created = yield* session.create({ location })
-      const unavailable = (effect: Effect.Effect<void, SessionV2.NotFoundError | SessionV2.OperationUnavailableError>) =>
+      const unavailable = (
+        effect: Effect.Effect<void, SessionV2.NotFoundError | SessionV2.OperationUnavailableError>,
+      ) =>
         effect.pipe(
           Effect.flip,
           Effect.map((error) => (error instanceof SessionV2.OperationUnavailableError ? error.operation : "not-found")),

+ 33 - 16
packages/core/test/session-projector.test.ts

@@ -87,19 +87,15 @@ describe("SessionProjector", () => {
       })
       expect(secondPage.map((message) => (message.type === "user" ? message.text : message.type))).toEqual(["second"])
       expect(
-        (
-          yield* sessions.messages({
-            sessionID,
-            limit: 1,
-            order: "asc",
-            cursor: { id: secondPage[0]!.id, direction: "previous" },
-          })
-        ).map((message) => (message.type === "user" ? message.text : message.type)),
+        (yield* sessions.messages({
+          sessionID,
+          limit: 1,
+          order: "asc",
+          cursor: { id: secondPage[0]!.id, direction: "previous" },
+        })).map((message) => (message.type === "user" ? message.text : message.type)),
       ).toEqual(["first"])
       expect(
-        (yield* sessions.context(sessionID)).map((message) =>
-          message.type === "user" ? message.text : message.type,
-        ),
+        (yield* sessions.context(sessionID)).map((message) => (message.type === "user" ? message.text : message.type)),
       ).toEqual(["first", "second"])
     }).pipe(
       Effect.provide(
@@ -222,7 +218,9 @@ describe("SessionProjector", () => {
         summary: "summary",
         include: "msg-1",
       })
-      expect(yield* db.select().from(SessionTable).where(eq(SessionTable.id, sessionID)).get().pipe(Effect.orDie)).toMatchObject({
+      expect(
+        yield* db.select().from(SessionTable).where(eq(SessionTable.id, sessionID)).get().pipe(Effect.orDie),
+      ).toMatchObject({
         agent: "build",
         model,
         time_updated: DateTime.toEpochMillis(created),
@@ -272,17 +270,36 @@ describe("SessionProjector", () => {
   it.effect("rejects a Prompted delivery mode that conflicts with an admitted inbox row", () =>
     Effect.gen(function* () {
       const { db } = yield* Database.Service
-      yield* db.insert(ProjectTable).values({ id: Project.ID.global, worktree: AbsolutePath.make("/project"), sandboxes: [] }).run().pipe(Effect.orDie)
-      yield* db.insert(SessionTable).values({ id: sessionID, project_id: Project.ID.global, slug: "test", directory: "/project", title: "test", version: "test" }).run().pipe(Effect.orDie)
+      yield* db
+        .insert(ProjectTable)
+        .values({ id: Project.ID.global, worktree: AbsolutePath.make("/project"), sandboxes: [] })
+        .run()
+        .pipe(Effect.orDie)
+      yield* db
+        .insert(SessionTable)
+        .values({
+          id: sessionID,
+          project_id: Project.ID.global,
+          slug: "test",
+          directory: "/project",
+          title: "test",
+          version: "test",
+        })
+        .run()
+        .pipe(Effect.orDie)
       const events = yield* EventV2.Service
       const id = SessionMessage.ID.make("evt_delivery_conflict")
       const prompt = new Prompt({ text: "admitted" })
       yield* SessionInput.admit(db, { id, sessionID, prompt, delivery: "queue" })
 
-      const exit = yield* events.publish(SessionEvent.Prompted, { sessionID, timestamp: created, prompt, delivery: "steer" }, { id }).pipe(Effect.exit)
+      const exit = yield* events
+        .publish(SessionEvent.Prompted, { sessionID, timestamp: created, prompt, delivery: "steer" }, { id })
+        .pipe(Effect.exit)
 
       expect(String(exit)).toContain("Prompt projection conflicts with admitted input")
-      expect(yield* db.select().from(SessionInputTable).where(eq(SessionInputTable.id, id)).get().pipe(Effect.orDie)).toMatchObject({ delivery: "queue", promoted_seq: null })
+      expect(
+        yield* db.select().from(SessionInputTable).where(eq(SessionInputTable.id, id)).get().pipe(Effect.orDie),
+      ).toMatchObject({ delivery: "queue", promoted_seq: null })
     }),
   )
 

+ 3 - 1
packages/core/test/session-prompt.test.ts

@@ -383,7 +383,9 @@ describe("SessionV2.prompt", () => {
       yield* SessionInput.promoteSteers(db, events, sessionID)
 
       expect(yield* admitted(messageID)).toMatchObject({ promotedSeq: 0 })
-      expect(yield* session.messages({ sessionID })).toMatchObject([{ id: messageID, type: "user", text: "Reserved prompt" }])
+      expect(yield* session.messages({ sessionID })).toMatchObject([
+        { id: messageID, type: "user", text: "Reserved prompt" },
+      ])
     }),
   )
 

+ 159 - 142
packages/core/test/session-runner-message.test.ts

@@ -19,46 +19,54 @@ describe("toLLMMessages", () => {
   test("maps every top-level V2 Session message type", () => {
     const file = new FileAttachment({ uri: "data:image/png;base64,aGVsbG8=", mime: "image/png", name: "hello.png" })
     const reference = new ReferenceAttachment({ name: "docs", kind: "local", uri: "file:///docs" })
-    const messages = toLLMMessages([
-      new SessionMessage.AgentSwitched({ id: id("agent"), type: "agent-switched", agent: "build", time: { created } }),
-      new SessionMessage.ModelSwitched({
-        id: id("model"),
-        type: "model-switched",
-        model: { id: ModelV2.ID.make("model"), providerID: ProviderV2.ID.make("provider") },
-        time: { created },
-      }),
-      new SessionMessage.User({
-        id: id("user"),
-        type: "user",
-        text: "Inspect this image",
-        files: [file],
-        agents: [new AgentAttachment({ name: "build" })],
-        references: [reference],
-        time: { created },
-      }),
-      new SessionMessage.Synthetic({
-        id: id("synthetic"),
-        type: "synthetic",
-        sessionID: SessionV2.ID.make("ses_translate"),
-        text: "Synthetic context",
-        time: { created },
-      }),
-      new SessionMessage.Shell({
-        id: id("shell"),
-        type: "shell",
-        callID: "shell-1",
-        command: "pwd",
-        output: "/project",
-        time: { created, completed: created },
-      }),
-      new SessionMessage.Compaction({
-        id: id("compaction"),
-        type: "compaction",
-        reason: "auto",
-        summary: "Earlier work",
-        time: { created },
-      }),
-    ], model)
+    const messages = toLLMMessages(
+      [
+        new SessionMessage.AgentSwitched({
+          id: id("agent"),
+          type: "agent-switched",
+          agent: "build",
+          time: { created },
+        }),
+        new SessionMessage.ModelSwitched({
+          id: id("model"),
+          type: "model-switched",
+          model: { id: ModelV2.ID.make("model"), providerID: ProviderV2.ID.make("provider") },
+          time: { created },
+        }),
+        new SessionMessage.User({
+          id: id("user"),
+          type: "user",
+          text: "Inspect this image",
+          files: [file],
+          agents: [new AgentAttachment({ name: "build" })],
+          references: [reference],
+          time: { created },
+        }),
+        new SessionMessage.Synthetic({
+          id: id("synthetic"),
+          type: "synthetic",
+          sessionID: SessionV2.ID.make("ses_translate"),
+          text: "Synthetic context",
+          time: { created },
+        }),
+        new SessionMessage.Shell({
+          id: id("shell"),
+          type: "shell",
+          callID: "shell-1",
+          command: "pwd",
+          output: "/project",
+          time: { created, completed: created },
+        }),
+        new SessionMessage.Compaction({
+          id: id("compaction"),
+          type: "compaction",
+          reason: "auto",
+          summary: "Earlier work",
+          time: { created },
+        }),
+      ],
+      model,
+    )
 
     expect(messages.map((message) => message.role)).toEqual(["user", "user", "user", "user"])
     expect(messages[0]).toEqual(
@@ -80,94 +88,97 @@ describe("toLLMMessages", () => {
   })
 
   test("expands assistant tool calls and settled outcomes into canonical tool messages", () => {
-    const messages = toLLMMessages([
-      new SessionMessage.Assistant({
-        id: id("assistant"),
-        type: "assistant",
-        agent: "build",
-        model: { id: ModelV2.ID.make("model"), providerID: ProviderV2.ID.make("provider") },
-        content: [
-          new SessionMessage.AssistantText({ type: "text", id: "text-1", text: "Checking" }),
-          new SessionMessage.AssistantReasoning({
-            type: "reasoning",
-            id: "reasoning-1",
-            text: "Think",
-            providerMetadata: { anthropic: { signature: "sig_1" } },
-          }),
-          new SessionMessage.AssistantTool({
-            type: "tool",
-            id: "pending",
-            name: "read",
-            state: new SessionMessage.ToolStatePending({ status: "pending", input: '{"path":"README.md"}' }),
-            time: { created },
-          }),
-          new SessionMessage.AssistantTool({
-            type: "tool",
-            id: "running",
-            name: "read",
-            state: new SessionMessage.ToolStateRunning({
-              status: "running",
-              input: { path: "README.md" },
-              content: [],
-              structured: {},
+    const messages = toLLMMessages(
+      [
+        new SessionMessage.Assistant({
+          id: id("assistant"),
+          type: "assistant",
+          agent: "build",
+          model: { id: ModelV2.ID.make("model"), providerID: ProviderV2.ID.make("provider") },
+          content: [
+            new SessionMessage.AssistantText({ type: "text", id: "text-1", text: "Checking" }),
+            new SessionMessage.AssistantReasoning({
+              type: "reasoning",
+              id: "reasoning-1",
+              text: "Think",
+              providerMetadata: { anthropic: { signature: "sig_1" } },
             }),
-            time: { created },
-          }),
-          new SessionMessage.AssistantTool({
-            type: "tool",
-            id: "completed",
-            name: "read",
-            state: new SessionMessage.ToolStateCompleted({
-              status: "completed",
-              input: { path: "README.md" },
-              content: [
-                new ToolOutput.TextContent({ type: "text", text: "Hello" }),
-                new ToolOutput.FileContent({
-                  type: "file",
-                  source: { type: "data", data: "aGVsbG8=" },
-                  mime: "image/png",
-                  name: "hello.png",
-                }),
-              ],
-              structured: {},
+            new SessionMessage.AssistantTool({
+              type: "tool",
+              id: "pending",
+              name: "read",
+              state: new SessionMessage.ToolStatePending({ status: "pending", input: '{"path":"README.md"}' }),
+              time: { created },
             }),
-            time: { created, completed: created },
-          }),
-          new SessionMessage.AssistantTool({
-            type: "tool",
-            id: "hosted",
-            name: "web_search",
-            provider: {
-              executed: true,
-              metadata: { fake: { continuation: "hosted-call" } },
-              resultMetadata: { fake: { continuation: "hosted-result" } },
-            },
-            state: new SessionMessage.ToolStateCompleted({
-              status: "completed",
-              input: { query: "Effect" },
-              content: [new ToolOutput.TextContent({ type: "text", text: "Found it" })],
-              structured: {},
+            new SessionMessage.AssistantTool({
+              type: "tool",
+              id: "running",
+              name: "read",
+              state: new SessionMessage.ToolStateRunning({
+                status: "running",
+                input: { path: "README.md" },
+                content: [],
+                structured: {},
+              }),
+              time: { created },
             }),
-            time: { created, completed: created },
-          }),
-          new SessionMessage.AssistantTool({
-            type: "tool",
-            id: "hosted-failed",
-            name: "write",
-            provider: { executed: true, metadata: { fake: { continuation: "failed" } } },
-            state: new SessionMessage.ToolStateError({
-              status: "error",
-              input: { path: "README.md" },
-              content: [],
-              structured: {},
-              error: { type: "unknown", message: "Denied" },
+            new SessionMessage.AssistantTool({
+              type: "tool",
+              id: "completed",
+              name: "read",
+              state: new SessionMessage.ToolStateCompleted({
+                status: "completed",
+                input: { path: "README.md" },
+                content: [
+                  new ToolOutput.TextContent({ type: "text", text: "Hello" }),
+                  new ToolOutput.FileContent({
+                    type: "file",
+                    source: { type: "data", data: "aGVsbG8=" },
+                    mime: "image/png",
+                    name: "hello.png",
+                  }),
+                ],
+                structured: {},
+              }),
+              time: { created, completed: created },
             }),
-            time: { created, completed: created },
-          }),
-        ],
-        time: { created, completed: created },
-      }),
-    ], model)
+            new SessionMessage.AssistantTool({
+              type: "tool",
+              id: "hosted",
+              name: "web_search",
+              provider: {
+                executed: true,
+                metadata: { fake: { continuation: "hosted-call" } },
+                resultMetadata: { fake: { continuation: "hosted-result" } },
+              },
+              state: new SessionMessage.ToolStateCompleted({
+                status: "completed",
+                input: { query: "Effect" },
+                content: [new ToolOutput.TextContent({ type: "text", text: "Found it" })],
+                structured: {},
+              }),
+              time: { created, completed: created },
+            }),
+            new SessionMessage.AssistantTool({
+              type: "tool",
+              id: "hosted-failed",
+              name: "write",
+              provider: { executed: true, metadata: { fake: { continuation: "failed" } } },
+              state: new SessionMessage.ToolStateError({
+                status: "error",
+                input: { path: "README.md" },
+                content: [],
+                structured: {},
+                error: { type: "unknown", message: "Denied" },
+              }),
+              time: { created, completed: created },
+            }),
+          ],
+          time: { created, completed: created },
+        }),
+      ],
+      model,
+    )
 
     expect(messages.map((message) => message.role)).toEqual(["assistant", "tool"])
     expect(messages[0]?.content).toEqual([
@@ -211,7 +222,10 @@ describe("toLLMMessages", () => {
         name: "write",
         providerExecuted: true,
         providerMetadata: { fake: { continuation: "failed" } },
-        result: { type: "error", value: { error: { type: "unknown", message: "Denied" }, content: [], structured: {} } },
+        result: {
+          type: "error",
+          value: { error: { type: "unknown", message: "Denied" }, content: [], structured: {} },
+        },
       },
     ])
     expect(messages[1]?.content).toEqual([
@@ -231,23 +245,26 @@ describe("toLLMMessages", () => {
   })
 
   test("restores OpenAI encrypted reasoning metadata", () => {
-    const messages = toLLMMessages([
-      new SessionMessage.Assistant({
-        id: id("assistant-openai-reasoning"),
-        type: "assistant",
-        agent: "build",
-        model: { id: ModelV2.ID.make("model"), providerID: ProviderV2.ID.make("provider") },
-        content: [
-          new SessionMessage.AssistantReasoning({
-            type: "reasoning",
-            id: "reasoning-openai",
-            text: "Think",
-            providerMetadata: { openai: { itemId: "rs_1", reasoningEncryptedContent: "encrypted-state" } },
-          }),
-        ],
-        time: { created, completed: created },
-      }),
-    ], model)
+    const messages = toLLMMessages(
+      [
+        new SessionMessage.Assistant({
+          id: id("assistant-openai-reasoning"),
+          type: "assistant",
+          agent: "build",
+          model: { id: ModelV2.ID.make("model"), providerID: ProviderV2.ID.make("provider") },
+          content: [
+            new SessionMessage.AssistantReasoning({
+              type: "reasoning",
+              id: "reasoning-openai",
+              text: "Think",
+              providerMetadata: { openai: { itemId: "rs_1", reasoningEncryptedContent: "encrypted-state" } },
+            }),
+          ],
+          time: { created, completed: created },
+        }),
+      ],
+      model,
+    )
 
     expect(messages[0]?.content).toEqual([
       {

+ 3 - 1
packages/core/test/session-runner-tool-registry.test.ts

@@ -13,7 +13,9 @@ const permission = Layer.succeed(
   PermissionV2.Service.of({
     assert: (input) =>
       Effect.sync(() => assertions.push(input)).pipe(
-        Effect.andThen(input.action === denyAction ? Effect.fail(new PermissionV2.DeniedError({ rules: [] })) : Effect.void),
+        Effect.andThen(
+          input.action === denyAction ? Effect.fail(new PermissionV2.DeniedError({ rules: [] })) : Effect.void,
+        ),
       ),
     ask: () => Effect.die("unused"),
     reply: () => Effect.die("unused"),

+ 5 - 1
packages/core/test/session-runner.test.ts

@@ -1979,7 +1979,11 @@ describe("SessionRunnerLLM", () => {
     Effect.gen(function* () {
       yield* setup
       const session = yield* SessionV2.Service
-      yield* session.prompt({ sessionID, prompt: new Prompt({ text: "Fail hosted tool on raw failure" }), resume: false })
+      yield* session.prompt({
+        sessionID,
+        prompt: new Prompt({ text: "Fail hosted tool on raw failure" }),
+        resume: false,
+      })
       const failure = providerUnavailable()
       responseStream = Stream.concat(
         Stream.fromIterable([

+ 106 - 17
packages/core/test/session-tool-progress.test.ts

@@ -32,36 +32,125 @@ describe("Tool.Progress", () => {
       const { db } = yield* Database.Service
       const service = yield* EventV2.Service
       const sessionID = SessionV2.ID.make("ses_tool_progress_projector")
-      yield* db.insert(ProjectTable).values({ id: Project.ID.global, worktree: AbsolutePath.make("/project"), sandboxes: [] }).onConflictDoNothing().run().pipe(Effect.orDie)
-      yield* db.insert(SessionTable).values({ id: sessionID, project_id: Project.ID.global, slug: "progress", directory: "/project", title: "progress", version: "test" }).run().pipe(Effect.orDie)
-      const assistantMessageID = (yield* service.publish(SessionEvent.Step.Started, { sessionID, timestamp, agent: "build", model })).id
+      yield* db
+        .insert(ProjectTable)
+        .values({ id: Project.ID.global, worktree: AbsolutePath.make("/project"), sandboxes: [] })
+        .onConflictDoNothing()
+        .run()
+        .pipe(Effect.orDie)
+      yield* db
+        .insert(SessionTable)
+        .values({
+          id: sessionID,
+          project_id: Project.ID.global,
+          slug: "progress",
+          directory: "/project",
+          title: "progress",
+          version: "test",
+        })
+        .run()
+        .pipe(Effect.orDie)
+      const assistantMessageID = (yield* service.publish(SessionEvent.Step.Started, {
+        sessionID,
+        timestamp,
+        agent: "build",
+        model,
+      })).id
       const readAssistant = Effect.gen(function* () {
-        const row = yield* db.select().from(SessionMessageTable).where(eq(SessionMessageTable.id, assistantMessageID)).get().pipe(Effect.orDie)
+        const row = yield* db
+          .select()
+          .from(SessionMessageTable)
+          .where(eq(SessionMessageTable.id, assistantMessageID))
+          .get()
+          .pipe(Effect.orDie)
         if (!row) return yield* Effect.die("Missing projected assistant")
         return Schema.decodeUnknownSync(SessionMessage.Assistant)({ ...row.data, id: row.id, type: row.type })
       })
-      const start = (callID: string) => Effect.gen(function* () {
-        yield* service.publish(SessionEvent.Tool.Input.Started, { sessionID, timestamp, assistantMessageID, callID, name: "bash" })
-        yield* service.publish(SessionEvent.Tool.Called, { sessionID, timestamp, assistantMessageID, callID, tool: "bash", input: { command: "pwd" }, provider: { executed: false } })
-      })
+      const start = (callID: string) =>
+        Effect.gen(function* () {
+          yield* service.publish(SessionEvent.Tool.Input.Started, {
+            sessionID,
+            timestamp,
+            assistantMessageID,
+            callID,
+            name: "bash",
+          })
+          yield* service.publish(SessionEvent.Tool.Called, {
+            sessionID,
+            timestamp,
+            assistantMessageID,
+            callID,
+            tool: "bash",
+            input: { command: "pwd" },
+            provider: { executed: false },
+          })
+        })
 
       yield* start("call-success")
-      expect((yield* readAssistant).content[0]).toMatchObject({ state: { status: "running", structured: {}, content: [] } })
+      expect((yield* readAssistant).content[0]).toMatchObject({
+        state: { status: "running", structured: {}, content: [] },
+      })
 
-      yield* service.publish(SessionEvent.Tool.Progress, { sessionID, timestamp, assistantMessageID, callID: "call-success", structured: { phase: "checkpoint" }, content: content("saved") })
-      expect((yield* readAssistant).content[0]).toMatchObject({ state: { status: "running", structured: { phase: "checkpoint" }, content: content("saved") } })
+      yield* service.publish(SessionEvent.Tool.Progress, {
+        sessionID,
+        timestamp,
+        assistantMessageID,
+        callID: "call-success",
+        structured: { phase: "checkpoint" },
+        content: content("saved"),
+      })
+      expect((yield* readAssistant).content[0]).toMatchObject({
+        state: { status: "running", structured: { phase: "checkpoint" }, content: content("saved") },
+      })
 
-      const success = yield* service.publish(SessionEvent.Tool.Success, { sessionID, timestamp, assistantMessageID, callID: "call-success", structured: { phase: "done" }, content: content("complete"), provider: { executed: false } })
-      expect((yield* readAssistant).content[0]).toMatchObject({ state: { status: "completed", structured: { phase: "done" }, content: content("complete") } })
+      const success = yield* service.publish(SessionEvent.Tool.Success, {
+        sessionID,
+        timestamp,
+        assistantMessageID,
+        callID: "call-success",
+        structured: { phase: "done" },
+        content: content("complete"),
+        provider: { executed: false },
+      })
+      expect((yield* readAssistant).content[0]).toMatchObject({
+        state: { status: "completed", structured: { phase: "done" }, content: content("complete") },
+      })
 
       yield* start("call-failed")
-      yield* service.publish(SessionEvent.Tool.Progress, { sessionID, timestamp, assistantMessageID, callID: "call-failed", structured: { phase: "checkpoint" }, content: content("before failure") })
-      const failed = yield* service.publish(SessionEvent.Tool.Failed, { sessionID, timestamp, assistantMessageID, callID: "call-failed", error: { type: "unknown", message: "boom" }, provider: { executed: false } })
-      expect((yield* readAssistant).content[1]).toMatchObject({ state: { status: "error", structured: { phase: "checkpoint" }, content: content("before failure"), error: { type: "unknown", message: "boom" } } })
+      yield* service.publish(SessionEvent.Tool.Progress, {
+        sessionID,
+        timestamp,
+        assistantMessageID,
+        callID: "call-failed",
+        structured: { phase: "checkpoint" },
+        content: content("before failure"),
+      })
+      const failed = yield* service.publish(SessionEvent.Tool.Failed, {
+        sessionID,
+        timestamp,
+        assistantMessageID,
+        callID: "call-failed",
+        error: { type: "unknown", message: "boom" },
+        provider: { executed: false },
+      })
+      expect((yield* readAssistant).content[1]).toMatchObject({
+        state: {
+          status: "error",
+          structured: { phase: "checkpoint" },
+          content: content("before failure"),
+          error: { type: "unknown", message: "boom" },
+        },
+      })
       expect(Schema.is(SessionEvent.Durable)(success)).toBe(true)
       expect(Schema.is(SessionEvent.Durable)(failed)).toBe(true)
 
-      const rows = yield* db.select({ type: EventTable.type }).from(EventTable).where(eq(EventTable.aggregate_id, sessionID)).orderBy(asc(EventTable.seq)).all().pipe(Effect.orDie)
+      const rows = yield* db
+        .select({ type: EventTable.type })
+        .from(EventTable)
+        .where(eq(EventTable.aggregate_id, sessionID))
+        .orderBy(asc(EventTable.seq))
+        .all()
+        .pipe(Effect.orDie)
       expect(rows.map((row) => row.type)).toContain(EventV2.versionedType(SessionEvent.Tool.Progress.type, 1))
       expect(rows.map((row) => row.type)).toContain(EventV2.versionedType(SessionEvent.Tool.Success.type, 1))
       expect(rows.map((row) => row.type)).toContain(EventV2.versionedType(SessionEvent.Tool.Failed.type, 1))

+ 4 - 7
packages/core/test/skill-discovery.test.ts

@@ -86,13 +86,10 @@ describe("SkillDiscovery.pull", () => {
   })
 
   test("downloads safe nested files under the skill root", async () => {
-    const result = await pull(
-      [{ name: "deploy", files: ["SKILL.md", "references/guide.md"] }],
-      {
-        [`${base}deploy/SKILL.md`]: "# Deploy",
-        [`${base}deploy/references/guide.md`]: "# Guide",
-      },
-    )
+    const result = await pull([{ name: "deploy", files: ["SKILL.md", "references/guide.md"] }], {
+      [`${base}deploy/SKILL.md`]: "# Deploy",
+      [`${base}deploy/references/guide.md`]: "# Guide",
+    })
     try {
       expect(result.directories).toHaveLength(1)
       expect(result.requests.toSorted()).toEqual(

+ 65 - 17
packages/core/test/tool-apply-patch.test.ts

@@ -33,7 +33,9 @@ const permission = Layer.succeed(
         assertions.push(input)
         if (input.action === "edit") editApproved = true
       }).pipe(
-        Effect.andThen(input.action === denyAction ? Effect.fail(new PermissionV2.DeniedError({ rules: [] })) : Effect.void),
+        Effect.andThen(
+          input.action === denyAction ? Effect.fail(new PermissionV2.DeniedError({ rules: [] })) : Effect.void,
+        ),
       ),
     ask: () => Effect.die("unused"),
     reply: () => Effect.die("unused"),
@@ -67,7 +69,10 @@ const filesystem = Layer.effect(
       remove: (target, options) => {
         if (failRemoveTarget && path.basename(target) === failRemoveTarget) return Effect.die("forced remove failure")
         if (blockRemoveTarget && path.basename(target) === blockRemoveTarget && removeStarted && releaseRemove)
-          return Deferred.succeed(removeStarted, undefined).pipe(Effect.andThen(Deferred.await(releaseRemove)), Effect.andThen(fs.remove(target, options)))
+          return Deferred.succeed(removeStarted, undefined).pipe(
+            Effect.andThen(Deferred.await(releaseRemove)),
+            Effect.andThen(fs.remove(target, options)),
+          )
         return fs.remove(target, options)
       },
     })
@@ -98,7 +103,13 @@ const call = (patchText: string, id = "call-apply-patch") => ({
   call: { type: "tool-call" as const, id, name: "apply_patch", input: { patchText } },
 })
 
-const exists = (target: string) => Effect.promise(() => fs.stat(target).then(() => true, () => false))
+const exists = (target: string) =>
+  Effect.promise(() =>
+    fs.stat(target).then(
+      () => true,
+      () => false,
+    ),
+  )
 const it = testEffect(Layer.empty)
 
 describe("ApplyPatchTool", () => {
@@ -109,13 +120,17 @@ describe("ApplyPatchTool", () => {
         reset()
         const update = path.join(tmp.path, "update.txt")
         const remove = path.join(tmp.path, "remove.txt")
-        return Effect.promise(() => Promise.all([fs.writeFile(update, "before\n"), fs.writeFile(remove, "remove\n")])).pipe(
+        return Effect.promise(() =>
+          Promise.all([fs.writeFile(update, "before\n"), fs.writeFile(remove, "remove\n")]),
+        ).pipe(
           Effect.andThen(
             withTool(tmp.path, (registry) =>
               Effect.gen(function* () {
                 expect((yield* registry.definitions()).map((tool) => tool.name)).toEqual(["apply_patch"])
                 const settled = yield* registry.settle(
-                  call("*** Begin Patch\n*** Add File: nested/new.txt\n+created\n*** Update File: update.txt\n@@\n-before\n+after\n*** Delete File: remove.txt\n*** End Patch"),
+                  call(
+                    "*** Begin Patch\n*** Add File: nested/new.txt\n+created\n*** Update File: update.txt\n@@\n-before\n+after\n*** Delete File: remove.txt\n*** End Patch",
+                  ),
                 )
                 expect(settled.result).toEqual({
                   type: "text",
@@ -132,7 +147,9 @@ describe("ApplyPatchTool", () => {
                   { sessionID, action: "edit", resources: ["nested/new.txt", "update.txt", "remove.txt"], save: ["*"] },
                 ])
                 expect(readsBeforeEditApproval).toBe(0)
-                expect(yield* Effect.promise(() => fs.readFile(path.join(tmp.path, "nested/new.txt"), "utf8"))).toBe("created\n")
+                expect(yield* Effect.promise(() => fs.readFile(path.join(tmp.path, "nested/new.txt"), "utf8"))).toBe(
+                  "created\n",
+                )
                 expect(yield* Effect.promise(() => fs.readFile(update, "utf8"))).toBe("after\n")
                 expect(yield* exists(remove)).toBe(false)
               }),
@@ -155,7 +172,11 @@ describe("ApplyPatchTool", () => {
             withTool(tmp.path, (registry) =>
               Effect.gen(function* () {
                 expect(
-                  yield* registry.execute(call("*** Begin Patch\n*** Add File: created.txt\n+created\n*** Update File: old.txt\n*** Move to: moved.txt\n@@\n-before\n+after\n*** End Patch")),
+                  yield* registry.execute(
+                    call(
+                      "*** Begin Patch\n*** Add File: created.txt\n+created\n*** Update File: old.txt\n*** Move to: moved.txt\n@@\n-before\n+after\n*** End Patch",
+                    ),
+                  ),
                 ).toEqual({ type: "error", value: "apply_patch moves are not supported yet" })
                 expect(yield* exists(path.join(tmp.path, "created.txt"))).toBe(false)
                 expect(assertions).toEqual([])
@@ -179,7 +200,9 @@ describe("ApplyPatchTool", () => {
             withTool(active.path, (registry) =>
               Effect.gen(function* () {
                 expect(
-                  yield* registry.execute(call(`*** Begin Patch\n*** Update File: ${target}\n@@\n-before\n+after\n*** End Patch`)),
+                  yield* registry.execute(
+                    call(`*** Begin Patch\n*** Update File: ${target}\n@@\n-before\n+after\n*** End Patch`),
+                  ),
                 ).toMatchObject({ type: "text" })
                 expect(assertions.map((input) => input.action)).toEqual(["external_directory", "edit"])
                 expect(readsBeforeEditApproval).toBe(0)
@@ -190,7 +213,9 @@ describe("ApplyPatchTool", () => {
         )
       },
       ([active, outside]) =>
-        Effect.promise(() => Promise.all([active[Symbol.asyncDispose](), outside[Symbol.asyncDispose]()]).then(() => undefined)),
+        Effect.promise(() =>
+          Promise.all([active[Symbol.asyncDispose](), outside[Symbol.asyncDispose]()]).then(() => undefined),
+        ),
     ),
   )
 
@@ -201,12 +226,18 @@ describe("ApplyPatchTool", () => {
         reset()
         const first = path.join(outside.path, "first.txt")
         const second = path.join(outside.path, "second.txt")
-        return Effect.promise(() => Promise.all([fs.writeFile(first, "before\n"), fs.writeFile(second, "before\n")])).pipe(
+        return Effect.promise(() =>
+          Promise.all([fs.writeFile(first, "before\n"), fs.writeFile(second, "before\n")]),
+        ).pipe(
           Effect.andThen(
             withTool(active.path, (registry) =>
               Effect.gen(function* () {
                 expect(
-                  yield* registry.execute(call(`*** Begin Patch\n*** Update File: ${first}\n@@\n-before\n+after\n*** Update File: ${second}\n@@\n-before\n+after\n*** End Patch`)),
+                  yield* registry.execute(
+                    call(
+                      `*** Begin Patch\n*** Update File: ${first}\n@@\n-before\n+after\n*** Update File: ${second}\n@@\n-before\n+after\n*** End Patch`,
+                    ),
+                  ),
                 ).toMatchObject({ type: "text" })
                 expect(assertions.map((input) => input.action)).toEqual(["external_directory", "edit"])
                 expect(assertions[0]?.resources).toEqual([
@@ -218,7 +249,9 @@ describe("ApplyPatchTool", () => {
         )
       },
       ([active, outside]) =>
-        Effect.promise(() => Promise.all([active[Symbol.asyncDispose](), outside[Symbol.asyncDispose]()]).then(() => undefined)),
+        Effect.promise(() =>
+          Promise.all([active[Symbol.asyncDispose](), outside[Symbol.asyncDispose]()]).then(() => undefined),
+        ),
     ),
   )
 
@@ -230,7 +263,11 @@ describe("ApplyPatchTool", () => {
         return withTool(tmp.path, (registry) =>
           Effect.gen(function* () {
             expect(
-              yield* registry.execute(call("*** Begin Patch\n*** Add File: created.txt\n+created\n*** Update File: missing.txt\n@@\n-before\n+after\n*** End Patch")),
+              yield* registry.execute(
+                call(
+                  "*** Begin Patch\n*** Add File: created.txt\n+created\n*** Update File: missing.txt\n@@\n-before\n+after\n*** End Patch",
+                ),
+              ),
             ).toEqual({ type: "error", value: "Unable to apply patch at missing.txt" })
             expect(yield* exists(path.join(tmp.path, "created.txt"))).toBe(false)
           }),
@@ -251,7 +288,9 @@ describe("ApplyPatchTool", () => {
             withTool(tmp.path, (registry) =>
               Effect.gen(function* () {
                 expect(
-                  yield* registry.execute(call("*** Begin Patch\n*** Add File: existing.txt\n+replacement\n*** End Patch")),
+                  yield* registry.execute(
+                    call("*** Begin Patch\n*** Add File: existing.txt\n+replacement\n*** End Patch"),
+                  ),
                 ).toEqual({ type: "error", value: "Unable to apply patch at existing.txt" })
                 expect(yield* Effect.promise(() => fs.readFile(target, "utf8"))).toBe("sentinel\n")
               }),
@@ -276,8 +315,13 @@ describe("ApplyPatchTool", () => {
             withTool(tmp.path, (registry) =>
               Effect.gen(function* () {
                 expect(
-                  yield* registry.execute(call("*** Begin Patch\n*** Delete File: first.txt\n*** Delete File: second.txt\n*** End Patch")),
-                ).toEqual({ type: "error", value: "Patch partially applied before failing at second.txt. Applied: first.txt" })
+                  yield* registry.execute(
+                    call("*** Begin Patch\n*** Delete File: first.txt\n*** Delete File: second.txt\n*** End Patch"),
+                  ),
+                ).toEqual({
+                  type: "error",
+                  value: "Patch partially applied before failing at second.txt. Applied: first.txt",
+                })
                 expect(yield* exists(first)).toBe(false)
                 expect(yield* exists(second)).toBe(true)
               }),
@@ -303,7 +347,11 @@ describe("ApplyPatchTool", () => {
           yield* Effect.promise(() => Promise.all([fs.writeFile(first, "first"), fs.writeFile(second, "second")]))
           yield* withTool(tmp.path, (registry) =>
             Effect.gen(function* () {
-              const run = yield* registry.execute(call("*** Begin Patch\n*** Delete File: first.txt\n*** Delete File: second.txt\n*** End Patch")).pipe(Effect.forkChild)
+              const run = yield* registry
+                .execute(
+                  call("*** Begin Patch\n*** Delete File: first.txt\n*** Delete File: second.txt\n*** End Patch"),
+                )
+                .pipe(Effect.forkChild)
               yield* Deferred.await(removeStarted!)
               const interrupt = yield* Fiber.interrupt(run).pipe(Effect.forkChild)
               yield* Deferred.succeed(releaseRemove!, undefined)

+ 10 - 5
packages/core/test/tool-edit.test.ts

@@ -59,11 +59,13 @@ const filesystem = Layer.effect(
     return FSUtil.Service.of({
       ...fs,
       readFile: (target) =>
-        fs.readFile(target).pipe(
-          Effect.tap((content) =>
-            Effect.sync(() => reads++).pipe(Effect.andThen(Effect.suspend(() => afterRead(target, content)))),
+        fs
+          .readFile(target)
+          .pipe(
+            Effect.tap((content) =>
+              Effect.sync(() => reads++).pipe(Effect.andThen(Effect.suspend(() => afterRead(target, content)))),
+            ),
           ),
-        ),
       writeWithDirs: (target, content, mode) =>
         Effect.sync(() => writes.push(target)).pipe(Effect.andThen(fs.writeWithDirs(target, content, mode))),
     })
@@ -369,7 +371,10 @@ describe("EditTool", () => {
           ),
           Effect.andThen((result) =>
             Effect.gen(function* () {
-              expect(result).toEqual({ type: "error", value: "File changed after permission approval. Read it again before editing." })
+              expect(result).toEqual({
+                type: "error",
+                value: "File changed after permission approval. Read it again before editing.",
+              })
               expect(yield* Effect.promise(() => fs.readFile(target, "utf8"))).toBe("newer\n")
               expect(writes).toEqual([])
             }),

+ 24 - 3
packages/core/test/tool-glob.test.ts

@@ -153,7 +153,14 @@ describe("GlobTool", () => {
     Effect.gen(function* () {
       reset()
       result = new LocationSearch.FilesResult({
-        items: [new LocationSearch.File({ path: RelativePath.make("src/index.ts"), canonical: "/project/src/index.ts", resource: "src/index.ts", mtime: 1 })],
+        items: [
+          new LocationSearch.File({
+            path: RelativePath.make("src/index.ts"),
+            canonical: "/project/src/index.ts",
+            resource: "src/index.ts",
+            mtime: 1,
+          }),
+        ],
         truncated: false,
         partial: false,
       })
@@ -172,7 +179,14 @@ describe("GlobTool", () => {
     Effect.gen(function* () {
       reset()
       result = new LocationSearch.FilesResult({
-        items: [new LocationSearch.File({ path: RelativePath.make("guide.md"), canonical: "/project/docs/guide.md", resource: "docs:guide.md", mtime: 1 })],
+        items: [
+          new LocationSearch.File({
+            path: RelativePath.make("guide.md"),
+            canonical: "/project/docs/guide.md",
+            resource: "docs:guide.md",
+            mtime: 1,
+          }),
+        ],
         truncated: false,
         partial: false,
       })
@@ -197,7 +211,14 @@ describe("GlobTool", () => {
   it.effect("formats bounded and partial results without discarding structured output", () =>
     Effect.sync(() => {
       const output = new LocationSearch.FilesResult({
-        items: [new LocationSearch.File({ path: RelativePath.make("one.ts"), canonical: "/project/one.ts", resource: "one.ts", mtime: 1 })],
+        items: [
+          new LocationSearch.File({
+            path: RelativePath.make("one.ts"),
+            canonical: "/project/one.ts",
+            resource: "one.ts",
+            mtime: 1,
+          }),
+        ],
         truncated: true,
         partial: true,
       })

+ 25 - 7
packages/core/test/tool-grep.test.ts

@@ -43,7 +43,7 @@ const filesystem = Layer.succeed(
           real: `/project/${input.path ?? "."}`,
           directory: "/project",
           root: "/project",
-          resource: input.reference === undefined ? input.path ?? "." : `${input.reference}:${input.path ?? "."}`,
+          resource: input.reference === undefined ? (input.path ?? ".") : `${input.reference}:${input.path ?? "."}`,
           reference: input.reference,
           type: "directory",
           dev: 1,
@@ -87,7 +87,12 @@ const permission = Layer.succeed(
   }),
 )
 const registry = ToolRegistry.layer.pipe(Layer.provide(permission))
-const grep = GrepTool.layer.pipe(Layer.provide(registry), Layer.provide(filesystem), Layer.provide(search), Layer.provide(permission))
+const grep = GrepTool.layer.pipe(
+  Layer.provide(registry),
+  Layer.provide(filesystem),
+  Layer.provide(search),
+  Layer.provide(permission),
+)
 const it = testEffect(Layer.mergeAll(registry, filesystem, search, permission, grep))
 const sessionID = SessionV2.ID.make("ses_grep_tool_test")
 
@@ -136,7 +141,12 @@ function provideLive(directory: string, projectReferences = references({})) {
     Layer.provide(dependencies),
   )
   const registry = ToolRegistry.layer.pipe(Layer.provide(permission))
-  const grep = GrepTool.layer.pipe(Layer.provide(registry), Layer.provide(filesystem), Layer.provide(search), Layer.provide(permission))
+  const grep = GrepTool.layer.pipe(
+    Layer.provide(registry),
+    Layer.provide(filesystem),
+    Layer.provide(search),
+    Layer.provide(permission),
+  )
   return Layer.mergeAll(registry, filesystem, search, permission, grep)
 }
 
@@ -178,7 +188,9 @@ describe("GrepTool", () => {
         resources: ["guide"],
         metadata: { root: "manual:docs", reference: "manual", path: RelativePath.make("docs"), include: "*.md" },
       })
-      expect(searches).toEqual([{ pattern: "guide", path: RelativePath.make("docs"), reference: "manual", include: "*.md" }])
+      expect(searches).toEqual([
+        { pattern: "guide", path: RelativePath.make("docs"), reference: "manual", include: "*.md" },
+      ])
     }),
   )
 
@@ -218,7 +230,8 @@ describe("GrepTool", () => {
       expect(settlement.output?.structured).toEqual(result)
       expect(settlement.result).toEqual({
         type: "text",
-        value: "Found 1 matches\nsrc/index.ts:\n  Line 3: needle preview...\n\n(Results are truncated: showing first 1 matches. Consider using a more specific path or pattern.)\n\n(Some paths were inaccessible and skipped)",
+        value:
+          "Found 1 matches\nsrc/index.ts:\n  Line 3: needle preview...\n\n(Results are truncated: showing first 1 matches. Consider using a more specific path or pattern.)\n\n(Some paths were inaccessible and skipped)",
       })
     }),
   )
@@ -226,7 +239,10 @@ describe("GrepTool", () => {
   it.effect("returns a useful tool error for an invalid regex", () =>
     Effect.gen(function* () {
       reset()
-      searchFailure = new Ripgrep.InvalidPatternError({ pattern: "[", message: "regex parse error: unclosed character class" })
+      searchFailure = new Ripgrep.InvalidPatternError({
+        pattern: "[",
+        message: "regex parse error: unclosed character class",
+      })
 
       expect(yield* execute({ pattern: "[" })).toEqual({
         type: "error",
@@ -261,7 +277,9 @@ describe("GrepTool", () => {
             type: "text",
             value: "Found 1 matches\ndocs:guide.md:\n  Line 1: needle docs\n",
           })
-        }).pipe(Effect.provide(provideLive(tmp.path, references({ docs: { name: "docs", kind: "local", path: docs } }))))
+        }).pipe(
+          Effect.provide(provideLive(tmp.path, references({ docs: { name: "docs", kind: "local", path: docs } }))),
+        )
       }),
     ),
   )

+ 30 - 10
packages/core/test/tool-output-store.test.ts

@@ -47,9 +47,10 @@ describe("ToolOutputStore", () => {
   it.live("returns under-limit text unchanged without writing a resource", () =>
     withStore(({ store }) =>
       Effect.gen(function* () {
-        expect(
-          yield* store.truncate({ sessionID, toolCallID: "call-short", content: "line one\nline two" }),
-        ).toEqual({ content: "line one\nline two", truncated: false })
+        expect(yield* store.truncate({ sessionID, toolCallID: "call-short", content: "line one\nline two" })).toEqual({
+          content: "line one\nline two",
+          truncated: false,
+        })
       }),
     ),
   )
@@ -92,7 +93,12 @@ describe("ToolOutputStore", () => {
   it.live("keeps one-line previews bounded", () =>
     withStore(({ store }) =>
       Effect.gen(function* () {
-        const result = yield* store.truncate({ sessionID, toolCallID: "call-one-line", content: "one\ntwo\nthree", maxLines: 1 })
+        const result = yield* store.truncate({
+          sessionID,
+          toolCallID: "call-one-line",
+          content: "one\ntwo\nthree",
+          maxLines: 1,
+        })
 
         expect(result.truncated).toBe(true)
         if (!result.truncated) throw new Error("expected truncation")
@@ -105,7 +111,12 @@ describe("ToolOutputStore", () => {
   it.live("pages reads within the bounded managed-resource limit", () =>
     withStore(({ root, store, fs }) =>
       Effect.gen(function* () {
-        const resource = yield* store.write({ sessionID, toolCallID: "call-page", content: "0123456789", name: "out.txt" })
+        const resource = yield* store.write({
+          sessionID,
+          toolCallID: "call-page",
+          content: "0123456789",
+          name: "out.txt",
+        })
         const first = yield* store.read({ sessionID, uri: resource.uri, limit: 4 })
         const second = yield* store.read({ sessionID, uri: resource.uri, offset: first.next, limit: 4 })
         const last = yield* store.read({ sessionID, uri: resource.uri, offset: second.next, limit: 4 })
@@ -114,7 +125,13 @@ describe("ToolOutputStore", () => {
         expect(second).toMatchObject({ content: "4567", offset: 4, truncated: true, next: 8 })
         expect(last).toMatchObject({ content: "89", offset: 8, truncated: false })
         expect(last.resource).toEqual({ uri: resource.uri, mime: "text/plain", name: "out.txt", size: 10 })
-        expect(JSON.parse(yield* fs.readFileString(path.join(root, "tool-output", "managed", `${resource.uri.slice("tool-output://".length)}.json`)))).toMatchObject({
+        expect(
+          JSON.parse(
+            yield* fs.readFileString(
+              path.join(root, "tool-output", "managed", `${resource.uri.slice("tool-output://".length)}.json`),
+            ),
+          ),
+        ).toMatchObject({
           sessionID,
           toolCallID: "call-page",
         })
@@ -165,9 +182,9 @@ describe("ToolOutputStore", () => {
       ({ store }) =>
         Effect.gen(function* () {
           expect(yield* store.limits()).toEqual({ maxLines: 2, maxBytes: 1_000 })
-          expect((yield* store.truncate({ sessionID, toolCallID: "call-config", content: "one\ntwo\nthree" })).truncated).toBe(
-            true,
-          )
+          expect(
+            (yield* store.truncate({ sessionID, toolCallID: "call-config", content: "one\ntwo\nthree" })).truncated,
+          ).toBe(true)
         }),
       new Config.Info({ tool_output: new ConfigToolOutput.Info({ max_lines: 2, max_bytes: 1_000 }) }),
     ),
@@ -186,7 +203,10 @@ describe("ToolOutputStore", () => {
         const unrelatedManaged = path.join(directory, "unrelated.txt")
         const record = JSON.parse(yield* fs.readFileString(oldMetadata))
 
-        yield* fs.writeFileString(oldMetadata, JSON.stringify({ ...record, created: Date.now() - 8 * 24 * 60 * 60 * 1_000 }))
+        yield* fs.writeFileString(
+          oldMetadata,
+          JSON.stringify({ ...record, created: Date.now() - 8 * 24 * 60 * 60 * 1_000 }),
+        )
         yield* fs.writeFileString(unrelated, "keep")
         yield* fs.writeFileString(unrelatedManaged, "keep")
         yield* store.cleanup()

+ 6 - 1
packages/core/test/tool-read.test.ts

@@ -362,7 +362,12 @@ describe("ReadTool", () => {
       expect(
         yield* registry.execute({
           sessionID,
-          call: { type: "tool-call", id: "call-large", name: "read", input: { path: "large.txt", offset: 2, limit: 1 } },
+          call: {
+            type: "tool-call",
+            id: "call-large",
+            name: "read",
+            input: { path: "large.txt", offset: 2, limit: 1 },
+          },
         }),
       ).toEqual({
         type: "json",

+ 6 - 1
packages/core/test/tool-skill.test.ts

@@ -45,7 +45,12 @@ describe("SkillTool", () => {
           let bootWaited = false
           const boot = Layer.succeed(
             PluginBoot.Service,
-            PluginBoot.Service.of({ wait: () => Effect.sync(() => { bootWaited = true }) }),
+            PluginBoot.Service.of({
+              wait: () =>
+                Effect.sync(() => {
+                  bootWaited = true
+                }),
+            }),
           )
           const permission = Layer.succeed(
             PermissionV2.Service,

+ 34 - 8
packages/core/test/tool-websearch.test.ts

@@ -28,7 +28,9 @@ describe("WebSearchTool provider selection", () => {
   })
 
   test("supports an explicit operational override", () => {
-    expect(WebSearchTool.selectProvider(sessionID, { enableExa: false, enableParallel: false }, "parallel")).toBe("parallel")
+    expect(WebSearchTool.selectProvider(sessionID, { enableExa: false, enableParallel: false }, "parallel")).toBe(
+      "parallel",
+    )
     expect(WebSearchTool.selectProvider(sessionID, { enableExa: false, enableParallel: false }, "exa")).toBe("exa")
   })
 
@@ -47,9 +49,11 @@ describe("WebSearchTool MCP response parser", () => {
   })
 
   test("parses SSE JSON-RPC responses and ignores non-JSON frames", async () => {
-    expect(await Effect.runPromise(WebSearchTool.parseResponse(`data: [DONE]\nevent: message\ndata: ${payload("search results")}\n\n`))).toBe(
-      "search results",
-    )
+    expect(
+      await Effect.runPromise(
+        WebSearchTool.parseResponse(`data: [DONE]\nevent: message\ndata: ${payload("search results")}\n\n`),
+      ),
+    ).toBe("search results")
   })
 })
 
@@ -151,7 +155,13 @@ describe("WebSearchTool contribution", () => {
             type: "tool-call",
             id: "call-exa",
             name: "websearch",
-            input: { query: "effect typescript", numResults: 3, livecrawl: "preferred", type: "fast", contextMaxCharacters: 2500 },
+            input: {
+              query: "effect typescript",
+              numResults: 3,
+              livecrawl: "preferred",
+              type: "fast",
+              contextMaxCharacters: 2500,
+            },
           },
         }),
       ).toEqual({ type: "text", value: "exa results" })
@@ -161,7 +171,14 @@ describe("WebSearchTool contribution", () => {
           action: "websearch",
           resources: ["effect typescript"],
           save: ["*"],
-          metadata: { query: "effect typescript", numResults: 3, livecrawl: "preferred", type: "fast", contextMaxCharacters: 2500, provider: "exa" },
+          metadata: {
+            query: "effect typescript",
+            numResults: 3,
+            livecrawl: "preferred",
+            type: "fast",
+            contextMaxCharacters: 2500,
+            provider: "exa",
+          },
         },
       ])
       expect(requests).toEqual([
@@ -174,7 +191,13 @@ describe("WebSearchTool contribution", () => {
             method: "tools/call",
             params: {
               name: "web_search_exa",
-              arguments: { query: "effect typescript", type: "fast", numResults: 3, livecrawl: "preferred", contextMaxCharacters: 2500 },
+              arguments: {
+                query: "effect typescript",
+                type: "fast",
+                numResults: 3,
+                livecrawl: "preferred",
+                contextMaxCharacters: 2500,
+              },
             },
           },
         },
@@ -211,7 +234,10 @@ describe("WebSearchTool contribution", () => {
       expect(requests[0]?.body).not.toHaveProperty("params.arguments.model_name")
       expect(settled).toEqual({
         result: { type: "text", value: "parallel results" },
-        output: { structured: { provider: "parallel", text: "parallel results", truncated: false }, content: [{ type: "text", text: "parallel results" }] },
+        output: {
+          structured: { provider: "parallel", text: "parallel results", truncated: false },
+          content: [{ type: "text", text: "parallel results" }],
+        },
       })
       expect(JSON.stringify(settled)).not.toContain("parallel-secret")
     }),

+ 16 - 12
packages/llm/example/tutorial.ts

@@ -105,18 +105,22 @@ const streamWithTools = Effect.gen(function* () {
   })
   const events = Array.from(yield* LLM.stream(request).pipe(Stream.runCollect))
   for (const event of events) {
-      if (event.type === "tool-call") console.log("tool call", event.name, event.input)
-      if (event.type === "text-delta") process.stdout.write(event.text)
-      if (event.type !== "tool-call" || event.providerExecuted) continue
-      const dispatched = yield* ToolRuntime.dispatch(tools, event)
-      console.log("tool result", event.name, dispatched.result)
-
-      // A durable agent would persist these messages before starting another
-      // raw model turn. This tutorial keeps the boundary visible instead.
-      const followUp = LLM.updateRequest(request, {
-        messages: [...request.messages, Message.assistant([event]), Message.tool({ ...event, result: dispatched.result })],
-      })
-      console.log("follow-up history messages:", followUp.messages.length)
+    if (event.type === "tool-call") console.log("tool call", event.name, event.input)
+    if (event.type === "text-delta") process.stdout.write(event.text)
+    if (event.type !== "tool-call" || event.providerExecuted) continue
+    const dispatched = yield* ToolRuntime.dispatch(tools, event)
+    console.log("tool result", event.name, dispatched.result)
+
+    // A durable agent would persist these messages before starting another
+    // raw model turn. This tutorial keeps the boundary visible instead.
+    const followUp = LLM.updateRequest(request, {
+      messages: [
+        ...request.messages,
+        Message.assistant([event]),
+        Message.tool({ ...event, result: dispatched.result }),
+      ],
+    })
+    console.log("follow-up history messages:", followUp.messages.length)
   }
 })
 

+ 11 - 4
packages/llm/src/protocols/anthropic-messages.ts

@@ -363,11 +363,15 @@ const validateNativeSystemUpdate = Effect.fn("AnthropicMessages.validateNativeSy
   const previous = messages[index - 1]
   const next = messages[index + 1]
   if (!previous)
-    return yield* invalid("Anthropic Messages chronological system updates cannot be the first message; use LLMRequest.system")
+    return yield* invalid(
+      "Anthropic Messages chronological system updates cannot be the first message; use LLMRequest.system",
+    )
   if (previous.role === "system")
     return yield* invalid("Anthropic Messages chronological system updates cannot be consecutive")
   if (endsInLocalToolUse(previous))
-    return yield* invalid("Anthropic Messages chronological system updates cannot appear between a local tool call and its tool result")
+    return yield* invalid(
+      "Anthropic Messages chronological system updates cannot appear between a local tool call and its tool result",
+    )
   if (previous.role !== "user" && previous.role !== "tool" && !endsInServerToolUse(previous))
     return yield* invalid(
       "Anthropic Messages chronological system updates must follow a user message, tool result, or assistant server tool use",
@@ -375,7 +379,9 @@ const validateNativeSystemUpdate = Effect.fn("AnthropicMessages.validateNativeSy
   if (next?.role === "system")
     return yield* invalid("Anthropic Messages chronological system updates cannot be consecutive")
   if (next && next.role !== "assistant")
-    return yield* invalid("Anthropic Messages chronological system updates must end the messages array or immediately precede an assistant message")
+    return yield* invalid(
+      "Anthropic Messages chronological system updates must end the messages array or immediately precede an assistant message",
+    )
 })
 
 const lowerNativeSystemUpdate = Effect.fn("AnthropicMessages.lowerNativeSystemUpdate")(function* (
@@ -409,7 +415,8 @@ const lowerMessages = Effect.fn("AnthropicMessages.lowerMessages")(function* (
       const part = yield* ProviderShared.wrappedSystemUpdate("Anthropic Messages", message)
       const block = { type: "text" as const, text: part.text, cache_control: cacheControl(breakpoints, part.cache) }
       const previous = messages.at(-1)
-      if (previous?.role === "user") messages[messages.length - 1] = { role: "user", content: [...previous.content, block] }
+      if (previous?.role === "user")
+        messages[messages.length - 1] = { role: "user", content: [...previous.content, block] }
       else messages.push({ role: "user", content: [block] })
       continue
     }

+ 9 - 3
packages/llm/src/protocols/bedrock-converse.ts

@@ -243,7 +243,10 @@ const bedrockMetadata = (metadata: Record<string, unknown>): ProviderMetadata =>
 
 const reasoningSignature = (part: ReasoningPart) => {
   const bedrock = part.providerMetadata?.bedrock
-  return part.encrypted ?? (ProviderShared.isRecord(bedrock) && typeof bedrock.signature === "string" ? bedrock.signature : undefined)
+  return (
+    part.encrypted ??
+    (ProviderShared.isRecord(bedrock) && typeof bedrock.signature === "string" ? bedrock.signature : undefined)
+  )
 }
 
 const lowerToolCall = (part: ToolCallPart): BedrockToolUseBlock => ({
@@ -294,7 +297,8 @@ const lowerMessages = Effect.fn("BedrockConverse.lowerMessages")(function* (
       const part = yield* ProviderShared.wrappedSystemUpdate("Bedrock Converse", message)
       const content = textWithCache(breakpoints, part.text, part.cache)
       const previous = messages.at(-1)
-      if (previous?.role === "user") messages[messages.length - 1] = { role: "user", content: [...previous.content, ...content] }
+      if (previous?.role === "user")
+        messages[messages.length - 1] = { role: "user", content: [...previous.content, ...content] }
       else messages.push({ role: "user", content })
       continue
     }
@@ -532,7 +536,9 @@ const step = (state: ParserState, event: BedrockEvent) =>
             Lifecycle.textEnd(state.lifecycle, events, `text-${index}`),
             events,
             `reasoning-${index}`,
-            state.reasoningSignatures[index] ? bedrockMetadata({ signature: state.reasoningSignatures[index] }) : undefined,
+            state.reasoningSignatures[index]
+              ? bedrockMetadata({ signature: state.reasoningSignatures[index] })
+              : undefined,
           )
       events.push(...resultEvents)
       return [

+ 5 - 2
packages/llm/src/protocols/gemini.ts

@@ -204,7 +204,8 @@ const lowerMessages = Effect.fn("Gemini.lowerMessages")(function* (request: LLMR
     if (message.role === "system") {
       const part = yield* ProviderShared.wrappedSystemUpdate("Gemini", message)
       const previous = contents.at(-1)
-      if (previous?.role === "user") contents[contents.length - 1] = { role: "user", parts: [...previous.parts, { text: part.text }] }
+      if (previous?.role === "user")
+        contents[contents.length - 1] = { role: "user", parts: [...previous.parts, { text: part.text }] }
       else contents.push({ role: "user", parts: [{ text: part.text }] })
       continue
     }
@@ -405,7 +406,9 @@ const step = (state: ParserState, event: GeminiEvent) => {
           id,
           name: part.functionCall.name,
           input,
-          providerMetadata: part.thoughtSignature ? googleMetadata({ thoughtSignature: part.thoughtSignature }) : undefined,
+          providerMetadata: part.thoughtSignature
+            ? googleMetadata({ thoughtSignature: part.thoughtSignature })
+            : undefined,
         }),
       )
       hasToolCalls = true

+ 3 - 1
packages/llm/src/protocols/openai-chat.ts

@@ -226,7 +226,9 @@ const lowerAssistantMessage = Effect.fn("OpenAIChat.lowerAssistantMessage")(func
     content: content.length === 0 ? null : ProviderShared.joinText(content),
     tool_calls: toolCalls.length === 0 ? undefined : toolCalls,
     reasoning_content:
-      reasoning.length > 0 ? reasoning.map((part) => part.text).join("") : openAICompatibleReasoningContent(message.native?.openaiCompatible),
+      reasoning.length > 0
+        ? reasoning.map((part) => part.text).join("")
+        : openAICompatibleReasoningContent(message.native?.openaiCompatible),
   }
 })
 

+ 6 - 2
packages/llm/src/protocols/openai-responses.ts

@@ -343,7 +343,10 @@ const lowerMessages = Effect.fn("OpenAIResponses.lowerMessages")(function* (requ
       const part = yield* ProviderShared.wrappedSystemUpdate("OpenAI Responses", message)
       const previous = input.at(-1)
       if (previous && "role" in previous && previous.role === "user")
-        input[input.length - 1] = { role: "user", content: [...previous.content, { type: "input_text", text: part.text }] }
+        input[input.length - 1] = {
+          role: "user",
+          content: [...previous.content, { type: "input_text", text: part.text }],
+        }
       else input.push({ role: "user", content: [{ type: "input_text", text: part.text }] })
       continue
     }
@@ -397,7 +400,8 @@ const lowerMessages = Effect.fn("OpenAIResponses.lowerMessages")(function* (requ
         if (part.type === "tool-result" && part.providerExecuted === true) {
           flushText()
           const itemID = hostedToolItemID(part)
-          if (store !== false && itemID && !hostedToolReferences.has(itemID)) input.push({ type: "item_reference", id: itemID })
+          if (store !== false && itemID && !hostedToolReferences.has(itemID))
+            input.push({ type: "item_reference", id: itemID })
           if (itemID) hostedToolReferences.add(itemID)
           continue
         }

+ 2 - 1
packages/llm/src/protocols/shared.ts

@@ -105,7 +105,8 @@ export const parseJson = (route: string, input: string, message: string) =>
  */
 export const joinText = (parts: ReadonlyArray<{ readonly text: string }>) => parts.map((part) => part.text).join("\n")
 
-const escapeSystemUpdateText = (text: string) => text.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;")
+const escapeSystemUpdateText = (text: string) =>
+  text.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;")
 
 /**
  * Stable fallback representation for chronological `Message.system(...)`

+ 2 - 1
packages/llm/src/schema/messages.ts

@@ -193,7 +193,8 @@ export const ToolOutput = Object.assign(
         type: "content",
         value: output.content.map((item) => {
           if (item.type === "text") return { type: "text", text: item.text }
-          if (item.source.type !== "data") throw new Error("Unmaterialized tool file source reached provider conversion")
+          if (item.source.type !== "data")
+            throw new Error("Unmaterialized tool file source reached provider conversion")
           return { type: "media", mediaType: item.mime, data: item.source.data, filename: item.name }
         }),
       }

+ 10 - 6
packages/llm/src/tool-runtime.ts

@@ -1,5 +1,13 @@
 import { Effect } from "effect"
-import { LLMEvent, type ToolCallPart, ToolFailure, ToolOutput, ToolResultValue, type ToolOutput as ToolOutputType, type ToolResultValue as ToolResultValueType } from "./schema"
+import {
+  LLMEvent,
+  type ToolCallPart,
+  ToolFailure,
+  ToolOutput,
+  ToolResultValue,
+  type ToolOutput as ToolOutputType,
+  type ToolResultValue as ToolResultValueType,
+} from "./schema"
 import { type AnyTool, type Tools } from "./tool"
 
 export interface ToolSettlement {
@@ -52,11 +60,7 @@ const decodeAndExecute = (tool: AnyTool, call: ToolCallPart): Effect.Effect<Tool
     ),
   )
 
-const result = (
-  call: ToolCallPart,
-  value: ToolResultValueType | ToolSettlement,
-  error?: unknown,
-): DispatchResult => {
+const result = (call: ToolCallPart, value: ToolResultValueType | ToolSettlement, error?: unknown): DispatchResult => {
   const settlement = ToolResultValue.is(value) ? { result: value } : value
   return {
     result: settlement.result,

+ 11 - 2
packages/llm/src/tool.ts

@@ -1,5 +1,10 @@
 import { Effect, JsonSchema, Schema } from "effect"
-import type { ToolCallPart, ToolContent, ToolDefinition as ToolDefinitionClass, ToolOutput as ToolOutputType } from "./schema"
+import type {
+  ToolCallPart,
+  ToolContent,
+  ToolDefinition as ToolDefinitionClass,
+  ToolOutput as ToolOutputType,
+} from "./schema"
 import { ToolDefinition, ToolFailure, ToolOutput, toolText } from "./schema"
 
 /**
@@ -51,7 +56,11 @@ export interface Tool<Parameters extends ToolSchema<any>, Success extends ToolSc
   /** @internal */
   readonly _encode: (value: Schema.Schema.Type<Success>) => Effect.Effect<unknown, Schema.SchemaError>
   /** @internal */
-  readonly _project: (parameters: Schema.Schema.Type<Parameters>, callID: ToolCallPart["id"], output: unknown) => ToolOutputType
+  readonly _project: (
+    parameters: Schema.Schema.Type<Parameters>,
+    callID: ToolCallPart["id"],
+    output: unknown,
+  ) => ToolOutputType
   /** @internal */
   readonly _legacyResult: boolean
   /** @internal */

+ 5 - 1
packages/llm/test/lib/tool-runtime.ts

@@ -117,7 +117,11 @@ const appendText = (
 ) => {
   const last = content.at(-1)
   if (last?.type === type) {
-    content[content.length - 1] = { ...last, text: `${last.text}${text}`, providerMetadata: providerMetadata ?? last.providerMetadata }
+    content[content.length - 1] = {
+      ...last,
+      text: `${last.text}${text}`,
+      providerMetadata: providerMetadata ?? last.providerMetadata,
+    }
     return
   }
   content.push({ type, text, providerMetadata })

+ 3 - 1
packages/llm/test/llm.test.ts

@@ -136,7 +136,9 @@ describe("llm constructors", () => {
   })
 
   test("builds chronological text-only system updates separately from the initial system prompt", () => {
-    const update = Message.system([{ type: "text", text: "Use parameterized SQL.", cache: new CacheHint({ type: "ephemeral" }) }])
+    const update = Message.system([
+      { type: "text", text: "Use parameterized SQL.", cache: new CacheHint({ type: "ephemeral" }) },
+    ])
     const request = LLM.request({
       model: Model.make({ id: "fake-model", provider: "fake", route: chatRoute }),
       system: "Initial operator prompt.",

+ 17 - 13
packages/llm/test/provider/anthropic-messages.test.ts

@@ -87,7 +87,11 @@ describe("Anthropic Messages route", () => {
       const prepared = yield* LLMClient.prepare<AnthropicMessages.AnthropicMessagesBody>(
         LLM.request({
           model,
-          messages: [Message.user("Before."), Message.system("Treat </system-update> literally."), Message.assistant("After.")],
+          messages: [
+            Message.user("Before."),
+            Message.system("Treat </system-update> literally."),
+            Message.assistant("After."),
+          ],
           cache: "none",
         }),
       )
@@ -127,19 +131,19 @@ describe("Anthropic Messages route", () => {
         LLMClient.prepare(LLM.request({ model: opus48, messages, cache: "none" })).pipe(Effect.flip)
 
       expect((yield* placementError([Message.system("First.")])).message).toContain("cannot be the first message")
-      expect((yield* placementError([Message.user("Before."), Message.system("One."), Message.system("Two.")])).message)
-        .toContain("cannot be consecutive")
-      expect((yield* placementError([Message.assistant("Plain."), Message.system("After plain assistant.")])).message)
-        .toContain("must follow a user message, tool result, or assistant server tool use")
       expect(
-        (
-          yield* placementError([
-            Message.user("Use the tool."),
-            Message.assistant([ToolCallPart.make({ id: "call_1", name: "lookup", input: {} })]),
-            Message.system("Too early."),
-            Message.tool({ id: "call_1", name: "lookup", result: "Done." }),
-          ])
-        ).message,
+        (yield* placementError([Message.user("Before."), Message.system("One."), Message.system("Two.")])).message,
+      ).toContain("cannot be consecutive")
+      expect(
+        (yield* placementError([Message.assistant("Plain."), Message.system("After plain assistant.")])).message,
+      ).toContain("must follow a user message, tool result, or assistant server tool use")
+      expect(
+        (yield* placementError([
+          Message.user("Use the tool."),
+          Message.assistant([ToolCallPart.make({ id: "call_1", name: "lookup", input: {} })]),
+          Message.system("Too early."),
+          Message.tool({ id: "call_1", name: "lookup", result: "Done." }),
+        ])).message,
       ).toContain("cannot appear between a local tool call and its tool result")
     }),
   )

+ 4 - 1
packages/llm/test/provider/bedrock-converse.test.ts

@@ -327,7 +327,10 @@ describe("Bedrock Converse route", () => {
         }),
       )
       expect(prepared.body.messages).toEqual([
-        { role: "assistant", content: [{ reasoningContent: { reasoningText: { text: "Let me think.", signature: "sig_1" } } }] },
+        {
+          role: "assistant",
+          content: [{ reasoningContent: { reasoningText: { text: "Let me think.", signature: "sig_1" } } }],
+        },
       ])
     }),
   )

+ 15 - 3
packages/llm/test/provider/openai-chat.test.ts

@@ -55,12 +55,19 @@ describe("OpenAI Chat route", () => {
       const prepared = yield* LLMClient.prepare<OpenAIChat.OpenAIChatBody>(
         LLM.request({
           model,
-          messages: [Message.user("Before."), Message.system("Treat <admin> & data literally."), Message.assistant("After.")],
+          messages: [
+            Message.user("Before."),
+            Message.system("Treat <admin> & data literally."),
+            Message.assistant("After."),
+          ],
         }),
       )
 
       expect(prepared.body.messages).toEqual([
-        { role: "user", content: "Before.\n<system-update>\nTreat &lt;admin&gt; &amp; data literally.\n</system-update>" },
+        {
+          role: "user",
+          content: "Before.\n<system-update>\nTreat &lt;admin&gt; &amp; data literally.\n</system-update>",
+        },
         { role: "assistant", content: "After." },
       ])
     }),
@@ -71,7 +78,12 @@ describe("OpenAI Chat route", () => {
       const prepared = yield* LLMClient.prepare<OpenAIChat.OpenAIChatBody>(
         LLM.request({
           model,
-          messages: [Message.assistant([{ type: "reasoning", text: "thinking" }, { type: "text", text: "Hello" }])],
+          messages: [
+            Message.assistant([
+              { type: "reasoning", text: "thinking" },
+              { type: "text", text: "Hello" },
+            ]),
+          ],
         }),
       )
 

+ 5 - 1
packages/llm/test/provider/openai-responses.test.ts

@@ -62,7 +62,11 @@ describe("OpenAI Responses route", () => {
       const prepared = yield* LLMClient.prepare<OpenAIResponses.OpenAIResponsesBody>(
         LLM.request({
           model,
-          messages: [Message.user("Before."), Message.system("Treat </system-update> literally."), Message.assistant("After.")],
+          messages: [
+            Message.user("Before."),
+            Message.system("Treat </system-update> literally."),
+            Message.assistant("After."),
+          ],
         }),
       )
 

+ 42 - 12
packages/llm/test/tool-runtime.test.ts

@@ -1,6 +1,17 @@
 import { describe, expect } from "bun:test"
 import { Effect, Schema, Stream } from "effect"
-import { GenerationOptions, LLM, LLMEvent, LLMRequest, LLMResponse, ToolChoice, ToolContent, ToolOutput, toolFileSourceFromUri, toDefinitions } from "../src"
+import {
+  GenerationOptions,
+  LLM,
+  LLMEvent,
+  LLMRequest,
+  LLMResponse,
+  ToolChoice,
+  ToolContent,
+  ToolOutput,
+  toolFileSourceFromUri,
+  toDefinitions,
+} from "../src"
 import { Auth, LLMClient } from "../src/route"
 import * as AnthropicMessages from "../src/protocols/anthropic-messages"
 import * as OpenAIChat from "../src/protocols/openai-chat"
@@ -188,10 +199,12 @@ describe("LLMClient tools", () => {
         execute: () => Effect.succeed({ ok: true }),
       })
 
-      expect((yield* ToolRuntime.dispatch({ text }, LLMEvent.toolCall({ id: "call_text", name: "text", input: {} }))).output)
-        .toEqual({ structured: "hello", content: [{ type: "text", text: "hello" }] })
-      expect((yield* ToolRuntime.dispatch({ json }, LLMEvent.toolCall({ id: "call_json", name: "json", input: {} }))).output)
-        .toEqual({ structured: { ok: true }, content: [] })
+      expect(
+        (yield* ToolRuntime.dispatch({ text }, LLMEvent.toolCall({ id: "call_text", name: "text", input: {} }))).output,
+      ).toEqual({ structured: "hello", content: [{ type: "text", text: "hello" }] })
+      expect(
+        (yield* ToolRuntime.dispatch({ json }, LLMEvent.toolCall({ id: "call_json", name: "json", input: {} }))).output,
+      ).toEqual({ structured: { ok: true }, content: [] })
     }),
   )
 
@@ -204,12 +217,16 @@ describe("LLMClient tools", () => {
         source: { type: "data", data: "AAAA" },
         mime: "image/png",
       })
-      expect(decode({ type: "file", source: { type: "url", url: "https://example.test/image.png" }, mime: "image/png" })).toEqual({
+      expect(
+        decode({ type: "file", source: { type: "url", url: "https://example.test/image.png" }, mime: "image/png" }),
+      ).toEqual({
         type: "file",
         source: { type: "url", url: "https://example.test/image.png" },
         mime: "image/png",
       })
-      expect(decode({ type: "file", source: { type: "file", uri: "file:///tmp/image.png" }, mime: "image/png" })).toEqual({
+      expect(
+        decode({ type: "file", source: { type: "file", uri: "file:///tmp/image.png" }, mime: "image/png" }),
+      ).toEqual({
         type: "file",
         source: { type: "file", uri: "file:///tmp/image.png" },
         mime: "image/png",
@@ -226,16 +243,29 @@ describe("LLMClient tools", () => {
       ).toEqual({ type: "content", value: [{ type: "media", mediaType: "image/png", data: "AAAA" }] })
       expect(
         ToolOutput.toResultValue(
-          ToolOutput.make({}, [{ type: "file", source: { type: "url", url: "https://example.test/image.png" }, mime: "image/png" }]),
+          ToolOutput.make({}, [
+            { type: "file", source: { type: "url", url: "https://example.test/image.png" }, mime: "image/png" },
+          ]),
         ),
-      ).toEqual({ type: "error", value: 'Tool file source "url" must be materialized to inline data before provider conversion' })
+      ).toEqual({
+        type: "error",
+        value: 'Tool file source "url" must be materialized to inline data before provider conversion',
+      })
       expect(
         ToolOutput.toResultValue(
-          ToolOutput.make({}, [{ type: "file", source: { type: "file", uri: "file:///tmp/image.png" }, mime: "image/png" }]),
+          ToolOutput.make({}, [
+            { type: "file", source: { type: "file", uri: "file:///tmp/image.png" }, mime: "image/png" },
+          ]),
         ),
-      ).toEqual({ type: "error", value: 'Tool file source "file" must be materialized to inline data before provider conversion' })
+      ).toEqual({
+        type: "error",
+        value: 'Tool file source "file" must be materialized to inline data before provider conversion',
+      })
       expect(toolFileSourceFromUri("data:image/png;base64,AAAA")).toEqual({ type: "data", data: "AAAA" })
-      expect(toolFileSourceFromUri("https://example.test/image.png")).toEqual({ type: "url", url: "https://example.test/image.png" })
+      expect(toolFileSourceFromUri("https://example.test/image.png")).toEqual({
+        type: "url",
+        url: "https://example.test/image.png",
+      })
       expect(toolFileSourceFromUri("file:///tmp/image.png")).toEqual({ type: "file", uri: "file:///tmp/image.png" })
       expect(() => toolFileSourceFromUri("opaque-value")).toThrow("Unsupported tool file URI")
       expect(() =>

+ 3 - 1
packages/llm/test/tool.types.ts

@@ -27,7 +27,9 @@ Tool.make({
   parameters: Schema.Struct({ city: Schema.String }),
   success: Schema.Struct({ forecast: Schema.NumberFromString }),
   execute: () => Effect.succeed({ forecast: 1 }),
-  toModelOutput: ({ callID, parameters, output }) => [{ type: "text", text: `${callID}:${parameters.city}:${output.forecast}` }],
+  toModelOutput: ({ callID, parameters, output }) => [
+    { type: "text", text: `${callID}:${parameters.city}:${output.forecast}` },
+  ],
 })
 
 LLM.stream(request)

+ 29 - 8
packages/opencode/src/cli/cmd/tui/context/sync-v2.tsx

@@ -43,7 +43,9 @@ function latestTool(assistant: SessionMessageAssistant | undefined, callID?: str
 }
 
 function latestText(assistant: SessionMessageAssistant | undefined, textID: string) {
-  return assistant?.content.findLast((item): item is SessionMessageAssistantText => item.type === "text" && item.id === textID)
+  return assistant?.content.findLast(
+    (item): item is SessionMessageAssistantText => item.type === "text" && item.id === textID,
+  )
 }
 
 function latestReasoning(assistant: SessionMessageAssistant | undefined, reasoningID: string) {
@@ -208,19 +210,28 @@ export const { use: useSyncV2, provider: SyncProviderV2 } = createSimpleContext(
           break
         case "session.next.tool.input.delta":
           update(event.properties.sessionID, (draft) => {
-            const match = latestTool(ownedAssistant(draft, event.properties.assistantMessageID), event.properties.callID)
+            const match = latestTool(
+              ownedAssistant(draft, event.properties.assistantMessageID),
+              event.properties.callID,
+            )
             if (match?.state.status === "pending") match.state.input += event.properties.delta
           })
           break
         case "session.next.tool.input.ended":
           update(event.properties.sessionID, (draft) => {
-            const match = latestTool(ownedAssistant(draft, event.properties.assistantMessageID), event.properties.callID)
+            const match = latestTool(
+              ownedAssistant(draft, event.properties.assistantMessageID),
+              event.properties.callID,
+            )
             if (match?.state.status === "pending") match.state.input = event.properties.text
           })
           break
         case "session.next.tool.called":
           update(event.properties.sessionID, (draft) => {
-            const match = latestTool(ownedAssistant(draft, event.properties.assistantMessageID), event.properties.callID)
+            const match = latestTool(
+              ownedAssistant(draft, event.properties.assistantMessageID),
+              event.properties.callID,
+            )
             if (!match) return
             match.time.ran = event.properties.timestamp
             match.provider = event.properties.provider
@@ -229,7 +240,10 @@ export const { use: useSyncV2, provider: SyncProviderV2 } = createSimpleContext(
           break
         case "session.next.tool.progress":
           update(event.properties.sessionID, (draft) => {
-            const match = latestTool(ownedAssistant(draft, event.properties.assistantMessageID), event.properties.callID)
+            const match = latestTool(
+              ownedAssistant(draft, event.properties.assistantMessageID),
+              event.properties.callID,
+            )
             if (match?.state.status !== "running") return
             match.state.structured = event.properties.structured
             match.state.content = [...event.properties.content]
@@ -237,7 +251,10 @@ export const { use: useSyncV2, provider: SyncProviderV2 } = createSimpleContext(
           break
         case "session.next.tool.success":
           update(event.properties.sessionID, (draft) => {
-            const match = latestTool(ownedAssistant(draft, event.properties.assistantMessageID), event.properties.callID)
+            const match = latestTool(
+              ownedAssistant(draft, event.properties.assistantMessageID),
+              event.properties.callID,
+            )
             if (match?.state.status !== "running") return
             match.state = {
               status: "completed",
@@ -252,7 +269,10 @@ export const { use: useSyncV2, provider: SyncProviderV2 } = createSimpleContext(
           break
         case "session.next.tool.failed":
           update(event.properties.sessionID, (draft) => {
-            const match = latestTool(ownedAssistant(draft, event.properties.assistantMessageID), event.properties.callID)
+            const match = latestTool(
+              ownedAssistant(draft, event.properties.assistantMessageID),
+              event.properties.callID,
+            )
             if (!match || (match.state.status !== "pending" && match.state.status !== "running")) return
             match.state = {
               status: "error",
@@ -287,7 +307,8 @@ export const { use: useSyncV2, provider: SyncProviderV2 } = createSimpleContext(
             const match = latestReasoning(activeAssistant(draft), event.properties.reasoningID)
             if (match) {
               match.text = event.properties.text
-              if (event.properties.providerMetadata !== undefined) match.providerMetadata = event.properties.providerMetadata
+              if (event.properties.providerMetadata !== undefined)
+                match.providerMetadata = event.properties.providerMetadata
             }
           })
           break

+ 2 - 1
packages/opencode/src/cli/cmd/tui/feature-plugins/system/session-v2.tsx

@@ -1087,7 +1087,8 @@ function toolOutput(content?: Array<ToolTextContent | ToolFileContent>) {
   return (content ?? [])
     .map((item) => {
       if (item.type === "text") return item.text.trim()
-      const source = item.source.type === "data" ? "inline data" : item.source.type === "url" ? item.source.url : item.source.uri
+      const source =
+        item.source.type === "data" ? "inline data" : item.source.type === "url" ? item.source.url : item.source.uri
       return `[file ${item.name ?? source}]`
     })
     .filter(Boolean)

+ 20 - 12
packages/opencode/src/session/llm/native-runtime.ts

@@ -7,7 +7,15 @@ import { asSchema, type ModelMessage, type Tool } from "ai"
 import { Cause, Effect, FiberSet, Queue } from "effect"
 import * as Stream from "effect/Stream"
 import { FetchHttpClient } from "effect/unstable/http"
-import { LLMRequest, Tool as NativeTool, ToolFailure, ToolRuntime, toDefinitions, type JsonSchema, type LLMEvent } from "@opencode-ai/llm"
+import {
+  LLMRequest,
+  Tool as NativeTool,
+  ToolFailure,
+  ToolRuntime,
+  toDefinitions,
+  type JsonSchema,
+  type LLMEvent,
+} from "@opencode-ai/llm"
 import type { LLMClientShape } from "@opencode-ai/llm/route"
 import { LLMNative } from "./native-request"
 
@@ -80,17 +88,17 @@ export function stream(input: StreamInput): StreamResult {
   // translation belongs here, not split across both packages.
   const tools = nativeTools(input.tools, input)
   const request = LLMNative.request({
-      model: input.model,
-      apiKey: current.apiKey,
-      baseURL: current.baseURL,
-      messages: ProviderTransform.message(input.messages, input.model, input.providerOptions ?? {}),
-      toolChoice: input.toolChoice,
-      temperature: input.temperature,
-      topP: input.topP,
-      topK: input.topK,
-      maxOutputTokens: input.maxOutputTokens,
-      providerOptions: ProviderTransform.providerOptions(input.model, input.providerOptions ?? {}),
-      headers: { ...providerHeaders(input.provider.options.headers), ...input.headers },
+    model: input.model,
+    apiKey: current.apiKey,
+    baseURL: current.baseURL,
+    messages: ProviderTransform.message(input.messages, input.model, input.providerOptions ?? {}),
+    toolChoice: input.toolChoice,
+    temperature: input.temperature,
+    topP: input.topP,
+    topK: input.topK,
+    maxOutputTokens: input.maxOutputTokens,
+    providerOptions: ProviderTransform.providerOptions(input.model, input.providerOptions ?? {}),
+    headers: { ...providerHeaders(input.provider.options.headers), ...input.headers },
   })
   const stream = Stream.scoped(
     Stream.unwrap(

+ 26 - 16
packages/opencode/src/session/processor.ts

@@ -426,7 +426,9 @@ export const layer = Layer.effect(
           case "tool-input-delta":
             {
               const toolCall = yield* ensureToolCall(value)
-              const assistantMessageID = flags.experimentalEventSystem ? yield* requireV2AssistantMessage(toolCall.call) : undefined
+              const assistantMessageID = flags.experimentalEventSystem
+                ? yield* requireV2AssistantMessage(toolCall.call)
+                : undefined
               if (assistantMessageID) {
                 yield* events.publish(SessionEvent.Tool.Input.Delta, {
                   sessionID: ctx.sessionID,
@@ -589,12 +591,19 @@ export const layer = Layer.effect(
               const content = [
                 ToolOutput.text({ type: "text", text: output.output }),
                 ...(output.attachments?.map((item: SessionV1.FilePart) =>
-                  ToolOutput.file({ type: "file", source: toolFileSourceFromUri(item.url), mime: item.mime, name: item.filename }),
+                  ToolOutput.file({
+                    type: "file",
+                    source: toolFileSourceFromUri(item.url),
+                    mime: item.mime,
+                    name: item.filename,
+                  }),
                 ) ?? []),
               ]
               const unsupported = content.find((item) => item.type === "file" && item.source.type !== "data")
               if (unsupported?.type === "file") {
-                const error = new Error(`Tool attachment source "${unsupported.source.type}" must be materialized before durable V2 settlement`)
+                const error = new Error(
+                  `Tool attachment source "${unsupported.source.type}" must be materialized before durable V2 settlement`,
+                )
                 yield* events.publish(SessionEvent.Tool.Failed, {
                   sessionID: ctx.sessionID,
                   assistantMessageID,
@@ -611,19 +620,20 @@ export const layer = Layer.effect(
                 })
                 yield* failToolCall(value.id, error)
                 return
-              } else yield* events.publish(SessionEvent.Tool.Success, {
-                sessionID: ctx.sessionID,
-                assistantMessageID,
-                callID: value.id,
-                structured: output.metadata,
-                content,
-                result: value.result,
-                provider: {
-                  executed: value.providerExecuted === true || toolCall?.part.metadata?.providerExecuted === true,
-                  ...(value.providerMetadata ? { metadata: value.providerMetadata } : {}),
-                },
-                timestamp: DateTime.makeUnsafe(Date.now()),
-              })
+              } else
+                yield* events.publish(SessionEvent.Tool.Success, {
+                  sessionID: ctx.sessionID,
+                  assistantMessageID,
+                  callID: value.id,
+                  structured: output.metadata,
+                  content,
+                  result: value.result,
+                  provider: {
+                    executed: value.providerExecuted === true || toolCall?.part.metadata?.providerExecuted === true,
+                    ...(value.providerMetadata ? { metadata: value.providerMetadata } : {}),
+                  },
+                  timestamp: DateTime.makeUnsafe(Date.now()),
+                })
             }
             yield* completeToolCall(value.id, output)
             return

+ 5 - 1
packages/opencode/test/cli/tui/sync-v2.test.tsx

@@ -107,7 +107,11 @@ test("sync v2 settles pending tools when a live failure arrives", async () => {
 
     await wait(() => {
       const assistant = sync.session.message.fromSession("session-1")[0]
-      return assistant?.type === "assistant" && assistant.content[0]?.type === "tool" && assistant.content[0].state.status === "error"
+      return (
+        assistant?.type === "assistant" &&
+        assistant.content[0]?.type === "tool" &&
+        assistant.content[0].state.status === "error"
+      )
     })
 
     const assistant = sync.session.message.fromSession("session-1")[0]

+ 1 - 1
packages/opencode/test/server/httpapi-exercise/backend.ts

@@ -47,7 +47,7 @@ const appCache: Partial<Record<string, CachedApp>> = {}
 export async function disposeApps() {
   const apps = Object.values(appCache)
   for (const key of Object.keys(appCache)) delete appCache[key]
-  await Promise.all(apps.flatMap((app) => app === undefined ? [] : [app.dispose()]))
+  await Promise.all(apps.flatMap((app) => (app === undefined ? [] : [app.dispose()])))
 }
 
 function app(modules: Runtime, options: CallOptions) {

+ 9 - 7
packages/opencode/test/server/httpapi-session.test.ts

@@ -493,10 +493,12 @@ describe("session HttpApi", () => {
           direction: "next",
         })
 
-        const nextMessagePage = yield* request(`/api/session/${session.id}/message?cursor=${messageCursor}`, { headers })
-        expect((yield* json<{ items: SessionMessage.Message[] }>(nextMessagePage)).items.map((message) => message.id)).toEqual([
-          firstMessage.id,
-        ])
+        const nextMessagePage = yield* request(`/api/session/${session.id}/message?cursor=${messageCursor}`, {
+          headers,
+        })
+        expect(
+          (yield* json<{ items: SessionMessage.Message[] }>(nextMessagePage)).items.map((message) => message.id),
+        ).toEqual([firstMessage.id])
 
         const legacyMessageCursor = Buffer.from(
           JSON.stringify({ id: secondMessage.id, time: 1, order: "desc", direction: "next" }),
@@ -504,9 +506,9 @@ describe("session HttpApi", () => {
         const legacyMessagePage = yield* request(`/api/session/${session.id}/message?cursor=${legacyMessageCursor}`, {
           headers,
         })
-        expect((yield* json<{ items: SessionMessage.Message[] }>(legacyMessagePage)).items.map((message) => message.id)).toEqual([
-          firstMessage.id,
-        ])
+        expect(
+          (yield* json<{ items: SessionMessage.Message[] }>(legacyMessagePage)).items.map((message) => message.id),
+        ).toEqual([firstMessage.id])
 
         const messageCursorWithOrder = yield* request(
           `/api/session/${session.id}/message?cursor=${messageCursor}&order=asc`,

+ 11 - 13
packages/opencode/test/session/llm-native-recorded.test.ts

@@ -280,20 +280,18 @@ function recordedNativeLLMLayer(scenario: RecordedScenario) {
   )
   // Only the HTTP client is recorded; RequestExecutor and the opencode LLM stack remain real.
   const recordedHttp = HttpRecorder.cassetteLayer(scenario.cassette, {
-        directory: FIXTURES_DIR,
-        mode: shouldRecord ? "record" : "replay",
-        metadata: {
-          provider: scenario.providerID,
-          protocol: scenario.protocol,
-          route: scenario.protocol,
-          tags: scenario.tags,
-        },
-        redactor: recordingRedactor,
-      })
+    directory: FIXTURES_DIR,
+    mode: shouldRecord ? "record" : "replay",
+    metadata: {
+      provider: scenario.providerID,
+      protocol: scenario.protocol,
+      route: scenario.protocol,
+      tags: scenario.tags,
+    },
+    redactor: recordingRedactor,
+  })
   const recordedClient = LLMClient.layer.pipe(
-    Layer.provide(
-      Layer.mergeAll(RequestExecutor.layer.pipe(Layer.provide(recordedHttp)), WebSocketExecutor.layer),
-    ),
+    Layer.provide(Layer.mergeAll(RequestExecutor.layer.pipe(Layer.provide(recordedHttp)), WebSocketExecutor.layer)),
   )
 
   return Layer.mergeAll(

+ 4 - 2
packages/opencode/test/session/processor-effect.test.ts

@@ -1004,7 +1004,8 @@ itProviderError.live("session.processor effect tests fail provider-executed erro
         const mdl = yield* provider.getModel(ref.providerID, ref.modelID)
         const settlements: Array<typeof SessionEvent.Tool.Failed.Type> = []
         const off = yield* events.listen((event) => {
-          if (event.type === SessionEvent.Tool.Failed.type) settlements.push(event as typeof SessionEvent.Tool.Failed.Type)
+          if (event.type === SessionEvent.Tool.Failed.type)
+            settlements.push(event as typeof SessionEvent.Tool.Failed.Type)
           return Effect.void
         })
         const handle = yield* processors.create({ assistantMessage: msg, sessionID: chat.id, model: mdl })
@@ -1059,7 +1060,8 @@ itFragmentFailure.live("session.processor effect tests flush partial v2 fragment
         let reasoning: string | undefined
         const off = yield* events.listen((event) => {
           seen.push(event.type)
-          if (event.type === SessionEvent.Text.Ended.type) text = (event.data as typeof SessionEvent.Text.Ended.data.Type).text
+          if (event.type === SessionEvent.Text.Ended.type)
+            text = (event.data as typeof SessionEvent.Text.Ended.data.Type).text
           if (event.type === SessionEvent.Reasoning.Ended.type)
             reasoning = (event.data as typeof SessionEvent.Reasoning.Ended.data.Type).text
           return Effect.void

+ 60 - 60
packages/sdk/js/src/v2/gen/types.gen.ts

@@ -46,12 +46,12 @@ export type Event =
   | EventSessionError
   | EventInstallationUpdated
   | EventInstallationUpdateAvailable
-  | EventPermissionV2Asked
-  | EventPermissionV2Replied
   | EventFileEdited
   | EventAccountAdded
   | EventAccountRemoved
   | EventAccountSwitched
+  | EventPermissionV2Asked
+  | EventPermissionV2Replied
   | EventFileWatcherUpdated
   | EventPtyCreated
   | EventPtyUpdated
@@ -1163,30 +1163,6 @@ export type GlobalEvent = {
           version: string
         }
       }
-    | {
-        id: string
-        type: "permission.v2.asked"
-        properties: {
-          id: string
-          sessionID: string
-          action: string
-          resources: Array<string>
-          save?: Array<string>
-          metadata?: {
-            [key: string]: unknown
-          }
-          source?: PermissionV2Source
-        }
-      }
-    | {
-        id: string
-        type: "permission.v2.replied"
-        properties: {
-          sessionID: string
-          requestID: string
-          reply: PermissionV2Reply
-        }
-      }
     | {
         id: string
         type: "file.edited"
@@ -1217,6 +1193,30 @@ export type GlobalEvent = {
           to?: string
         }
       }
+    | {
+        id: string
+        type: "permission.v2.asked"
+        properties: {
+          id: string
+          sessionID: string
+          action: string
+          resources: Array<string>
+          save?: Array<string>
+          metadata?: {
+            [key: string]: unknown
+          }
+          source?: PermissionV2Source
+        }
+      }
+    | {
+        id: string
+        type: "permission.v2.replied"
+        properties: {
+          sessionID: string
+          requestID: string
+          reply: PermissionV2Reply
+        }
+      }
     | {
         id: string
         type: "file.watcher.updated"
@@ -2893,14 +2893,6 @@ export type SessionNextRetryError = {
   }
 }
 
-export type PermissionV2Source = {
-  type: "tool"
-  messageID: string
-  callID: string
-}
-
-export type PermissionV2Reply = "once" | "always" | "reject"
-
 export type AuthOAuthCredential = {
   type: "oauth"
   refresh: string
@@ -2925,6 +2917,14 @@ export type AuthInfo = {
   credential: AuthCredential
 }
 
+export type PermissionV2Source = {
+  type: "tool"
+  messageID: string
+  callID: string
+}
+
+export type PermissionV2Reply = "once" | "always" | "reject"
+
 export type QuestionV2Option = {
   /**
    * Display text (1-5 words, concise)
@@ -4404,32 +4404,6 @@ export type EventInstallationUpdateAvailable = {
   }
 }
 
-export type EventPermissionV2Asked = {
-  id: string
-  type: "permission.v2.asked"
-  properties: {
-    id: string
-    sessionID: string
-    action: string
-    resources: Array<string>
-    save?: Array<string>
-    metadata?: {
-      [key: string]: unknown
-    }
-    source?: PermissionV2Source
-  }
-}
-
-export type EventPermissionV2Replied = {
-  id: string
-  type: "permission.v2.replied"
-  properties: {
-    sessionID: string
-    requestID: string
-    reply: PermissionV2Reply
-  }
-}
-
 export type EventFileEdited = {
   id: string
   type: "file.edited"
@@ -4464,6 +4438,32 @@ export type EventAccountSwitched = {
   }
 }
 
+export type EventPermissionV2Asked = {
+  id: string
+  type: "permission.v2.asked"
+  properties: {
+    id: string
+    sessionID: string
+    action: string
+    resources: Array<string>
+    save?: Array<string>
+    metadata?: {
+      [key: string]: unknown
+    }
+    source?: PermissionV2Source
+  }
+}
+
+export type EventPermissionV2Replied = {
+  id: string
+  type: "permission.v2.replied"
+  properties: {
+    sessionID: string
+    requestID: string
+    reply: PermissionV2Reply
+  }
+}
+
 export type EventFileWatcherUpdated = {
   id: string
   type: "file.watcher.updated"

+ 155 - 155
packages/sdk/openapi.json

@@ -11655,12 +11655,6 @@
           {
             "$ref": "#/components/schemas/EventInstallationUpdate-available"
           },
-          {
-            "$ref": "#/components/schemas/EventPermissionV2Asked"
-          },
-          {
-            "$ref": "#/components/schemas/EventPermissionV2Replied"
-          },
           {
             "$ref": "#/components/schemas/EventFileEdited"
           },
@@ -11673,6 +11667,12 @@
           {
             "$ref": "#/components/schemas/EventAccountSwitched"
           },
+          {
+            "$ref": "#/components/schemas/EventPermissionV2Asked"
+          },
+          {
+            "$ref": "#/components/schemas/EventPermissionV2Replied"
+          },
           {
             "$ref": "#/components/schemas/EventFileWatcherUpdated"
           },
@@ -15120,42 +15120,16 @@
                   },
                   "type": {
                     "type": "string",
-                    "enum": ["permission.v2.asked"]
+                    "enum": ["file.edited"]
                   },
                   "properties": {
                     "type": "object",
                     "properties": {
-                      "id": {
-                        "type": "string",
-                        "pattern": "^per"
-                      },
-                      "sessionID": {
-                        "type": "string",
-                        "pattern": "^ses"
-                      },
-                      "action": {
+                      "file": {
                         "type": "string"
-                      },
-                      "resources": {
-                        "type": "array",
-                        "items": {
-                          "type": "string"
-                        }
-                      },
-                      "save": {
-                        "type": "array",
-                        "items": {
-                          "type": "string"
-                        }
-                      },
-                      "metadata": {
-                        "type": "object"
-                      },
-                      "source": {
-                        "$ref": "#/components/schemas/PermissionV2Source"
                       }
                     },
-                    "required": ["id", "sessionID", "action", "resources"],
+                    "required": ["file"],
                     "additionalProperties": false
                   }
                 },
@@ -15170,24 +15144,16 @@
                   },
                   "type": {
                     "type": "string",
-                    "enum": ["permission.v2.replied"]
+                    "enum": ["account.added"]
                   },
                   "properties": {
                     "type": "object",
                     "properties": {
-                      "sessionID": {
-                        "type": "string",
-                        "pattern": "^ses"
-                      },
-                      "requestID": {
-                        "type": "string",
-                        "pattern": "^per"
-                      },
-                      "reply": {
-                        "$ref": "#/components/schemas/PermissionV2Reply"
+                      "account": {
+                        "$ref": "#/components/schemas/AuthInfo"
                       }
                     },
-                    "required": ["sessionID", "requestID", "reply"],
+                    "required": ["account"],
                     "additionalProperties": false
                   }
                 },
@@ -15202,16 +15168,16 @@
                   },
                   "type": {
                     "type": "string",
-                    "enum": ["file.edited"]
+                    "enum": ["account.removed"]
                   },
                   "properties": {
                     "type": "object",
                     "properties": {
-                      "file": {
-                        "type": "string"
+                      "account": {
+                        "$ref": "#/components/schemas/AuthInfo"
                       }
                     },
-                    "required": ["file"],
+                    "required": ["account"],
                     "additionalProperties": false
                   }
                 },
@@ -15226,16 +15192,22 @@
                   },
                   "type": {
                     "type": "string",
-                    "enum": ["account.added"]
+                    "enum": ["account.switched"]
                   },
                   "properties": {
                     "type": "object",
                     "properties": {
-                      "account": {
-                        "$ref": "#/components/schemas/AuthInfo"
+                      "serviceID": {
+                        "type": "string"
+                      },
+                      "from": {
+                        "type": "string"
+                      },
+                      "to": {
+                        "type": "string"
                       }
                     },
-                    "required": ["account"],
+                    "required": ["serviceID"],
                     "additionalProperties": false
                   }
                 },
@@ -15250,16 +15222,42 @@
                   },
                   "type": {
                     "type": "string",
-                    "enum": ["account.removed"]
+                    "enum": ["permission.v2.asked"]
                   },
                   "properties": {
                     "type": "object",
                     "properties": {
-                      "account": {
-                        "$ref": "#/components/schemas/AuthInfo"
+                      "id": {
+                        "type": "string",
+                        "pattern": "^per"
+                      },
+                      "sessionID": {
+                        "type": "string",
+                        "pattern": "^ses"
+                      },
+                      "action": {
+                        "type": "string"
+                      },
+                      "resources": {
+                        "type": "array",
+                        "items": {
+                          "type": "string"
+                        }
+                      },
+                      "save": {
+                        "type": "array",
+                        "items": {
+                          "type": "string"
+                        }
+                      },
+                      "metadata": {
+                        "type": "object"
+                      },
+                      "source": {
+                        "$ref": "#/components/schemas/PermissionV2Source"
                       }
                     },
-                    "required": ["account"],
+                    "required": ["id", "sessionID", "action", "resources"],
                     "additionalProperties": false
                   }
                 },
@@ -15274,22 +15272,24 @@
                   },
                   "type": {
                     "type": "string",
-                    "enum": ["account.switched"]
+                    "enum": ["permission.v2.replied"]
                   },
                   "properties": {
                     "type": "object",
                     "properties": {
-                      "serviceID": {
-                        "type": "string"
+                      "sessionID": {
+                        "type": "string",
+                        "pattern": "^ses"
                       },
-                      "from": {
-                        "type": "string"
+                      "requestID": {
+                        "type": "string",
+                        "pattern": "^per"
                       },
-                      "to": {
-                        "type": "string"
+                      "reply": {
+                        "$ref": "#/components/schemas/PermissionV2Reply"
                       }
                     },
-                    "required": ["serviceID"],
+                    "required": ["sessionID", "requestID", "reply"],
                     "additionalProperties": false
                   }
                 },
@@ -20175,27 +20175,6 @@
         "required": ["message", "isRetryable"],
         "additionalProperties": false
       },
-      "PermissionV2Source": {
-        "type": "object",
-        "properties": {
-          "type": {
-            "type": "string",
-            "enum": ["tool"]
-          },
-          "messageID": {
-            "type": "string"
-          },
-          "callID": {
-            "type": "string"
-          }
-        },
-        "required": ["type", "messageID", "callID"],
-        "additionalProperties": false
-      },
-      "PermissionV2Reply": {
-        "type": "string",
-        "enum": ["once", "always", "reject"]
-      },
       "AuthOAuthCredential": {
         "type": "object",
         "properties": {
@@ -20266,6 +20245,27 @@
         "required": ["id", "serviceID", "description", "credential"],
         "additionalProperties": false
       },
+      "PermissionV2Source": {
+        "type": "object",
+        "properties": {
+          "type": {
+            "type": "string",
+            "enum": ["tool"]
+          },
+          "messageID": {
+            "type": "string"
+          },
+          "callID": {
+            "type": "string"
+          }
+        },
+        "required": ["type", "messageID", "callID"],
+        "additionalProperties": false
+      },
+      "PermissionV2Reply": {
+        "type": "string",
+        "enum": ["once", "always", "reject"]
+      },
       "QuestionV2Option": {
         "type": "object",
         "properties": {
@@ -24737,7 +24737,7 @@
         "required": ["id", "type", "properties"],
         "additionalProperties": false
       },
-      "EventPermissionV2Asked": {
+      "EventFileEdited": {
         "type": "object",
         "properties": {
           "id": {
@@ -24745,49 +24745,23 @@
           },
           "type": {
             "type": "string",
-            "enum": ["permission.v2.asked"]
+            "enum": ["file.edited"]
           },
           "properties": {
             "type": "object",
             "properties": {
-              "id": {
-                "type": "string",
-                "pattern": "^per"
-              },
-              "sessionID": {
-                "type": "string",
-                "pattern": "^ses"
-              },
-              "action": {
+              "file": {
                 "type": "string"
-              },
-              "resources": {
-                "type": "array",
-                "items": {
-                  "type": "string"
-                }
-              },
-              "save": {
-                "type": "array",
-                "items": {
-                  "type": "string"
-                }
-              },
-              "metadata": {
-                "type": "object"
-              },
-              "source": {
-                "$ref": "#/components/schemas/PermissionV2Source"
               }
             },
-            "required": ["id", "sessionID", "action", "resources"],
+            "required": ["file"],
             "additionalProperties": false
           }
         },
         "required": ["id", "type", "properties"],
         "additionalProperties": false
       },
-      "EventPermissionV2Replied": {
+      "EventAccountAdded": {
         "type": "object",
         "properties": {
           "id": {
@@ -24795,31 +24769,23 @@
           },
           "type": {
             "type": "string",
-            "enum": ["permission.v2.replied"]
+            "enum": ["account.added"]
           },
           "properties": {
             "type": "object",
             "properties": {
-              "sessionID": {
-                "type": "string",
-                "pattern": "^ses"
-              },
-              "requestID": {
-                "type": "string",
-                "pattern": "^per"
-              },
-              "reply": {
-                "$ref": "#/components/schemas/PermissionV2Reply"
+              "account": {
+                "$ref": "#/components/schemas/AuthInfo"
               }
             },
-            "required": ["sessionID", "requestID", "reply"],
+            "required": ["account"],
             "additionalProperties": false
           }
         },
         "required": ["id", "type", "properties"],
         "additionalProperties": false
       },
-      "EventFileEdited": {
+      "EventAccountRemoved": {
         "type": "object",
         "properties": {
           "id": {
@@ -24827,23 +24793,23 @@
           },
           "type": {
             "type": "string",
-            "enum": ["file.edited"]
+            "enum": ["account.removed"]
           },
           "properties": {
             "type": "object",
             "properties": {
-              "file": {
-                "type": "string"
+              "account": {
+                "$ref": "#/components/schemas/AuthInfo"
               }
             },
-            "required": ["file"],
+            "required": ["account"],
             "additionalProperties": false
           }
         },
         "required": ["id", "type", "properties"],
         "additionalProperties": false
       },
-      "EventAccountAdded": {
+      "EventAccountSwitched": {
         "type": "object",
         "properties": {
           "id": {
@@ -24851,23 +24817,29 @@
           },
           "type": {
             "type": "string",
-            "enum": ["account.added"]
+            "enum": ["account.switched"]
           },
           "properties": {
             "type": "object",
             "properties": {
-              "account": {
-                "$ref": "#/components/schemas/AuthInfo"
+              "serviceID": {
+                "type": "string"
+              },
+              "from": {
+                "type": "string"
+              },
+              "to": {
+                "type": "string"
               }
             },
-            "required": ["account"],
+            "required": ["serviceID"],
             "additionalProperties": false
           }
         },
         "required": ["id", "type", "properties"],
         "additionalProperties": false
       },
-      "EventAccountRemoved": {
+      "EventPermissionV2Asked": {
         "type": "object",
         "properties": {
           "id": {
@@ -24875,23 +24847,49 @@
           },
           "type": {
             "type": "string",
-            "enum": ["account.removed"]
+            "enum": ["permission.v2.asked"]
           },
           "properties": {
             "type": "object",
             "properties": {
-              "account": {
-                "$ref": "#/components/schemas/AuthInfo"
+              "id": {
+                "type": "string",
+                "pattern": "^per"
+              },
+              "sessionID": {
+                "type": "string",
+                "pattern": "^ses"
+              },
+              "action": {
+                "type": "string"
+              },
+              "resources": {
+                "type": "array",
+                "items": {
+                  "type": "string"
+                }
+              },
+              "save": {
+                "type": "array",
+                "items": {
+                  "type": "string"
+                }
+              },
+              "metadata": {
+                "type": "object"
+              },
+              "source": {
+                "$ref": "#/components/schemas/PermissionV2Source"
               }
             },
-            "required": ["account"],
+            "required": ["id", "sessionID", "action", "resources"],
             "additionalProperties": false
           }
         },
         "required": ["id", "type", "properties"],
         "additionalProperties": false
       },
-      "EventAccountSwitched": {
+      "EventPermissionV2Replied": {
         "type": "object",
         "properties": {
           "id": {
@@ -24899,22 +24897,24 @@
           },
           "type": {
             "type": "string",
-            "enum": ["account.switched"]
+            "enum": ["permission.v2.replied"]
           },
           "properties": {
             "type": "object",
             "properties": {
-              "serviceID": {
-                "type": "string"
+              "sessionID": {
+                "type": "string",
+                "pattern": "^ses"
               },
-              "from": {
-                "type": "string"
+              "requestID": {
+                "type": "string",
+                "pattern": "^per"
               },
-              "to": {
-                "type": "string"
+              "reply": {
+                "$ref": "#/components/schemas/PermissionV2Reply"
               }
             },
-            "required": ["serviceID"],
+            "required": ["sessionID", "requestID", "reply"],
             "additionalProperties": false
           }
         },

+ 4 - 1
specs/v2/provider-model.md

@@ -232,7 +232,10 @@ export interface Interface {
   }
 
   readonly model: {
-    readonly get: (providerID: ProviderV2.ID, modelID: ModelV2.ID) => Effect.Effect<ModelV2.Info, ProviderNotFoundError | ModelNotFoundError>
+    readonly get: (
+      providerID: ProviderV2.ID,
+      modelID: ModelV2.ID,
+    ) => Effect.Effect<ModelV2.Info, ProviderNotFoundError | ModelNotFoundError>
     readonly all: () => Effect.Effect<ModelV2.Info[]>
     readonly available: () => Effect.Effect<ModelV2.Info[]>
     readonly default: () => Effect.Effect<Option.Option<ModelV2.Info>>