Compare commits

..

52 Commits

Author SHA1 Message Date
James Long b79e75e01a refactor(tui): migrate worktree workflows 2026-08-12 21:57:30 +00:00
James Long eb75637742 refactor(server): expose global worktree api 2026-08-12 21:57:21 +00:00
James Long 2bba2915db refactor(core): consolidate worktree domain 2026-08-12 21:57:11 +00:00
Simon Klee 66c2967520 deps: update OpenTUI to 0.5.2 (#42148) 2026-08-12 23:09:41 +02:00
opencode-agent[bot] 883c9e3cb4 chore: generate 2026-08-12 20:49:54 +00:00
Kit Langton 0d703d39e7 fix(core): adopt pre-project sessions on directory resolution (#42100) 2026-08-12 20:47:55 +00:00
Kit Langton bc2251d6d8 feat(sdk): configure external workspace providers (#42142) 2026-08-12 16:47:12 -04:00
opencode-agent[bot] b034154e09 chore: generate 2026-08-12 20:45:10 +00:00
Kit Langton c894f87771 feat(tui): refine session tab activity indicators (#42144) 2026-08-12 16:43:58 -04:00
opencode-agent[bot] fc5c781085 chore: generate 2026-08-12 20:29:39 +00:00
Kit Langton f6aa1a67f0 fix(tui): improve selected spinner contrast (#42141) 2026-08-12 16:27:52 -04:00
Dax 0859f77153 fix(core): continue sessions after location move (#42138) 2026-08-12 16:25:50 -04:00
Kit Langton 7a22ac865d fix(tui): navigate filtered plugins (#42127) 2026-08-12 16:20:52 -04:00
Dax b75dd58f7c fix(tui): continue pending work after interrupt (#42131) 2026-08-12 15:55:53 -04:00
Kit Langton 56197e621a refactor(codemode): route transpilation through a #transpile conditional import (#42121) 2026-08-12 15:43:30 -04:00
Kit Langton d8f62cfdcb fix(core): resolve watcher binding require lazily (#42122) 2026-08-12 15:40:41 -04:00
Dax Raad c9539979bd fix(tui): highlight moved session location 2026-08-12 15:37:41 -04:00
opencode-agent[bot] f5e6b8c769 chore: generate 2026-08-12 19:29:55 +00:00
Kit Langton 2025ed939a feat(server): typed no-execution-plane environment for workerd profile (#42113) 2026-08-12 19:28:07 +00:00
Dax a7642737c7 feat(core): add generic session inbox (#42109) 2026-08-12 15:28:04 -04:00
opencode-agent[bot] c081e12c60 chore: generate 2026-08-12 19:10:52 +00:00
opencode-agent[bot] 8d5dd206a6 fix(core): validate tool definitions on registration (#42117)
Co-authored-by: Aiden Cline <rekram1-node@users.noreply.github.com>
2026-08-12 14:08:13 -05:00
Kit Langton c6a86acb0b fix(tui): harden Mermaid rendering limits (#42123) 2026-08-12 15:06:28 -04:00
opencode-agent[bot] c7bee09632 fix(core): preserve Copilot fallback provider (#42099)
Co-authored-by: Aiden Cline <rekram1-node@users.noreply.github.com>
2026-08-12 14:03:27 -05:00
opencode-agent[bot] a3625cd2cb chore: generate 2026-08-12 18:50:05 +00:00
Kit Langton 0dec446ee6 fix(tui): restore tab marquee hover (#42093) 2026-08-12 14:47:34 -04:00
James Long dff6eb631b fix(tui): show worktree names in session list (#42108) 2026-08-12 14:44:48 -04:00
opencode-agent[bot] 3251bff6ff chore: generate 2026-08-12 18:35:52 +00:00
Kit Langton f713d39c61 fix(tui): keep interrupt confirmation visible (#42096) 2026-08-12 14:33:53 -04:00
opencode-agent[bot] c80472b6d5 chore: add neriousy to team members (#42106)
Co-authored-by: Aiden Cline <rekram1-node@users.noreply.github.com>
2026-08-12 13:30:49 -05:00
Kit Langton c86b3079ac fix(tui): render Mermaid rich labels compactly (#42103) 2026-08-12 14:28:54 -04:00
opencode-agent[bot] f8f50ee375 chore: generate 2026-08-12 18:10:29 +00:00
Kit Langton 912a1c4e8a fix(tui): move debug overlay into developer tools (#42098) 2026-08-12 14:08:25 -04:00
Kit Langton a841fc22bb feat(server): workerd runtime profile and SDK workerd entrypoint (#41918) 2026-08-12 14:07:56 -04:00
Kit Langton 723f5e0028 fix(tui): consolidate developer tools controls (#42088) 2026-08-12 13:23:14 -04:00
Kit Langton 17cae23dce fix(tui): stabilize repeated open menu (#42086) 2026-08-12 13:17:13 -04:00
Kit Langton fd30b9765d refactor(core): spawn stdio MCP servers through the location environment (#42076) 2026-08-12 13:16:12 -04:00
Dax Raad a20d945245 docs: base worktrees on v2 2026-08-12 12:56:00 -04:00
Kit Langton 10ebf70a07 feat(tui): add session tab context menu (#42068) 2026-08-12 16:45:59 +00:00
opencode-agent[bot] b50924b993 test: stabilize Windows integration coverage (#42079)
Co-authored-by: Kit Langton <kit.langton@gmail.com>
2026-08-12 12:45:38 -04:00
Kit Langton b24b1b3f16 fix(tui): keep exact-fit tab titles stationary (#42073) 2026-08-12 12:17:45 -04:00
Kit Langton 930b0751b1 fix(core): generate session titles before model execution (#42067) 2026-08-12 12:08:24 -04:00
Matt Robinson f06a86eeac feat(client): support service version ranges (#42023)
Co-authored-by: Dax Raad <d@ironbay.co>
2026-08-12 09:01:37 -07:00
Kit Langton 653b7d79cd fix(tui): restore navigation keybind defaults (#42066) 2026-08-12 11:57:39 -04:00
Kit Langton 70ce0d0970 feat(tui): jump between open menu sections (#42061) 2026-08-12 15:52:51 +00:00
opencode-agent[bot] 0777e84598 fix(tui): fill image message background (#42062)
Co-authored-by: Simon Klee <hello@simonklee.dk>
2026-08-12 15:51:19 +00:00
Kit Langton bef795b2fe fix(tui): smooth session tab marquees (#42055) 2026-08-12 15:49:17 +00:00
Kit Langton 70853b1e5b feat(tui): surface plugin failures (#41940) 2026-08-12 10:51:00 -04:00
opencode-agent[bot] 1fea1c2ebc fix(core): route Muse models to Meta prompt (#42036)
Co-authored-by: Aiden Cline <rekram1-node@users.noreply.github.com>
2026-08-12 09:31:59 -05:00
opencode-agent[bot] 1da591b84d fix(core): return content-only Code Mode results (#41954)
Co-authored-by: Aiden Cline <rekram1-node@users.noreply.github.com>
2026-08-12 09:14:41 -05:00
opencode-agent[bot] b990f9a5c1 chore: generate 2026-08-12 13:32:36 +00:00
Dax Raad 9769e7012c feat(core): configure package publishing 2026-08-12 09:30:34 -04:00
282 changed files with 14863 additions and 8055 deletions
+1
View File
@@ -10,6 +10,7 @@ kitlangton
kommander
ludvigrask
MrMushrooooom
neriousy
nexxeln
R44VC0RP
rekram1-node
+1 -1
View File
@@ -4,7 +4,7 @@ import { tool } from "@opencode-ai/plugin"
const TEAM = {
tui: ["kommander", "simonklee"],
desktop_web: ["Hona", "Brendonovich"],
core: ["jlongster", "rekram1-node", "nexxeln", "kitlangton"],
core: ["jlongster", "rekram1-node", "neriousy", "nexxeln", "kitlangton"],
inference: ["fwang", "MrMushrooooom", "starptech"],
windows: ["Hona"],
} as const
+3 -2
View File
@@ -1,8 +1,9 @@
- After changing the public Protocol or Server `HttpApi`, run `bun run generate` from `packages/client`. Do not edit `src/generated` or `src/generated-effect` directly.
- Keep runtime dependencies directed from Schema to Core and Protocol, then from Core and Protocol to Server. Client runtime code may depend on Schema and Protocol but never Core or Server; `sdk-next` composes Client, Core, and Server.
- Do not modify `packages/opencode` unless the user explicitly asks for V1 work. `packages/opencode` is the V1 implementation and is present for reference only. New implementation changes should land in the V2 package set: `packages/core`, `packages/cli`, `packages/server`, `packages/protocol`, `packages/schema`, and related generated client surfaces when required.
- The default branch in this repo is `dev`.
- Local `main` ref may not exist; use `dev` or `origin/dev` for diffs.
- The default branch in this repo is `v2`.
- Base all new branches and worktrees on `v2`, or `origin/v2` when the local `v2` ref is unavailable. Do not base them on `dev`.
- Local `main` ref may not exist; use `v2` or `origin/v2` for diffs.
## Live V2 TUI Testing
+414 -169
View File
@@ -345,9 +345,6 @@
"packages/core": {
"name": "@opencode-ai/core",
"version": "1.18.4",
"bin": {
"opencode": "./bin/opencode",
},
"dependencies": {
"@ai-sdk/alibaba": "1.0.17",
"@ai-sdk/amazon-bedrock": "4.0.112",
@@ -367,8 +364,6 @@
"@ai-sdk/togetherai": "2.0.41",
"@ai-sdk/vercel": "2.0.39",
"@aws-sdk/credential-providers": "3.1057.0",
"@effect/platform-node": "catalog:",
"@effect/sql-sqlite-bun": "catalog:",
"@ff-labs/fff-bun": "0.10.1",
"@ff-labs/fff-node": "0.10.1",
"@lydell/node-pty": "catalog:",
@@ -404,6 +399,8 @@
"zod": "catalog:",
},
"devDependencies": {
"@effect/platform-node": "catalog:",
"@effect/sql-sqlite-bun": "catalog:",
"@opencode-ai/http-recorder": "workspace:*",
"@parcel/watcher-darwin-arm64": "2.5.1",
"@parcel/watcher-darwin-x64": "2.5.1",
@@ -599,9 +596,9 @@
},
"peerDependencies": {
"@opencode-ai/theme": "workspace:*",
"@opentui/core": "0.0.0-20260808-9ecf7c0a",
"@opentui/keymap": "0.0.0-20260808-9ecf7c0a",
"@opentui/solid": "0.0.0-20260808-9ecf7c0a",
"@opentui/core": ">=0.5.2",
"@opentui/keymap": ">=0.5.2",
"@opentui/solid": ">=0.5.2",
"solid-js": ">=1.9.0",
},
"optionalPeers": [
@@ -658,6 +655,7 @@
"@opencode-ai/plugin": "workspace:*",
"@opencode-ai/schema": "workspace:*",
"@opencode-ai/server": "workspace:*",
"@opencode-ai/util": "workspace:*",
"effect": "catalog:",
},
"devDependencies": {
@@ -680,7 +678,6 @@
"@opencode-ai/util": "workspace:*",
"drizzle-orm": "catalog:",
"effect": "catalog:",
"modal": "0.9.0",
},
"devDependencies": {
"@tsconfig/bun": "catalog:",
@@ -1035,6 +1032,31 @@
"typescript": "catalog:",
},
},
"packages/workerd-spike": {
"name": "@opencode-ai/workerd-spike",
"version": "0.0.0",
"dependencies": {
"@opencode-ai/core": "workspace:*",
"@opencode-ai/server": "workspace:*",
"effect": "catalog:",
},
"devDependencies": {
"@cloudflare/vitest-pool-workers": "0.12.6",
"@cloudflare/workers-types": "^4.20250808.0",
"@effect/platform-node": "catalog:",
"@vitest/expect": "3.2.7",
"@vitest/mocker": "3.2.7",
"@vitest/pretty-format": "3.2.7",
"@vitest/runner": "3.2.7",
"@vitest/snapshot": "3.2.7",
"@vitest/spy": "3.2.7",
"@vitest/utils": "3.2.7",
"unenv": "2.0.0-rc.24",
"vitest": "3.2.7",
"wrangler": "4.28.0",
"xdg-basedir": "5.1.0",
},
},
"packages/www": {
"name": "@opencode-ai/www",
"dependencies": {
@@ -1064,6 +1086,7 @@
"effect@4.0.0-beta.101": "patches/effect@4.0.0-beta.101.patch",
"@modelcontextprotocol/sdk@1.29.0": "patches/@modelcontextprotocol%2Fsdk@1.29.0.patch",
"@tanstack/virtual-core@3.17.3": "patches/@tanstack%2Fvirtual-core@3.17.3.patch",
"@cloudflare/vitest-pool-workers@0.12.6": "patches/@cloudflare%2Fvitest-pool-workers@0.12.6.patch",
"@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch",
"@ai-sdk/mistral@3.0.51": "patches/@ai-sdk%2Fmistral@3.0.51.patch",
"@ff-labs/fff-bun@0.10.1": "patches/@ff-labs%2Ffff-bun@0.10.1.patch",
@@ -1075,9 +1098,9 @@
"@ai-sdk/openai-compatible@2.0.41": "patches/@ai-sdk%2Fopenai-compatible@2.0.41.patch",
},
"overrides": {
"@opentui/core": "0.0.0-20260808-9ecf7c0a",
"@opentui/keymap": "0.0.0-20260808-9ecf7c0a",
"@opentui/solid": "0.0.0-20260808-9ecf7c0a",
"@opentui/core": "catalog:",
"@opentui/keymap": "catalog:",
"@opentui/solid": "catalog:",
"@types/bun": "catalog:",
"@types/node": "catalog:",
"effect": "catalog:",
@@ -1095,9 +1118,9 @@
"@npmcli/arborist": "9.4.0",
"@octokit/rest": "22.0.0",
"@openauthjs/openauth": "0.0.0-20250322224806",
"@opentui/core": "0.0.0-20260808-9ecf7c0a",
"@opentui/keymap": "0.0.0-20260808-9ecf7c0a",
"@opentui/solid": "0.0.0-20260808-9ecf7c0a",
"@opentui/core": "0.5.2",
"@opentui/keymap": "0.5.2",
"@opentui/solid": "0.5.2",
"@pierre/diffs": "1.2.10",
"@playwright/test": "1.59.1",
"@sentry/solid": "10.36.0",
@@ -1513,18 +1536,6 @@
"@capsizecss/unpack": ["@capsizecss/unpack@2.4.0", "", { "dependencies": { "blob-to-buffer": "^1.2.8", "cross-fetch": "^3.0.4", "fontkit": "^2.0.2" } }, "sha512-GrSU71meACqcmIUxPYOJvGKF0yryjN/L1aCuE9DViCTJI7bfkjgYDPD1zbNDcINJwSSP6UaBZY9GAbYDO7re0Q=="],
"@cbor-extract/cbor-extract-darwin-arm64": ["@cbor-extract/cbor-extract-darwin-arm64@2.2.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ZKZ/F8US7JR92J4DMct6cLW/Y66o2K576+zjlEN/MevH70bFIsB10wkZEQPLzl2oNh2SMGy55xpJ9JoBRl5DOA=="],
"@cbor-extract/cbor-extract-darwin-x64": ["@cbor-extract/cbor-extract-darwin-x64@2.2.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-32b1mgc+P61Js+KW9VZv/c+xRw5EfmOcPx990JbCBSkYJFY0l25VinvyyWfl+3KjibQmAcYwmyzKF9J4DyKP/Q=="],
"@cbor-extract/cbor-extract-linux-arm": ["@cbor-extract/cbor-extract-linux-arm@2.2.2", "", { "os": "linux", "cpu": "arm" }, "sha512-tNg0za41TpQfkhWjptD+0gSD2fggMiDCSacuIeELyb2xZhr7PrhPe5h66Jc67B/5dmpIhI2QOUtv4SBsricyYQ=="],
"@cbor-extract/cbor-extract-linux-arm64": ["@cbor-extract/cbor-extract-linux-arm64@2.2.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-wfqgzqCAy/Vn8i6WVIh7qZd0DdBFaWBjPdB6ma+Wihcjv0gHqD/mw3ouVv7kbbUNrab6dKEx/w3xQZEdeXIlzg=="],
"@cbor-extract/cbor-extract-linux-x64": ["@cbor-extract/cbor-extract-linux-x64@2.2.2", "", { "os": "linux", "cpu": "x64" }, "sha512-rpiLnVEsqtPJ+mXTdx1rfz4RtUGYIUg2rUAZgd1KjiC1SehYUSkJN7Yh+aVfSjvCGtVP0/bfkQkXpPXKbmSUaA=="],
"@cbor-extract/cbor-extract-win32-x64": ["@cbor-extract/cbor-extract-win32-x64@2.2.2", "", { "os": "win32", "cpu": "x64" }, "sha512-dI+9P7cfWxkTQ+oE+7Aa6onEn92PHgfWXZivjNheCRmTBDBf2fx6RyTi0cmgpYLnD1KLZK9ZYrMxaPZ4oiXhGA=="],
"@chevrotain/types": ["@chevrotain/types@11.1.2", "", {}, "sha512-U+HFai5+zmJCkK86QsaJtoITlboZHBqrVketcO2ROv865xfCMSFpELQoz1GkX5GzME8pTa+3kbKrZHQtI0gdbw=="],
"@clack/core": ["@clack/core@1.0.0-alpha.1", "", { "dependencies": { "picocolors": "^1.0.0", "sisteransi": "^1.0.5" } }, "sha512-rFbCU83JnN7l3W1nfgCqqme4ZZvTTgsiKQ6FM0l+r0P+o2eJpExcocBUWUIwnDzL76Aca9VhUdWmB2MbUv+Qyg=="],
@@ -1537,6 +1548,8 @@
"@cloudflare/vite-plugin": ["@cloudflare/vite-plugin@1.15.2", "", { "dependencies": { "@cloudflare/unenv-preset": "2.7.11", "@remix-run/node-fetch-server": "^0.8.0", "get-port": "^7.1.0", "miniflare": "4.20251118.1", "picocolors": "^1.1.1", "tinyglobby": "^0.2.12", "unenv": "2.0.0-rc.24", "wrangler": "4.50.0", "ws": "8.18.0" }, "peerDependencies": { "vite": "^6.1.0 || ^7.0.0" } }, "sha512-SPMxsesbABOjzcAa4IzW+yM+fTIjx3GG1doh229Pg16FjSEZJhknyRpcld4gnaZioK3JKwG9FWdKsUhbplKY8w=="],
"@cloudflare/vitest-pool-workers": ["@cloudflare/vitest-pool-workers@0.12.6", "", { "dependencies": { "cjs-module-lexer": "^1.2.3", "esbuild": "0.27.0", "miniflare": "4.20260120.0", "wrangler": "4.60.0", "zod": "^3.25.76" }, "peerDependencies": { "@vitest/runner": "2.0.x - 3.2.x", "@vitest/snapshot": "2.0.x - 3.2.x", "vitest": "2.0.x - 3.2.x" } }, "sha512-smzhKzBdB4JYKo9bb8x3i70CtSj5GRhDfGvUhbLI7uPUv4QwMubddmZ+XYNekcy3C8/FG1fjLiwUqisITik8oQ=="],
"@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20251118.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-UmWmYEYS/LkK/4HFKN6xf3Hk8cw70PviR+ftr3hUvs9HYZS92IseZEp16pkL6ZBETrPRpZC7OrzoYF7ky6kHsg=="],
"@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20251118.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RockU7Qzf4rxNfY1lx3j4rvwutNLjTIX7rr2hogbQ4mzLo8Ea40/oZTzXVxl+on75joLBrt0YpenGW8o/r44QA=="],
@@ -1733,10 +1746,6 @@
"@graphql-typed-document-node/core": ["@graphql-typed-document-node/core@3.2.0", "", { "peerDependencies": { "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ=="],
"@grpc/grpc-js": ["@grpc/grpc-js@1.14.4", "", { "dependencies": { "@grpc/proto-loader": "^0.8.0", "@js-sdsl/ordered-map": "^4.4.2" } }, "sha512-k9Dj3DV/itK9D06Y8f190Qgop7/Ui+D0njFV3LHMPwPT75DpXLQohE9Wmz0QElrJnzsjB7KPWiKJbOl7IPDArQ=="],
"@grpc/proto-loader": ["@grpc/proto-loader@0.8.1", "", { "dependencies": { "lodash.camelcase": "^4.3.0", "long": "^5.0.0", "protobufjs": "^7.5.5", "yargs": "^17.7.2" }, "bin": { "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" } }, "sha512-wtF6h+DY6M3YaDBPAmvuuA6jV8Sif9MjtOI5euKFWRgCDl5PeDpPsHR9u2l6St5ceY8AZgoNDww5+HvEsXFsGg=="],
"@happy-dom/global-registrator": ["@happy-dom/global-registrator@20.0.11", "", { "dependencies": { "@types/node": "^20.0.0", "happy-dom": "^20.0.11" } }, "sha512-GqNqiShBT/lzkHTMC/slKBrvN0DsD4Di8ssBk4aDaVgEn+2WMzE6DXxq701ndSXj7/0cJ8mNT71pM7Bnrr6JRw=="],
"@hono/node-server": ["@hono/node-server@1.19.15", "", { "peerDependencies": { "hono": "^4" } }, "sha512-Za2ai6TLdKjUvnur+eenO6nuYYipVAEhyCAdaV8IRvmU9kK8crOZUSYvIXn72E4f8fJqyAbpcJuTsYYmZp9Deg=="],
@@ -1831,8 +1840,6 @@
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
"@js-sdsl/ordered-map": ["@js-sdsl/ordered-map@4.4.2", "", {}, "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw=="],
"@js-temporal/polyfill": ["@js-temporal/polyfill@0.5.1", "", { "dependencies": { "jsbi": "^4.3.0" } }, "sha512-hloP58zRVCRSpgDxmqCWJNlizAlUgJFqG2ypq79DCvyv9tHjRYMDOcPFjzfl/A1/YxDvRCZz8wvZvmapQnKwFQ=="],
"@jsx-email/all": ["@jsx-email/all@2.2.3", "", { "dependencies": { "@jsx-email/body": "1.0.2", "@jsx-email/button": "1.0.4", "@jsx-email/column": "1.0.3", "@jsx-email/container": "1.0.2", "@jsx-email/font": "1.0.3", "@jsx-email/head": "1.0.2", "@jsx-email/heading": "1.0.2", "@jsx-email/hr": "1.0.2", "@jsx-email/html": "1.0.2", "@jsx-email/img": "1.0.2", "@jsx-email/link": "1.0.2", "@jsx-email/markdown": "2.0.4", "@jsx-email/preview": "1.0.2", "@jsx-email/render": "1.1.1", "@jsx-email/row": "1.0.2", "@jsx-email/section": "1.0.2", "@jsx-email/tailwind": "2.4.4", "@jsx-email/text": "1.0.2" }, "peerDependencies": { "react": "^18.2.0" } }, "sha512-OBvLe/hVSQc0LlMSTJnkjFoqs3bmxcC4zpy/5pT5agPCSKMvAKQjzmsc2xJ2wO73jSpRV1K/g38GmvdCfrhSoQ=="],
@@ -2123,6 +2130,8 @@
"@opencode-ai/web": ["@opencode-ai/web@workspace:packages/web"],
"@opencode-ai/workerd-spike": ["@opencode-ai/workerd-spike@workspace:packages/workerd-spike"],
"@opencode-ai/www": ["@opencode-ai/www@workspace:packages/www"],
"@openrouter/ai-sdk-provider": ["@openrouter/ai-sdk-provider@2.9.0", "", { "peerDependencies": { "ai": "^6.0.0", "zod": "^3.25.0 || ^4.0.0" } }, "sha512-Seva+NCa0WUQnJIUE5GzHsUv1WTIeyqwz0ELl2VtS6NP+eF+77yCXGFVOMbvoCM7QMjlnhv7931e89R+8pJdcQ=="],
@@ -2153,27 +2162,27 @@
"@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.43.0", "", {}, "sha512-eSYWTm620tTk45EKSedaUL8MFYI8hW164hIXsgIHyxu3VobUB3fFCu5t0hQby6OoWRPsG1KkKUG2M5UadiLiVg=="],
"@opentui/core": ["@opentui/core@0.0.0-20260808-9ecf7c0a", "", { "dependencies": { "bun-ffi-structs": "0.3.1", "diff": "9.0.0", "marked": "17.0.1", "string-width": "7.2.0", "strip-ansi": "7.1.2" }, "optionalDependencies": { "@opentui/core-darwin-arm64": "0.0.0-20260808-9ecf7c0a", "@opentui/core-darwin-x64": "0.0.0-20260808-9ecf7c0a", "@opentui/core-linux-arm64": "0.0.0-20260808-9ecf7c0a", "@opentui/core-linux-arm64-musl": "0.0.0-20260808-9ecf7c0a", "@opentui/core-linux-x64": "0.0.0-20260808-9ecf7c0a", "@opentui/core-linux-x64-musl": "0.0.0-20260808-9ecf7c0a", "@opentui/core-win32-arm64": "0.0.0-20260808-9ecf7c0a", "@opentui/core-win32-x64": "0.0.0-20260808-9ecf7c0a" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-y9MKL8LMup4ebW3uZ57RSsPyOv37sIZxuWiHq4XJK9wrzpzTMnVomzxpWOI0S/QtLH2QzZ7znnbF+IuvIQ1hLQ=="],
"@opentui/core": ["@opentui/core@0.5.2", "", { "dependencies": { "bun-ffi-structs": "0.3.1", "diff": "9.0.0", "marked": "17.0.1", "string-width": "7.2.0", "strip-ansi": "7.1.2" }, "optionalDependencies": { "@opentui/core-darwin-arm64": "0.5.2", "@opentui/core-darwin-x64": "0.5.2", "@opentui/core-linux-arm64": "0.5.2", "@opentui/core-linux-arm64-musl": "0.5.2", "@opentui/core-linux-x64": "0.5.2", "@opentui/core-linux-x64-musl": "0.5.2", "@opentui/core-win32-arm64": "0.5.2", "@opentui/core-win32-x64": "0.5.2" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-GOuD0Mq1ylybq9eEX6L0TdYlfJ2zJVLGecuh6yEM8RpT9oMtR0f+gEWqGJsBN/ay+Y5TOCH9b3vePEcwOW01fg=="],
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.0.0-20260808-9ecf7c0a", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Bn6fUZrwbUojkJG4YHowg/h/u3SPANmEivrl8YVbxBPCYOhZzQWxAVf7Fwg6PD0BpWk5gt9inQrir2DzWh5UpA=="],
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.5.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-fGi/RubZIhxcU8+M3GXNaBmEHqf127ZOnKmUTSp4ty2QdQ0kT4RTd+t91a/oyxrLZjjCmry0jXyquT0CZR1Byw=="],
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.0.0-20260808-9ecf7c0a", "", { "os": "darwin", "cpu": "x64" }, "sha512-lrVskzbjcT9yJCUgjOpvPpEq2ipmuDyHTMJ+OebsJFdATdryJLzRAr5BZiFGXEo4CJ2CS/+6vT+ki7dmhpM+0g=="],
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.5.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-ezFpB+5mQTd9BS4VvHOYEgQK6YxiYwVUmFb2sfTbJBnTqbOFP3Z/cdE5qNUHoeLQ01/nOs2dmUPcGyWxtMZIzw=="],
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.0.0-20260808-9ecf7c0a", "", { "os": "linux", "cpu": "arm64" }, "sha512-czqIqdnBvyFRSJ0BEdNn7moRN0KQo5gvhSsOSfrzueHxsktgWcmk1cMeCvTkzC3JCBvCwlB1KnSdt4jlwNO1EQ=="],
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.5.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-9S53zkIkbwRb0iIv2thibK+IofpTCEz/c48rJNaBx3qDylaDcTA11k5oqt1gkFcOLernlqIcow8aMCNDnRYMAA=="],
"@opentui/core-linux-arm64-musl": ["@opentui/core-linux-arm64-musl@0.0.0-20260808-9ecf7c0a", "", { "os": "linux", "cpu": "arm64" }, "sha512-6nixv6wlgjelUVJVHkJsCpuARb1rE/oxhIIthyLd062u5veoBt83cRGH9KmcZiwowKvso58hXn2KC21qAfylhQ=="],
"@opentui/core-linux-arm64-musl": ["@opentui/core-linux-arm64-musl@0.5.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-s2DB6he7jgbBqWOHBAIV/p4X12mj+Hlk1pAEcwK36j2wg+L4nVVdWecUJm1xeJn6LJSRcaJYE29sA45w5b7B2g=="],
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.0.0-20260808-9ecf7c0a", "", { "os": "linux", "cpu": "x64" }, "sha512-cxJMfrSsMsrZdU1Ke6awrxz0rdIJuI34l8Le3QlUbM5yLpp09ygJ7CElILMT50g3F+EnRf6BKvVmkbRrDzFQ6g=="],
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.5.2", "", { "os": "linux", "cpu": "x64" }, "sha512-HO0TWovqxo95hxFoTBx47MTP6pzzTW/QMoOIiD8jw0hkrn1DF8rSVPva0vLQMRer/drO9RpjyroFRNra7710og=="],
"@opentui/core-linux-x64-musl": ["@opentui/core-linux-x64-musl@0.0.0-20260808-9ecf7c0a", "", { "os": "linux", "cpu": "x64" }, "sha512-7zVbZ0XgCDi3yImmd9Cj+e0WhSuQzWo1ziSQiFgPCR8cs/dfrpYLgBlX9PP0gFOcJKeRccJJv7rnBetl3CB7tg=="],
"@opentui/core-linux-x64-musl": ["@opentui/core-linux-x64-musl@0.5.2", "", { "os": "linux", "cpu": "x64" }, "sha512-zyuJaRGBRbUOABgf3nJ2Jk/NmD+gS2dQpJzC2ikr2OMqclPVk7gFbPTHW2s/A3SJH7D8d0PJ6wL2wzXWMUdrWQ=="],
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.0.0-20260808-9ecf7c0a", "", { "os": "win32", "cpu": "arm64" }, "sha512-K3SNYJDvB7XD5nxi8jt4jWtgmqPG0Qbob0Px9afqMH3g/R5hIoi2P3GDbqn8JNv82c4dPIGJjXXuNhHa0tZQbw=="],
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.5.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-J1QxYJqxyAO49RyocqZSoGWs5uszlYot8sInLasi6nUbjCFg4h7Kvh+64+gxE2i4LASeyvMRpAIYwblI6eMbtg=="],
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.0.0-20260808-9ecf7c0a", "", { "os": "win32", "cpu": "x64" }, "sha512-3JCpPS8+Gz3PWlQHiFwqYuJ0AM0vKlfbe4+O+0m2EYej/QjN5KjL1nBfVLq5XDj4dVPEIktQKN6e//YzSzdaqA=="],
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.5.2", "", { "os": "win32", "cpu": "x64" }, "sha512-9/F1Q19GdtkZ1jypa0dRP6v9SJHHc91XHs4ZSroamDeqSDyTaGu1bbWnKiC503gpNpQMY0mjOVg4g+WVJLdDig=="],
"@opentui/keymap": ["@opentui/keymap@0.0.0-20260808-9ecf7c0a", "", { "dependencies": { "@opentui/core": "0.0.0-20260808-9ecf7c0a" }, "peerDependencies": { "@opentui/react": "0.0.0-20260808-9ecf7c0a", "@opentui/solid": "0.0.0-20260808-9ecf7c0a", "react": ">=19.2.0", "solid-js": "1.9.12" }, "optionalPeers": ["@opentui/react", "@opentui/solid", "react", "solid-js"] }, "sha512-aGxw6P0RJSuUU3y3QtAnKa1B6tQShxR0mjmnN3Cadr4LoduSuWn/l1kE+27KwH0ap9WTAeGVsN7u2oawB9eWKw=="],
"@opentui/keymap": ["@opentui/keymap@0.5.2", "", { "dependencies": { "@opentui/core": "0.5.2" }, "peerDependencies": { "@opentui/react": "0.5.2", "@opentui/solid": "0.5.2", "react": ">=19.2.0", "solid-js": "1.9.12" }, "optionalPeers": ["@opentui/react", "@opentui/solid", "react", "solid-js"] }, "sha512-MKESKHGeGit/i416E4zOujJIn17ladl6w9oOACmrjgff3a1naLFF3salrkUjD0C+uTUvh+nNTah795RORTTMyQ=="],
"@opentui/solid": ["@opentui/solid@0.0.0-20260808-9ecf7c0a", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.0.0-20260808-9ecf7c0a", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.12", "entities": "7.0.1", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.12" } }, "sha512-Q7ouv2KxZKO0/Sh0RbhNd7kEw6E7tlrU+yw36xxlnyXOqlK3TkQ+u1RMSZulYAn8mP20pnYLM9OxY4pOC/wK8Q=="],
"@opentui/solid": ["@opentui/solid@0.5.2", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.5.2", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.12", "entities": "7.0.1", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.12" } }, "sha512-mPB6xhG0bufjhUpWj5e8g/1M3LYZUKPM0mPPz6O6KIXFtakHVCJZuff2l1vvDpKuuLOAjivL8jLU7OJ3ZSCRuw=="],
"@orama/orama": ["@orama/orama@3.1.18", "", {}, "sha512-a61ljmRVVyG5MC/698C8/FfFDw5a8LOIvyOLW5fztgUXqUpc1jOfQzOitSCbge657OgXXThmY3Tk8fpiDb4UcA=="],
@@ -3253,19 +3262,19 @@
"@vitejs/plugin-react": ["@vitejs/plugin-react@4.7.0", "", { "dependencies": { "@babel/core": "^7.28.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.27", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA=="],
"@vitest/expect": ["@vitest/expect@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" } }, "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig=="],
"@vitest/expect": ["@vitest/expect@3.2.7", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/spy": "3.2.7", "@vitest/utils": "3.2.7", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" } }, "sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w=="],
"@vitest/mocker": ["@vitest/mocker@4.1.10", "", { "dependencies": { "@vitest/spy": "4.1.10", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow=="],
"@vitest/mocker": ["@vitest/mocker@3.2.7", "", { "dependencies": { "@vitest/spy": "3.2.7", "estree-walker": "^3.0.3", "magic-string": "^0.30.17" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "optionalPeers": ["msw", "vite"] }, "sha512-Trr0hYO9CM3Wj6ksWHRhK9IZpIY6wTMO5u/MqXurMxT57sWBaOPEtP3Oq60ihZuh5JsiagKfz95OcxdEP6dBrA=="],
"@vitest/pretty-format": ["@vitest/pretty-format@4.1.10", "", { "dependencies": { "tinyrainbow": "^3.1.0" } }, "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q=="],
"@vitest/pretty-format": ["@vitest/pretty-format@3.2.7", "", { "dependencies": { "tinyrainbow": "^2.0.0" } }, "sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA=="],
"@vitest/runner": ["@vitest/runner@4.1.10", "", { "dependencies": { "@vitest/utils": "4.1.10", "pathe": "^2.0.3" } }, "sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg=="],
"@vitest/runner": ["@vitest/runner@3.2.7", "", { "dependencies": { "@vitest/utils": "3.2.7", "pathe": "^2.0.3", "strip-literal": "^3.0.0" } }, "sha512-sB9y4ovltoQP+WaUPwmSxO9WIg9Ig694Di5PalVPsYHklAdE027mehpWF2SQSVq+k6sFgaivbTjTJwZLSHbedA=="],
"@vitest/snapshot": ["@vitest/snapshot@4.1.10", "", { "dependencies": { "@vitest/pretty-format": "4.1.10", "@vitest/utils": "4.1.10", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw=="],
"@vitest/snapshot": ["@vitest/snapshot@3.2.7", "", { "dependencies": { "@vitest/pretty-format": "3.2.7", "magic-string": "^0.30.17", "pathe": "^2.0.3" } }, "sha512-7C+MwShwtBSI5Buwoyg3s/iY1eHL9PKAf+O1wVh/TdnjXUtkoL/9YQtre90i4MtNXM6edP1wJ2zOBpfCyhIS7g=="],
"@vitest/spy": ["@vitest/spy@3.2.4", "", { "dependencies": { "tinyspy": "^4.0.3" } }, "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw=="],
"@vitest/spy": ["@vitest/spy@3.2.7", "", { "dependencies": { "tinyspy": "^4.0.3" } }, "sha512-Q2eQGI6d2L/hBtZ0qNuKcAGid68XK6cv1xsoaIma6PaJhHPoqcEJhYpXZ/5myCMqkNgtP6UKuBhbc0nHKnrkuQ=="],
"@vitest/utils": ["@vitest/utils@4.1.10", "", { "dependencies": { "@vitest/pretty-format": "4.1.10", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" } }, "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA=="],
"@vitest/utils": ["@vitest/utils@3.2.7", "", { "dependencies": { "@vitest/pretty-format": "3.2.7", "loupe": "^3.1.4", "tinyrainbow": "^2.0.0" } }, "sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw=="],
"@volar/kit": ["@volar/kit@2.4.28", "", { "dependencies": { "@volar/language-service": "2.4.28", "@volar/typescript": "2.4.28", "typesafe-path": "^0.2.2", "vscode-languageserver-textdocument": "^1.0.11", "vscode-uri": "^3.0.8" }, "peerDependencies": { "typescript": "*" } }, "sha512-cKX4vK9dtZvDRaAzeoUdaAJEew6IdxHNCRrdp5Kvcl6zZOqb6jTOfk3kXkIkG3T7oTFXguEMt5+9ptyqYR84Pg=="],
@@ -3317,8 +3326,6 @@
"abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="],
"abort-controller-x": ["abort-controller-x@0.5.0", "", {}, "sha512-yTt9CI0x+nRfX6BFMenEGP8ooPvErGH6AbFz20C2IeOLIlDsrw/VHpgne3GsCEuTA410IiFiaLVFKmgM4bKEPQ=="],
"accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
"acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
@@ -3559,10 +3566,6 @@
"caniuse-lite": ["caniuse-lite@1.0.30001806", "", {}, "sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw=="],
"cbor-extract": ["cbor-extract@2.2.2", "", { "dependencies": { "node-gyp-build-optional-packages": "5.1.1" }, "optionalDependencies": { "@cbor-extract/cbor-extract-darwin-arm64": "2.2.2", "@cbor-extract/cbor-extract-darwin-x64": "2.2.2", "@cbor-extract/cbor-extract-linux-arm": "2.2.2", "@cbor-extract/cbor-extract-linux-arm64": "2.2.2", "@cbor-extract/cbor-extract-linux-x64": "2.2.2", "@cbor-extract/cbor-extract-win32-x64": "2.2.2" }, "bin": { "download-cbor-prebuilds": "bin/download-prebuilds.js" } }, "sha512-hlSxxI9XO2yQfe9g6msd3g4xCfDqK5T5P0fRMLuaLHhxn4ViPrm+a+MUfhrvH2W962RGxcBwEGzLQyjbDG1gng=="],
"cbor-x": ["cbor-x@1.6.5", "", { "optionalDependencies": { "cbor-extract": "^2.2.2" } }, "sha512-yO64CxnSh6kp+pHNRK9IfwnMvCB+c8HvmUjQY/9l9YRF0/cAPka/tUHLwS64QqUpFCq3/OtbKziVJYXH2EaRig=="],
"ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="],
"chai": ["chai@5.3.3", "", { "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", "deep-eql": "^5.0.1", "loupe": "^3.1.0", "pathval": "^2.0.0" } }, "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw=="],
@@ -3597,6 +3600,8 @@
"citty": ["citty@0.1.6", "", { "dependencies": { "consola": "^3.2.3" } }, "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ=="],
"cjs-module-lexer": ["cjs-module-lexer@1.4.3", "", {}, "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q=="],
"classnames": ["classnames@2.3.2", "", {}, "sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw=="],
"clean-css": ["clean-css@5.3.3", "", { "dependencies": { "source-map": "~0.6.0" } }, "sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg=="],
@@ -4037,6 +4042,8 @@
"expressive-code": ["expressive-code@0.41.7", "", { "dependencies": { "@expressive-code/core": "^0.41.7", "@expressive-code/plugin-frames": "^0.41.7", "@expressive-code/plugin-shiki": "^0.41.7", "@expressive-code/plugin-text-markers": "^0.41.7" } }, "sha512-2wZjC8OQ3TaVEMcBtYY4Va3lo6J+Ai9jf3d4dbhURMJcU4Pbqe6EcHe424MIZI0VHUA1bR6xdpoHYi3yxokWqA=="],
"exsolve": ["exsolve@1.1.1", "", {}, "sha512-9U/jZUgjnSGyntRr6y5Muu1MJcwFl6kPu7k8qLF0IMNfLqvw0NZ4nnVDq0RVoZ0RvCyumib4Ez3KYrVfilrw+g=="],
"ext-list": ["ext-list@2.2.2", "", { "dependencies": { "mime-db": "^1.28.0" } }, "sha512-u+SQgsubraE6zItfVA0tBuCBhfU9ogSRnsvygI7wht9TS510oLkBRXBsqopeUG/GBOIQyKZO9wjTqIu/sf5zFA=="],
"ext-name": ["ext-name@5.0.0", "", { "dependencies": { "ext-list": "^2.0.0", "sort-keys-length": "^1.0.0" } }, "sha512-yblEwXAbGv1VQDmow7s38W77hzAgJAO50ztBLMcUyUBfxv1HC+LGwtiEN+Co6LtlqT/5uwVOxsD4TNIilWhwdQ=="],
@@ -4591,8 +4598,6 @@
"lodash-es": ["lodash-es@4.18.1", "", {}, "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A=="],
"lodash.camelcase": ["lodash.camelcase@4.3.0", "", {}, "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA=="],
"lodash.escaperegexp": ["lodash.escaperegexp@4.1.2", "", {}, "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw=="],
"lodash.includes": ["lodash.includes@4.3.0", "", {}, "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w=="],
@@ -4817,8 +4822,6 @@
"mkdirp": ["mkdirp@0.5.6", "", { "dependencies": { "minimist": "^1.2.6" }, "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw=="],
"modal": ["modal@0.9.0", "", { "dependencies": { "cbor-x": "^1.6.0", "long": "^5.3.1", "nice-grpc": "^2.1.12", "protobufjs": "^7.5.0", "smol-toml": "^1.3.3", "uuid": "^11.1.0" } }, "sha512-kCXcdJkhbJorf/q/6T9Wdlg6in9JmRnCNQnV6rVBMyeqNV/iXI6BYk4IzY4cvZ6dbauNeDMjk/Q08cbxvoIaXg=="],
"morphdom": ["morphdom@2.7.8", "", {}, "sha512-D/fR4xgGUyVRbdMGU6Nejea1RFzYxYtyurG4Fbv2Fi/daKlWKuXGLOdXtl+3eIwL110cI2hz1ZojGICjjFLgTg=="],
"motion": ["motion@12.34.5", "", { "dependencies": { "framer-motion": "^12.34.5", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-N06NLJ9IeBHeielRqIvYvjPfXuRdyTxa+9++BgpGa+hY2D7TcMkI6QzV3jaRuv0aZRXgMa7cPy9YcBUBisPzAQ=="],
@@ -4857,10 +4860,6 @@
"nf3": ["nf3@0.1.12", "", {}, "sha512-qbMXT7RTGh74MYWPeqTIED8nDW70NXOULVHpdWcdZ7IVHVnAsMV9fNugSNnvooipDc1FMOzpis7T9nXJEbJhvQ=="],
"nice-grpc": ["nice-grpc@2.1.16", "", { "dependencies": { "@grpc/grpc-js": "^1.14.0", "abort-controller-x": "^0.5.0", "nice-grpc-common": "^2.0.3" } }, "sha512-Cl3Pn00212Hl8/U6bpgMxmhZj5lyv3nWoJov4cd3FjWarktrMHP4DNvSjCnDwkMWYx4W1tyscEia4JX6Y4GVCQ=="],
"nice-grpc-common": ["nice-grpc-common@2.0.3", "", { "dependencies": { "ts-error": "^1.0.6" } }, "sha512-MEhnD3JMah0mgyivpb9hpRDbOBuXBxI/TVO+OK1h6rC97WM42HsPMR+zzRNQ0C5BqYJTw1nyWiQRD0DucO+pjQ=="],
"nitro": ["nitro@3.0.1-alpha.1", "", { "dependencies": { "consola": "^3.4.2", "crossws": "^0.4.1", "db0": "^0.3.4", "h3": "2.0.1-rc.5", "jiti": "^2.6.1", "nf3": "^0.1.10", "ofetch": "^2.0.0-alpha.3", "ohash": "^2.0.11", "oxc-minify": "^0.96.0", "oxc-transform": "^0.96.0", "srvx": "^0.9.5", "undici": "^7.16.0", "unenv": "^2.0.0-rc.24", "unstorage": "^2.0.0-alpha.4" }, "peerDependencies": { "rolldown": "*", "rollup": "^4", "vite": "^7", "xml2js": "^0.6.2" }, "optionalPeers": ["rolldown", "rollup", "vite", "xml2js"], "bin": { "nitro": "dist/cli/index.mjs" } }, "sha512-U4AxIsXxdkxzkFrK0XAw0e5Qbojk8jQ50MjjRBtBakC4HurTtQoiZvF+lSe382jhuQZCfAyywGWOFa9QzXLFaw=="],
"nlcst-to-string": ["nlcst-to-string@4.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0" } }, "sha512-YKLBCcUYKAg0FNlOBT6aI91qFmSiFKiluk655WzPF+DDMA02qIyy8uiRqI8QXtcFpEvll12LpL5MXqEmAZ+dcA=="],
@@ -5507,7 +5506,7 @@
"statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
"std-env": ["std-env@4.2.0", "", {}, "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw=="],
"std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="],
"stop-iteration-iterator": ["stop-iteration-iterator@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "internal-slot": "^1.1.0" } }, "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ=="],
@@ -5545,6 +5544,8 @@
"strip-indent": ["strip-indent@3.0.0", "", { "dependencies": { "min-indent": "^1.0.0" } }, "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ=="],
"strip-literal": ["strip-literal@3.1.0", "", { "dependencies": { "js-tokens": "^9.0.1" } }, "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg=="],
"stripe": ["stripe@18.0.0", "", { "dependencies": { "@types/node": ">=8.1.0", "qs": "^6.11.0" } }, "sha512-3Fs33IzKUby//9kCkCa1uRpinAoTvj6rJgQ2jrBEysoxEvfsclvXdna1amyEYbA2EKkjynuB4+L/kleCCaWTpA=="],
"strnum": ["strnum@1.1.2", "", {}, "sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA=="],
@@ -5617,7 +5618,9 @@
"tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="],
"tinyrainbow": ["tinyrainbow@3.1.0", "", {}, "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw=="],
"tinypool": ["tinypool@1.1.1", "", {}, "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg=="],
"tinyrainbow": ["tinyrainbow@2.0.0", "", {}, "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw=="],
"tinyspy": ["tinyspy@4.0.4", "", {}, "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q=="],
@@ -5659,8 +5662,6 @@
"ts-dedent": ["ts-dedent@2.3.0", "", {}, "sha512-JfJeIHke7y2egdGGgRAvpCwYFUsHlM2gPcrVOxFkznt/4uzQ7HFmvE63iFHVLBJNDuyDOQgijDK/tXH/f6Msjg=="],
"ts-error": ["ts-error@1.0.6", "", {}, "sha512-tLJxacIQUM82IR7JO1UUkKlYuUTmoY9HBJAmNWFzheSlDS5SPMcNIepejHJa4BpPQLAcbRhRf3GDJzyj6rbKvA=="],
"ts-interface-checker": ["ts-interface-checker@0.1.13", "", {}, "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="],
"tsconfck": ["tsconfck@3.1.6", "", { "peerDependencies": { "typescript": "^5.0.0" }, "optionalPeers": ["typescript"], "bin": { "tsconfck": "bin/tsconfck.js" } }, "sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w=="],
@@ -5805,6 +5806,8 @@
"vite": ["vite@7.1.4", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.14" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-X5QFK4SGynAeeIt+A7ZWnApdUyHYm+pzv/8/A57LqSGcI88U6R6ipOs3uCesdc6yl7nl+zNO0t8LmqAdXcQihw=="],
"vite-node": ["vite-node@3.2.4", "", { "dependencies": { "cac": "^6.7.14", "debug": "^4.4.1", "es-module-lexer": "^1.7.0", "pathe": "^2.0.3", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "bin": { "vite-node": "vite-node.mjs" } }, "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg=="],
"vite-plugin-dynamic-import": ["vite-plugin-dynamic-import@1.6.0", "", { "dependencies": { "acorn": "^8.12.1", "es-module-lexer": "^1.5.4", "fast-glob": "^3.3.2", "magic-string": "^0.30.11" } }, "sha512-TM0sz70wfzTIo9YCxVFwS8OA9lNREsh+0vMHGSkWDTZ7bgd1Yjs5RV8EgB634l/91IsXJReg0xtmuQqP0mf+rg=="],
"vite-plugin-icons-spritesheet": ["vite-plugin-icons-spritesheet@3.0.1", "", { "dependencies": { "chalk": "^5.4.1", "glob": "^11.0.1", "node-html-parser": "^7.0.1", "tinyexec": "^0.3.2" }, "peerDependencies": { "vite": ">=5.2.0" } }, "sha512-Cr0+Z6wRMwSwKisWW9PHeTjqmQFv0jwRQQMc3YgAhAgZEe03j21el0P/CA31KN/L5eiL1LhR14VTXl96LetonA=="],
@@ -5813,7 +5816,7 @@
"vitefu": ["vitefu@1.1.3", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["vite"] }, "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg=="],
"vitest": ["vitest@4.1.10", "", { "dependencies": { "@vitest/expect": "4.1.10", "@vitest/mocker": "4.1.10", "@vitest/pretty-format": "4.1.10", "@vitest/runner": "4.1.10", "@vitest/snapshot": "4.1.10", "@vitest/spy": "4.1.10", "@vitest/utils": "4.1.10", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.1.0", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.1.10", "@vitest/browser-preview": "4.1.10", "@vitest/browser-webdriverio": "4.1.10", "@vitest/coverage-istanbul": "4.1.10", "@vitest/coverage-v8": "4.1.10", "@vitest/ui": "4.1.10", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/coverage-istanbul", "@vitest/coverage-v8", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "./vitest.mjs" } }, "sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw=="],
"vitest": ["vitest@3.2.7", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.7", "@vitest/mocker": "3.2.7", "@vitest/pretty-format": "^3.2.7", "@vitest/runner": "3.2.7", "@vitest/snapshot": "3.2.7", "@vitest/spy": "3.2.7", "@vitest/utils": "3.2.7", "chai": "^5.2.0", "debug": "^4.4.1", "expect-type": "^1.2.1", "magic-string": "^0.30.17", "pathe": "^2.0.3", "picomatch": "^4.0.2", "std-env": "^3.9.0", "tinybench": "^2.9.0", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.14", "tinypool": "^1.1.1", "tinyrainbow": "^2.0.0", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", "vite-node": "3.2.4", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "@vitest/browser": "3.2.7", "@vitest/ui": "3.2.7", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@types/debug", "@types/node", "@vitest/browser", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "./vitest.mjs" } }, "sha512-KrxIJ62Fd89gfysR4WotlgZABiz2dqFPgqGzX7s+CwsqLFomRH7777ZcrOD6+WVAh7khPQP41A+BKbpcJFrdEg=="],
"volar-service-css": ["volar-service-css@0.0.71", "", { "dependencies": { "vscode-css-languageservice": "^6.3.0", "vscode-languageserver-textdocument": "^1.0.11", "vscode-uri": "^3.0.8" }, "peerDependencies": { "@volar/language-service": "~2.4.0" }, "optionalPeers": ["@volar/language-service"] }, "sha512-wRRFt9BpjMKCazcgOh67MSjUjiWUCAh99DyYSDIOTuxaRjEtDC7PpB0k1Y1wbJIW/pVtMUSVbpPo3UGSm0Byxw=="],
@@ -6227,6 +6230,14 @@
"@cloudflare/vite-plugin/ws": ["ws@8.18.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw=="],
"@cloudflare/vitest-pool-workers/esbuild": ["esbuild@0.27.0", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.0", "@esbuild/android-arm": "0.27.0", "@esbuild/android-arm64": "0.27.0", "@esbuild/android-x64": "0.27.0", "@esbuild/darwin-arm64": "0.27.0", "@esbuild/darwin-x64": "0.27.0", "@esbuild/freebsd-arm64": "0.27.0", "@esbuild/freebsd-x64": "0.27.0", "@esbuild/linux-arm": "0.27.0", "@esbuild/linux-arm64": "0.27.0", "@esbuild/linux-ia32": "0.27.0", "@esbuild/linux-loong64": "0.27.0", "@esbuild/linux-mips64el": "0.27.0", "@esbuild/linux-ppc64": "0.27.0", "@esbuild/linux-riscv64": "0.27.0", "@esbuild/linux-s390x": "0.27.0", "@esbuild/linux-x64": "0.27.0", "@esbuild/netbsd-arm64": "0.27.0", "@esbuild/netbsd-x64": "0.27.0", "@esbuild/openbsd-arm64": "0.27.0", "@esbuild/openbsd-x64": "0.27.0", "@esbuild/openharmony-arm64": "0.27.0", "@esbuild/sunos-x64": "0.27.0", "@esbuild/win32-arm64": "0.27.0", "@esbuild/win32-ia32": "0.27.0", "@esbuild/win32-x64": "0.27.0" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-jd0f4NHbD6cALCyGElNpGAOtWxSq46l9X/sWB0Nzd5er4Kz2YTm+Vl0qKFT9KUJvD8+fiO8AvoHhFvEatfVixA=="],
"@cloudflare/vitest-pool-workers/miniflare": ["miniflare@4.20260120.0", "", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "sharp": "^0.34.5", "undici": "7.18.2", "workerd": "1.20260120.0", "ws": "8.18.0", "youch": "4.1.0-beta.10", "zod": "^3.25.76" }, "bin": { "miniflare": "bootstrap.js" } }, "sha512-XXZyE2pDKMtP5OLuv0LPHEAzIYhov4jrYjcqrhhqtxGGtXneWOHvXIPo+eV8sqwqWd3R7j4DlEKcyb+87BR49Q=="],
"@cloudflare/vitest-pool-workers/wrangler": ["wrangler@4.60.0", "", { "dependencies": { "@cloudflare/kv-asset-handler": "0.4.2", "@cloudflare/unenv-preset": "2.11.0", "blake3-wasm": "2.1.5", "esbuild": "0.27.0", "miniflare": "4.20260120.0", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.24", "workerd": "1.20260120.0" }, "optionalDependencies": { "fsevents": "~2.3.2" }, "peerDependencies": { "@cloudflare/workers-types": "^4.20260120.0" }, "optionalPeers": ["@cloudflare/workers-types"], "bin": { "wrangler": "bin/wrangler.js", "wrangler2": "bin/wrangler.js" } }, "sha512-n4kibm/xY0Qd5G2K/CbAQeVeOIlwPNVglmFjlDRCCYk3hZh8IggO/rg8AXt/vByK2Sxsugl5Z7yvgWxrUbmS6g=="],
"@cloudflare/vitest-pool-workers/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
"@cspotcode/source-map-support/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.9", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" } }, "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ=="],
"@dot/log/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
@@ -6373,6 +6384,8 @@
"@opencode-ai/web/@shikijs/transformers": ["@shikijs/transformers@3.20.0", "", { "dependencies": { "@shikijs/core": "3.20.0", "@shikijs/types": "3.20.0" } }, "sha512-PrHHMRr3Q5W1qB/42kJW6laqFyWdhrPF2hNR9qjOm1xcSiAO3hAHo7HaVyHE6pMyevmy3i51O8kuGGXC78uK3g=="],
"@opencode-ai/workerd-spike/wrangler": ["wrangler@4.28.0", "", { "dependencies": { "@cloudflare/kv-asset-handler": "0.4.0", "@cloudflare/unenv-preset": "2.6.0", "blake3-wasm": "2.1.5", "esbuild": "0.25.4", "miniflare": "4.20250803.0", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.19", "workerd": "1.20250803.0" }, "optionalDependencies": { "fsevents": "~2.3.2" }, "peerDependencies": { "@cloudflare/workers-types": "^4.20250803.0" }, "optionalPeers": ["@cloudflare/workers-types"], "bin": { "wrangler": "bin/wrangler.js", "wrangler2": "bin/wrangler.js" } }, "sha512-y0yHIuScpok9oSErLqDbxkBChC2+/jZpvqMg2NxOto1JCyUtDUuKljOfcVMaI48d9GuhOCSoWSumYxLAHNxaLA=="],
"@opencode-ai/www/@astrojs/cloudflare": ["@astrojs/cloudflare@14.1.4", "", { "dependencies": { "@astrojs/internal-helpers": "0.10.1", "@astrojs/underscore-redirects": "1.0.3", "@cloudflare/vite-plugin": "^1.39.0", "piccolore": "^0.1.3", "vite": "^8.0.13" }, "peerDependencies": { "astro": "^7.0.0", "wrangler": "^4.83.0" } }, "sha512-Zyo1E/5/dmegmKODbwUzOd67euNd6oKcbllhAwe3uFFprA7mIFNkpF6yBBa78vIkpMNuU13MkBJHCLd+yJSuEA=="],
"@opencode-ai/www/astro": ["astro@7.1.3", "", { "dependencies": { "@astrojs/compiler-rs": "^0.3.1", "@astrojs/internal-helpers": "0.10.1", "@astrojs/markdown-satteri": "0.3.4", "@astrojs/telemetry": "3.3.3", "@capsizecss/unpack": "^4.0.0", "@clack/prompts": "^1.1.0", "@oslojs/encoding": "^1.1.0", "@rollup/pluginutils": "^5.3.0", "am-i-vibing": "^0.4.0", "aria-query": "^5.3.2", "axobject-query": "^4.1.0", "ci-info": "^4.4.0", "clsx": "^2.1.1", "common-ancestor-path": "^2.0.0", "cookie": "^2.0.1", "devalue": "^5.8.1", "diff": "^8.0.3", "dset": "^3.1.4", "es-module-lexer": "^2.0.0", "esbuild": "^0.28.0", "flattie": "^1.1.1", "fontace": "~0.4.1", "get-tsconfig": "5.0.0-beta.4", "github-slugger": "^2.0.0", "html-escaper": "3.0.3", "http-cache-semantics": "^4.2.0", "js-yaml": "^4.1.1", "jsonc-parser": "^3.3.1", "magic-string": "^0.30.21", "magicast": "^0.5.2", "mrmime": "^2.0.1", "neotraverse": "^1.0.1", "obug": "^2.1.1", "p-limit": "^7.3.0", "p-queue": "^9.1.0", "package-manager-detector": "^1.6.0", "piccolore": "^0.1.3", "picomatch": "^4.0.4", "semver": "^7.7.4", "shiki": "^4.0.2", "smol-toml": "^1.6.0", "svgo": "^4.0.1", "tinyclip": "^0.1.12", "tinyexec": "^1.0.4", "tinyglobby": "^0.2.15", "ultrahtml": "^1.6.0", "unifont": "~0.7.4", "unstorage": "^1.17.5", "vite": "^8.0.13", "vitefu": "^1.1.2", "xxhash-wasm": "^1.1.0", "yargs-parser": "^22.0.0", "zod": "^4.3.6" }, "optionalDependencies": { "sharp": "^0.34.0 || ^0.35.0" }, "peerDependencies": { "@astrojs/markdown-remark": "7.2.1" }, "optionalPeers": ["@astrojs/markdown-remark"], "bin": { "astro": "./bin/astro.mjs" } }, "sha512-4dhPyAAXthf3xLEYnG8SeL7yr/nTPPABfY7e9YF0yuO+vK9Xp+8Q5j4xzsmL3GueukQv4oNwGNTBepLOiDGeJA=="],
@@ -6477,6 +6490,8 @@
"@solidjs/start/vite": ["vite@7.1.10", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-CmuvUBzVJ/e3HGxhg6cYk88NGgTnBoOo7ogtfJJ0fefUWAxN/WDSUa50o+oVBxuIhO8FoEZW0j2eW7sfjs5EtA=="],
"@solidjs/start/vitest": ["vitest@4.1.10", "", { "dependencies": { "@vitest/expect": "4.1.10", "@vitest/mocker": "4.1.10", "@vitest/pretty-format": "4.1.10", "@vitest/runner": "4.1.10", "@vitest/snapshot": "4.1.10", "@vitest/spy": "4.1.10", "@vitest/utils": "4.1.10", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.1.0", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.1.10", "@vitest/browser-preview": "4.1.10", "@vitest/browser-webdriverio": "4.1.10", "@vitest/coverage-istanbul": "4.1.10", "@vitest/coverage-v8": "4.1.10", "@vitest/ui": "4.1.10", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/coverage-istanbul", "@vitest/coverage-v8", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "./vitest.mjs" } }, "sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw=="],
"@storybook/addon-docs/react": ["react@19.2.8", "", {}, "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw=="],
"@storybook/addon-docs/react-dom": ["react-dom@19.2.8", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.8" } }, "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ=="],
@@ -6523,12 +6538,6 @@
"@vitejs/plugin-react/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.27", "", {}, "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA=="],
"@vitest/expect/@vitest/utils": ["@vitest/utils@3.2.4", "", { "dependencies": { "@vitest/pretty-format": "3.2.4", "loupe": "^3.1.4", "tinyrainbow": "^2.0.0" } }, "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA=="],
"@vitest/expect/tinyrainbow": ["tinyrainbow@2.0.0", "", {}, "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw=="],
"@vitest/mocker/@vitest/spy": ["@vitest/spy@4.1.10", "", {}, "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw=="],
"@vscode/emmet-helper/jsonc-parser": ["jsonc-parser@2.3.1", "", {}, "sha512-H8jvkz1O50L3dMZCsLqiuB2tA7muqbSg1AtGEkN0leAqGjsUzDJir3Zwr02BhqdcITPg3ei3mZ+HjMocAknhhg=="],
"@vscode/emmet-helper/vscode-languageserver-types": ["vscode-languageserver-types@3.18.0", "", {}, "sha512-8TsGPNMIMiiBdkORgRSvLjuiEIiAFtO+KssmYWxQ+uSVvlf7RjK8YKCOjPzZ+YA04jXEV7+7LvkSmHkhpNS99g=="],
@@ -6613,8 +6622,6 @@
"builder-util/js-yaml": ["js-yaml@4.3.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q=="],
"cbor-extract/node-gyp-build-optional-packages": ["node-gyp-build-optional-packages@5.1.1", "", { "dependencies": { "detect-libc": "^2.0.1" }, "bin": { "node-gyp-build-optional-packages": "bin.js", "node-gyp-build-optional-packages-test": "build-test.js", "node-gyp-build-optional-packages-optional": "optional.js" } }, "sha512-+P72GAjVAbTxjjwUmwjVrqrdZROD4nf8KgpBoDxqXXTiYZZt/ud60dE5yvCSr9lRO8e8yv6kgJIC0K0PfZFVQw=="],
"cliui/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
"cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
@@ -6763,8 +6770,6 @@
"minipass-pipeline/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
"modal/uuid": ["uuid@11.1.1", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ=="],
"motion/framer-motion": ["framer-motion@12.42.2", "", { "dependencies": { "motion-dom": "^12.42.2", "motion-utils": "^12.39.0", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-5XY9luDiu0oHfHBjpDthFMh0ES+122w6p/papSJBweMkO8Sn+PW2QaEgRblQBpWFnuvZS5qvarpt/hO2pjGmnw=="],
"nitro/h3": ["h3@2.0.1-rc.5", "", { "dependencies": { "rou3": "^0.7.9", "srvx": "^0.9.1" }, "peerDependencies": { "crossws": "^0.4.1" }, "optionalPeers": ["crossws"] }, "sha512-qkohAzCab0nLzXNm78tBjZDvtKMTmtygS8BJLT3VPczAQofdqlFXDPkXdLMJN4r05+xqneG8snZJ0HgkERCZTg=="],
@@ -6843,6 +6848,10 @@
"sst/jose": ["jose@5.2.3", "", {}, "sha512-KUXdbctm1uHVL8BYhnyHkgp3zDX5KW8ZhAKVFEfUbU2P8Alpzjb+48hHvjOdQIyPshoblhzsuqOwEEAbtHVirA=="],
"storybook/@vitest/expect": ["@vitest/expect@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" } }, "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig=="],
"storybook/@vitest/spy": ["@vitest/spy@3.2.4", "", { "dependencies": { "tinyspy": "^4.0.3" } }, "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw=="],
"storybook/esbuild": ["esbuild@0.28.1", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.1", "@esbuild/android-arm": "0.28.1", "@esbuild/android-arm64": "0.28.1", "@esbuild/android-x64": "0.28.1", "@esbuild/darwin-arm64": "0.28.1", "@esbuild/darwin-x64": "0.28.1", "@esbuild/freebsd-arm64": "0.28.1", "@esbuild/freebsd-x64": "0.28.1", "@esbuild/linux-arm": "0.28.1", "@esbuild/linux-arm64": "0.28.1", "@esbuild/linux-ia32": "0.28.1", "@esbuild/linux-loong64": "0.28.1", "@esbuild/linux-mips64el": "0.28.1", "@esbuild/linux-ppc64": "0.28.1", "@esbuild/linux-riscv64": "0.28.1", "@esbuild/linux-s390x": "0.28.1", "@esbuild/linux-x64": "0.28.1", "@esbuild/netbsd-arm64": "0.28.1", "@esbuild/netbsd-x64": "0.28.1", "@esbuild/openbsd-arm64": "0.28.1", "@esbuild/openbsd-x64": "0.28.1", "@esbuild/openharmony-arm64": "0.28.1", "@esbuild/sunos-x64": "0.28.1", "@esbuild/win32-arm64": "0.28.1", "@esbuild/win32-ia32": "0.28.1", "@esbuild/win32-x64": "0.28.1" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw=="],
"storybook/open": ["open@10.2.0", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="],
@@ -6853,6 +6862,8 @@
"strip-ansi-cjs/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"strip-literal/js-tokens": ["js-tokens@9.0.1", "", {}, "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ=="],
"sucrase/commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="],
"svgo/commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="],
@@ -6889,17 +6900,11 @@
"venice-ai-sdk-provider/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.40", "", { "dependencies": { "@ai-sdk/provider": "3.0.14", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-OL5IrpUm9Y8Dwy+w/vvFwPotS6m52O9W0op2oXgXdCROMJIBalBI0oro6OIBYkPxvm5Xg02GSkoQN25RlR0bnw=="],
"vite-node/vite": ["vite@7.1.10", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-CmuvUBzVJ/e3HGxhg6cYk88NGgTnBoOo7ogtfJJ0fefUWAxN/WDSUa50o+oVBxuIhO8FoEZW0j2eW7sfjs5EtA=="],
"vite-plugin-icons-spritesheet/glob": ["glob@11.1.0", "", { "dependencies": { "foreground-child": "^3.3.1", "jackspeak": "^4.1.1", "minimatch": "^10.1.1", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^2.0.0" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw=="],
"vitest/@vitest/expect": ["@vitest/expect@4.1.10", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.1.10", "@vitest/utils": "4.1.10", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" } }, "sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA=="],
"vitest/@vitest/spy": ["@vitest/spy@4.1.10", "", {}, "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw=="],
"vitest/es-module-lexer": ["es-module-lexer@2.3.1", "", {}, "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA=="],
"vitest/tinyexec": ["tinyexec@1.2.4", "", {}, "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg=="],
"vitest/vite": ["vite@8.1.5", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.5", "postcss": "^8.5.17", "rolldown": "~1.1.5", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.3.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw=="],
"vitest/vite": ["vite@7.1.10", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-CmuvUBzVJ/e3HGxhg6cYk88NGgTnBoOo7ogtfJJ0fefUWAxN/WDSUa50o+oVBxuIhO8FoEZW0j2eW7sfjs5EtA=="],
"vscode-json-languageservice/vscode-languageserver-types": ["vscode-languageserver-types@3.18.0", "", {}, "sha512-8TsGPNMIMiiBdkORgRSvLjuiEIiAFtO+KssmYWxQ+uSVvlf7RjK8YKCOjPzZ+YA04jXEV7+7LvkSmHkhpNS99g=="],
@@ -7197,6 +7202,72 @@
"@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
"@cloudflare/vitest-pool-workers/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.0", "", { "os": "aix", "cpu": "ppc64" }, "sha512-KuZrd2hRjz01y5JK9mEBSD3Vj3mbCvemhT466rSuJYeE/hjuBrHfjjcjMdTm/sz7au+++sdbJZJmuBwQLuw68A=="],
"@cloudflare/vitest-pool-workers/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.27.0", "", { "os": "android", "cpu": "arm" }, "sha512-j67aezrPNYWJEOHUNLPj9maeJte7uSMM6gMoxfPC9hOg8N02JuQi/T7ewumf4tNvJadFkvLZMlAq73b9uwdMyQ=="],
"@cloudflare/vitest-pool-workers/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.0", "", { "os": "android", "cpu": "arm64" }, "sha512-CC3vt4+1xZrs97/PKDkl0yN7w8edvU2vZvAFGD16n9F0Cvniy5qvzRXjfO1l94efczkkQE6g1x0i73Qf5uthOQ=="],
"@cloudflare/vitest-pool-workers/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.27.0", "", { "os": "android", "cpu": "x64" }, "sha512-wurMkF1nmQajBO1+0CJmcN17U4BP6GqNSROP8t0X/Jiw2ltYGLHpEksp9MpoBqkrFR3kv2/te6Sha26k3+yZ9Q=="],
"@cloudflare/vitest-pool-workers/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-uJOQKYCcHhg07DL7i8MzjvS2LaP7W7Pn/7uA0B5S1EnqAirJtbyw4yC5jQ5qcFjHK9l6o/MX9QisBg12kNkdHg=="],
"@cloudflare/vitest-pool-workers/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-8mG6arH3yB/4ZXiEnXof5MK72dE6zM9cDvUcPtxhUZsDjESl9JipZYW60C3JGreKCEP+p8P/72r69m4AZGJd5g=="],
"@cloudflare/vitest-pool-workers/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-9FHtyO988CwNMMOE3YIeci+UV+x5Zy8fI2qHNpsEtSF83YPBmE8UWmfYAQg6Ux7Gsmd4FejZqnEUZCMGaNQHQw=="],
"@cloudflare/vitest-pool-workers/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-zCMeMXI4HS/tXvJz8vWGexpZj2YVtRAihHLk1imZj4efx1BQzN76YFeKqlDr3bUWI26wHwLWPd3rwh6pe4EV7g=="],
"@cloudflare/vitest-pool-workers/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.0", "", { "os": "linux", "cpu": "arm" }, "sha512-t76XLQDpxgmq2cNXKTVEB7O7YMb42atj2Re2Haf45HkaUpjM2J0UuJZDuaGbPbamzZ7bawyGFUkodL+zcE+jvQ=="],
"@cloudflare/vitest-pool-workers/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-AS18v0V+vZiLJyi/4LphvBE+OIX682Pu7ZYNsdUHyUKSoRwdnOsMf6FDekwoAFKej14WAkOef3zAORJgAtXnlQ=="],
"@cloudflare/vitest-pool-workers/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.0", "", { "os": "linux", "cpu": "ia32" }, "sha512-Mz1jxqm/kfgKkc/KLHC5qIujMvnnarD9ra1cEcrs7qshTUSksPihGrWHVG5+osAIQ68577Zpww7SGapmzSt4Nw=="],
"@cloudflare/vitest-pool-workers/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.0", "", { "os": "linux", "cpu": "none" }, "sha512-QbEREjdJeIreIAbdG2hLU1yXm1uu+LTdzoq1KCo4G4pFOLlvIspBm36QrQOar9LFduavoWX2msNFAAAY9j4BDg=="],
"@cloudflare/vitest-pool-workers/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.0", "", { "os": "linux", "cpu": "none" }, "sha512-sJz3zRNe4tO2wxvDpH/HYJilb6+2YJxo/ZNbVdtFiKDufzWq4JmKAiHy9iGoLjAV7r/W32VgaHGkk35cUXlNOg=="],
"@cloudflare/vitest-pool-workers/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-z9N10FBD0DCS2dmSABDBb5TLAyF1/ydVb+N4pi88T45efQ/w4ohr/F/QYCkxDPnkhkp6AIpIcQKQ8F0ANoA2JA=="],
"@cloudflare/vitest-pool-workers/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.0", "", { "os": "linux", "cpu": "none" }, "sha512-pQdyAIZ0BWIC5GyvVFn5awDiO14TkT/19FTmFcPdDec94KJ1uZcmFs21Fo8auMXzD4Tt+diXu1LW1gHus9fhFQ=="],
"@cloudflare/vitest-pool-workers/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-hPlRWR4eIDDEci953RI1BLZitgi5uqcsjKMxwYfmi4LcwyWo2IcRP+lThVnKjNtk90pLS8nKdroXYOqW+QQH+w=="],
"@cloudflare/vitest-pool-workers/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.0", "", { "os": "linux", "cpu": "x64" }, "sha512-1hBWx4OUJE2cab++aVZ7pObD6s+DK4mPGpemtnAORBvb5l/g5xFGk0vc0PjSkrDs0XaXj9yyob3d14XqvnQ4gw=="],
"@cloudflare/vitest-pool-workers/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.0", "", { "os": "none", "cpu": "arm64" }, "sha512-6m0sfQfxfQfy1qRuecMkJlf1cIzTOgyaeXaiVaaki8/v+WB+U4hc6ik15ZW6TAllRlg/WuQXxWj1jx6C+dfy3w=="],
"@cloudflare/vitest-pool-workers/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.0", "", { "os": "none", "cpu": "x64" }, "sha512-xbbOdfn06FtcJ9d0ShxxvSn2iUsGd/lgPIO2V3VZIPDbEaIj1/3nBBe1AwuEZKXVXkMmpr6LUAgMkLD/4D2PPA=="],
"@cloudflare/vitest-pool-workers/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.0", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fWgqR8uNbCQ/GGv0yhzttj6sU/9Z5/Sv/VGU3F5OuXK6J6SlriONKrQ7tNlwBrJZXRYk5jUhuWvF7GYzGguBZQ=="],
"@cloudflare/vitest-pool-workers/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-aCwlRdSNMNxkGGqQajMUza6uXzR/U0dIl1QmLjPtRbLOx3Gy3otfFu/VjATy4yQzo9yFDGTxYDo1FfAD9oRD2A=="],
"@cloudflare/vitest-pool-workers/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.0", "", { "os": "none", "cpu": "arm64" }, "sha512-nyvsBccxNAsNYz2jVFYwEGuRRomqZ149A39SHWk4hV0jWxKM0hjBPm3AmdxcbHiFLbBSwG6SbpIcUbXjgyECfA=="],
"@cloudflare/vitest-pool-workers/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.0", "", { "os": "sunos", "cpu": "x64" }, "sha512-Q1KY1iJafM+UX6CFEL+F4HRTgygmEW568YMqDA5UV97AuZSm21b7SXIrRJDwXWPzr8MGr75fUZPV67FdtMHlHA=="],
"@cloudflare/vitest-pool-workers/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-W1eyGNi6d+8kOmZIwi/EDjrL9nxQIQ0MiGqe/AWc6+IaHloxHSGoeRgDRKHFISThLmsewZ5nHFvGFWdBYlgKPg=="],
"@cloudflare/vitest-pool-workers/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-30z1aKL9h22kQhilnYkORFYt+3wp7yZsHWus+wSKAJR8JtdfI76LJ4SBdMsCopTR3z/ORqVu5L1vtnHZWVj4cQ=="],
"@cloudflare/vitest-pool-workers/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.0", "", { "os": "win32", "cpu": "x64" }, "sha512-aIitBcjQeyOhMTImhLZmtxfdOcuNRpwlPNmlFKPcHQYPhEssw75Cl1TSXJXpMkzaua9FUetx/4OQKq7eJul5Cg=="],
"@cloudflare/vitest-pool-workers/miniflare/sharp": ["sharp@0.34.5", "", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", "semver": "^7.7.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.5", "@img/sharp-darwin-x64": "0.34.5", "@img/sharp-libvips-darwin-arm64": "1.2.4", "@img/sharp-libvips-darwin-x64": "1.2.4", "@img/sharp-libvips-linux-arm": "1.2.4", "@img/sharp-libvips-linux-arm64": "1.2.4", "@img/sharp-libvips-linux-ppc64": "1.2.4", "@img/sharp-libvips-linux-riscv64": "1.2.4", "@img/sharp-libvips-linux-s390x": "1.2.4", "@img/sharp-libvips-linux-x64": "1.2.4", "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", "@img/sharp-libvips-linuxmusl-x64": "1.2.4", "@img/sharp-linux-arm": "0.34.5", "@img/sharp-linux-arm64": "0.34.5", "@img/sharp-linux-ppc64": "0.34.5", "@img/sharp-linux-riscv64": "0.34.5", "@img/sharp-linux-s390x": "0.34.5", "@img/sharp-linux-x64": "0.34.5", "@img/sharp-linuxmusl-arm64": "0.34.5", "@img/sharp-linuxmusl-x64": "0.34.5", "@img/sharp-wasm32": "0.34.5", "@img/sharp-win32-arm64": "0.34.5", "@img/sharp-win32-ia32": "0.34.5", "@img/sharp-win32-x64": "0.34.5" } }, "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg=="],
"@cloudflare/vitest-pool-workers/miniflare/undici": ["undici@7.18.2", "", {}, "sha512-y+8YjDFzWdQlSE9N5nzKMT3g4a5UBX1HKowfdXh0uvAnTaqqwqB92Jt4UXBAeKekDs5IaDKyJFR4X1gYVCgXcw=="],
"@cloudflare/vitest-pool-workers/miniflare/workerd": ["workerd@1.20260120.0", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20260120.0", "@cloudflare/workerd-darwin-arm64": "1.20260120.0", "@cloudflare/workerd-linux-64": "1.20260120.0", "@cloudflare/workerd-linux-arm64": "1.20260120.0", "@cloudflare/workerd-windows-64": "1.20260120.0" }, "bin": { "workerd": "bin/workerd" } }, "sha512-R6X/VQOkwLTBGLp4VRUwLQZZVxZ9T9J8pGiJ6GQUMaRkY7TVWrCSkVfoNMM1/YyFsY5UYhhPoQe5IehnhZ3Pdw=="],
"@cloudflare/vitest-pool-workers/miniflare/ws": ["ws@8.18.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw=="],
"@cloudflare/vitest-pool-workers/wrangler/@cloudflare/kv-asset-handler": ["@cloudflare/kv-asset-handler@0.4.2", "", {}, "sha512-SIOD2DxrRRwQ+jgzlXCqoEFiKOFqaPjhnNTGKXSRLvp1HiOvapLaFG2kEr9dYQTYe8rKrd9uvDUzmAITeNyaHQ=="],
"@cloudflare/vitest-pool-workers/wrangler/@cloudflare/unenv-preset": ["@cloudflare/unenv-preset@2.11.0", "", { "peerDependencies": { "unenv": "2.0.0-rc.24", "workerd": "^1.20260115.0" }, "optionalPeers": ["workerd"] }, "sha512-z3hxFajL765VniNPGV0JRStZolNz63gU3B3AktwoGdDlnQvz5nP+Ah4RL04PONlZQjwmDdGHowEStJ94+RsaJg=="],
"@cloudflare/vitest-pool-workers/wrangler/workerd": ["workerd@1.20260120.0", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20260120.0", "@cloudflare/workerd-darwin-arm64": "1.20260120.0", "@cloudflare/workerd-linux-64": "1.20260120.0", "@cloudflare/workerd-linux-arm64": "1.20260120.0", "@cloudflare/workerd-windows-64": "1.20260120.0" }, "bin": { "workerd": "bin/workerd" } }, "sha512-R6X/VQOkwLTBGLp4VRUwLQZZVxZ9T9J8pGiJ6GQUMaRkY7TVWrCSkVfoNMM1/YyFsY5UYhhPoQe5IehnhZ3Pdw=="],
"@electron/asar/minimatch/brace-expansion": ["brace-expansion@1.1.16", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw=="],
"@electron/fuses/fs-extra/jsonfile": ["jsonfile@6.2.1", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q=="],
@@ -7397,6 +7468,16 @@
"@opencode-ai/web/@shikijs/transformers/@shikijs/types": ["@shikijs/types@3.20.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-lhYAATn10nkZcBQ0BlzSbJA3wcmL5MXUUF8d2Zzon6saZDlToKaiRX60n2+ZaHJCmXEcZRWNzn+k9vplr8Jhsw=="],
"@opencode-ai/workerd-spike/wrangler/@cloudflare/unenv-preset": ["@cloudflare/unenv-preset@2.6.0", "", { "peerDependencies": { "unenv": "2.0.0-rc.19", "workerd": "^1.20250802.0" }, "optionalPeers": ["workerd"] }, "sha512-h7Txw0WbDuUbrvZwky6+x7ft+U/Gppfn/rWx6IdR+e9gjygozRJnV26Y2TOr3yrIFa6OsZqqR2lN+jWTrakHXg=="],
"@opencode-ai/workerd-spike/wrangler/esbuild": ["esbuild@0.25.4", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.4", "@esbuild/android-arm": "0.25.4", "@esbuild/android-arm64": "0.25.4", "@esbuild/android-x64": "0.25.4", "@esbuild/darwin-arm64": "0.25.4", "@esbuild/darwin-x64": "0.25.4", "@esbuild/freebsd-arm64": "0.25.4", "@esbuild/freebsd-x64": "0.25.4", "@esbuild/linux-arm": "0.25.4", "@esbuild/linux-arm64": "0.25.4", "@esbuild/linux-ia32": "0.25.4", "@esbuild/linux-loong64": "0.25.4", "@esbuild/linux-mips64el": "0.25.4", "@esbuild/linux-ppc64": "0.25.4", "@esbuild/linux-riscv64": "0.25.4", "@esbuild/linux-s390x": "0.25.4", "@esbuild/linux-x64": "0.25.4", "@esbuild/netbsd-arm64": "0.25.4", "@esbuild/netbsd-x64": "0.25.4", "@esbuild/openbsd-arm64": "0.25.4", "@esbuild/openbsd-x64": "0.25.4", "@esbuild/sunos-x64": "0.25.4", "@esbuild/win32-arm64": "0.25.4", "@esbuild/win32-ia32": "0.25.4", "@esbuild/win32-x64": "0.25.4" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q=="],
"@opencode-ai/workerd-spike/wrangler/miniflare": ["miniflare@4.20250803.0", "", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "acorn": "8.14.0", "acorn-walk": "8.3.2", "exit-hook": "2.2.1", "glob-to-regexp": "0.4.1", "sharp": "^0.33.5", "stoppable": "1.1.0", "undici": "^7.10.0", "workerd": "1.20250803.0", "ws": "8.18.0", "youch": "4.1.0-beta.10", "zod": "3.22.3" }, "bin": { "miniflare": "bootstrap.js" } }, "sha512-1tmCLfmMw0SqRBF9PPII9CVLQRzOrO7uIBmSng8BMSmtgs2kos7OeoM0sg6KbR9FrvP/zAniLyZuCAMAjuu4fQ=="],
"@opencode-ai/workerd-spike/wrangler/unenv": ["unenv@2.0.0-rc.19", "", { "dependencies": { "defu": "^6.1.4", "exsolve": "^1.0.7", "ohash": "^2.0.11", "pathe": "^2.0.3", "ufo": "^1.6.1" } }, "sha512-t/OMHBNAkknVCI7bVB9OWjUUAwhVv9vsPIAGnNUxnu3FxPQN11rjh0sksLMzc3g7IlTgvHmOTl4JM7JHpcv5wA=="],
"@opencode-ai/workerd-spike/wrangler/workerd": ["workerd@1.20250803.0", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20250803.0", "@cloudflare/workerd-darwin-arm64": "1.20250803.0", "@cloudflare/workerd-linux-64": "1.20250803.0", "@cloudflare/workerd-linux-arm64": "1.20250803.0", "@cloudflare/workerd-windows-64": "1.20250803.0" }, "bin": { "workerd": "bin/workerd" } }, "sha512-oYH29mE/wNolPc32NHHQbySaNorj6+KASUtOvQHySxB5mO1NWdGuNv49woxNCF5971UYceGQndY+OLT+24C3wQ=="],
"@opencode-ai/www/@astrojs/cloudflare/@astrojs/internal-helpers": ["@astrojs/internal-helpers@0.10.1", "", { "dependencies": { "@types/hast": "^3.0.4", "@types/mdast": "^4.0.4", "js-yaml": "^4.1.1", "picomatch": "^4.0.4", "retext-smartypants": "^6.2.0", "shiki": "^4.0.2", "smol-toml": "^1.6.0", "unified": "^11.0.5" } }, "sha512-5phcroT/vmOOrYuuAxtkbPixy5hePtlz9i8K4OeDv3dNK6/UQRuXPOSRTxIOBbUY5Sonw2UaxjbuVc43Mcir6Q=="],
"@opencode-ai/www/@astrojs/cloudflare/@astrojs/underscore-redirects": ["@astrojs/underscore-redirects@1.0.3", "", {}, "sha512-cxnGSw+sJigBLdX4TMSZKkzV6C3gMLJMucDk2W+n281Xhie68T2/9f1+1NMNDCZsc5i0FED7Qt5I10g2O9wtZg=="],
@@ -7533,6 +7614,30 @@
"@solidjs/start/shiki/@shikijs/types": ["@shikijs/types@1.29.2", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.1", "@types/hast": "^3.0.4" } }, "sha512-VJjK0eIijTZf0QSTODEXCqinjBn0joAHQ+aPSBzrv4O2d/QSbsMw+ZeSRx03kV34Hy7NzUvV/7NqfYGRLrASmw=="],
"@solidjs/start/vitest/@vitest/expect": ["@vitest/expect@4.1.10", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.1.10", "@vitest/utils": "4.1.10", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" } }, "sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA=="],
"@solidjs/start/vitest/@vitest/mocker": ["@vitest/mocker@4.1.10", "", { "dependencies": { "@vitest/spy": "4.1.10", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow=="],
"@solidjs/start/vitest/@vitest/pretty-format": ["@vitest/pretty-format@4.1.10", "", { "dependencies": { "tinyrainbow": "^3.1.0" } }, "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q=="],
"@solidjs/start/vitest/@vitest/runner": ["@vitest/runner@4.1.10", "", { "dependencies": { "@vitest/utils": "4.1.10", "pathe": "^2.0.3" } }, "sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg=="],
"@solidjs/start/vitest/@vitest/snapshot": ["@vitest/snapshot@4.1.10", "", { "dependencies": { "@vitest/pretty-format": "4.1.10", "@vitest/utils": "4.1.10", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw=="],
"@solidjs/start/vitest/@vitest/spy": ["@vitest/spy@4.1.10", "", {}, "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw=="],
"@solidjs/start/vitest/@vitest/utils": ["@vitest/utils@4.1.10", "", { "dependencies": { "@vitest/pretty-format": "4.1.10", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" } }, "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA=="],
"@solidjs/start/vitest/es-module-lexer": ["es-module-lexer@2.3.1", "", {}, "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA=="],
"@solidjs/start/vitest/std-env": ["std-env@4.2.0", "", {}, "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw=="],
"@solidjs/start/vitest/tinyexec": ["tinyexec@1.2.4", "", {}, "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg=="],
"@solidjs/start/vitest/tinyrainbow": ["tinyrainbow@3.1.0", "", {}, "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw=="],
"@solidjs/start/vitest/vite": ["vite@8.1.5", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.5", "postcss": "^8.5.17", "rolldown": "~1.1.5", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.3.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw=="],
"@storybook/addon-docs/react-dom/scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
"@storybook/csf-plugin/unplugin/acorn": ["acorn@8.17.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg=="],
@@ -7545,8 +7650,6 @@
"@vercel/routing-utils/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
"@vitest/expect/@vitest/utils/@vitest/pretty-format": ["@vitest/pretty-format@3.2.4", "", { "dependencies": { "tinyrainbow": "^2.0.0" } }, "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA=="],
"ai-gateway-provider/@ai-sdk/openai/@ai-sdk/provider": ["@ai-sdk/provider@3.0.14", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-5X1k57JBJ4H7H1QjX7CnJYAB1I19r/trVZTMcSms7/kLNZ8RaU4Nt2agcwZzv82Hfx6Q7/TOLU7agAKeFfc8cA=="],
"ai-gateway-provider/@ai-sdk/openai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.38", "", { "dependencies": { "@ai-sdk/provider": "3.0.14", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-/HHGmtKllqjg1OLc023v9w9kK3laW7Z6TzfZukYQWCsGBbzB9p60zTvvpXFVcs44NZBVXL3viOa1HRKUbeee8g=="],
@@ -7767,6 +7870,8 @@
"rimraf/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="],
"storybook/@vitest/expect/@vitest/utils": ["@vitest/utils@3.2.4", "", { "dependencies": { "@vitest/pretty-format": "3.2.4", "loupe": "^3.1.4", "tinyrainbow": "^2.0.0" } }, "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA=="],
"storybook/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.1", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ=="],
"storybook/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.28.1", "", { "os": "android", "cpu": "arm" }, "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ=="],
@@ -7841,12 +7946,6 @@
"venice-ai-sdk-provider/@ai-sdk/openai-compatible/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.35", "", { "dependencies": { "@ai-sdk/provider": "3.0.13", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-bjYld/2KGPLt78kpqbya+fD4LYS7BqVQJyUjE3qAHrYB0FR2Q90BaWEVIBZaguTWXf/A8L6uG1zO1v9TxVlGWg=="],
"vitest/@vitest/expect/chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="],
"vitest/vite/esbuild": ["esbuild@0.28.1", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.1", "@esbuild/android-arm": "0.28.1", "@esbuild/android-arm64": "0.28.1", "@esbuild/android-x64": "0.28.1", "@esbuild/darwin-arm64": "0.28.1", "@esbuild/darwin-x64": "0.28.1", "@esbuild/freebsd-arm64": "0.28.1", "@esbuild/freebsd-x64": "0.28.1", "@esbuild/linux-arm": "0.28.1", "@esbuild/linux-arm64": "0.28.1", "@esbuild/linux-ia32": "0.28.1", "@esbuild/linux-loong64": "0.28.1", "@esbuild/linux-mips64el": "0.28.1", "@esbuild/linux-ppc64": "0.28.1", "@esbuild/linux-riscv64": "0.28.1", "@esbuild/linux-s390x": "0.28.1", "@esbuild/linux-x64": "0.28.1", "@esbuild/netbsd-arm64": "0.28.1", "@esbuild/netbsd-x64": "0.28.1", "@esbuild/openbsd-arm64": "0.28.1", "@esbuild/openbsd-x64": "0.28.1", "@esbuild/openharmony-arm64": "0.28.1", "@esbuild/sunos-x64": "0.28.1", "@esbuild/win32-arm64": "0.28.1", "@esbuild/win32-ia32": "0.28.1", "@esbuild/win32-x64": "0.28.1" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw=="],
"vitest/vite/lightningcss": ["lightningcss@1.33.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.33.0", "lightningcss-darwin-arm64": "1.33.0", "lightningcss-darwin-x64": "1.33.0", "lightningcss-freebsd-x64": "1.33.0", "lightningcss-linux-arm-gnueabihf": "1.33.0", "lightningcss-linux-arm64-gnu": "1.33.0", "lightningcss-linux-arm64-musl": "1.33.0", "lightningcss-linux-x64-gnu": "1.33.0", "lightningcss-linux-x64-musl": "1.33.0", "lightningcss-win32-arm64-msvc": "1.33.0", "lightningcss-win32-x64-msvc": "1.33.0" } }, "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA=="],
"vscode-languageserver/vscode-languageserver-protocol/vscode-jsonrpc": ["vscode-jsonrpc@8.2.0", "", {}, "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA=="],
"wrangler/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.4", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q=="],
@@ -8247,6 +8346,74 @@
"@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.933.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.932.0", "@aws-sdk/middleware-host-header": "3.930.0", "@aws-sdk/middleware-logger": "3.930.0", "@aws-sdk/middleware-recursion-detection": "3.933.0", "@aws-sdk/middleware-user-agent": "3.932.0", "@aws-sdk/region-config-resolver": "3.930.0", "@aws-sdk/types": "3.930.0", "@aws-sdk/util-endpoints": "3.930.0", "@aws-sdk/util-user-agent-browser": "3.930.0", "@aws-sdk/util-user-agent-node": "3.932.0", "@smithy/config-resolver": "^4.4.3", "@smithy/core": "^3.18.2", "@smithy/fetch-http-handler": "^5.3.6", "@smithy/hash-node": "^4.2.5", "@smithy/invalid-dependency": "^4.2.5", "@smithy/middleware-content-length": "^4.2.5", "@smithy/middleware-endpoint": "^4.3.9", "@smithy/middleware-retry": "^4.4.9", "@smithy/middleware-serde": "^4.2.5", "@smithy/middleware-stack": "^4.2.5", "@smithy/node-config-provider": "^4.3.5", "@smithy/node-http-handler": "^4.4.5", "@smithy/protocol-http": "^5.3.5", "@smithy/smithy-client": "^4.9.5", "@smithy/types": "^4.9.0", "@smithy/url-parser": "^4.2.5", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.8", "@smithy/util-defaults-mode-node": "^4.2.11", "@smithy/util-endpoints": "^3.2.5", "@smithy/util-middleware": "^4.2.5", "@smithy/util-retry": "^4.2.5", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-o1GX0+IPlFi/D8ei9y/jj3yucJWNfPnbB5appVBWevAyUdZA5KzQ2nK/hDxiu9olTZlFEFpf1m1Rn3FaGxHqsw=="],
"@cloudflare/vitest-pool-workers/miniflare/sharp/@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w=="],
"@cloudflare/vitest-pool-workers/miniflare/sharp/@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.2.4" }, "os": "darwin", "cpu": "x64" }, "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw=="],
"@cloudflare/vitest-pool-workers/miniflare/sharp/@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g=="],
"@cloudflare/vitest-pool-workers/miniflare/sharp/@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.2.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg=="],
"@cloudflare/vitest-pool-workers/miniflare/sharp/@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.2.4", "", { "os": "linux", "cpu": "arm" }, "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A=="],
"@cloudflare/vitest-pool-workers/miniflare/sharp/@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw=="],
"@cloudflare/vitest-pool-workers/miniflare/sharp/@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.2.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA=="],
"@cloudflare/vitest-pool-workers/miniflare/sharp/@img/sharp-libvips-linux-riscv64": ["@img/sharp-libvips-linux-riscv64@1.2.4", "", { "os": "linux", "cpu": "none" }, "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA=="],
"@cloudflare/vitest-pool-workers/miniflare/sharp/@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.2.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ=="],
"@cloudflare/vitest-pool-workers/miniflare/sharp/@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw=="],
"@cloudflare/vitest-pool-workers/miniflare/sharp/@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw=="],
"@cloudflare/vitest-pool-workers/miniflare/sharp/@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg=="],
"@cloudflare/vitest-pool-workers/miniflare/sharp/@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.2.4" }, "os": "linux", "cpu": "arm" }, "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw=="],
"@cloudflare/vitest-pool-workers/miniflare/sharp/@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg=="],
"@cloudflare/vitest-pool-workers/miniflare/sharp/@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.2.4" }, "os": "linux", "cpu": "ppc64" }, "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA=="],
"@cloudflare/vitest-pool-workers/miniflare/sharp/@img/sharp-linux-riscv64": ["@img/sharp-linux-riscv64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-riscv64": "1.2.4" }, "os": "linux", "cpu": "none" }, "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw=="],
"@cloudflare/vitest-pool-workers/miniflare/sharp/@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.2.4" }, "os": "linux", "cpu": "s390x" }, "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg=="],
"@cloudflare/vitest-pool-workers/miniflare/sharp/@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ=="],
"@cloudflare/vitest-pool-workers/miniflare/sharp/@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg=="],
"@cloudflare/vitest-pool-workers/miniflare/sharp/@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q=="],
"@cloudflare/vitest-pool-workers/miniflare/sharp/@img/sharp-wasm32": ["@img/sharp-wasm32@0.34.5", "", { "dependencies": { "@emnapi/runtime": "^1.7.0" }, "cpu": "none" }, "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw=="],
"@cloudflare/vitest-pool-workers/miniflare/sharp/@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.34.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g=="],
"@cloudflare/vitest-pool-workers/miniflare/sharp/@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.34.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg=="],
"@cloudflare/vitest-pool-workers/miniflare/sharp/@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.5", "", { "os": "win32", "cpu": "x64" }, "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw=="],
"@cloudflare/vitest-pool-workers/miniflare/workerd/@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20260120.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-JLHx3p5dpwz4wjVSis45YNReftttnI3ndhdMh5BUbbpdreN/g0jgxNt5Qp9tDFqEKl++N63qv+hxJiIIvSLR+Q=="],
"@cloudflare/vitest-pool-workers/miniflare/workerd/@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20260120.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-1Md2tCRhZjwajsZNOiBeOVGiS3zbpLPzUDjHr4+XGTXWOA6FzzwScJwQZLa0Doc28Cp4Nr1n7xGL0Dwiz1XuOA=="],
"@cloudflare/vitest-pool-workers/miniflare/workerd/@cloudflare/workerd-linux-64": ["@cloudflare/workerd-linux-64@1.20260120.0", "", { "os": "linux", "cpu": "x64" }, "sha512-O0mIfJfvU7F8N5siCoRDaVDuI12wkz2xlG4zK6/Ct7U9c9FiE0ViXNFWXFQm5PPj+qbkNRyhjUwhP+GCKTk5EQ=="],
"@cloudflare/vitest-pool-workers/miniflare/workerd/@cloudflare/workerd-linux-arm64": ["@cloudflare/workerd-linux-arm64@1.20260120.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-aRHO/7bjxVpjZEmVVcpmhbzpN6ITbFCxuLLZSW0H9O0C0w40cDCClWSi19T87Ax/PQcYjFNT22pTewKsupkckA=="],
"@cloudflare/vitest-pool-workers/miniflare/workerd/@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20260120.0", "", { "os": "win32", "cpu": "x64" }, "sha512-ASZIz1E8sqZQqQCgcfY1PJbBpUDrxPt8NZ+lqNil0qxnO4qX38hbCsdDF2/TDAuq0Txh7nu8ztgTelfNDlb4EA=="],
"@cloudflare/vitest-pool-workers/wrangler/workerd/@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20260120.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-JLHx3p5dpwz4wjVSis45YNReftttnI3ndhdMh5BUbbpdreN/g0jgxNt5Qp9tDFqEKl++N63qv+hxJiIIvSLR+Q=="],
"@cloudflare/vitest-pool-workers/wrangler/workerd/@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20260120.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-1Md2tCRhZjwajsZNOiBeOVGiS3zbpLPzUDjHr4+XGTXWOA6FzzwScJwQZLa0Doc28Cp4Nr1n7xGL0Dwiz1XuOA=="],
"@cloudflare/vitest-pool-workers/wrangler/workerd/@cloudflare/workerd-linux-64": ["@cloudflare/workerd-linux-64@1.20260120.0", "", { "os": "linux", "cpu": "x64" }, "sha512-O0mIfJfvU7F8N5siCoRDaVDuI12wkz2xlG4zK6/Ct7U9c9FiE0ViXNFWXFQm5PPj+qbkNRyhjUwhP+GCKTk5EQ=="],
"@cloudflare/vitest-pool-workers/wrangler/workerd/@cloudflare/workerd-linux-arm64": ["@cloudflare/workerd-linux-arm64@1.20260120.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-aRHO/7bjxVpjZEmVVcpmhbzpN6ITbFCxuLLZSW0H9O0C0w40cDCClWSi19T87Ax/PQcYjFNT22pTewKsupkckA=="],
"@cloudflare/vitest-pool-workers/wrangler/workerd/@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20260120.0", "", { "os": "win32", "cpu": "x64" }, "sha512-ASZIz1E8sqZQqQCgcfY1PJbBpUDrxPt8NZ+lqNil0qxnO4qX38hbCsdDF2/TDAuq0Txh7nu8ztgTelfNDlb4EA=="],
"@electron/asar/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
"@electron/universal/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
@@ -8391,6 +8558,74 @@
"@opencode-ai/updates/wrangler/workerd/@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20260708.1", "", { "os": "win32", "cpu": "x64" }, "sha512-bC/aSAwLy16Vjo24i9XU3aWH+eRgz7NeR5xPKavGbembO18ZywYTQbXh14eXtY6fAqN3RzRG8psijTdhX4xydA=="],
"@opencode-ai/workerd-spike/wrangler/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.4", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q=="],
"@opencode-ai/workerd-spike/wrangler/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.4", "", { "os": "android", "cpu": "arm" }, "sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ=="],
"@opencode-ai/workerd-spike/wrangler/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.4", "", { "os": "android", "cpu": "arm64" }, "sha512-bBy69pgfhMGtCnwpC/x5QhfxAz/cBgQ9enbtwjf6V9lnPI/hMyT9iWpR1arm0l3kttTr4L0KSLpKmLp/ilKS9A=="],
"@opencode-ai/workerd-spike/wrangler/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.25.4", "", { "os": "android", "cpu": "x64" }, "sha512-TVhdVtQIFuVpIIR282btcGC2oGQoSfZfmBdTip2anCaVYcqWlZXGcdcKIUklfX2wj0JklNYgz39OBqh2cqXvcQ=="],
"@opencode-ai/workerd-spike/wrangler/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Y1giCfM4nlHDWEfSckMzeWNdQS31BQGs9/rouw6Ub91tkK79aIMTH3q9xHvzH8d0wDru5Ci0kWB8b3up/nl16g=="],
"@opencode-ai/workerd-spike/wrangler/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-CJsry8ZGM5VFVeyUYB3cdKpd/H69PYez4eJh1W/t38vzutdjEjtP7hB6eLKBoOdxcAlCtEYHzQ/PJ/oU9I4u0A=="],
"@opencode-ai/workerd-spike/wrangler/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.4", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-yYq+39NlTRzU2XmoPW4l5Ifpl9fqSk0nAJYM/V/WUGPEFfek1epLHJIkTQM6bBs1swApjO5nWgvr843g6TjxuQ=="],
"@opencode-ai/workerd-spike/wrangler/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.4", "", { "os": "freebsd", "cpu": "x64" }, "sha512-0FgvOJ6UUMflsHSPLzdfDnnBBVoCDtBTVyn/MrWloUNvq/5SFmh13l3dvgRPkDihRxb77Y17MbqbCAa2strMQQ=="],
"@opencode-ai/workerd-spike/wrangler/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.4", "", { "os": "linux", "cpu": "arm" }, "sha512-kro4c0P85GMfFYqW4TWOpvmF8rFShbWGnrLqlzp4X1TNWjRY3JMYUfDCtOxPKOIY8B0WC8HN51hGP4I4hz4AaQ=="],
"@opencode-ai/workerd-spike/wrangler/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-+89UsQTfXdmjIvZS6nUnOOLoXnkUTB9hR5QAeLrQdzOSWZvNSAXAtcRDHWtqAUtAmv7ZM1WPOOeSxDzzzMogiQ=="],
"@opencode-ai/workerd-spike/wrangler/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.4", "", { "os": "linux", "cpu": "ia32" }, "sha512-yTEjoapy8UP3rv8dB0ip3AfMpRbyhSN3+hY8mo/i4QXFeDxmiYbEKp3ZRjBKcOP862Ua4b1PDfwlvbuwY7hIGQ=="],
"@opencode-ai/workerd-spike/wrangler/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.4", "", { "os": "linux", "cpu": "none" }, "sha512-NeqqYkrcGzFwi6CGRGNMOjWGGSYOpqwCjS9fvaUlX5s3zwOtn1qwg1s2iE2svBe4Q/YOG1q6875lcAoQK/F4VA=="],
"@opencode-ai/workerd-spike/wrangler/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.4", "", { "os": "linux", "cpu": "none" }, "sha512-IcvTlF9dtLrfL/M8WgNI/qJYBENP3ekgsHbYUIzEzq5XJzzVEV/fXY9WFPfEEXmu3ck2qJP8LG/p3Q8f7Zc2Xg=="],
"@opencode-ai/workerd-spike/wrangler/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-HOy0aLTJTVtoTeGZh4HSXaO6M95qu4k5lJcH4gxv56iaycfz1S8GO/5Jh6X4Y1YiI0h7cRyLi+HixMR+88swag=="],
"@opencode-ai/workerd-spike/wrangler/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.4", "", { "os": "linux", "cpu": "none" }, "sha512-i8JUDAufpz9jOzo4yIShCTcXzS07vEgWzyX3NH2G7LEFVgrLEhjwL3ajFE4fZI3I4ZgiM7JH3GQ7ReObROvSUA=="],
"@opencode-ai/workerd-spike/wrangler/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-jFnu+6UbLlzIjPQpWCNh5QtrcNfMLjgIavnwPQAfoGx4q17ocOU9MsQ2QVvFxwQoWpZT8DvTLooTvmOQXkO51g=="],
"@opencode-ai/workerd-spike/wrangler/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.4", "", { "os": "linux", "cpu": "x64" }, "sha512-6e0cvXwzOnVWJHq+mskP8DNSrKBr1bULBvnFLpc1KY+d+irZSgZ02TGse5FsafKS5jg2e4pbvK6TPXaF/A6+CA=="],
"@opencode-ai/workerd-spike/wrangler/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.4", "", { "os": "none", "cpu": "arm64" }, "sha512-vUnkBYxZW4hL/ie91hSqaSNjulOnYXE1VSLusnvHg2u3jewJBz3YzB9+oCw8DABeVqZGg94t9tyZFoHma8gWZQ=="],
"@opencode-ai/workerd-spike/wrangler/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.4", "", { "os": "none", "cpu": "x64" }, "sha512-XAg8pIQn5CzhOB8odIcAm42QsOfa98SBeKUdo4xa8OvX8LbMZqEtgeWE9P/Wxt7MlG2QqvjGths+nq48TrUiKw=="],
"@opencode-ai/workerd-spike/wrangler/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.4", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-Ct2WcFEANlFDtp1nVAXSNBPDxyU+j7+tId//iHXU2f/lN5AmO4zLyhDcpR5Cz1r08mVxzt3Jpyt4PmXQ1O6+7A=="],
"@opencode-ai/workerd-spike/wrangler/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.4", "", { "os": "openbsd", "cpu": "x64" }, "sha512-xAGGhyOQ9Otm1Xu8NT1ifGLnA6M3sJxZ6ixylb+vIUVzvvd6GOALpwQrYrtlPouMqd/vSbgehz6HaVk4+7Afhw=="],
"@opencode-ai/workerd-spike/wrangler/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.4", "", { "os": "sunos", "cpu": "x64" }, "sha512-Mw+tzy4pp6wZEK0+Lwr76pWLjrtjmJyUB23tHKqEDP74R3q95luY/bXqXZeYl4NYlvwOqoRKlInQialgCKy67Q=="],
"@opencode-ai/workerd-spike/wrangler/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-AVUP428VQTSddguz9dO9ngb+E5aScyg7nOeJDrF1HPYu555gmza3bDGMPhmVXL8svDSoqPCsCPjb265yG/kLKQ=="],
"@opencode-ai/workerd-spike/wrangler/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.4", "", { "os": "win32", "cpu": "ia32" }, "sha512-i1sW+1i+oWvQzSgfRcxxG2k4I9n3O9NRqy8U+uugaT2Dy7kLO9Y7wI72haOahxceMX8hZAzgGou1FhndRldxRg=="],
"@opencode-ai/workerd-spike/wrangler/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.4", "", { "os": "win32", "cpu": "x64" }, "sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ=="],
"@opencode-ai/workerd-spike/wrangler/miniflare/acorn": ["acorn@8.14.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA=="],
"@opencode-ai/workerd-spike/wrangler/miniflare/undici": ["undici@7.29.0", "", {}, "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw=="],
"@opencode-ai/workerd-spike/wrangler/miniflare/ws": ["ws@8.18.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw=="],
"@opencode-ai/workerd-spike/wrangler/miniflare/zod": ["zod@3.22.3", "", {}, "sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug=="],
"@opencode-ai/workerd-spike/wrangler/workerd/@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20250803.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-6QciMnJp1p3F1qUiN0LaLfmw7SuZA/gfUBOe8Ft81pw16JYZ3CyiqIKPJvc1SV8jgDx8r+gz/PRi1NwOMt329A=="],
"@opencode-ai/workerd-spike/wrangler/workerd/@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20250803.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-DoIgghDowtqoNhL6OoN/F92SKtrk7mRQKc4YSs/Dst8IwFZq+pCShOlWfB0MXqHKPSoiz5xLSrUKR9H6gQMPvw=="],
"@opencode-ai/workerd-spike/wrangler/workerd/@cloudflare/workerd-linux-64": ["@cloudflare/workerd-linux-64@1.20250803.0", "", { "os": "linux", "cpu": "x64" }, "sha512-mYdz4vNWX3+PoqRjssepVQqgh42IBiSrl+wb7vbh7VVWUVzBnQKtW3G+UFiBF62hohCLexGIEi7L0cFfRlcKSQ=="],
"@opencode-ai/workerd-spike/wrangler/workerd/@cloudflare/workerd-linux-arm64": ["@cloudflare/workerd-linux-arm64@1.20250803.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-RmrtUYLRUg6djKU7Z6yebS6YGJVnaDVY6bbXca+2s26vw4ibJDOTPLuBHFQF62Grw3fAfsNbjQh5i14vG2mqUg=="],
"@opencode-ai/workerd-spike/wrangler/workerd/@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20250803.0", "", { "os": "win32", "cpu": "x64" }, "sha512-uLV8gdudz36o9sUaAKbBxxTwZwLFz1KyW7QpBvOo4+r3Ib8yVKXGiySIMWGD7A0urSMrjf3e5LlLcJKgZUOjMA=="],
"@opencode-ai/www/@astrojs/cloudflare/@astrojs/internal-helpers/js-yaml": ["js-yaml@4.3.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q=="],
"@opencode-ai/www/@astrojs/cloudflare/@cloudflare/vite-plugin/@cloudflare/unenv-preset": ["@cloudflare/unenv-preset@2.16.1", "", { "peerDependencies": { "unenv": "2.0.0-rc.24", "workerd": ">1.20260305.0 <2.0.0-0" }, "optionalPeers": ["workerd"] }, "sha512-ECxObrMfyTl5bhQf/lZCXwo5G6xX9IAUo+nDMKK4SZ8m4Jvvxp52vilxyySSWh2YTZz8+HQ07qGH/2rEom1vDw=="],
@@ -8601,6 +8836,12 @@
"@solidjs/start/shiki/@shikijs/engine-javascript/oniguruma-to-es": ["oniguruma-to-es@2.3.0", "", { "dependencies": { "emoji-regex-xs": "^1.0.0", "regex": "^5.1.1", "regex-recursion": "^5.1.1" } }, "sha512-bwALDxriqfKGfUufKGGepCzu9x7nJQuoRoAFp4AnwehhC2crqrDIAP/uN2qdlsAvSMpeRC3+Yzhqc7hLmle5+g=="],
"@solidjs/start/vitest/@vitest/expect/chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="],
"@solidjs/start/vitest/vite/esbuild": ["esbuild@0.28.1", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.1", "@esbuild/android-arm": "0.28.1", "@esbuild/android-arm64": "0.28.1", "@esbuild/android-x64": "0.28.1", "@esbuild/darwin-arm64": "0.28.1", "@esbuild/darwin-x64": "0.28.1", "@esbuild/freebsd-arm64": "0.28.1", "@esbuild/freebsd-x64": "0.28.1", "@esbuild/linux-arm": "0.28.1", "@esbuild/linux-arm64": "0.28.1", "@esbuild/linux-ia32": "0.28.1", "@esbuild/linux-loong64": "0.28.1", "@esbuild/linux-mips64el": "0.28.1", "@esbuild/linux-ppc64": "0.28.1", "@esbuild/linux-riscv64": "0.28.1", "@esbuild/linux-s390x": "0.28.1", "@esbuild/linux-x64": "0.28.1", "@esbuild/netbsd-arm64": "0.28.1", "@esbuild/netbsd-x64": "0.28.1", "@esbuild/openbsd-arm64": "0.28.1", "@esbuild/openbsd-x64": "0.28.1", "@esbuild/openharmony-arm64": "0.28.1", "@esbuild/sunos-x64": "0.28.1", "@esbuild/win32-arm64": "0.28.1", "@esbuild/win32-ia32": "0.28.1", "@esbuild/win32-x64": "0.28.1" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw=="],
"@solidjs/start/vitest/vite/lightningcss": ["lightningcss@1.33.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.33.0", "lightningcss-darwin-arm64": "1.33.0", "lightningcss-darwin-x64": "1.33.0", "lightningcss-freebsd-x64": "1.33.0", "lightningcss-linux-arm-gnueabihf": "1.33.0", "lightningcss-linux-arm64-gnu": "1.33.0", "lightningcss-linux-arm64-musl": "1.33.0", "lightningcss-linux-x64-gnu": "1.33.0", "lightningcss-linux-x64-musl": "1.33.0", "lightningcss-win32-arm64-msvc": "1.33.0", "lightningcss-win32-x64-msvc": "1.33.0" } }, "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA=="],
"ansi-align/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"app-builder-lib/@electron/get/fs-extra/universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="],
@@ -8759,6 +9000,8 @@
"rimraf/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
"storybook/@vitest/expect/@vitest/utils/@vitest/pretty-format": ["@vitest/pretty-format@3.2.4", "", { "dependencies": { "tinyrainbow": "^2.0.0" } }, "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA=="],
"temp/rimraf/glob/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="],
"tw-to-css/tailwindcss/chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
@@ -8767,78 +9010,6 @@
"unplugin/chokidar/readdirp/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="],
"vitest/vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.1", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ=="],
"vitest/vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.28.1", "", { "os": "android", "cpu": "arm" }, "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ=="],
"vitest/vite/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.28.1", "", { "os": "android", "cpu": "arm64" }, "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg=="],
"vitest/vite/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.28.1", "", { "os": "android", "cpu": "x64" }, "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng=="],
"vitest/vite/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.28.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q=="],
"vitest/vite/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.28.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ=="],
"vitest/vite/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.28.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw=="],
"vitest/vite/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.28.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ=="],
"vitest/vite/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.28.1", "", { "os": "linux", "cpu": "arm" }, "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ=="],
"vitest/vite/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.28.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g=="],
"vitest/vite/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.28.1", "", { "os": "linux", "cpu": "ia32" }, "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w=="],
"vitest/vite/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg=="],
"vitest/vite/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ=="],
"vitest/vite/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.28.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ=="],
"vitest/vite/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ=="],
"vitest/vite/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.28.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag=="],
"vitest/vite/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.28.1", "", { "os": "linux", "cpu": "x64" }, "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA=="],
"vitest/vite/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.28.1", "", { "os": "none", "cpu": "arm64" }, "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw=="],
"vitest/vite/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.28.1", "", { "os": "none", "cpu": "x64" }, "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg=="],
"vitest/vite/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.28.1", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q=="],
"vitest/vite/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.28.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw=="],
"vitest/vite/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.28.1", "", { "os": "none", "cpu": "arm64" }, "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg=="],
"vitest/vite/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.28.1", "", { "os": "sunos", "cpu": "x64" }, "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ=="],
"vitest/vite/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.28.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA=="],
"vitest/vite/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.28.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg=="],
"vitest/vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.1", "", { "os": "win32", "cpu": "x64" }, "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A=="],
"vitest/vite/lightningcss/lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.33.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg=="],
"vitest/vite/lightningcss/lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.33.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ=="],
"vitest/vite/lightningcss/lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.33.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg=="],
"vitest/vite/lightningcss/lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.33.0", "", { "os": "linux", "cpu": "arm" }, "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ=="],
"vitest/vite/lightningcss/lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.33.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg=="],
"vitest/vite/lightningcss/lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.33.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ=="],
"vitest/vite/lightningcss/lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.33.0", "", { "os": "linux", "cpu": "x64" }, "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg=="],
"vitest/vite/lightningcss/lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.33.0", "", { "os": "linux", "cpu": "x64" }, "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw=="],
"vitest/vite/lightningcss/lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.33.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA=="],
"vitest/vite/lightningcss/lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.33.0", "", { "os": "win32", "cpu": "x64" }, "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA=="],
"yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"@astrojs/cloudflare/wrangler/miniflare/sharp/@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w=="],
@@ -8943,6 +9114,8 @@
"@aws-sdk/client-sts/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.782.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.775.0", "@aws-sdk/middleware-host-header": "3.775.0", "@aws-sdk/middleware-logger": "3.775.0", "@aws-sdk/middleware-recursion-detection": "3.775.0", "@aws-sdk/middleware-user-agent": "3.782.0", "@aws-sdk/region-config-resolver": "3.775.0", "@aws-sdk/types": "3.775.0", "@aws-sdk/util-endpoints": "3.782.0", "@aws-sdk/util-user-agent-browser": "3.775.0", "@aws-sdk/util-user-agent-node": "3.782.0", "@smithy/config-resolver": "^4.1.0", "@smithy/core": "^3.2.0", "@smithy/fetch-http-handler": "^5.0.2", "@smithy/hash-node": "^4.0.2", "@smithy/invalid-dependency": "^4.0.2", "@smithy/middleware-content-length": "^4.0.2", "@smithy/middleware-endpoint": "^4.1.0", "@smithy/middleware-retry": "^4.1.0", "@smithy/middleware-serde": "^4.0.3", "@smithy/middleware-stack": "^4.0.2", "@smithy/node-config-provider": "^4.0.2", "@smithy/node-http-handler": "^4.0.4", "@smithy/protocol-http": "^5.1.0", "@smithy/smithy-client": "^4.2.0", "@smithy/types": "^4.2.0", "@smithy/url-parser": "^4.0.2", "@smithy/util-base64": "^4.0.0", "@smithy/util-body-length-browser": "^4.0.0", "@smithy/util-body-length-node": "^4.0.0", "@smithy/util-defaults-mode-browser": "^4.0.8", "@smithy/util-defaults-mode-node": "^4.0.8", "@smithy/util-endpoints": "^3.0.2", "@smithy/util-middleware": "^4.0.2", "@smithy/util-retry": "^4.0.2", "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-QOYC8q7luzHFXrP0xYAqBctoPkynjfV0r9dqntFu4/IWMTyC1vlo1UTxFAjIPyclYw92XJyEkVCVg9v/nQnsUA=="],
"@cloudflare/vitest-pool-workers/miniflare/sharp/@img/sharp-wasm32/@emnapi/runtime": ["@emnapi/runtime@1.11.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA=="],
"@jsx-email/cli/tailwindcss/chokidar/readdirp/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="],
"@opencode-ai/desktop/@actions/artifact/@actions/core/@actions/exec/@actions/io": ["@actions/io@1.1.3", "", {}, "sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q=="],
@@ -9171,6 +9344,78 @@
"@solidjs/start/shiki/@shikijs/engine-javascript/oniguruma-to-es/regex-recursion": ["regex-recursion@5.1.1", "", { "dependencies": { "regex": "^5.1.1", "regex-utilities": "^2.3.0" } }, "sha512-ae7SBCbzVNrIjgSbh7wMznPcQel1DNlDtzensnFxpiNpXt1U2ju/bHugH422r+4LAVS1FpW1YCwilmnNsjum9w=="],
"@solidjs/start/vitest/vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.1", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ=="],
"@solidjs/start/vitest/vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.28.1", "", { "os": "android", "cpu": "arm" }, "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ=="],
"@solidjs/start/vitest/vite/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.28.1", "", { "os": "android", "cpu": "arm64" }, "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg=="],
"@solidjs/start/vitest/vite/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.28.1", "", { "os": "android", "cpu": "x64" }, "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng=="],
"@solidjs/start/vitest/vite/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.28.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q=="],
"@solidjs/start/vitest/vite/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.28.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ=="],
"@solidjs/start/vitest/vite/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.28.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw=="],
"@solidjs/start/vitest/vite/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.28.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ=="],
"@solidjs/start/vitest/vite/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.28.1", "", { "os": "linux", "cpu": "arm" }, "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ=="],
"@solidjs/start/vitest/vite/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.28.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g=="],
"@solidjs/start/vitest/vite/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.28.1", "", { "os": "linux", "cpu": "ia32" }, "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w=="],
"@solidjs/start/vitest/vite/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg=="],
"@solidjs/start/vitest/vite/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ=="],
"@solidjs/start/vitest/vite/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.28.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ=="],
"@solidjs/start/vitest/vite/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ=="],
"@solidjs/start/vitest/vite/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.28.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag=="],
"@solidjs/start/vitest/vite/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.28.1", "", { "os": "linux", "cpu": "x64" }, "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA=="],
"@solidjs/start/vitest/vite/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.28.1", "", { "os": "none", "cpu": "arm64" }, "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw=="],
"@solidjs/start/vitest/vite/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.28.1", "", { "os": "none", "cpu": "x64" }, "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg=="],
"@solidjs/start/vitest/vite/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.28.1", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q=="],
"@solidjs/start/vitest/vite/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.28.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw=="],
"@solidjs/start/vitest/vite/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.28.1", "", { "os": "none", "cpu": "arm64" }, "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg=="],
"@solidjs/start/vitest/vite/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.28.1", "", { "os": "sunos", "cpu": "x64" }, "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ=="],
"@solidjs/start/vitest/vite/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.28.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA=="],
"@solidjs/start/vitest/vite/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.28.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg=="],
"@solidjs/start/vitest/vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.1", "", { "os": "win32", "cpu": "x64" }, "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A=="],
"@solidjs/start/vitest/vite/lightningcss/lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.33.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg=="],
"@solidjs/start/vitest/vite/lightningcss/lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.33.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ=="],
"@solidjs/start/vitest/vite/lightningcss/lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.33.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg=="],
"@solidjs/start/vitest/vite/lightningcss/lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.33.0", "", { "os": "linux", "cpu": "arm" }, "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ=="],
"@solidjs/start/vitest/vite/lightningcss/lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.33.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg=="],
"@solidjs/start/vitest/vite/lightningcss/lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.33.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ=="],
"@solidjs/start/vitest/vite/lightningcss/lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.33.0", "", { "os": "linux", "cpu": "x64" }, "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg=="],
"@solidjs/start/vitest/vite/lightningcss/lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.33.0", "", { "os": "linux", "cpu": "x64" }, "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw=="],
"@solidjs/start/vitest/vite/lightningcss/lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.33.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA=="],
"@solidjs/start/vitest/vite/lightningcss/lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.33.0", "", { "os": "win32", "cpu": "x64" }, "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA=="],
"archiver-utils/glob/jackspeak/@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="],
"archiver-utils/glob/jackspeak/@isaacs/cliui/wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="],
+7 -6
View File
@@ -46,9 +46,9 @@
"@octokit/rest": "22.0.0",
"@hono/standard-validator": "0.2.0",
"@hono/zod-validator": "0.4.2",
"@opentui/core": "0.0.0-20260808-9ecf7c0a",
"@opentui/keymap": "0.0.0-20260808-9ecf7c0a",
"@opentui/solid": "0.0.0-20260808-9ecf7c0a",
"@opentui/core": "0.5.2",
"@opentui/keymap": "0.5.2",
"@opentui/solid": "0.5.2",
"@tanstack/solid-virtual": "3.13.32",
"@shikijs/stream": "4.2.0",
"@standard-schema/spec": "1.1.0",
@@ -150,9 +150,9 @@
"electron"
],
"overrides": {
"@opentui/core": "0.0.0-20260808-9ecf7c0a",
"@opentui/keymap": "0.0.0-20260808-9ecf7c0a",
"@opentui/solid": "0.0.0-20260808-9ecf7c0a",
"@opentui/core": "catalog:",
"@opentui/keymap": "catalog:",
"@opentui/solid": "catalog:",
"@types/bun": "catalog:",
"@types/node": "catalog:",
"effect": "catalog:"
@@ -172,6 +172,7 @@
"@modelcontextprotocol/sdk@1.29.0": "patches/@modelcontextprotocol%2Fsdk@1.29.0.patch",
"effect@4.0.0-beta.101": "patches/effect@4.0.0-beta.101.patch",
"@tanstack/virtual-core@3.17.3": "patches/@tanstack%2Fvirtual-core@3.17.3.patch",
"@cloudflare/vitest-pool-workers@0.12.6": "patches/@cloudflare%2Fvitest-pool-workers@0.12.6.patch",
"@ff-labs/fff-bun@0.10.1": "patches/@ff-labs%2Ffff-bun@0.10.1.patch"
}
}
@@ -1,4 +1,5 @@
import { Binary } from "@opencode-ai/core/util/binary"
import { ProjectDirectories } from "@opencode-ai/schema/project-directories"
import { produce, reconcile, type SetStoreFunction, type Store } from "solid-js/store"
import type { Message, Part, Project, Todo } from "@/types"
import type {
@@ -187,6 +188,18 @@ export function applyDirectoryEvent(input: {
input.setStore("sessionTotal", (value) => Math.max(0, value - 1))
break
}
case "project.directory.resolved": {
const properties = event.properties as { projectID: string; directory: string; previous: string }
input.store.session.forEach((session, index) => {
const adopted = ProjectDirectories.adopt(
{ projectID: session.projectID, directory: session.location.directory },
properties,
)
if (!adopted) return
input.setStore("session", index, (current) => ({ ...current, ...adopted }))
})
break
}
case "session.renamed": {
const properties = event.properties as { sessionID: string; title: string }
const result = Binary.search(input.store.session, properties.sessionID, (session) => session.id)
@@ -18,18 +18,18 @@ describe("v2 session reducer", () => {
apply({
...base,
id: "evt_admitted",
type: "session.input.admitted",
type: "session.inbox.enqueued",
data: {
sessionID: "ses_1",
inputID: "msg_user",
input: { type: "user", delivery: "steer", data: { text: "hello" } },
inboxID: "msg_user",
item: { type: "user", delivery: "steer", payload: { text: "hello" } },
},
})
apply({
...base,
id: "evt_promoted",
type: "session.input.promoted",
data: { sessionID: "ses_1", inputID: "msg_user" },
type: "session.inbox.delivered",
data: { sessionID: "ses_1", inboxID: "msg_user" },
})
apply({
...base,
@@ -203,8 +203,8 @@ describe("v2 session reducer", () => {
event({
...base,
id: "evt_promoted",
type: "session.input.promoted",
data: { sessionID: "ses_1", inputID: "msg_user" },
type: "session.inbox.delivered",
data: { sessionID: "ses_1", inboxID: "msg_user" },
}),
)
@@ -218,11 +218,11 @@ describe("v2 session reducer", () => {
event({
...base,
id: "evt_admitted",
type: "session.input.admitted",
type: "session.inbox.enqueued",
data: {
sessionID: "ses_1",
inputID: "msg_user",
input: { type: "user", delivery: "queue", data: { text: "cancel me" } },
inboxID: "msg_user",
item: { type: "user", delivery: "queue", payload: { text: "cancel me" } },
},
}),
)
@@ -231,8 +231,8 @@ describe("v2 session reducer", () => {
event({
...base,
id: "evt_cancelled",
type: "session.input.cancelled",
data: { sessionID: "ses_1", inputID: "msg_user" },
type: "session.inbox.cancelled",
data: { sessionID: "ses_1", inboxID: "msg_user" },
}),
)
const result = reducer.reduce(
@@ -240,8 +240,8 @@ describe("v2 session reducer", () => {
event({
...base,
id: "evt_promoted",
type: "session.input.promoted",
data: { sessionID: "ses_1", inputID: "msg_user" },
type: "session.inbox.delivered",
data: { sessionID: "ses_1", inboxID: "msg_user" },
}),
)
@@ -255,11 +255,11 @@ describe("v2 session reducer", () => {
event({
...base,
id: "evt_admitted",
type: "session.input.admitted",
type: "session.inbox.enqueued",
data: {
sessionID: "ses_1",
inputID: "msg_user",
input: { type: "user", delivery: "queue", data: { text: "steer me" } },
inboxID: "msg_user",
item: { type: "user", delivery: "queue", payload: { text: "steer me" } },
},
}),
)
@@ -268,8 +268,8 @@ describe("v2 session reducer", () => {
event({
...base,
id: "evt_steered",
type: "session.input.steered",
data: { sessionID: "ses_1", inputID: "msg_user" },
type: "session.inbox.delivery.changed",
data: { sessionID: "ses_1", inboxID: "msg_user", delivery: "steer" },
}),
)
reducer.reduce(
@@ -277,8 +277,8 @@ describe("v2 session reducer", () => {
event({
...base,
id: "evt_queued",
type: "session.input.queued",
data: { sessionID: "ses_1", inputID: "msg_user" },
type: "session.inbox.delivery.changed",
data: { sessionID: "ses_1", inboxID: "msg_user", delivery: "queue" },
}),
)
@@ -287,8 +287,8 @@ describe("v2 session reducer", () => {
event({
...base,
id: "evt_promoted",
type: "session.input.promoted",
data: { sessionID: "ses_1", inputID: "msg_user" },
type: "session.inbox.delivered",
data: { sessionID: "ses_1", inboxID: "msg_user" },
}),
)
@@ -1,4 +1,4 @@
import type { OpenCodeEvent, SessionMessageInfo, SessionPendingMessage } from "@opencode-ai/client/promise"
import type { OpenCodeEvent, SessionInboxItem, SessionMessageInfo } from "@opencode-ai/client/promise"
type Assistant = Extract<SessionMessageInfo, { type: "assistant" }>
type Compaction = Extract<SessionMessageInfo, { type: "compaction" }>
@@ -12,7 +12,7 @@ export type V2SessionReduction = {
}
export function createV2SessionReducer() {
const pending = new Map<string, SessionPendingMessage>()
const pending = new Map<string, SessionInboxItem>()
const reduce = (source: readonly SessionMessageInfo[], event: OpenCodeEvent): V2SessionReduction | undefined => {
if (!("data" in event) || !("sessionID" in event.data) || typeof event.data.sessionID !== "string") return
@@ -26,32 +26,33 @@ export function createV2SessionReducer() {
result(source.some((item) => item.id === message.id) ? [...source] : [...source, message], [message.id])
switch (event.type) {
case "session.input.admitted":
pending.set(key(sessionID, event.data.inputID), event.data.input)
case "session.inbox.enqueued":
pending.set(key(sessionID, event.data.inboxID), event.data.item)
return result([...source])
case "session.input.cancelled":
pending.delete(key(sessionID, event.data.inputID))
case "session.inbox.cancelled":
pending.delete(key(sessionID, event.data.inboxID))
return
case "session.input.promoted": {
const input = pending.get(key(sessionID, event.data.inputID))
pending.delete(key(sessionID, event.data.inputID))
if (!input) return { ...result([...source]), missing: event.data.inputID }
case "session.inbox.delivered": {
const input = pending.get(key(sessionID, event.data.inboxID))
pending.delete(key(sessionID, event.data.inboxID))
if (!input) return { ...result([...source]), missing: event.data.inboxID }
if (input.type === "user")
return append({
id: event.data.inputID,
id: event.data.inboxID,
type: "user",
metadata: input.data.metadata,
text: input.data.text,
files: input.data.files,
agents: input.data.agents,
metadata: input.payload.metadata,
text: input.payload.text,
files: input.payload.files,
agents: input.payload.agents,
time: { created: event.created },
})
if (input.type !== "synthetic") return result([...source])
return append({
id: event.data.inputID,
id: event.data.inboxID,
type: "synthetic",
metadata: input.data.metadata,
text: input.data.text,
description: input.data.description,
metadata: input.payload.metadata,
text: input.payload.text,
description: input.payload.description,
time: { created: event.created },
})
}
@@ -351,7 +352,7 @@ export function createV2SessionReducer() {
metadata: event.metadata,
reason: event.data.reason,
summary: "",
recent: event.data.recent,
recent: event.data.recent ?? "",
time: { created: event.created },
})
case "session.compaction.delta":
@@ -1,4 +1,5 @@
import { Binary } from "@opencode-ai/core/util/binary"
import { ProjectDirectories } from "@opencode-ai/schema/project-directories"
import { retry } from "@opencode-ai/core/util/retry"
import type { OpenCodeEvent, SessionApi, SessionInfo, SessionMessageInfo } from "@opencode-ai/client/promise"
import type { Message, Part, Todo } from "@/types"
@@ -894,6 +895,17 @@ export function createServerSession(
}
const applyV2 = (event: OpenCodeEvent) => {
if (event.type === "project.directory.resolved") {
Object.values(data.info).forEach((info) => {
if (!info) return
const adopted = ProjectDirectories.adopt(
{ projectID: info.projectID, directory: info.location.directory },
event.data,
)
if (adopted) remember({ ...info, ...adopted })
})
return
}
if (!("data" in event) || !("sessionID" in event.data) || typeof event.data.sessionID !== "string") return
const sessionID = event.data.sessionID
const reduction = v2.reduce(data.session_message[sessionID] ?? [], event)
+2 -3
View File
@@ -538,9 +538,8 @@ async function replayMessage(
}
function matchesStart(event: EventSubscribeOutput, start: TurnStart) {
if (start.type === "input") return event.type === "session.input.promoted" && event.data.inputID === start.id
if (start.type === "compaction")
return event.type === "session.compaction.admitted" && event.data.inputID === start.id
if (start.type === "input") return event.type === "session.inbox.delivered" && event.data.inboxID === start.id
if (start.type === "compaction") return event.type === "session.inbox.delivered" && event.data.inboxID === start.id
return event.type === "session.skill.activated" && event.id === start.id.replace(/^msg_/, "evt_")
}
@@ -15,6 +15,8 @@ export default Runtime.handler(Commands, (input) =>
Effect.gen(function* () {
const requestedDirectory = Option.getOrUndefined(input.directory)
if (requestedDirectory !== undefined) process.chdir(requestedDirectory)
const updater = yield* Updater.Service
yield* updater.check().pipe(Effect.forkScoped)
const preflight = UpdatePreflight.make()
yield* Effect.addFinalizer(() => Effect.promise(() => preflight.close()))
const server = yield* ServerConnection.resolve({
@@ -34,8 +36,6 @@ export default Runtime.handler(Commands, (input) =>
Effect.promise(() => preflight.fail("OpenCode update could not start the new background service")),
),
)
const updater = yield* Updater.Service
yield* updater.check().pipe(Effect.forkScoped)
preflight.loading()
const config = yield* Config.Service
const npm = yield* Npm.Service
+2 -2
View File
@@ -195,8 +195,8 @@ export async function runNonInteractivePrompt(input: Input) {
if (!("sessionID" in event.data) || event.data.sessionID !== input.sessionID) continue
const time = toMillis("created" in event ? event.created : undefined)
if (event.type === "session.input.promoted") {
if (event.data.inputID === messageID) {
if (event.type === "session.inbox.delivered") {
if (event.data.inboxID === messageID) {
promoted = true
prePromotionError = undefined
continue
+18 -18
View File
@@ -22,8 +22,8 @@ describe("acp event behavior", () => {
delta: "before admission",
}),
)
send(durableEvent("session.input.promoted", { sessionID: "ses_b", inputID: id }))
send(durableEvent("session.input.promoted", { sessionID: "ses_a", inputID: "input_other" }))
send(durableEvent("session.inbox.delivered", { sessionID: "ses_b", inboxID: id }))
send(durableEvent("session.inbox.delivered", { sessionID: "ses_a", inboxID: "input_other" }))
send(
ephemeralEvent("session.text.delta", {
sessionID: "ses_a",
@@ -32,7 +32,7 @@ describe("acp event behavior", () => {
delta: "wrong input",
}),
)
send(durableEvent("session.input.promoted", { sessionID: "ses_a", inputID: id }))
send(durableEvent("session.inbox.delivered", { sessionID: "ses_a", inboxID: id }))
send(
ephemeralEvent("session.text.delta", {
sessionID: "ses_b",
@@ -68,7 +68,7 @@ describe("acp event behavior", () => {
fixture,
connection: recordingConnection(updates),
sessionID: "ses_a",
inputID: "input_a",
inboxID: "input_a",
})
expect(fixture.requests.slice(0, 2).map((request) => request.path)).toEqual([
@@ -99,7 +99,7 @@ describe("acp event behavior", () => {
const updates: SessionUpdateParams[] = []
const fixture = createSseFixture({
async onPrompt({ id, send }) {
send(durableEvent("session.input.promoted", { sessionID: "ses_order", inputID: id }))
send(durableEvent("session.inbox.delivered", { sessionID: "ses_order", inboxID: id }))
send(
ephemeralEvent("session.reasoning.delta", {
sessionID: "ses_order",
@@ -148,7 +148,7 @@ describe("acp event behavior", () => {
},
requestPermission: async () => ({ outcome: { outcome: "cancelled" } }),
} satisfies Connection
const result = turn({ fixture, connection, sessionID: "ses_order", inputID: "input_order" })
const result = turn({ fixture, connection, sessionID: "ses_order", inboxID: "input_order" })
try {
await withTimeout(firstUpdate.promise, "first ordered update was not delivered")
@@ -195,7 +195,7 @@ describe("acp event behavior", () => {
const updates: SessionUpdateParams[] = []
const fixture = createSseFixture({
onPrompt({ id, send }) {
send(durableEvent("session.input.promoted", { sessionID: "ses_parent", inputID: id }))
send(durableEvent("session.inbox.delivered", { sessionID: "ses_parent", inboxID: id }))
send(
durableEvent("session.created", {
sessionID: "ses_child",
@@ -240,7 +240,7 @@ describe("acp event behavior", () => {
fixture,
connection: recordingConnection(updates),
sessionID: "ses_parent",
inputID: "input_parent",
inboxID: "input_parent",
})
expect(updates.map((item) => [item.sessionId, item.update.sessionUpdate])).toEqual([
@@ -276,7 +276,7 @@ describe("acp event behavior", () => {
const completed = Promise.withResolvers<void>()
const fixture = createSseFixture({
onPrompt({ id, send }) {
send(durableEvent("session.input.promoted", { sessionID: "ses_parent", inputID: id }))
send(durableEvent("session.inbox.delivered", { sessionID: "ses_parent", inboxID: id }))
send(
durableEvent("session.created", {
sessionID: "ses_background",
@@ -292,7 +292,7 @@ describe("acp event behavior", () => {
fixture,
connection: recordingConnection(updates),
sessionID: "ses_parent",
inputID: "input_parent",
inboxID: "input_parent",
childSessionUpdate: async (update) => {
childUpdates.push(update)
if (update.type === "status" && update.status === "completed") completed.resolve()
@@ -370,7 +370,7 @@ describe("acp event behavior", () => {
const updates: SessionUpdateParams[] = []
const fixture = createSseFixture({
onPrompt({ id, send }) {
send(durableEvent("session.input.promoted", { sessionID: "ses_tools", inputID: id }))
send(durableEvent("session.inbox.delivered", { sessionID: "ses_tools", inboxID: id }))
send(
durableEvent("session.tool.input.started", {
sessionID: "ses_tools",
@@ -460,7 +460,7 @@ describe("acp event behavior", () => {
fixture,
connection: recordingConnection(updates),
sessionID: "ses_tools",
inputID: "input_tools",
inboxID: "input_tools",
})
expect(
@@ -597,7 +597,7 @@ describe("acp event behavior", () => {
const control: TurnControl = { cancelled: false, admission: new AbortController() }
const fixture = createSseFixture({
onPrompt({ id, send }) {
send(durableEvent("session.input.promoted", { sessionID: "ses_cancel", inputID: id }))
send(durableEvent("session.inbox.delivered", { sessionID: "ses_cancel", inboxID: id }))
},
onInterrupt({ sessionID, send }) {
send(durableEvent("session.execution.interrupted", { sessionID, reason: "user" }))
@@ -680,7 +680,7 @@ describe("acp event behavior", () => {
test("cancels unsupported session forms so execution can continue", async () => {
const fixture = createSseFixture({
onPrompt({ id, send }) {
send(durableEvent("session.input.promoted", { sessionID: "ses_form", inputID: id }))
send(durableEvent("session.inbox.delivered", { sessionID: "ses_form", inboxID: id }))
send(
ephemeralEvent("form.created", {
form: {
@@ -704,7 +704,7 @@ describe("acp event behavior", () => {
fixture,
connection: recordingConnection([]),
sessionID: "ses_form",
inputID: "input_form",
inboxID: "input_form",
})
expect(response.stopReason).toBe("end_turn")
@@ -730,7 +730,7 @@ function turn(input: {
readonly fixture: Fixture
readonly connection: Connection
readonly sessionID: string
readonly inputID: string
readonly inboxID: string
readonly childSessionUpdate?: (update: ChildSessionUpdate) => Promise<void>
}) {
return streamTurn({
@@ -738,12 +738,12 @@ function turn(input: {
connection: input.connection,
sessionID: input.sessionID,
cwd: "/workspace",
start: { type: "input", id: input.inputID },
start: { type: "input", id: input.inboxID },
writeTextFile: false,
control: { cancelled: false, admission: new AbortController() },
childSessionUpdate: input.childSessionUpdate,
submit: (signal) =>
input.fixture.client.session.prompt({ sessionID: input.sessionID, id: input.inputID, text: "hello" }, { signal }),
input.fixture.client.session.prompt({ sessionID: input.sessionID, id: input.inboxID, text: "hello" }, { signal }),
})
}
+2 -2
View File
@@ -34,8 +34,8 @@ test("acp prompt resolves after ordered turn updates", async () => {
send(events, {
id: "evt_promoted",
created: 1,
type: "session.input.promoted",
data: { sessionID: "ses_test", inputID: id },
type: "session.inbox.delivered",
data: { sessionID: "ses_test", inboxID: id },
})
send(events, {
id: "evt_text",
@@ -38,7 +38,7 @@ describe("acp permission behavior", () => {
const permissionRequests: RequestPermissionRequest[] = []
const fixture = createSseFixture({
onPrompt({ id, send }) {
send(durableEvent("session.input.promoted", { sessionID: "ses_allow", inputID: id }))
send(durableEvent("session.inbox.delivered", { sessionID: "ses_allow", inboxID: id }))
send(
permissionAsked("ses_allow", "perm_once", {
action: "shell",
@@ -112,7 +112,7 @@ describe("acp permission behavior", () => {
const permissionRequests: RequestPermissionRequest[] = []
const fixture = createSseFixture({
onPrompt({ id, send }) {
send(durableEvent("session.input.promoted", { sessionID: "ses_external", inputID: id }))
send(durableEvent("session.inbox.delivered", { sessionID: "ses_external", inboxID: id }))
send(
permissionAsked("ses_external", "perm_external", {
action: "external_directory",
@@ -157,7 +157,7 @@ describe("acp permission behavior", () => {
const permissionRequests: RequestPermissionRequest[] = []
const fixture = createSseFixture({
onPrompt({ id, send }) {
send(durableEvent("session.input.promoted", { sessionID: "ses_parent", inputID: id }))
send(durableEvent("session.inbox.delivered", { sessionID: "ses_parent", inboxID: id }))
send(
durableEvent("session.created", {
sessionID: "ses_child",
@@ -219,7 +219,7 @@ describe("acp permission behavior", () => {
const writes: Parameters<AgentSideConnection["writeTextFile"]>[0][] = []
const fixture = createSseFixture({
onPrompt({ id, send }) {
send(durableEvent("session.input.promoted", { sessionID: "ses_edit", inputID: id }))
send(durableEvent("session.inbox.delivered", { sessionID: "ses_edit", inboxID: id }))
send(
durableEvent("session.tool.input.started", {
sessionID: "ses_edit",
@@ -309,7 +309,7 @@ describe("acp permission behavior", () => {
const writes: Parameters<AgentSideConnection["writeTextFile"]>[0][] = []
const fixture = createSseFixture({
onPrompt({ id, send }) {
send(durableEvent("session.input.promoted", { sessionID: "ses_patch", inputID: id }))
send(durableEvent("session.inbox.delivered", { sessionID: "ses_patch", inboxID: id }))
send(
durableEvent("session.tool.input.started", {
sessionID: "ses_patch",
@@ -389,7 +389,7 @@ describe("acp permission behavior", () => {
test("rejects explicit rejection, cancellation, and permission UI failure", async () => {
const fixture = createSseFixture({
onPrompt({ id, send }) {
send(durableEvent("session.input.promoted", { sessionID: "ses_reject", inputID: id }))
send(durableEvent("session.inbox.delivered", { sessionID: "ses_reject", inboxID: id }))
send(permissionAsked("ses_reject", "perm_selected_reject"))
send(permissionAsked("ses_reject", "perm_cancelled"))
send(permissionAsked("ses_reject", "perm_failed"))
@@ -426,7 +426,7 @@ describe("acp permission behavior", () => {
const permissionRequests: RequestPermissionRequest[] = []
const fixture = createSseFixture({
onPrompt({ id, send }) {
send(durableEvent("session.input.promoted", { sessionID: "ses_serial", inputID: id }))
send(durableEvent("session.inbox.delivered", { sessionID: "ses_serial", inboxID: id }))
send(permissionAsked("ses_serial", "perm_1"))
send(permissionAsked("ses_serial", "perm_2"))
send(durableEvent("session.execution.succeeded", { sessionID: "ses_serial" }))
@@ -477,8 +477,8 @@ describe("acp permission behavior", () => {
const blockedID = promptIDs.get("ses_blocked")
const freeID = promptIDs.get("ses_free")
if (!blockedID || !freeID) throw new Error("both permission test prompts must be registered")
send(durableEvent("session.input.promoted", { sessionID: "ses_blocked", inputID: blockedID }))
send(durableEvent("session.input.promoted", { sessionID: "ses_free", inputID: freeID }))
send(durableEvent("session.inbox.delivered", { sessionID: "ses_blocked", inboxID: blockedID }))
send(durableEvent("session.inbox.delivered", { sessionID: "ses_free", inboxID: freeID }))
send(permissionAsked("ses_blocked", "perm_blocked"))
send(
ephemeralEvent("session.text.delta", {
@@ -538,16 +538,16 @@ describe("acp permission behavior", () => {
})
})
function startTurn(fixture: Fixture, connection: Connection, sessionID: string, inputID: string, cwd = "/workspace") {
function startTurn(fixture: Fixture, connection: Connection, sessionID: string, inboxID: string, cwd = "/workspace") {
return streamTurn({
client: fixture.client,
connection,
sessionID,
cwd,
start: { type: "input", id: inputID },
start: { type: "input", id: inboxID },
writeTextFile: true,
control: { cancelled: false, admission: new AbortController() },
submit: (signal) => fixture.client.session.prompt({ sessionID, id: inputID, text: "hello" }, { signal }),
submit: (signal) => fixture.client.session.prompt({ sessionID, id: inboxID, text: "hello" }, { signal }),
})
}
+8 -8
View File
@@ -15,8 +15,8 @@ describe("acp service prompt routing and usage", () => {
const id = requestID(request)
completeTurn(context, "ses_routes", {
id: `evt_${id}`,
type: "session.input.promoted",
data: { sessionID: "ses_routes", inputID: id },
type: "session.inbox.delivered",
data: { sessionID: "ses_routes", inboxID: id },
})
return Response.json({ data: {} })
}
@@ -33,8 +33,8 @@ describe("acp service prompt routing and usage", () => {
const id = requestID(request)
completeTurn(context, "ses_routes", {
id: `evt_${id}`,
type: "session.compaction.admitted",
data: { sessionID: "ses_routes", inputID: id },
type: "session.inbox.delivered",
data: { sessionID: "ses_routes", inboxID: id },
})
return Response.json({ data: {} })
}
@@ -95,8 +95,8 @@ describe("acp service prompt routing and usage", () => {
const id = requestID(request)
context.send({
id: `evt_${id}`,
type: "session.input.promoted",
data: { sessionID: "ses_usage", inputID: id },
type: "session.inbox.delivered",
data: { sessionID: "ses_usage", inboxID: id },
})
context.send({
id: "evt_step",
@@ -189,8 +189,8 @@ describe("acp service prompt routing and usage", () => {
const id = requestID(request)
context.send({
id: `evt_${id}`,
type: "session.input.promoted",
data: { sessionID: "ses_usage_failure", inputID: id },
type: "session.inbox.delivered",
data: { sessionID: "ses_usage_failure", inboxID: id },
})
context.send({
id: "evt_step_failure",
+10 -10
View File
@@ -28,13 +28,13 @@ function formCreated(info: FormInfo, eventLocation = location): V2Event {
return { id: `evt_${info.id}`, created: 0, type: "form.created", location: eventLocation, data: { form: info } }
}
function prompted(inputID: string): V2Event {
function prompted(inboxID: string): V2Event {
return {
id: "evt_prompted",
created: 0,
type: "session.input.promoted",
type: "session.inbox.delivered",
durable: { aggregateID: "ses_1", seq: 0, version: 1 },
data: { sessionID: "ses_1", inputID },
data: { sessionID: "ses_1", inboxID },
}
}
@@ -98,9 +98,9 @@ function executionFailed(message: string): V2Event {
}
}
function failedTool(inputID: string): V2Event[] {
function failedTool(inboxID: string): V2Event[] {
return [
prompted(inputID),
prompted(inboxID),
{
id: "evt_failed_tool_input",
created: 1,
@@ -156,10 +156,10 @@ function failedTool(inputID: string): V2Event[] {
]
}
function successfulGrep(inputID: string): V2Event[] {
function successfulGrep(inboxID: string): V2Event[] {
const text = "Found 2 matches\n/src/a.ts:\n Line 1: needle\n/src/b.ts:\n Line 2: needle"
return [
prompted(inputID),
prompted(inboxID),
{
id: "evt_grep_input",
created: 1,
@@ -206,7 +206,7 @@ function successfulGrep(inputID: string): V2Event[] {
// Runs one non-interactive prompt against a mocked SDK. `turn` produces the
// live events the prompt admission triggers, keyed by the generated message ID.
async function run(input: {
turn: (inputID: string) => V2Event[]
turn: (inboxID: string) => V2Event[]
pendingForms?: FormInfo[]
attached?: boolean
format?: "default" | "json"
@@ -214,7 +214,7 @@ async function run(input: {
cancel?: (input: { sessionID: string; formID: string }) => Promise<void>
renderTool?: (part: SessionMessageAssistantTool) => Promise<void>
renderToolError?: (part: SessionMessageAssistantTool) => Promise<void>
messages?: (inputID: string) => SessionMessageInfo[]
messages?: (inboxID: string) => SessionMessageInfo[]
wait?: () => Promise<void>
terminalDelay?: number
}) {
@@ -515,7 +515,7 @@ describe("runNonInteractivePrompt", () => {
const rendered: SessionMessageAssistantTool[] = []
const failed: SessionMessageAssistantTool[] = []
await capture({
turn: (inputID) => failedTool(inputID).filter((event) => event.type !== "session.tool.progress"),
turn: (inboxID) => failedTool(inboxID).filter((event) => event.type !== "session.tool.progress"),
renderTool: (part) => {
rendered.push(part)
return Promise.resolve()
+4 -4
View File
@@ -19,7 +19,7 @@ import { Permission } from "@opencode-ai/schema/permission"
import { PermissionSaved } from "@opencode-ai/schema/permission-saved"
import { Plugin } from "@opencode-ai/schema/plugin"
import { Project } from "@opencode-ai/schema/project"
import { ProjectCopy } from "@opencode-ai/schema/project-copy"
import { Worktree } from "@opencode-ai/schema/worktree"
import { AgentAttachment, FileAttachment, Prompt, PromptMention } from "@opencode-ai/schema/prompt"
import { PromptInput } from "@opencode-ai/schema/prompt-input"
import { Provider } from "@opencode-ai/schema/provider"
@@ -30,7 +30,7 @@ import { Reference } from "@opencode-ai/schema/reference"
import { AbsolutePath, PositiveInt, RelativePath } from "@opencode-ai/schema/schema"
import { Session } from "@opencode-ai/schema/session"
import { SessionMessage } from "@opencode-ai/schema/session-message"
import { SessionPending } from "@opencode-ai/schema/session-pending"
import { SessionInbox } from "@opencode-ai/schema/session-inbox"
import { Shell } from "@opencode-ai/schema/shell"
import { Skill } from "@opencode-ai/schema/skill"
import { Vcs } from "@opencode-ai/schema/vcs"
@@ -60,7 +60,7 @@ const effectTypeReferences = [
...namespaceTypes("PermissionSaved", "@opencode-ai/schema/permission-saved", PermissionSaved),
...namespaceTypes("Plugin", "@opencode-ai/schema/plugin", Plugin),
...namespaceTypes("Project", "@opencode-ai/schema/project", Project),
...namespaceTypes("ProjectCopy", "@opencode-ai/schema/project-copy", ProjectCopy),
...namespaceTypes("Worktree", "@opencode-ai/schema/worktree", Worktree),
...namespaceTypes("PromptInput", "@opencode-ai/schema/prompt-input", PromptInput),
...namespaceTypes("Provider", "@opencode-ai/schema/provider", Provider),
...namespaceTypes("Pty", "@opencode-ai/schema/pty", Pty),
@@ -69,7 +69,7 @@ const effectTypeReferences = [
...namespaceTypes("Reference", "@opencode-ai/schema/reference", Reference),
...namespaceTypes("Session", "@opencode-ai/schema/session", Session),
...namespaceTypes("SessionMessage", "@opencode-ai/schema/session-message", SessionMessage),
...namespaceTypes("SessionPending", "@opencode-ai/schema/session-pending", SessionPending),
...namespaceTypes("SessionInbox", "@opencode-ai/schema/session-inbox", SessionInbox),
...namespaceTypes("Shell", "@opencode-ai/schema/shell", Shell),
...namespaceTypes("Skill", "@opencode-ai/schema/skill", Skill),
...namespaceTypes("Vcs", "@opencode-ai/schema/vcs", Vcs),
+62 -91
View File
@@ -11,9 +11,9 @@ import type { RelativePath } from "@opencode-ai/schema/schema"
import type { Brand } from "effect"
import type { Model } from "@opencode-ai/schema/model"
import type { SessionMessage } from "@opencode-ai/schema/session-message"
import type { SessionInbox } from "@opencode-ai/schema/session-inbox"
import type { PromptInput } from "@opencode-ai/schema/prompt-input"
import type { AgentAttachment } from "@opencode-ai/schema/prompt"
import type { SessionPending } from "@opencode-ai/schema/session-pending"
import type { Skill } from "@opencode-ai/schema/skill"
import type { Event } from "@opencode-ai/schema/event"
import type { InstructionEntry } from "@opencode-ai/schema/instruction-entry"
@@ -34,7 +34,7 @@ import type { OpenCodeEvent } from "@opencode-ai/protocol/groups/event"
import type { Pty } from "@opencode-ai/schema/pty"
import type { Question } from "@opencode-ai/schema/question"
import type { Reference } from "@opencode-ai/schema/reference"
import type { ProjectCopy } from "@opencode-ai/schema/project-copy"
import type { Worktree } from "@opencode-ai/schema/worktree"
import type { Vcs } from "@opencode-ai/schema/vcs"
import type { FileDiff } from "@opencode-ai/schema/file-diff"
import type { WebSearch } from "@opencode-ai/schema/websearch"
@@ -170,6 +170,7 @@ export type Endpoint5_11Input = {
readonly sessionID: Session.ID
readonly directory: AbsolutePath
readonly workspaceID?: Workspace.ID | undefined
readonly delivery?: SessionInbox.Delivery | undefined
}
export type Endpoint5_11Output = void
export type SessionMoveOperation<E = never> = (input: Endpoint5_11Input) => Effect.Effect<Endpoint5_11Output, E>
@@ -182,10 +183,10 @@ export type Endpoint5_12Input = {
readonly agents?: ReadonlyArray<AgentAttachment> | undefined
readonly skills?: ReadonlyArray<PromptInput.SkillAttachment> | undefined
readonly metadata?: { readonly [x: string]: unknown } | undefined
readonly delivery?: "steer" | "queue" | undefined
readonly delivery?: SessionInbox.Delivery | undefined
readonly resume?: boolean | undefined
}
export type Endpoint5_12Output = SessionPending.User
export type Endpoint5_12Output = SessionInbox.User
export type SessionPromptOperation<E = never> = (input: Endpoint5_12Input) => Effect.Effect<Endpoint5_12Output, E>
export type Endpoint5_13Input = {
@@ -198,10 +199,10 @@ export type Endpoint5_13Input = {
readonly files?: ReadonlyArray<PromptInput.FileAttachment> | undefined
readonly agents?: ReadonlyArray<AgentAttachment> | undefined
readonly skills?: ReadonlyArray<PromptInput.SkillAttachment> | undefined
readonly delivery?: "steer" | "queue" | undefined
readonly delivery?: SessionInbox.Delivery | undefined
readonly resume?: boolean | undefined
}
export type Endpoint5_13Output = SessionPending.User
export type Endpoint5_13Output = SessionInbox.User
export type SessionCommandOperation<E = never> = (input: Endpoint5_13Input) => Effect.Effect<Endpoint5_13Output, E>
export type Endpoint5_14Input = {
@@ -219,10 +220,10 @@ export type Endpoint5_15Input = {
readonly text: string
readonly description?: string | undefined
readonly metadata?: { readonly [x: string]: unknown } | undefined
readonly delivery?: "steer" | "queue" | undefined
readonly delivery?: SessionInbox.Delivery | undefined
readonly resume?: boolean | undefined
}
export type Endpoint5_15Output = SessionPending.Synthetic
export type Endpoint5_15Output = SessionInbox.Synthetic
export type SessionSyntheticOperation<E = never> = (input: Endpoint5_15Input) => Effect.Effect<Endpoint5_15Output, E>
export type Endpoint5_16Input = {
@@ -233,8 +234,12 @@ export type Endpoint5_16Input = {
export type Endpoint5_16Output = void
export type SessionShellOperation<E = never> = (input: Endpoint5_16Input) => Effect.Effect<Endpoint5_16Output, E>
export type Endpoint5_17Input = { readonly sessionID: Session.ID; readonly id?: SessionMessage.ID | undefined }
export type Endpoint5_17Output = SessionPending.Compaction
export type Endpoint5_17Input = {
readonly sessionID: Session.ID
readonly id?: SessionMessage.ID | undefined
readonly delivery?: SessionInbox.Delivery | undefined
}
export type Endpoint5_17Output = SessionInbox.Compaction
export type SessionCompactOperation<E = never> = (input: Endpoint5_17Input) => Effect.Effect<Endpoint5_17Output, E>
export type Endpoint5_18Input = { readonly sessionID: Session.ID }
@@ -262,22 +267,20 @@ export type Endpoint5_22Output = ReadonlyArray<SessionMessage.Info>
export type SessionContextOperation<E = never> = (input: Endpoint5_22Input) => Effect.Effect<Endpoint5_22Output, E>
export type Endpoint5_23Input = { readonly sessionID: Session.ID }
export type Endpoint5_23Output = ReadonlyArray<SessionPending.Info>
export type SessionPendingListOperation<E = never> = (input: Endpoint5_23Input) => Effect.Effect<Endpoint5_23Output, E>
export type Endpoint5_23Output = ReadonlyArray<SessionInbox.Info>
export type SessionInboxListOperation<E = never> = (input: Endpoint5_23Input) => Effect.Effect<Endpoint5_23Output, E>
export type Endpoint5_24Input = { readonly sessionID: Session.ID; readonly inputID: SessionMessage.ID }
export type Endpoint5_24Input = { readonly sessionID: Session.ID; readonly inboxID: SessionMessage.ID }
export type Endpoint5_24Output = void
export type SessionPendingCancelOperation<E = never> = (
input: Endpoint5_24Input,
) => Effect.Effect<Endpoint5_24Output, E>
export type SessionInboxCancelOperation<E = never> = (input: Endpoint5_24Input) => Effect.Effect<Endpoint5_24Output, E>
export type Endpoint5_25Input = { readonly sessionID: Session.ID; readonly inputID: SessionMessage.ID }
export type Endpoint5_25Input = { readonly sessionID: Session.ID; readonly inboxID: SessionMessage.ID }
export type Endpoint5_25Output = void
export type SessionPendingSteerOperation<E = never> = (input: Endpoint5_25Input) => Effect.Effect<Endpoint5_25Output, E>
export type SessionInboxSteerOperation<E = never> = (input: Endpoint5_25Input) => Effect.Effect<Endpoint5_25Output, E>
export type Endpoint5_26Input = { readonly sessionID: Session.ID; readonly inputID: SessionMessage.ID }
export type Endpoint5_26Input = { readonly sessionID: Session.ID; readonly inboxID: SessionMessage.ID }
export type Endpoint5_26Output = void
export type SessionPendingQueueOperation<E = never> = (input: Endpoint5_26Input) => Effect.Effect<Endpoint5_26Output, E>
export type SessionInboxQueueOperation<E = never> = (input: Endpoint5_26Input) => Effect.Effect<Endpoint5_26Output, E>
export type Endpoint5_27Input = { readonly sessionID: Session.ID }
export type Endpoint5_27Output = ReadonlyArray<InstructionEntry.Info>
@@ -358,15 +361,6 @@ export type Endpoint5_31Output =
readonly previous?: Model.Ref | undefined
}
}
| {
readonly id: Event.ID
readonly created: DateTime.Utc
readonly metadata?: { readonly [x: string]: unknown } | undefined
readonly type: "session.move.admitted"
readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version }
readonly location?: Location.Ref | undefined
readonly data: { readonly sessionID: Session.ID; readonly move: SessionPending.MoveData }
}
| {
readonly id: Event.ID
readonly created: DateTime.Utc
@@ -376,9 +370,8 @@ export type Endpoint5_31Output =
readonly location?: Location.Ref | undefined
readonly data: {
readonly sessionID: Session.ID
readonly moveID?: Event.ID | undefined
readonly location: Location.Ref
readonly projectID?: Project.ID | undefined
readonly projectID: Project.ID
readonly subpath?: RelativePath | undefined
}
}
@@ -420,50 +413,45 @@ export type Endpoint5_31Output =
readonly id: Event.ID
readonly created: DateTime.Utc
readonly metadata?: { readonly [x: string]: unknown } | undefined
readonly type: "session.input.promoted"
readonly type: "session.inbox.delivered"
readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version }
readonly location?: Location.Ref | undefined
readonly data: { readonly sessionID: Session.ID; readonly inputID: SessionMessage.ID }
readonly data: { readonly sessionID: Session.ID; readonly inboxID: SessionMessage.ID }
}
| {
readonly id: Event.ID
readonly created: DateTime.Utc
readonly metadata?: { readonly [x: string]: unknown } | undefined
readonly type: "session.input.admitted"
readonly type: "session.inbox.enqueued"
readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version }
readonly location?: Location.Ref | undefined
readonly data: {
readonly sessionID: Session.ID
readonly inputID: SessionMessage.ID
readonly input: SessionPending.Message
readonly inboxID: SessionMessage.ID
readonly item: SessionInbox.Item
}
}
| {
readonly id: Event.ID
readonly created: DateTime.Utc
readonly metadata?: { readonly [x: string]: unknown } | undefined
readonly type: "session.input.cancelled"
readonly type: "session.inbox.cancelled"
readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version }
readonly location?: Location.Ref | undefined
readonly data: { readonly sessionID: Session.ID; readonly inputID: SessionMessage.ID }
readonly data: { readonly sessionID: Session.ID; readonly inboxID: SessionMessage.ID }
}
| {
readonly id: Event.ID
readonly created: DateTime.Utc
readonly metadata?: { readonly [x: string]: unknown } | undefined
readonly type: "session.input.steered"
readonly type: "session.inbox.delivery.changed"
readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version }
readonly location?: Location.Ref | undefined
readonly data: { readonly sessionID: Session.ID; readonly inputID: SessionMessage.ID }
}
| {
readonly id: Event.ID
readonly created: DateTime.Utc
readonly metadata?: { readonly [x: string]: unknown } | undefined
readonly type: "session.input.queued"
readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version }
readonly location?: Location.Ref | undefined
readonly data: { readonly sessionID: Session.ID; readonly inputID: SessionMessage.ID }
readonly data: {
readonly sessionID: Session.ID
readonly inboxID: SessionMessage.ID
readonly delivery: SessionInbox.Delivery
}
}
| {
readonly id: Event.ID
@@ -824,15 +812,6 @@ export type Endpoint5_31Output =
readonly error: { readonly type: string; readonly message: string; readonly status?: number | undefined }
}
}
| {
readonly id: Event.ID
readonly created: DateTime.Utc
readonly metadata?: { readonly [x: string]: unknown } | undefined
readonly type: "session.compaction.admitted"
readonly durable: { readonly aggregateID: string; readonly seq: Event.Seq; readonly version: Event.Version }
readonly location?: Location.Ref | undefined
readonly data: { readonly sessionID: Session.ID; readonly inputID: SessionMessage.ID }
}
| {
readonly id: Event.ID
readonly created: DateTime.Utc
@@ -963,11 +942,11 @@ export interface SessionApi<E = never> {
readonly commit: SessionRevertCommitOperation<E>
}
readonly context: SessionContextOperation<E>
readonly pending: {
readonly list: SessionPendingListOperation<E>
readonly cancel: SessionPendingCancelOperation<E>
readonly steer: SessionPendingSteerOperation<E>
readonly queue: SessionPendingQueueOperation<E>
readonly inbox: {
readonly list: SessionInboxListOperation<E>
readonly cancel: SessionInboxCancelOperation<E>
readonly steer: SessionInboxSteerOperation<E>
readonly queue: SessionInboxQueueOperation<E>
}
readonly instructions: {
readonly entry: {
@@ -1251,17 +1230,9 @@ export type Endpoint13_1Input = {
export type Endpoint13_1Output = Project.Current
export type ProjectCurrentOperation<E = never> = (input?: Endpoint13_1Input) => Effect.Effect<Endpoint13_1Output, E>
export type Endpoint13_2Input = {
readonly projectID: Project.ID
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}
export type Endpoint13_2Output = Project.Directories
export type ProjectDirectoriesOperation<E = never> = (input: Endpoint13_2Input) => Effect.Effect<Endpoint13_2Output, E>
export interface ProjectApi<E = never> {
readonly list: ProjectListOperation<E>
readonly current: ProjectCurrentOperation<E>
readonly directories: ProjectDirectoriesOperation<E>
}
export type Endpoint14_0Input = {
@@ -1570,36 +1541,36 @@ export interface ReferenceApi<E = never> {
readonly list: ReferenceListOperation<E>
}
export type Endpoint24_0Input = {
readonly projectID: Project.ID
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
readonly strategy: ProjectCopy.StrategyID
readonly directory: AbsolutePath
readonly name?: string | undefined
}
export type Endpoint24_0Output = ProjectCopy.Copy
export type ProjectCopyCreateOperation<E = never> = (input: Endpoint24_0Input) => Effect.Effect<Endpoint24_0Output, E>
export type Endpoint24_0Input = { readonly projectID: Project.ID }
export type Endpoint24_0Output = Worktree.List
export type WorktreeListOperation<E = never> = (input: Endpoint24_0Input) => Effect.Effect<Endpoint24_0Output, E>
export type Endpoint24_1Input = {
readonly projectID: Project.ID
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
readonly strategy: Worktree.StrategyID
readonly directory: AbsolutePath
readonly force: boolean
readonly name?: string | undefined
}
export type Endpoint24_1Output = void
export type ProjectCopyRemoveOperation<E = never> = (input: Endpoint24_1Input) => Effect.Effect<Endpoint24_1Output, E>
export type Endpoint24_1Output = Worktree.Info
export type WorktreeCreateOperation<E = never> = (input: Endpoint24_1Input) => Effect.Effect<Endpoint24_1Output, E>
export type Endpoint24_2Input = {
readonly projectID: Project.ID
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
readonly directory: AbsolutePath
readonly force: boolean
}
export type Endpoint24_2Output = void
export type ProjectCopyRefreshOperation<E = never> = (input: Endpoint24_2Input) => Effect.Effect<Endpoint24_2Output, E>
export type WorktreeRemoveOperation<E = never> = (input: Endpoint24_2Input) => Effect.Effect<Endpoint24_2Output, E>
export interface ProjectCopyApi<E = never> {
readonly create: ProjectCopyCreateOperation<E>
readonly remove: ProjectCopyRemoveOperation<E>
readonly refresh: ProjectCopyRefreshOperation<E>
export type Endpoint24_3Input = { readonly projectID: Project.ID }
export type Endpoint24_3Output = void
export type WorktreeRefreshOperation<E = never> = (input: Endpoint24_3Input) => Effect.Effect<Endpoint24_3Output, E>
export interface WorktreeApi<E = never> {
readonly list: WorktreeListOperation<E>
readonly create: WorktreeCreateOperation<E>
readonly remove: WorktreeRemoveOperation<E>
readonly refresh: WorktreeRefreshOperation<E>
}
export type Endpoint25_0Input = {
@@ -1712,7 +1683,7 @@ export interface AppApi<E = never> {
readonly shell: ShellApi<E>
readonly question: QuestionApi<E>
readonly reference: ReferenceApi<E>
readonly projectCopy: ProjectCopyApi<E>
readonly worktree: WorktreeApi<E>
readonly vcs: VcsApi<E>
readonly debug: DebugApi<E>
readonly migration: MigrationApi<E>
+32 -40
View File
@@ -139,8 +139,6 @@ import type {
Endpoint13_0Output,
Endpoint13_1Input,
Endpoint13_1Output,
Endpoint13_2Input,
Endpoint13_2Output,
Endpoint14_0Input,
Endpoint14_0Output,
Endpoint14_1Input,
@@ -216,6 +214,8 @@ import type {
Endpoint24_1Output,
Endpoint24_2Input,
Endpoint24_2Output,
Endpoint24_3Input,
Endpoint24_3Output,
Endpoint25_0Input,
Endpoint25_0Output,
Endpoint25_1Input,
@@ -399,7 +399,7 @@ const Endpoint5_11 = (raw: RawClient["server.session"]) => (input: Endpoint5_11I
preserveEffect<Endpoint5_11Output>()(
raw["session.move"]({
params: { sessionID: input["sessionID"] },
payload: { directory: input["directory"], workspaceID: input["workspaceID"] },
payload: { directory: input["directory"], workspaceID: input["workspaceID"], delivery: input["delivery"] },
}).pipe(Effect.mapError(mapClientError)),
)
@@ -481,7 +481,10 @@ const Endpoint5_16 = (raw: RawClient["server.session"]) => (input: Endpoint5_16I
const Endpoint5_17 = (raw: RawClient["server.session"]) => (input: Endpoint5_17Input) =>
preserveEffect<Endpoint5_17Output>()(
raw["session.compact"]({ params: { sessionID: input["sessionID"] }, payload: { id: input["id"] } }).pipe(
raw["session.compact"]({
params: { sessionID: input["sessionID"] },
payload: { id: input["id"], delivery: input["delivery"] },
}).pipe(
Effect.mapError(mapClientError),
Effect.map((value) => value.data),
),
@@ -523,7 +526,7 @@ const Endpoint5_22 = (raw: RawClient["server.session"]) => (input: Endpoint5_22I
const Endpoint5_23 = (raw: RawClient["server.session"]) => (input: Endpoint5_23Input) =>
preserveEffect<Endpoint5_23Output>()(
raw["session.pending.list"]({ params: { sessionID: input["sessionID"] } }).pipe(
raw["session.inbox.list"]({ params: { sessionID: input["sessionID"] } }).pipe(
Effect.mapError(mapClientError),
Effect.map((value) => value.data),
),
@@ -531,21 +534,21 @@ const Endpoint5_23 = (raw: RawClient["server.session"]) => (input: Endpoint5_23I
const Endpoint5_24 = (raw: RawClient["server.session"]) => (input: Endpoint5_24Input) =>
preserveEffect<Endpoint5_24Output>()(
raw["session.pending.cancel"]({ params: { sessionID: input["sessionID"], inputID: input["inputID"] } }).pipe(
raw["session.inbox.cancel"]({ params: { sessionID: input["sessionID"], inboxID: input["inboxID"] } }).pipe(
Effect.mapError(mapClientError),
),
)
const Endpoint5_25 = (raw: RawClient["server.session"]) => (input: Endpoint5_25Input) =>
preserveEffect<Endpoint5_25Output>()(
raw["session.pending.steer"]({ params: { sessionID: input["sessionID"], inputID: input["inputID"] } }).pipe(
raw["session.inbox.steer"]({ params: { sessionID: input["sessionID"], inboxID: input["inboxID"] } }).pipe(
Effect.mapError(mapClientError),
),
)
const Endpoint5_26 = (raw: RawClient["server.session"]) => (input: Endpoint5_26Input) =>
preserveEffect<Endpoint5_26Output>()(
raw["session.pending.queue"]({ params: { sessionID: input["sessionID"], inputID: input["inputID"] } }).pipe(
raw["session.inbox.queue"]({ params: { sessionID: input["sessionID"], inboxID: input["inboxID"] } }).pipe(
Effect.mapError(mapClientError),
),
)
@@ -637,7 +640,7 @@ const adaptGroup5 = (raw: RawClient["server.session"]) => ({
wait: Endpoint5_18(raw),
revert: { stage: Endpoint5_19(raw), clear: Endpoint5_20(raw), commit: Endpoint5_21(raw) },
context: Endpoint5_22(raw),
pending: { list: Endpoint5_23(raw), cancel: Endpoint5_24(raw), steer: Endpoint5_25(raw), queue: Endpoint5_26(raw) },
inbox: { list: Endpoint5_23(raw), cancel: Endpoint5_24(raw), steer: Endpoint5_25(raw), queue: Endpoint5_26(raw) },
instructions: { entry: { list: Endpoint5_27(raw), put: Endpoint5_28(raw), remove: Endpoint5_29(raw) } },
generate: Endpoint5_30(raw),
log: Endpoint5_31(raw),
@@ -873,19 +876,7 @@ const Endpoint13_1 = (raw: RawClient["server.project"]) => (input?: Endpoint13_1
raw["project.current"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)),
)
const Endpoint13_2 = (raw: RawClient["server.project"]) => (input: Endpoint13_2Input) =>
preserveEffect<Endpoint13_2Output>()(
raw["project.directories"]({
params: { projectID: input["projectID"] },
query: { location: input["location"] },
}).pipe(Effect.mapError(mapClientError)),
)
const adaptGroup13 = (raw: RawClient["server.project"]) => ({
list: Endpoint13_0(raw),
current: Endpoint13_1(raw),
directories: Endpoint13_2(raw),
})
const adaptGroup13 = (raw: RawClient["server.project"]) => ({ list: Endpoint13_0(raw), current: Endpoint13_1(raw) })
const Endpoint14_0 = (raw: RawClient["server.form"]) => (input?: Endpoint14_0Input) =>
preserveEffect<Endpoint14_0Output>()(
@@ -1209,36 +1200,37 @@ const Endpoint23_0 = (raw: RawClient["server.reference"]) => (input?: Endpoint23
const adaptGroup23 = (raw: RawClient["server.reference"]) => ({ list: Endpoint23_0(raw) })
const Endpoint24_0 = (raw: RawClient["server.projectCopy"]) => (input: Endpoint24_0Input) =>
const Endpoint24_0 = (raw: RawClient["server.worktree"]) => (input: Endpoint24_0Input) =>
preserveEffect<Endpoint24_0Output>()(
raw["projectCopy.create"]({
raw["worktree.list"]({ params: { projectID: input["projectID"] } }).pipe(Effect.mapError(mapClientError)),
)
const Endpoint24_1 = (raw: RawClient["server.worktree"]) => (input: Endpoint24_1Input) =>
preserveEffect<Endpoint24_1Output>()(
raw["worktree.create"]({
params: { projectID: input["projectID"] },
query: { location: input["location"] },
payload: { strategy: input["strategy"], directory: input["directory"], name: input["name"] },
}).pipe(Effect.mapError(mapClientError)),
)
const Endpoint24_1 = (raw: RawClient["server.projectCopy"]) => (input: Endpoint24_1Input) =>
preserveEffect<Endpoint24_1Output>()(
raw["projectCopy.remove"]({
const Endpoint24_2 = (raw: RawClient["server.worktree"]) => (input: Endpoint24_2Input) =>
preserveEffect<Endpoint24_2Output>()(
raw["worktree.remove"]({
params: { projectID: input["projectID"] },
query: { location: input["location"] },
payload: { directory: input["directory"], force: input["force"] },
}).pipe(Effect.mapError(mapClientError)),
)
const Endpoint24_2 = (raw: RawClient["server.projectCopy"]) => (input: Endpoint24_2Input) =>
preserveEffect<Endpoint24_2Output>()(
raw["projectCopy.refresh"]({
params: { projectID: input["projectID"] },
query: { location: input["location"] },
}).pipe(Effect.mapError(mapClientError)),
const Endpoint24_3 = (raw: RawClient["server.worktree"]) => (input: Endpoint24_3Input) =>
preserveEffect<Endpoint24_3Output>()(
raw["worktree.refresh"]({ params: { projectID: input["projectID"] } }).pipe(Effect.mapError(mapClientError)),
)
const adaptGroup24 = (raw: RawClient["server.projectCopy"]) => ({
create: Endpoint24_0(raw),
remove: Endpoint24_1(raw),
refresh: Endpoint24_2(raw),
const adaptGroup24 = (raw: RawClient["server.worktree"]) => ({
list: Endpoint24_0(raw),
create: Endpoint24_1(raw),
remove: Endpoint24_2(raw),
refresh: Endpoint24_3(raw),
})
const Endpoint25_0 = (raw: RawClient["server.vcs"]) => (input?: Endpoint25_0Input) =>
@@ -1331,7 +1323,7 @@ const adaptClient = (raw: RawClient) => ({
shell: adaptGroup21(raw["server.shell"]),
question: adaptGroup22(raw["server.question"]),
reference: adaptGroup23(raw["server.reference"]),
projectCopy: adaptGroup24(raw["server.projectCopy"]),
worktree: adaptGroup24(raw["server.worktree"]),
vcs: adaptGroup25(raw["server.vcs"]),
debug: adaptGroup26(raw["server.debug"]),
migration: adaptGroup27(raw["server.migration"]),
+2 -2
View File
@@ -33,7 +33,7 @@ export { Model } from "@opencode-ai/schema/model"
export { Permission } from "@opencode-ai/schema/permission"
export { PermissionSaved } from "@opencode-ai/schema/permission-saved"
export { Project } from "@opencode-ai/schema/project"
export { ProjectCopy } from "@opencode-ai/schema/project-copy"
export { Worktree } from "@opencode-ai/schema/worktree"
export { Provider } from "@opencode-ai/schema/provider"
export { Pty } from "@opencode-ai/schema/pty"
export { Question } from "@opencode-ai/schema/question"
@@ -41,7 +41,7 @@ export { Reference } from "@opencode-ai/schema/reference"
export { WebSearch } from "@opencode-ai/schema/websearch"
export { AbsolutePath, RelativePath } from "@opencode-ai/schema/schema"
export { Session } from "@opencode-ai/schema/session"
export { SessionPending } from "@opencode-ai/schema/session-pending"
export { SessionInbox } from "@opencode-ai/schema/session-inbox"
export { SessionMessage } from "@opencode-ai/schema/session-message"
export { Skill } from "@opencode-ai/schema/skill"
export { Prompt } from "@opencode-ai/schema/prompt"
+4 -3
View File
@@ -10,6 +10,7 @@ import {
spawnServiceContender,
} from "../service-contender.js"
import { defaultEnsureTiming, ensureTiming, type EnsureTiming } from "../service-timing.js"
import { matchesVersion } from "../service-version.js"
export * from "../service.js"
/** Contents of the local service registration file. */
@@ -37,14 +38,14 @@ export const incumbent = Effect.fn("service.incumbent")(function* (
const info = yield* read(options.file)
const found = info === undefined ? undefined : yield* probe({ ...info, url: options.url })
if (found === undefined || found.legacy) return undefined
if (options.version !== undefined && found.version !== options.version) return undefined
if (!matchesVersion(found.version, options)) return undefined
return { endpoint: found.endpoint, state: found.state }
})
const discoverLocal = Effect.fnUntraced(function* (options: DiscoverOptions) {
const found = (yield* registered(options.file)).service
if (found?.state !== "ready") return undefined
if (options.version !== undefined && found.version !== options.version) return undefined
if (!matchesVersion(found.version, options)) return undefined
return found
})
@@ -93,7 +94,7 @@ export const ensure = Effect.fn("service.ensure")(function* (options: EnsureOpti
} else timeouts = undefined
if (service !== undefined) {
spawnDelay = timing.spawnDelay
const compatible = !service.legacy && (options.version === undefined || service.version === options.version)
const compatible = !service.legacy && matchesVersion(service.version, options)
if (compatible && service.state === "ready") return Option.some(service)
if (compatible && service.state === "failed")
return yield* Effect.fail(new Error("Background service failed to start"))
+52 -56
View File
@@ -56,14 +56,14 @@ import type {
SessionRevertCommitOutput,
SessionContextInput,
SessionContextOutput,
SessionPendingListInput,
SessionPendingListOutput,
SessionPendingCancelInput,
SessionPendingCancelOutput,
SessionPendingSteerInput,
SessionPendingSteerOutput,
SessionPendingQueueInput,
SessionPendingQueueOutput,
SessionInboxListInput,
SessionInboxListOutput,
SessionInboxCancelInput,
SessionInboxCancelOutput,
SessionInboxSteerInput,
SessionInboxSteerOutput,
SessionInboxQueueInput,
SessionInboxQueueOutput,
SessionInstructionsEntryListInput,
SessionInstructionsEntryListOutput,
SessionInstructionsEntryPutInput,
@@ -133,8 +133,6 @@ import type {
ProjectListOutput,
ProjectCurrentInput,
ProjectCurrentOutput,
ProjectDirectoriesInput,
ProjectDirectoriesOutput,
FormRequestListInput,
FormRequestListOutput,
FormListInput,
@@ -206,12 +204,14 @@ import type {
QuestionRejectOutput,
ReferenceListInput,
ReferenceListOutput,
ProjectCopyCreateInput,
ProjectCopyCreateOutput,
ProjectCopyRemoveInput,
ProjectCopyRemoveOutput,
ProjectCopyRefreshInput,
ProjectCopyRefreshOutput,
WorktreeListInput,
WorktreeListOutput,
WorktreeCreateInput,
WorktreeCreateOutput,
WorktreeRemoveInput,
WorktreeRemoveOutput,
WorktreeRefreshInput,
WorktreeRefreshOutput,
VcsGetInput,
VcsGetOutput,
VcsStatusInput,
@@ -598,7 +598,7 @@ export function make(options: ClientOptions) {
{
method: "POST",
path: `/api/session/${encodeURIComponent(input.sessionID)}/move`,
body: { directory: input["directory"], workspaceID: input["workspaceID"] },
body: { directory: input["directory"], workspaceID: input["workspaceID"], delivery: input["delivery"] },
successStatus: 204,
declaredStatuses: [404, 400, 401],
empty: true,
@@ -697,7 +697,7 @@ export function make(options: ClientOptions) {
{
method: "POST",
path: `/api/session/${encodeURIComponent(input.sessionID)}/compact`,
body: { id: input["id"] },
body: { id: input["id"], delivery: input["delivery"] },
successStatus: 200,
declaredStatuses: [409, 404, 400, 401],
empty: false,
@@ -762,45 +762,45 @@ export function make(options: ClientOptions) {
},
requestOptions,
).then((value) => value.data),
pending: {
list: (input: SessionPendingListInput, requestOptions?: RequestOptions) =>
request<{ readonly data: SessionPendingListOutput }>(
inbox: {
list: (input: SessionInboxListInput, requestOptions?: RequestOptions) =>
request<{ readonly data: SessionInboxListOutput }>(
{
method: "GET",
path: `/api/session/${encodeURIComponent(input.sessionID)}/pending`,
path: `/api/session/${encodeURIComponent(input.sessionID)}/inbox`,
successStatus: 200,
declaredStatuses: [404, 401, 400],
empty: false,
},
requestOptions,
).then((value) => value.data),
cancel: (input: SessionPendingCancelInput, requestOptions?: RequestOptions) =>
request<SessionPendingCancelOutput>(
cancel: (input: SessionInboxCancelInput, requestOptions?: RequestOptions) =>
request<SessionInboxCancelOutput>(
{
method: "DELETE",
path: `/api/session/${encodeURIComponent(input.sessionID)}/pending/${encodeURIComponent(input.inputID)}`,
path: `/api/session/${encodeURIComponent(input.sessionID)}/inbox/${encodeURIComponent(input.inboxID)}`,
successStatus: 204,
declaredStatuses: [409, 404, 401, 400],
empty: true,
},
requestOptions,
),
steer: (input: SessionPendingSteerInput, requestOptions?: RequestOptions) =>
request<SessionPendingSteerOutput>(
steer: (input: SessionInboxSteerInput, requestOptions?: RequestOptions) =>
request<SessionInboxSteerOutput>(
{
method: "POST",
path: `/api/session/${encodeURIComponent(input.sessionID)}/pending/${encodeURIComponent(input.inputID)}/steer`,
path: `/api/session/${encodeURIComponent(input.sessionID)}/inbox/${encodeURIComponent(input.inboxID)}/steer`,
successStatus: 204,
declaredStatuses: [409, 404, 401, 400],
empty: true,
},
requestOptions,
),
queue: (input: SessionPendingQueueInput, requestOptions?: RequestOptions) =>
request<SessionPendingQueueOutput>(
queue: (input: SessionInboxQueueInput, requestOptions?: RequestOptions) =>
request<SessionInboxQueueOutput>(
{
method: "POST",
path: `/api/session/${encodeURIComponent(input.sessionID)}/pending/${encodeURIComponent(input.inputID)}/queue`,
path: `/api/session/${encodeURIComponent(input.sessionID)}/inbox/${encodeURIComponent(input.inboxID)}/queue`,
successStatus: 204,
declaredStatuses: [409, 404, 401, 400],
empty: true,
@@ -1255,18 +1255,6 @@ export function make(options: ClientOptions) {
},
requestOptions,
),
directories: (input: ProjectDirectoriesInput, requestOptions?: RequestOptions) =>
request<ProjectDirectoriesOutput>(
{
method: "GET",
path: `/api/project/${encodeURIComponent(input.projectID)}/directories`,
query: { location: input["location"] },
successStatus: 200,
declaredStatuses: [401, 400],
empty: false,
},
requestOptions,
),
},
form: {
request: {
@@ -1736,13 +1724,23 @@ export function make(options: ClientOptions) {
requestOptions,
),
},
projectCopy: {
create: (input: ProjectCopyCreateInput, requestOptions?: RequestOptions) =>
request<ProjectCopyCreateOutput>(
worktree: {
list: (input: WorktreeListInput, requestOptions?: RequestOptions) =>
request<WorktreeListOutput>(
{
method: "GET",
path: `/api/experimental/project/${encodeURIComponent(input.projectID)}/worktree`,
successStatus: 200,
declaredStatuses: [401, 400],
empty: false,
},
requestOptions,
),
create: (input: WorktreeCreateInput, requestOptions?: RequestOptions) =>
request<WorktreeCreateOutput>(
{
method: "POST",
path: `/api/experimental/project/${encodeURIComponent(input.projectID)}/copy`,
query: { location: input["location"] },
path: `/api/experimental/project/${encodeURIComponent(input.projectID)}/worktree`,
body: { strategy: input["strategy"], directory: input["directory"], name: input["name"] },
successStatus: 200,
declaredStatuses: [400, 401],
@@ -1750,12 +1748,11 @@ export function make(options: ClientOptions) {
},
requestOptions,
),
remove: (input: ProjectCopyRemoveInput, requestOptions?: RequestOptions) =>
request<ProjectCopyRemoveOutput>(
remove: (input: WorktreeRemoveInput, requestOptions?: RequestOptions) =>
request<WorktreeRemoveOutput>(
{
method: "DELETE",
path: `/api/experimental/project/${encodeURIComponent(input.projectID)}/copy`,
query: { location: input["location"] },
path: `/api/experimental/project/${encodeURIComponent(input.projectID)}/worktree`,
body: { directory: input["directory"], force: input["force"] },
successStatus: 204,
declaredStatuses: [400, 401],
@@ -1763,12 +1760,11 @@ export function make(options: ClientOptions) {
},
requestOptions,
),
refresh: (input: ProjectCopyRefreshInput, requestOptions?: RequestOptions) =>
request<ProjectCopyRefreshOutput>(
refresh: (input: WorktreeRefreshInput, requestOptions?: RequestOptions) =>
request<WorktreeRefreshOutput>(
{
method: "POST",
path: `/api/experimental/project/${encodeURIComponent(input.projectID)}/copy/refresh`,
query: { location: input["location"] },
path: `/api/experimental/project/${encodeURIComponent(input.projectID)}/worktree/refresh`,
successStatus: 204,
declaredStatuses: [400, 401],
empty: true,
+199 -219
View File
@@ -130,15 +130,17 @@ export type SessionMessageCompactionCompleted = {
export type SessionActive = { type: "running" }
export type SessionPendingSyntheticData = { text: string; description?: string; metadata?: { [x: string]: JsonValue } }
export type SessionInboxDelivery = "steer" | "queue"
export type SessionPendingCompaction = { id: string; sessionID: string; timeCreated: number; type: "compaction" }
export type SessionInboxSyntheticPayload = { text: string; description?: string; metadata?: { [x: string]: JsonValue } }
export type SessionInboxCompactionPayload = {}
export type InstructionEntryKey = string
export type SessionGenerateResponse = { data: { text: string } }
export type SessionPendingSyntheticData1 = { text: string; description?: string; metadata?: { [x: string]: any } }
export type SessionInboxSyntheticPayload1 = { text: string; description?: string; metadata?: { [x: string]: any } }
export type ShellInfo = {
id: string
@@ -289,8 +291,6 @@ export type ProjectTime = { created: number; updated: number; initialized?: numb
export type ProjectCurrent = { id: string; directory: string; canonical: string }
export type ProjectDirectory = { directory: string; strategy?: string }
export type FormMetadata = { [x: string]: JsonValue }
export type FormValue = string | number | boolean | Array<string>
@@ -368,7 +368,9 @@ export type ReferenceGitSource = {
hidden?: boolean
}
export type ProjectCopyCopy = { directory: string }
export type WorktreeDirectory = { directory: string; strategy?: string }
export type WorktreeInfo = { directory: string }
export type VcsBranch = { current?: string; default?: string }
@@ -420,7 +422,7 @@ export type SessionMessageLocationSwitched = {
previous?: { location: LocationRef; projectID?: string; subpath?: string }
}
export type SessionPendingMoveData = { location: LocationRef; projectID: string; subpath?: string }
export type SessionInboxMovePayload = { location: LocationRef; projectID: string; subpath?: string }
export type SessionCreated = {
id: string
@@ -470,7 +472,7 @@ export type SessionMoved = {
type: "session.moved"
durable: { aggregateID: string; seq: number; version: 1 }
location?: LocationRef
data: { sessionID: string; moveID?: string; location: LocationRef; projectID?: string; subpath?: string }
data: { sessionID: string; location: LocationRef; projectID: string; subpath?: string }
}
export type SessionRenamed = {
@@ -503,44 +505,24 @@ export type SessionForked = {
data: { sessionID: string; parentID: string; boundary: SessionForkBoundary; instructions?: { [x: string]: string } }
}
export type SessionInputPromoted = {
export type SessionInboxDelivered = {
id: string
created: number
metadata?: { [x: string]: any }
type: "session.input.promoted"
type: "session.inbox.delivered"
durable: { aggregateID: string; seq: number; version: 1 }
location?: LocationRef
data: { sessionID: string; inputID: string }
data: { sessionID: string; inboxID: string }
}
export type SessionInputCancelled = {
export type SessionInboxCancelled = {
id: string
created: number
metadata?: { [x: string]: any }
type: "session.input.cancelled"
type: "session.inbox.cancelled"
durable: { aggregateID: string; seq: number; version: 1 }
location?: LocationRef
data: { sessionID: string; inputID: string }
}
export type SessionInputSteered = {
id: string
created: number
metadata?: { [x: string]: any }
type: "session.input.steered"
durable: { aggregateID: string; seq: number; version: 1 }
location?: LocationRef
data: { sessionID: string; inputID: string }
}
export type SessionInputQueued = {
id: string
created: number
metadata?: { [x: string]: any }
type: "session.input.queued"
durable: { aggregateID: string; seq: number; version: 1 }
location?: LocationRef
data: { sessionID: string; inputID: string }
data: { sessionID: string; inboxID: string }
}
export type SessionExecutionStarted = {
@@ -661,16 +643,6 @@ export type SessionToolInputEnded = {
data: { sessionID: string; assistantMessageID: string; id: string; text: string }
}
export type SessionCompactionAdmitted = {
id: string
created: number
metadata?: { [x: string]: any }
type: "session.compaction.admitted"
durable: { aggregateID: string; seq: number; version: 1 }
location?: LocationRef
data: { sessionID: string; inputID: string }
}
export type SessionCompactionStarted = {
id: string
created: number
@@ -856,15 +828,25 @@ export type PluginUpdated = {
data: {}
}
export type ProjectDirectoriesUpdated = {
export type WorktreeUpdated = {
id: string
created: number
metadata?: { [x: string]: any }
type: "project.directories.updated"
type: "worktree.updated"
location?: LocationRef
data: { projectID: string }
}
export type WorktreeResolved = {
id: string
created: number
metadata?: { [x: string]: any }
type: "worktree.resolved"
durable: { aggregateID: string; seq: number; version: 1 }
location?: LocationRef
data: { projectID: string; directory: string; previous: string }
}
export type CommandUpdated = {
id: string
created: number
@@ -1164,23 +1146,36 @@ export type SessionCompactionFailed = {
data: { sessionID: string; reason: "auto" | "manual"; error: SessionStructuredError; inputID?: string }
}
export type SessionPendingSynthetic = {
export type SessionInboxDeliveryChanged = {
id: string
created: number
metadata?: { [x: string]: any }
type: "session.inbox.delivery.changed"
durable: { aggregateID: string; seq: number; version: 1 }
location?: LocationRef
data: { sessionID: string; inboxID: string; delivery: SessionInboxDelivery }
}
export type SessionInboxSynthetic = {
id: string
sessionID: string
timeCreated: number
type: "synthetic"
data: SessionPendingSyntheticData
delivery: "steer" | "queue"
payload: SessionInboxSyntheticPayload
delivery: SessionInboxDelivery
}
export type SessionInboxCompaction = {
id: string
sessionID: string
timeCreated: number
type: "compaction"
payload: SessionInboxCompactionPayload
delivery: SessionInboxDelivery
}
export type InstructionEntryInfo = { key: InstructionEntryKey; value: JsonValue }
export type SessionPendingSyntheticMessage = {
type: "synthetic"
data: SessionPendingSyntheticData1
delivery: "steer" | "queue"
}
export type SessionShellStarted = {
id: string
created: number
@@ -1374,8 +1369,6 @@ export type Project = {
sandboxes: Array<string>
}
export type ProjectDirectories = Array<ProjectDirectory>
export type FormAnswer = { [x: string]: FormValue }
export type PermissionRequest = {
@@ -1525,26 +1518,19 @@ export type SessionStatus2 = {
export type ReferenceSource = ReferenceLocalSource | ReferenceGitSource
export type WorktreeList = Array<WorktreeDirectory>
export type VcsInfo = { branch: VcsBranch }
export type PermissionRuleset = Array<PermissionRule>
export type SessionPendingMove = {
export type SessionInboxMove = {
id: string
sessionID: string
timeCreated: number
type: "move"
data: SessionPendingMoveData
}
export type SessionMoveAdmitted = {
id: string
created: number
metadata?: { [x: string]: any }
type: "session.move.admitted"
durable: { aggregateID: string; seq: number; version: 1 }
location?: LocationRef
data: { sessionID: string; move: SessionPendingMoveData }
payload: SessionInboxMovePayload
delivery: SessionInboxDelivery
}
export type SessionInfo = {
@@ -1584,7 +1570,7 @@ export type SessionMessageUser = {
type: "user"
}
export type SessionPendingUserData = {
export type SessionInboxUserPayload = {
text: string
files?: Array<PromptFileAttachment>
agents?: Array<PromptAgentAttachment>
@@ -1592,7 +1578,7 @@ export type SessionPendingUserData = {
metadata?: { [x: string]: JsonValue }
}
export type SessionPendingUserData1 = {
export type SessionInboxUserPayload1 = {
text: string
files?: Array<PromptFileAttachment>
agents?: Array<PromptAgentAttachment>
@@ -1904,16 +1890,20 @@ export type ConfigEntry =
export type SessionsResponse = { data: Array<SessionInfo>; cursor: { previous?: string | null; next?: string | null } }
export type SessionPendingUser = {
export type SessionInboxUser = {
id: string
sessionID: string
timeCreated: number
type: "user"
data: SessionPendingUserData
delivery: "steer" | "queue"
payload: SessionInboxUserPayload
delivery: SessionInboxDelivery
}
export type SessionPendingUserMessage = { type: "user"; data: SessionPendingUserData1; delivery: "steer" | "queue" }
export type SessionInboxItem =
| { type: "user"; payload: SessionInboxUserPayload1; delivery: SessionInboxDelivery }
| { type: "synthetic"; payload: SessionInboxSyntheticPayload1; delivery: SessionInboxDelivery }
| { type: "compaction"; payload: SessionInboxCompactionPayload; delivery: SessionInboxDelivery }
| { type: "move"; payload: SessionInboxMovePayload; delivery: SessionInboxDelivery }
export type SessionMessageAssistantTool = {
type: "tool"
@@ -1934,13 +1924,17 @@ export type FormFields = [FormField, ...Array<FormField>]
export type FormFields3 = [FormField1, ...Array<FormField1>]
export type SessionPendingInfo =
| SessionPendingUser
| SessionPendingSynthetic
| SessionPendingCompaction
| SessionPendingMove
export type SessionInboxInfo = SessionInboxUser | SessionInboxSynthetic | SessionInboxCompaction | SessionInboxMove
export type SessionPendingMessage = SessionPendingUserMessage | SessionPendingSyntheticMessage
export type SessionInboxEnqueued = {
id: string
created: number
metadata?: { [x: string]: any }
type: "session.inbox.enqueued"
durable: { aggregateID: string; seq: number; version: 1 }
location?: LocationRef
data: { sessionID: string; inboxID: string; item: SessionInboxItem }
}
export type SessionMessageAssistant = {
id: string
@@ -1966,15 +1960,47 @@ export type FormInfo = { id: string; sessionID: string; title: string; metadata?
export type FormInfo1 = { id: string; sessionID: string; title: string; metadata?: FormMetadata1; fields: FormFields3 }
export type SessionInputAdmitted = {
id: string
created: number
metadata?: { [x: string]: any }
type: "session.input.admitted"
durable: { aggregateID: string; seq: number; version: 1 }
location?: LocationRef
data: { sessionID: string; inputID: string; input: SessionPendingMessage }
}
export type SessionEventDurable =
| SessionCreated
| SessionAgentSelected
| SessionModelSelected
| SessionMoved
| SessionRenamed
| SessionDeleted
| SessionForked
| SessionInboxDelivered
| SessionInboxEnqueued
| SessionInboxCancelled
| SessionInboxDeliveryChanged
| SessionExecutionStarted
| SessionExecutionSucceeded
| SessionExecutionFailed
| SessionExecutionInterrupted
| SessionInstructionsUpdated
| SessionSynthetic
| SessionSkillActivated
| SessionShellStarted
| SessionShellEnded
| SessionStepStarted
| SessionStepEnded
| SessionStepFailed
| SessionTextStarted
| SessionTextEnded
| SessionReasoningStarted
| SessionReasoningEnded
| SessionToolInputStarted
| SessionToolInputEnded
| SessionToolCalled
| SessionToolSuccess
| SessionToolFailed
| SessionRetryScheduled
| SessionCompactionStarted
| SessionCompactionEnded
| SessionCompactionFailed
| SessionRevertStaged
| SessionRevertCleared
| SessionRevertCommitted
| SessionUsageRecorded
export type SessionMessageInfo =
| SessionMessageAgentSelected
@@ -2003,50 +2029,7 @@ export type FormCreated = {
data: { form: FormInfo1 }
}
export type SessionEventDurable =
| SessionCreated
| SessionAgentSelected
| SessionModelSelected
| SessionMoveAdmitted
| SessionMoved
| SessionRenamed
| SessionDeleted
| SessionForked
| SessionInputPromoted
| SessionInputAdmitted
| SessionInputCancelled
| SessionInputSteered
| SessionInputQueued
| SessionExecutionStarted
| SessionExecutionSucceeded
| SessionExecutionFailed
| SessionExecutionInterrupted
| SessionInstructionsUpdated
| SessionSynthetic
| SessionSkillActivated
| SessionShellStarted
| SessionShellEnded
| SessionStepStarted
| SessionStepEnded
| SessionStepFailed
| SessionTextStarted
| SessionTextEnded
| SessionReasoningStarted
| SessionReasoningEnded
| SessionToolInputStarted
| SessionToolInputEnded
| SessionToolCalled
| SessionToolSuccess
| SessionToolFailed
| SessionRetryScheduled
| SessionCompactionAdmitted
| SessionCompactionStarted
| SessionCompactionEnded
| SessionCompactionFailed
| SessionRevertStaged
| SessionRevertCleared
| SessionRevertCommitted
| SessionUsageRecorded
export type SessionLogItem = SessionEventDurable | EventLogSynced
export type SessionTransferData = { info: SessionInfo; messages: Array<SessionMessageInfo> }
@@ -2071,17 +2054,15 @@ export type V2Event =
| SessionCreated
| SessionAgentSelected
| SessionModelSelected
| SessionMoveAdmitted
| SessionMoved
| SessionRenamed
| SessionUsageUpdated
| SessionDeleted
| SessionForked
| SessionInputPromoted
| SessionInputAdmitted
| SessionInputCancelled
| SessionInputSteered
| SessionInputQueued
| SessionInboxDelivered
| SessionInboxEnqueued
| SessionInboxCancelled
| SessionInboxDeliveryChanged
| SessionExecutionStarted
| SessionExecutionSucceeded
| SessionExecutionFailed
@@ -2108,7 +2089,6 @@ export type V2Event =
| SessionToolSuccess
| SessionToolFailed
| SessionRetryScheduled
| SessionCompactionAdmitted
| SessionCompactionStarted
| SessionCompactionDelta
| SessionCompactionEnded
@@ -2122,7 +2102,8 @@ export type V2Event =
| PermissionReplied
| PluginAdded
| PluginUpdated
| ProjectDirectoriesUpdated
| WorktreeUpdated
| WorktreeResolved
| CommandUpdated
| ConfigUpdated
| SkillUpdated
@@ -2153,8 +2134,6 @@ export type V2Event =
| McpResourcesChanged
| V2EventServerConnected
export type SessionLogItem = SessionEventDurable | EventLogSynced
export type UnauthorizedError = { readonly _tag: "UnauthorizedError"; readonly message: string }
export const isUnauthorizedError = (value: unknown): value is UnauthorizedError =>
typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "UnauthorizedError"
@@ -2325,12 +2304,12 @@ export type QuestionNotFoundError = {
export const isQuestionNotFoundError = (value: unknown): value is QuestionNotFoundError =>
typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "QuestionNotFoundError"
export type ProjectCopyError = {
readonly name: "ProjectCopyError"
export type WorktreeError = {
readonly name: "WorktreeError"
readonly data: { readonly message: string; readonly forceRequired?: boolean | undefined }
}
export const isProjectCopyError = (value: unknown): value is ProjectCopyError =>
typeof value === "object" && value !== null && "name" in value && value["name"] === "ProjectCopyError"
export const isWorktreeError = (value: unknown): value is WorktreeError =>
typeof value === "object" && value !== null && "name" in value && value["name"] === "WorktreeError"
export type HealthGetOutput = ServiceHealth
@@ -3387,8 +3366,21 @@ export type SessionRenameOutput = void
export type SessionMoveInput = {
readonly sessionID: { readonly sessionID: string }["sessionID"]
readonly directory: { readonly directory: string; readonly workspaceID?: string }["directory"]
readonly workspaceID?: { readonly directory: string; readonly workspaceID?: string }["workspaceID"]
readonly directory: {
readonly directory: string
readonly workspaceID?: string
readonly delivery?: ("steer" | "queue") | null
}["directory"]
readonly workspaceID?: {
readonly directory: string
readonly workspaceID?: string
readonly delivery?: ("steer" | "queue") | null
}["workspaceID"]
readonly delivery?: {
readonly directory: string
readonly workspaceID?: string
readonly delivery?: ("steer" | "queue") | null
}["delivery"]
}
export type SessionMoveOutput = void
@@ -3413,7 +3405,7 @@ export type SessionPromptInput = {
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
}>
readonly metadata?: { readonly [x: string]: JsonValue }
readonly delivery?: "steer" | "queue" | null
readonly delivery?: ("steer" | "queue") | null
readonly resume?: boolean | null
}["id"]
readonly text: {
@@ -3434,7 +3426,7 @@ export type SessionPromptInput = {
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
}>
readonly metadata?: { readonly [x: string]: JsonValue }
readonly delivery?: "steer" | "queue" | null
readonly delivery?: ("steer" | "queue") | null
readonly resume?: boolean | null
}["text"]
readonly files?: {
@@ -3455,7 +3447,7 @@ export type SessionPromptInput = {
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
}>
readonly metadata?: { readonly [x: string]: JsonValue }
readonly delivery?: "steer" | "queue" | null
readonly delivery?: ("steer" | "queue") | null
readonly resume?: boolean | null
}["files"]
readonly agents?: {
@@ -3476,7 +3468,7 @@ export type SessionPromptInput = {
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
}>
readonly metadata?: { readonly [x: string]: JsonValue }
readonly delivery?: "steer" | "queue" | null
readonly delivery?: ("steer" | "queue") | null
readonly resume?: boolean | null
}["agents"]
readonly skills?: {
@@ -3497,7 +3489,7 @@ export type SessionPromptInput = {
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
}>
readonly metadata?: { readonly [x: string]: JsonValue }
readonly delivery?: "steer" | "queue" | null
readonly delivery?: ("steer" | "queue") | null
readonly resume?: boolean | null
}["skills"]
readonly metadata?: {
@@ -3518,7 +3510,7 @@ export type SessionPromptInput = {
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
}>
readonly metadata?: { readonly [x: string]: JsonValue }
readonly delivery?: "steer" | "queue" | null
readonly delivery?: ("steer" | "queue") | null
readonly resume?: boolean | null
}["metadata"]
readonly delivery?: {
@@ -3539,7 +3531,7 @@ export type SessionPromptInput = {
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
}>
readonly metadata?: { readonly [x: string]: JsonValue }
readonly delivery?: "steer" | "queue" | null
readonly delivery?: ("steer" | "queue") | null
readonly resume?: boolean | null
}["delivery"]
readonly resume?: {
@@ -3560,12 +3552,12 @@ export type SessionPromptInput = {
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
}>
readonly metadata?: { readonly [x: string]: JsonValue }
readonly delivery?: "steer" | "queue" | null
readonly delivery?: ("steer" | "queue") | null
readonly resume?: boolean | null
}["resume"]
}
export type SessionPromptOutput = { data: SessionPendingUser }["data"]
export type SessionPromptOutput = { data: SessionInboxUser }["data"]
export type SessionCommandInput = {
readonly sessionID: { readonly sessionID: string }["sessionID"]
@@ -3589,7 +3581,7 @@ export type SessionCommandInput = {
readonly id: string
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
}>
readonly delivery?: "steer" | "queue" | null
readonly delivery?: ("steer" | "queue") | null
readonly resume?: boolean | null
}["id"]
readonly command: {
@@ -3612,7 +3604,7 @@ export type SessionCommandInput = {
readonly id: string
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
}>
readonly delivery?: "steer" | "queue" | null
readonly delivery?: ("steer" | "queue") | null
readonly resume?: boolean | null
}["command"]
readonly arguments?: {
@@ -3635,7 +3627,7 @@ export type SessionCommandInput = {
readonly id: string
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
}>
readonly delivery?: "steer" | "queue" | null
readonly delivery?: ("steer" | "queue") | null
readonly resume?: boolean | null
}["arguments"]
readonly agent?: {
@@ -3658,7 +3650,7 @@ export type SessionCommandInput = {
readonly id: string
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
}>
readonly delivery?: "steer" | "queue" | null
readonly delivery?: ("steer" | "queue") | null
readonly resume?: boolean | null
}["agent"]
readonly model?: {
@@ -3681,7 +3673,7 @@ export type SessionCommandInput = {
readonly id: string
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
}>
readonly delivery?: "steer" | "queue" | null
readonly delivery?: ("steer" | "queue") | null
readonly resume?: boolean | null
}["model"]
readonly files?: {
@@ -3704,7 +3696,7 @@ export type SessionCommandInput = {
readonly id: string
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
}>
readonly delivery?: "steer" | "queue" | null
readonly delivery?: ("steer" | "queue") | null
readonly resume?: boolean | null
}["files"]
readonly agents?: {
@@ -3727,7 +3719,7 @@ export type SessionCommandInput = {
readonly id: string
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
}>
readonly delivery?: "steer" | "queue" | null
readonly delivery?: ("steer" | "queue") | null
readonly resume?: boolean | null
}["agents"]
readonly skills?: {
@@ -3750,7 +3742,7 @@ export type SessionCommandInput = {
readonly id: string
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
}>
readonly delivery?: "steer" | "queue" | null
readonly delivery?: ("steer" | "queue") | null
readonly resume?: boolean | null
}["skills"]
readonly delivery?: {
@@ -3773,7 +3765,7 @@ export type SessionCommandInput = {
readonly id: string
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
}>
readonly delivery?: "steer" | "queue" | null
readonly delivery?: ("steer" | "queue") | null
readonly resume?: boolean | null
}["delivery"]
readonly resume?: {
@@ -3796,12 +3788,12 @@ export type SessionCommandInput = {
readonly id: string
readonly mention?: { readonly start: number; readonly end: number; readonly text: string }
}>
readonly delivery?: "steer" | "queue" | null
readonly delivery?: ("steer" | "queue") | null
readonly resume?: boolean | null
}["resume"]
}
export type SessionCommandOutput = { data: SessionPendingUser }["data"]
export type SessionCommandOutput = { data: SessionInboxUser }["data"]
export type SessionSkillInput = {
readonly sessionID: { readonly sessionID: string }["sessionID"]
@@ -3831,7 +3823,7 @@ export type SessionSyntheticInput = {
readonly text: string
readonly description?: string | null
readonly metadata?: { readonly [x: string]: JsonValue }
readonly delivery?: "steer" | "queue" | null
readonly delivery?: ("steer" | "queue") | null
readonly resume?: boolean | null
}["id"]
readonly text: {
@@ -3839,7 +3831,7 @@ export type SessionSyntheticInput = {
readonly text: string
readonly description?: string | null
readonly metadata?: { readonly [x: string]: JsonValue }
readonly delivery?: "steer" | "queue" | null
readonly delivery?: ("steer" | "queue") | null
readonly resume?: boolean | null
}["text"]
readonly description?: {
@@ -3847,7 +3839,7 @@ export type SessionSyntheticInput = {
readonly text: string
readonly description?: string | null
readonly metadata?: { readonly [x: string]: JsonValue }
readonly delivery?: "steer" | "queue" | null
readonly delivery?: ("steer" | "queue") | null
readonly resume?: boolean | null
}["description"]
readonly metadata?: {
@@ -3855,7 +3847,7 @@ export type SessionSyntheticInput = {
readonly text: string
readonly description?: string | null
readonly metadata?: { readonly [x: string]: JsonValue }
readonly delivery?: "steer" | "queue" | null
readonly delivery?: ("steer" | "queue") | null
readonly resume?: boolean | null
}["metadata"]
readonly delivery?: {
@@ -3863,7 +3855,7 @@ export type SessionSyntheticInput = {
readonly text: string
readonly description?: string | null
readonly metadata?: { readonly [x: string]: JsonValue }
readonly delivery?: "steer" | "queue" | null
readonly delivery?: ("steer" | "queue") | null
readonly resume?: boolean | null
}["delivery"]
readonly resume?: {
@@ -3871,12 +3863,12 @@ export type SessionSyntheticInput = {
readonly text: string
readonly description?: string | null
readonly metadata?: { readonly [x: string]: JsonValue }
readonly delivery?: "steer" | "queue" | null
readonly delivery?: ("steer" | "queue") | null
readonly resume?: boolean | null
}["resume"]
}
export type SessionSyntheticOutput = { data: SessionPendingSynthetic }["data"]
export type SessionSyntheticOutput = { data: SessionInboxSynthetic }["data"]
export type SessionShellInput = {
readonly sessionID: { readonly sessionID: string }["sessionID"]
@@ -3888,10 +3880,14 @@ export type SessionShellOutput = void
export type SessionCompactInput = {
readonly sessionID: { readonly sessionID: string }["sessionID"]
readonly id?: { readonly id?: string | undefined }["id"]
readonly id?: { readonly id?: string | undefined; readonly delivery?: ("steer" | "queue") | undefined }["id"]
readonly delivery?: {
readonly id?: string | undefined
readonly delivery?: ("steer" | "queue") | undefined
}["delivery"]
}
export type SessionCompactOutput = { data: SessionPendingCompaction }["data"]
export type SessionCompactOutput = { data: SessionInboxCompaction }["data"]
export type SessionWaitInput = { readonly sessionID: { readonly sessionID: string }["sessionID"] }
@@ -3917,30 +3913,30 @@ export type SessionContextInput = { readonly sessionID: { readonly sessionID: st
export type SessionContextOutput = { data: Array<SessionMessageInfo> }["data"]
export type SessionPendingListInput = { readonly sessionID: { readonly sessionID: string }["sessionID"] }
export type SessionInboxListInput = { readonly sessionID: { readonly sessionID: string }["sessionID"] }
export type SessionPendingListOutput = { data: Array<SessionPendingInfo> }["data"]
export type SessionInboxListOutput = { data: Array<SessionInboxInfo> }["data"]
export type SessionPendingCancelInput = {
readonly sessionID: { readonly sessionID: string; readonly inputID: string }["sessionID"]
readonly inputID: { readonly sessionID: string; readonly inputID: string }["inputID"]
export type SessionInboxCancelInput = {
readonly sessionID: { readonly sessionID: string; readonly inboxID: string }["sessionID"]
readonly inboxID: { readonly sessionID: string; readonly inboxID: string }["inboxID"]
}
export type SessionPendingCancelOutput = void
export type SessionInboxCancelOutput = void
export type SessionPendingSteerInput = {
readonly sessionID: { readonly sessionID: string; readonly inputID: string }["sessionID"]
readonly inputID: { readonly sessionID: string; readonly inputID: string }["inputID"]
export type SessionInboxSteerInput = {
readonly sessionID: { readonly sessionID: string; readonly inboxID: string }["sessionID"]
readonly inboxID: { readonly sessionID: string; readonly inboxID: string }["inboxID"]
}
export type SessionPendingSteerOutput = void
export type SessionInboxSteerOutput = void
export type SessionPendingQueueInput = {
readonly sessionID: { readonly sessionID: string; readonly inputID: string }["sessionID"]
readonly inputID: { readonly sessionID: string; readonly inputID: string }["inputID"]
export type SessionInboxQueueInput = {
readonly sessionID: { readonly sessionID: string; readonly inboxID: string }["sessionID"]
readonly inboxID: { readonly sessionID: string; readonly inboxID: string }["inboxID"]
}
export type SessionPendingQueueOutput = void
export type SessionInboxQueueOutput = void
export type SessionInstructionsEntryListInput = { readonly sessionID: { readonly sessionID: string }["sessionID"] }
@@ -4352,15 +4348,6 @@ export type ProjectCurrentInput = {
export type ProjectCurrentOutput = ProjectCurrent
export type ProjectDirectoriesInput = {
readonly projectID: { readonly projectID: string }["projectID"]
readonly location?: {
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
}
export type ProjectDirectoriesOutput = ProjectDirectories
export type FormRequestListInput = {
readonly location?: {
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
@@ -5646,37 +5633,30 @@ export type ReferenceListOutput = {
data: Array<ReferenceInfo>
}
export type ProjectCopyCreateInput = {
export type WorktreeListInput = { readonly projectID: { readonly projectID: string }["projectID"] }
export type WorktreeListOutput = WorktreeList
export type WorktreeCreateInput = {
readonly projectID: { readonly projectID: string }["projectID"]
readonly location?: {
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
readonly strategy: { readonly strategy: string; readonly directory: string; readonly name?: string }["strategy"]
readonly directory: { readonly strategy: string; readonly directory: string; readonly name?: string }["directory"]
readonly name?: { readonly strategy: string; readonly directory: string; readonly name?: string }["name"]
}
export type ProjectCopyCreateOutput = ProjectCopyCopy
export type WorktreeCreateOutput = WorktreeInfo
export type ProjectCopyRemoveInput = {
export type WorktreeRemoveInput = {
readonly projectID: { readonly projectID: string }["projectID"]
readonly location?: {
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
readonly directory: { readonly directory: string; readonly force: boolean }["directory"]
readonly force: { readonly directory: string; readonly force: boolean }["force"]
}
export type ProjectCopyRemoveOutput = void
export type WorktreeRemoveOutput = void
export type ProjectCopyRefreshInput = {
readonly projectID: { readonly projectID: string }["projectID"]
readonly location?: {
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
}
export type WorktreeRefreshInput = { readonly projectID: { readonly projectID: string }["projectID"] }
export type ProjectCopyRefreshOutput = void
export type WorktreeRefreshOutput = void
export type VcsGetInput = {
readonly location?: {
+3 -2
View File
@@ -9,6 +9,7 @@ import {
spawnServiceContender,
} from "../service-contender.js"
import { defaultEnsureTiming, ensureTiming, type EnsureTiming } from "../service-timing.js"
import { matchesVersion } from "../service-version.js"
import type { ServiceHealth, ServiceStopResponse } from "./generated/types.js"
export * from "../service.js"
@@ -27,7 +28,7 @@ export async function discover(options: DiscoverOptions = {}) {
async function discoverLocal(options: DiscoverOptions) {
const found = (await registered(options.file)).service
if (found?.state !== "ready") return undefined
if (options.version !== undefined && found.version !== options.version) return undefined
if (!matchesVersion(found.version, options)) return undefined
return found
}
@@ -76,7 +77,7 @@ export async function ensure(options: EnsureOptions = {}): Promise<Endpoint> {
if (registration.service !== undefined) {
spawnDelay = timing.spawnDelay
const service = registration.service
const compatible = !service.legacy && (options.version === undefined || service.version === options.version)
const compatible = !service.legacy && matchesVersion(service.version, options)
if (compatible && service.state === "ready") return service.endpoint
if (compatible && service.state === "failed") throw new Error("Background service failed to start")
if (!compatible) {
+8
View File
@@ -0,0 +1,8 @@
import type { DiscoverOptions } from "./service.js"
export function matchesVersion(version: string | undefined, options: DiscoverOptions) {
if (options.version === undefined) return true
if (version === undefined) return false
if (typeof options.version === "function") return options.version(version)
return version === options.version
}
+2 -2
View File
@@ -17,8 +17,8 @@ export type Endpoint = {
export type DiscoverOptions = {
/** Absolute registration file path. Defaults to the XDG state directory. */
readonly file?: string
/** Required service version. */
readonly version?: string
/** Required exact service version or compatibility predicate. */
readonly version?: string | ((version: string) => boolean)
}
/** Reason ensuring the service requires a new process. */
+15 -1
View File
@@ -1,6 +1,7 @@
import { Effect } from "effect"
import { OpenCode as EffectOpenCode, type AppApi as EffectApi } from "../src/effect"
import type { Session } from "@opencode-ai/schema/session"
import type { DiscoverOptions } from "../src/service"
type EffectClient = Effect.Success<ReturnType<typeof EffectOpenCode.make>>
type PromiseClient = ReturnType<typeof import("../src/promise").OpenCode.make>
@@ -8,6 +9,9 @@ type PromiseClient = ReturnType<typeof import("../src/promise").OpenCode.make>
declare const effectClient: EffectClient
declare const promiseClient: PromiseClient
const exactVersion: DiscoverOptions = { version: "2.0.0" }
const compatibleVersion: DiscoverOptions = { version: (version) => version.startsWith("2.") }
const effectApi: EffectApi<unknown> = effectClient
const effectSession: Effect.Effect<Session.Info, unknown> = effectClient.session.get({
@@ -42,4 +46,14 @@ const promiseRemove: Promise<void> = promiseClient.session.instructions.entry.re
key: "review-notes",
})
void [effectSession, effectList, effectPut, effectRemove, promiseList, promisePut, promiseRemove]
void [
effectSession,
effectList,
effectPut,
effectRemove,
promiseList,
promisePut,
promiseRemove,
exactVersion,
compatibleVersion,
]
+4 -2
View File
@@ -213,7 +213,7 @@ test("session methods retain decoded Effect inputs and outputs", async () => {
expect(Object.getPrototypeOf(result.created)).toBe(Object.prototype)
expect(result.created.id).toBe("ses_test")
expect(Object.getPrototypeOf(result.admitted)).toBe(Object.prototype)
expect(Object.getPrototypeOf(result.admitted.data)).toBe(Object.prototype)
expect(Object.getPrototypeOf(result.admitted.payload)).toBe(Object.prototype)
expect(DateTime.toEpochMillis(result.admitted.timeCreated)).toBe(1_717_171_717_000)
expect(result.context).toEqual([])
expect(logQueries[0]).toEqual({ after: "0" })
@@ -271,7 +271,7 @@ const admission = {
id: "msg_test",
sessionID: "ses_test",
type: "user",
data: { text: "Hello" },
payload: { text: "Hello" },
delivery: "steer",
timeCreated: 1_717_171_717_000,
},
@@ -280,6 +280,8 @@ const admission = {
const compactionAdmission = {
data: {
type: "compaction",
payload: {},
delivery: "queue",
id: "msg_compaction",
sessionID: "ses_test",
timeCreated: 1_717_171_717_000,
+4 -1
View File
@@ -27,7 +27,10 @@ if (mode === "delayed" || mode === "delayed-failed" || mode === "coordinated" ||
}
let requests = 0
const version = mode === "old" || mode === "reject-stop" ? "old" : "test"
let version = "test"
if (mode === "old" || mode === "reject-stop") version = "old"
if (mode === "incompatible") version = "1.9.0"
if (mode === "compatible" || mode === "delayed-compatible") version = "2.1.0-next.1"
const id = crypto.randomUUID()
const server = Bun.serve({
port: 0,
@@ -25,6 +25,19 @@ test("discovers a registered service", async () => {
expect(await Service.discover({ file: registration, version: "other" })).toBeUndefined()
})
test("discovers a compatible registered service", async () => {
const registration = await setup("compatible")
expect(await Service.discover({ file: registration, version: "2.1.0" })).toBeUndefined()
expect(await Service.discover({ file: registration, version: "2.1.0-next.1" })).toEqual(
expect.objectContaining({ url: expect.stringMatching(/^http:\/\//) }),
)
expect(await Service.discover({ file: registration, version: (version) => version.startsWith("2.") })).toEqual(
expect.objectContaining({ url: expect.stringMatching(/^http:\/\//) }),
)
expect(await Service.discover({ file: registration, version: (version) => version.startsWith("3.") })).toBeUndefined()
})
test("ensures a missing service with native promises", async () => {
const directory = await temp()
const registration = join(directory, "service.json")
+44 -29
View File
@@ -29,7 +29,7 @@ test("exposes every standard HTTP API group", () => {
"shell",
"question",
"reference",
"projectCopy",
"worktree",
"vcs",
"debug",
"migration",
@@ -49,7 +49,8 @@ test("exposes every standard HTTP API group", () => {
expect(Object.keys(client.vcs)).toEqual(["get", "status", "diff"])
expect(Object.keys(client.pty)).toEqual(["list", "create", "get", "update", "remove"])
expect(Object.keys(client.shell)).toEqual(["list", "create", "get", "timeout", "output", "remove"])
expect(Object.keys(client.project)).toEqual(["list", "current", "directories"])
expect(Object.keys(client.project)).toEqual(["list", "current"])
expect(Object.keys(client.worktree)).toEqual(["list", "create", "remove", "refresh"])
})
test("config.get returns ordered config entries for a location", async () => {
@@ -251,30 +252,44 @@ test("file.read returns binary content from the public HTTP contract", async ()
)
})
test("project methods use the public HTTP contract", async () => {
const requests: string[] = []
test("worktree methods use the global project contract", async () => {
const requests: Request[] = []
const client = OpenCode.make({
baseUrl: "http://localhost:3000",
fetch: async (input) => {
const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url
requests.push(url)
if (url.includes("/directories")) return Response.json([])
return Response.json({ id: "proj_test", directory: "/tmp/project" })
fetch: async (input, init) => {
const request = input instanceof Request ? input : new Request(input, init)
requests.push(request)
if (request.method === "GET") return Response.json([{ directory: "/tmp/project" }])
if (request.method === "POST" && !request.url.endsWith("/refresh"))
return Response.json({ directory: "/tmp/worktrees/api" })
return new Response(null, { status: 204 })
},
})
const current = await client.project.current({ location: { workspace: "wrk_test" } })
const directories = await client.project.directories({
projectID: current.id,
location: { directory: current.directory },
expect(await client.worktree.list({ projectID: "proj_test" })).toEqual([{ directory: "/tmp/project" }])
expect(
await client.worktree.create({
projectID: "proj_test",
strategy: "git",
directory: "/tmp/worktrees",
name: "api",
}),
).toEqual({ directory: "/tmp/worktrees/api" })
await client.worktree.remove({
projectID: "proj_test",
directory: "/tmp/worktrees/api",
force: false,
})
await client.worktree.refresh({ projectID: "proj_test" })
expect(current).toEqual({ id: "proj_test", directory: "/tmp/project" })
expect(directories).toEqual([])
expect(requests).toEqual([
"http://localhost:3000/api/project/current?location%5Bworkspace%5D=wrk_test",
"http://localhost:3000/api/project/proj_test/directories?location%5Bdirectory%5D=%2Ftmp%2Fproject",
expect(requests.map((request) => [request.method, request.url])).toEqual([
["GET", "http://localhost:3000/api/experimental/project/proj_test/worktree"],
["POST", "http://localhost:3000/api/experimental/project/proj_test/worktree"],
["DELETE", "http://localhost:3000/api/experimental/project/proj_test/worktree"],
["POST", "http://localhost:3000/api/experimental/project/proj_test/worktree/refresh"],
])
expect(await requests[1]?.json()).toEqual({ strategy: "git", directory: "/tmp/worktrees", name: "api" })
expect(await requests[2]?.json()).toEqual({ directory: "/tmp/worktrees/api", force: false })
})
test("shell list and remove use the public HTTP contract", async () => {
@@ -373,7 +388,7 @@ test("session instructions methods use the public HTTP contract", async () => {
])
})
test("session.pending.list uses the public HTTP contract", async () => {
test("session.inbox.list uses the public HTTP contract", async () => {
const requests: Array<{ method: string; url: string }> = []
const pending = [
{
@@ -381,7 +396,7 @@ test("session.pending.list uses the public HTTP contract", async () => {
sessionID: "ses_test",
timeCreated: 1_717_171_717_000,
type: "user",
data: { text: "Fix the failing tests" },
payload: { text: "Fix the failing tests" },
delivery: "steer",
},
]
@@ -394,13 +409,13 @@ test("session.pending.list uses the public HTTP contract", async () => {
},
})
const result = await client.session.pending.list({ sessionID: "ses_test" })
const result = await client.session.inbox.list({ sessionID: "ses_test" })
expect(result).toEqual(pending)
expect(requests).toEqual([{ method: "GET", url: "http://localhost:3000/api/session/ses_test/pending" }])
expect(requests).toEqual([{ method: "GET", url: "http://localhost:3000/api/session/ses_test/inbox" }])
})
test("session.pending mutations use the public HTTP contract", async () => {
test("session.inbox mutations use the public HTTP contract", async () => {
const requests: Array<{ method: string; url: string }> = []
const client = OpenCode.make({
baseUrl: "http://localhost:3000",
@@ -411,14 +426,14 @@ test("session.pending mutations use the public HTTP contract", async () => {
},
})
await client.session.pending.cancel({ sessionID: "ses_test", inputID: "msg_cancel" })
await client.session.pending.steer({ sessionID: "ses_test", inputID: "msg_steer" })
await client.session.pending.queue({ sessionID: "ses_test", inputID: "msg_queue" })
await client.session.inbox.cancel({ sessionID: "ses_test", inboxID: "msg_cancel" })
await client.session.inbox.steer({ sessionID: "ses_test", inboxID: "msg_steer" })
await client.session.inbox.queue({ sessionID: "ses_test", inboxID: "msg_queue" })
expect(requests).toEqual([
{ method: "DELETE", url: "http://localhost:3000/api/session/ses_test/pending/msg_cancel" },
{ method: "POST", url: "http://localhost:3000/api/session/ses_test/pending/msg_steer/steer" },
{ method: "POST", url: "http://localhost:3000/api/session/ses_test/pending/msg_queue/queue" },
{ method: "DELETE", url: "http://localhost:3000/api/session/ses_test/inbox/msg_cancel" },
{ method: "POST", url: "http://localhost:3000/api/session/ses_test/inbox/msg_steer/steer" },
{ method: "POST", url: "http://localhost:3000/api/session/ses_test/inbox/msg_queue/queue" },
])
})
+46
View File
@@ -47,6 +47,52 @@ test("a concurrent same-version start cannot invalidate a resolved endpoint", as
expect(await health(resolved.url)).toEqual({ healthy: true, version: "test", pid: original.pid })
})
test("reuses a compatible registered service", async () => {
const directory = await temp()
const registration = join(directory, "service.json")
const existing = spawn(registration, "compatible")
await waitForFile(registration)
const starts: EnsureReason[] = []
const endpoint = await run(
ensure({
file: registration,
version: (version) => version.startsWith("2."),
command: [],
onStart: (reason) => starts.push(reason),
}),
)
expect(endpoint.url).toBe((await Bun.file(registration).json()).url)
expect(starts).toEqual([])
expect(existing.exitCode).toBe(null)
})
test("replaces an incompatible registered service", async () => {
const directory = await temp()
const registration = join(directory, "service.json")
const existing = spawn(registration, "incompatible")
await waitForFile(registration)
const starts: EnsureReason[] = []
const endpoint = await run(
ensure({
file: registration,
version: (version) => version.startsWith("2."),
command: [process.execPath, fixture, registration, "delayed-compatible", "10"],
onStart: (reason) => starts.push(reason),
}),
)
const replacement = await Bun.file(registration).json()
expect(await existing.exited).toBe(0)
expect(replacement.version).toBe("2.1.0-next.1")
expect(endpoint.url).toBe(replacement.url)
expect(starts).toEqual(["version-mismatch"])
process.kill(replacement.pid, "SIGTERM")
await waitForExit(replacement.pid)
})
test("waits for a registered service to finish starting", async () => {
const directory = await temp()
const registration = join(directory, "service.json")
+6
View File
@@ -19,6 +19,12 @@
"exports": {
".": "./src/index.ts"
},
"imports": {
"#transpile": {
"workerd": "./src/interpreter/transpile.workerd.ts",
"default": "./src/interpreter/transpile.node.ts"
}
},
"scripts": {
"build": "bun run script/build.ts",
"typecheck": "tsgo --noEmit",
+6 -15
View File
@@ -1,6 +1,8 @@
import { parse } from "acorn"
import { Cause, Effect, Scope } from "effect"
import { DiagnosticCategory, ModuleKind, ScriptTarget, flattenDiagnosticMessageText, transpileModule } from "typescript"
// #transpile: conditional import — full typescript on node/bun, an identity
// pass-through on workerd (the compiler is ~11 MiB and can't init there).
import { transpile } from "#transpile"
import type { DataValue, Diagnostic, ExecuteOptions, ResolvedExecutionLimits, Result } from "../codemode.js"
import { copyIn, copyOut, ToolRuntime, type Services } from "../tool-runtime.js"
import type { Tools } from "../tools.js"
@@ -119,21 +121,10 @@ export const executeWithLimits = <const Provided extends Record<string, unknown>
}
const parseProgram = (code: string): ProgramNode => {
const transpiled = transpileModule(`async function __codemode__() {\n${code}\n}`, {
reportDiagnostics: true,
compilerOptions: {
target: ScriptTarget.ESNext,
module: ModuleKind.ESNext,
},
})
const diagnostic = transpiled.diagnostics?.find((item) => item.category === DiagnosticCategory.Error)
const transpiled = transpile(`async function __codemode__() {\n${code}\n}`)
if (diagnostic) {
throw new InterpreterRuntimeError(
`Failed to parse TypeScript: ${flattenDiagnosticMessageText(diagnostic.messageText, "\n")}`,
undefined,
"ParseError",
)
if (transpiled.error !== undefined) {
throw new InterpreterRuntimeError(`Failed to parse TypeScript: ${transpiled.error}`, undefined, "ParseError")
}
const bodyStart = transpiled.outputText.indexOf("{") + 1
@@ -0,0 +1,25 @@
import { DiagnosticCategory, ModuleKind, ScriptTarget, flattenDiagnosticMessageText, transpileModule } from "typescript"
export interface TranspileResult {
readonly outputText: string
readonly error?: string
}
// Full TypeScript transpilation on node/bun runtimes.
export const transpile = (source: string): TranspileResult => {
const transpiled = transpileModule(source, {
reportDiagnostics: true,
compilerOptions: {
target: ScriptTarget.ESNext,
module: ModuleKind.ESNext,
},
})
const diagnostic = transpiled.diagnostics?.find((item) => item.category === DiagnosticCategory.Error)
if (diagnostic) {
return {
outputText: transpiled.outputText,
error: flattenDiagnosticMessageText(diagnostic.messageText, "\n"),
}
}
return { outputText: transpiled.outputText }
}
@@ -0,0 +1,11 @@
export interface TranspileResult {
readonly outputText: string
readonly error?: string
}
// workerd profile: the typescript compiler is ~11 MiB and probes node
// internals at module init, so codemode programs are passed through
// untranspiled. Plain-JS programs (the overwhelmingly common case) parse
// fine downstream via acorn; TypeScript-only syntax surfaces as a parse
// error from the interpreter instead of a transpile diagnostic.
export const transpile = (source: string): TranspileResult => ({ outputText: source })
+3
View File
@@ -0,0 +1,3 @@
# @opencode-ai/core
Core runtime services for OpenCode.
+26 -13
View File
@@ -4,24 +4,28 @@
"name": "@opencode-ai/core",
"type": "module",
"license": "MIT",
"private": true,
"repository": {
"type": "git",
"url": "git+https://github.com/anomalyco/opencode.git",
"directory": "packages/core"
},
"publishConfig": {
"access": "public"
},
"files": [
"dist"
],
"scripts": {
"db": "bun drizzle-kit",
"migration": "bun run script/migration.ts",
"fix-node-pty": "bun run script/fix-node-pty.ts",
"benchmark:location": "bun run script/benchmark-location.ts",
"build": "bun run script/build.ts",
"update-models-snapshot": "bun run script/update-models-snapshot.ts",
"test": "bun test --only-failures",
"typecheck": "tsgo -b tsconfig.json tsconfig.tests.json"
},
"bin": {
"opencode": "./bin/opencode"
},
"exports": {
"./environment": "./src/environment/index.ts",
"./testing/environment-conformance": "./test/lib/environment-conformance.ts",
"./session/runner": "./src/session/runner/index.ts",
"./instructions": "./src/instructions/index.ts",
"./*": "./src/*.ts"
},
"imports": {
@@ -32,32 +36,46 @@
"default": "./src/database/sqlite.node.ts"
},
"#pty": {
"workerd": "./src/pty/pty.workerd.ts",
"bun": "./src/pty/pty.bun.ts",
"node": "./src/pty/pty.node.ts",
"default": "./src/pty/pty.bun.ts"
},
"#fff": {
"workerd": "./src/filesystem/fff.workerd.ts",
"bun": "./src/filesystem/fff.bun.ts",
"node": "./src/filesystem/fff.node.ts",
"default": "./src/filesystem/fff.bun.ts"
},
"#photon-wasm": {
"workerd": "./src/image/photon-wasm.workerd.ts",
"bun": "./src/image/photon-wasm.bun.ts",
"node": "./src/image/photon-wasm.node.ts",
"default": "./src/image/photon-wasm.bun.ts"
},
"#shell-parser-wasm": {
"workerd": "./src/shell/parser-wasm.workerd.ts",
"bun": "./src/shell/parser-wasm.bun.ts",
"node": "./src/shell/parser-wasm.node.ts",
"default": "./src/shell/parser-wasm.bun.ts"
},
"#process-lock-ffi": {
"workerd": "./src/util/process-lock-ffi.workerd.ts",
"bun": "./src/util/process-lock-ffi.bun.ts",
"node": "./src/util/process-lock-ffi.node.ts",
"default": "./src/util/process-lock-ffi.bun.ts"
},
"#v1-migration": {
"types": "./src/database/v1-migration.bun.ts",
"bun": "./src/database/v1-migration.bun.ts",
"node": "./src/database/v1-migration.noop.ts",
"workerd": "./src/database/v1-migration.noop.ts",
"default": "./src/database/v1-migration.noop.ts"
}
},
"devDependencies": {
"@effect/platform-node": "catalog:",
"@effect/sql-sqlite-bun": "catalog:",
"@tsconfig/bun": "catalog:",
"@types/bun": "catalog:",
"@types/node": "catalog:",
@@ -93,8 +111,6 @@
"@ai-sdk/togetherai": "2.0.41",
"@ai-sdk/vercel": "2.0.39",
"@aws-sdk/credential-providers": "3.1057.0",
"@effect/platform-node": "catalog:",
"@effect/sql-sqlite-bun": "catalog:",
"@lydell/node-pty": "catalog:",
"@modelcontextprotocol/sdk": "1.29.0",
"@ff-labs/fff-bun": "0.10.1",
@@ -128,8 +144,5 @@
"web-tree-sitter": "0.25.10",
"which": "6.0.1",
"zod": "catalog:"
},
"overrides": {
"drizzle-orm": "catalog:"
}
}
+320 -38
View File
@@ -1,8 +1,10 @@
{
"version": "7",
"dialect": "sqlite",
"id": "00924d88-1842-4d71-ac74-5682ddc47e1c",
"prevIds": ["15060ec5-05f7-4b86-b2a5-9108609432b3"],
"id": "dcde8e6b-4bf4-4f6b-b2be-4030c2c3e936",
"prevIds": [
"5c1aa56b-c3ee-4283-9a84-c0bf626dc604"
],
"ddl": [
{
"name": "account_state",
@@ -56,6 +58,10 @@
"name": "instruction_state",
"entityType": "tables"
},
{
"name": "session_inbox",
"entityType": "tables"
},
{
"name": "session_message",
"entityType": "tables"
@@ -72,6 +78,10 @@
"name": "workspace",
"entityType": "tables"
},
{
"name": "worktree",
"entityType": "tables"
},
{
"type": "integer",
"notNull": false,
@@ -842,6 +852,76 @@
"entityType": "columns",
"table": "instruction_state"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "id",
"entityType": "columns",
"table": "session_inbox"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "session_id",
"entityType": "columns",
"table": "session_inbox"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "type",
"entityType": "columns",
"table": "session_inbox"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "payload",
"entityType": "columns",
"table": "session_inbox"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "delivery",
"entityType": "columns",
"table": "session_inbox"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "enqueued_seq",
"entityType": "columns",
"table": "session_inbox"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_created",
"entityType": "columns",
"table": "session_inbox"
},
{
"type": "text",
"notNull": false,
@@ -1363,9 +1443,53 @@
"table": "workspace"
},
{
"columns": ["active_account_id"],
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "project_id",
"entityType": "columns",
"table": "worktree"
},
{
"type": "text",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "directory",
"entityType": "columns",
"table": "worktree"
},
{
"type": "text",
"notNull": false,
"autoincrement": false,
"default": null,
"generated": null,
"name": "strategy",
"entityType": "columns",
"table": "worktree"
},
{
"type": "integer",
"notNull": true,
"autoincrement": false,
"default": null,
"generated": null,
"name": "time_created",
"entityType": "columns",
"table": "worktree"
},
{
"columns": [
"active_account_id"
],
"tableTo": "account",
"columnsTo": ["id"],
"columnsTo": [
"id"
],
"onUpdate": "NO ACTION",
"onDelete": "SET NULL",
"nameExplicit": false,
@@ -1374,9 +1498,13 @@
"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,
@@ -1385,9 +1513,13 @@
"table": "event"
},
{
"columns": ["project_id"],
"columns": [
"project_id"
],
"tableTo": "project",
"columnsTo": ["id"],
"columnsTo": [
"id"
],
"onUpdate": "NO ACTION",
"onDelete": "CASCADE",
"nameExplicit": false,
@@ -1396,9 +1528,13 @@
"table": "permission"
},
{
"columns": ["project_id"],
"columns": [
"project_id"
],
"tableTo": "project",
"columnsTo": ["id"],
"columnsTo": [
"id"
],
"onUpdate": "NO ACTION",
"onDelete": "CASCADE",
"nameExplicit": false,
@@ -1407,9 +1543,13 @@
"table": "project_directory"
},
{
"columns": ["session_id"],
"columns": [
"session_id"
],
"tableTo": "session_v2",
"columnsTo": ["id"],
"columnsTo": [
"id"
],
"onUpdate": "NO ACTION",
"onDelete": "CASCADE",
"nameExplicit": false,
@@ -1418,9 +1558,13 @@
"table": "instruction_entry"
},
{
"columns": ["session_id"],
"columns": [
"session_id"
],
"tableTo": "session_v2",
"columnsTo": ["id"],
"columnsTo": [
"id"
],
"onUpdate": "NO ACTION",
"onDelete": "CASCADE",
"nameExplicit": false,
@@ -1429,9 +1573,28 @@
"table": "instruction_state"
},
{
"columns": ["session_id"],
"columns": [
"session_id"
],
"tableTo": "session_v2",
"columnsTo": ["id"],
"columnsTo": [
"id"
],
"onUpdate": "NO ACTION",
"onDelete": "CASCADE",
"nameExplicit": false,
"name": "fk_session_inbox_session_id_session_v2_id_fk",
"entityType": "fks",
"table": "session_inbox"
},
{
"columns": [
"session_id"
],
"tableTo": "session_v2",
"columnsTo": [
"id"
],
"onUpdate": "NO ACTION",
"onDelete": "CASCADE",
"nameExplicit": false,
@@ -1440,9 +1603,13 @@
"table": "session_message"
},
{
"columns": ["session_id"],
"columns": [
"session_id"
],
"tableTo": "session_v2",
"columnsTo": ["id"],
"columnsTo": [
"id"
],
"onUpdate": "NO ACTION",
"onDelete": "CASCADE",
"nameExplicit": false,
@@ -1451,9 +1618,13 @@
"table": "session_pending"
},
{
"columns": ["project_id"],
"columns": [
"project_id"
],
"tableTo": "project",
"columnsTo": ["id"],
"columnsTo": [
"id"
],
"onUpdate": "NO ACTION",
"onDelete": "CASCADE",
"nameExplicit": false,
@@ -1462,119 +1633,190 @@
"table": "session_v2"
},
{
"columns": ["email", "url"],
"columns": [
"project_id"
],
"tableTo": "project",
"columnsTo": [
"id"
],
"onUpdate": "NO ACTION",
"onDelete": "CASCADE",
"nameExplicit": false,
"name": "fk_worktree_project_id_project_id_fk",
"entityType": "fks",
"table": "worktree"
},
{
"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", "key"],
"columns": [
"session_id",
"key"
],
"nameExplicit": false,
"name": "instruction_entry_pk",
"entityType": "pks",
"table": "instruction_entry"
},
{
"columns": ["id"],
"columns": [
"project_id",
"directory"
],
"nameExplicit": false,
"name": "worktree_pk",
"entityType": "pks",
"table": "worktree"
},
{
"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": ["id"],
"columns": [
"id"
],
"nameExplicit": false,
"name": "credential_pk",
"table": "credential",
"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": ["key"],
"columns": [
"key"
],
"nameExplicit": false,
"name": "kv_pk",
"table": "kv",
"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": ["hash"],
"columns": [
"hash"
],
"nameExplicit": false,
"name": "instruction_blob_pk",
"table": "instruction_blob",
"entityType": "pks"
},
{
"columns": ["session_id"],
"columns": [
"session_id"
],
"nameExplicit": false,
"name": "instruction_state_pk",
"table": "instruction_state",
"entityType": "pks"
},
{
"columns": ["id"],
"columns": [
"id"
],
"nameExplicit": false,
"name": "session_inbox_pk",
"table": "session_inbox",
"entityType": "pks"
},
{
"columns": [
"id"
],
"nameExplicit": false,
"name": "session_message_pk",
"table": "session_message",
"entityType": "pks"
},
{
"columns": ["id"],
"columns": [
"id"
],
"nameExplicit": false,
"name": "session_pending_pk",
"table": "session_pending",
"entityType": "pks"
},
{
"columns": ["id"],
"columns": [
"id"
],
"nameExplicit": false,
"name": "session_v2_pk",
"table": "session_v2",
"entityType": "pks"
},
{
"columns": ["id"],
"columns": [
"id"
],
"nameExplicit": false,
"name": "workspace_pk",
"table": "workspace",
@@ -1642,6 +1884,46 @@
"entityType": "indexes",
"table": "permission"
},
{
"columns": [
{
"value": "session_id",
"isExpression": false
},
{
"value": "delivery",
"isExpression": false
},
{
"value": "enqueued_seq",
"isExpression": false
}
],
"isUnique": false,
"where": null,
"origin": "manual",
"name": "session_inbox_session_delivery_seq_idx",
"entityType": "indexes",
"table": "session_inbox"
},
{
"columns": [
{
"value": "session_id",
"isExpression": false
},
{
"value": "enqueued_seq",
"isExpression": false
}
],
"isUnique": true,
"where": null,
"origin": "manual",
"name": "session_inbox_session_enqueued_seq_idx",
"entityType": "indexes",
"table": "session_inbox"
},
{
"columns": [
{
@@ -1830,4 +2112,4 @@
}
],
"renames": []
}
}
+34
View File
@@ -0,0 +1,34 @@
#!/usr/bin/env bun
import { $ } from "bun"
import { rm } from "node:fs/promises"
import path from "node:path"
import { fileURLToPath } from "node:url"
process.chdir(fileURLToPath(new URL("..", import.meta.url)))
await rm("dist", { recursive: true, force: true })
await $`bun tsc -p tsconfig.build.json`
const root = path.resolve("src")
const files = await Array.fromAsync(new Bun.Glob("**/*.ts").scan({ cwd: root, absolute: true }))
const result = await Bun.build({
entrypoints: files.filter((file) => !file.endsWith(".d.ts")),
root,
outdir: "dist",
target: "node",
format: "esm",
packages: "external",
external: ["#sqlite", "#pty", "#fff", "#photon-wasm", "#shell-parser-wasm", "#process-lock-ffi", "#v1-migration"],
splitting: true,
loader: {
".txt": "text",
".md": "text",
},
naming: {
entry: "[dir]/[name].[ext]",
chunk: "chunks/[name]-[hash].[ext]",
asset: "assets/[name]-[hash].[ext]",
},
})
if (!result.success) throw new AggregateError(result.logs, "Failed to build Core")
+50
View File
@@ -0,0 +1,50 @@
#!/usr/bin/env bun
import { Script } from "@opencode-ai/script"
import { $ } from "bun"
import { rm } from "node:fs/promises"
import { fileURLToPath } from "node:url"
process.chdir(fileURLToPath(new URL("..", import.meta.url)))
const originalText = await Bun.file("package.json").text()
const pkg = JSON.parse(originalText) as {
name: string
version: string
exports: Record<string, string | { import: string; types: string }>
imports: Record<string, Record<string, string>>
}
const tarball = `${pkg.name.replace("@", "").replace("/", "-")}-${pkg.version}.tgz`
const output = (value: string, types = false) =>
value.replace("./src/", types ? "./dist/types/" : "./dist/").replace(/\.ts$/, types ? ".d.ts" : ".js")
if ((await $`npm view ${pkg.name}@${pkg.version} version`.nothrow()).exitCode === 0) {
console.log(`already published ${pkg.name}@${pkg.version}`)
process.exit(0)
}
try {
await $`bun run typecheck`
await $`bun run build`
pkg.exports = Object.fromEntries(
Object.entries(pkg.exports).map(([key, value]) => {
if (typeof value !== "string") return [key, value]
return [key, { import: output(value), types: output(value, true) }]
}),
)
pkg.imports = Object.fromEntries(
Object.entries(pkg.imports).map(([key, conditions]) => [
key,
Object.fromEntries(
Object.entries(conditions).map(([condition, value]) => [condition, output(value, condition === "types")]),
),
]),
)
await Bun.write("package.json", JSON.stringify(pkg, null, 2) + "\n")
await rm(tarball, { force: true })
await $`bun pm pack`
await $`npm publish ${tarball} --tag ${Script.channel} --access public`
} finally {
await Bun.write("package.json", originalText)
await rm(tarball, { force: true })
}
+200 -31
View File
@@ -6,7 +6,8 @@ import type { EventLog } from "@opencode-ai/schema/event-log"
import { and, asc, eq, gt, lte, sql } from "drizzle-orm"
import { Database } from "./database/database.js"
import { EventSequenceTable, EventTable } from "./event/sql.js"
import { Location } from "./location.js"
import type { Location } from "@opencode-ai/schema/location"
import { KeyedMutex } from "./effect/keyed-mutex.js"
import { makeGlobalNode } from "@opencode-ai/util/effect/app-node"
import { isDeepStrictEqual } from "node:util"
import { Durable } from "@opencode-ai/schema/durable-event-manifest"
@@ -92,6 +93,16 @@ export interface PublishOptions {
readonly commit?: (seq: number) => Effect.Effect<void>
}
export type PublishInput<D extends Event.DurableDefinition = Event.DurableDefinition> = readonly [
definition: D,
data: Event.Data<D>,
options?: PublishOptions,
]
export type PublishResult<I extends readonly PublishInput[]> = {
readonly [K in keyof I]: I[K] extends PublishInput<infer D> ? Event.Payload<D> : never
}
/** Marker/event union emitted by `log`. */
export type LogItem = Event.Payload | EventLog.Synced
@@ -124,6 +135,9 @@ export interface Interface {
data: Event.Data<D>,
options?: PublishOptions,
) => Effect.Effect<Event.Payload<D>>
readonly publishAll: <const I extends readonly [PublishInput, ...PublishInput[]]>(
events: I,
) => Effect.Effect<PublishResult<I>>
readonly subscribe: Subscribe
/**
* Durable, ordered per-aggregate log read. Forked aggregates may reserve an
@@ -169,6 +183,9 @@ export function configured(options?: Options) {
layer: Layer.effect(
Service,
Effect.gen(function* () {
// Deferred import: a static one would close the module cycle
// bus → location → project → bus and hit the node bindings in TDZ.
const { Location } = yield* Effect.promise(() => import("./location.js"))
const pubsub = {
live: yield* PubSub.unbounded<Event.Payload>(),
durable: new Map<string, Set<PubSub.PubSub<void>>>(),
@@ -176,6 +193,7 @@ export function configured(options?: Options) {
}
const projectors = new Map<string, Subscriber[]>()
const listeners = new Array<Subscriber>()
const durableLocks = KeyedMutex.makeUnsafe<string>()
const { db } = yield* Database.Service
const logReadPageSize = options?.logReadPageSize ?? 512
const persist = options?.persist ?? false
@@ -385,15 +403,23 @@ export function configured(options?: Options) {
}),
)
if (definition?.durable) {
const committed = yield* commitDurableEvent(definition, event as Event.Payload, undefined, commit)
if (committed) {
event = {
...event,
durable: envelope(committed.aggregateID, committed.seq, definition.durable.version),
}
yield* notify(event as Event.Payload, true)
return event
}
const aggregateID = (event.data as Record<string, unknown>)[definition.durable.aggregate]
if (typeof aggregateID !== "string")
return yield* commitDurableEvent(definition, event as Event.Payload, undefined, commit).pipe(
Effect.as(event),
)
return yield* durableLocks.withLock(aggregateID)(
Effect.gen(function* () {
const committed = yield* commitDurableEvent(definition, event as Event.Payload, undefined, commit)
if (!committed) return event
event = {
...event,
durable: envelope(committed.aggregateID, committed.seq, definition.durable.version),
}
yield* notify(event as Event.Payload, true)
return event
}),
)
}
yield* notify(event as Event.Payload, false)
return event
@@ -444,6 +470,144 @@ export function configured(options?: Options) {
})
}
function publishAll<const I extends readonly [PublishInput, ...PublishInput[]]>(events: I) {
return Effect.gen(function* () {
const serviceLocation = Option.getOrUndefined(yield* Effect.serviceOption(Location.Service))
const payloads = yield* Effect.forEach(events, ([definition, data, options]) =>
Effect.gen(function* () {
const aggregateID = (data as Record<string, unknown>)[definition.durable.aggregate]
if (typeof aggregateID !== "string") {
return yield* Effect.die(
new InvalidDurableEventError({
type: definition.type,
message: `Expected string aggregate field ${definition.durable.aggregate}`,
}),
)
}
const location =
options?.location ??
(serviceLocation
? { directory: serviceLocation.directory, workspaceID: serviceLocation.workspaceID }
: undefined)
return {
definition,
aggregateID,
commit: options?.commit,
event: {
id: options?.id ?? Event.ID.create(),
created: yield* DateTime.now,
...(options?.metadata ? { metadata: options.metadata } : {}),
type: definition.type,
...(location ? { location } : {}),
data,
} as Event.Payload,
}
}),
)
const aggregateID = payloads[0].aggregateID
if (payloads.some((item) => item.aggregateID !== aggregateID)) {
return yield* Effect.die(
new InvalidDurableEventError({
type: payloads[0].definition.type,
message: "Published events must belong to the same aggregate",
}),
)
}
return yield* durableLocks.withLock(aggregateID)(
Effect.uninterruptible(
Effect.gen(function* () {
const committed = yield* db
.transaction(
() =>
Effect.gen(function* () {
const row = yield* db
.select({ seq: EventSequenceTable.seq })
.from(EventSequenceTable)
.where(eq(EventSequenceTable.aggregate_id, aggregateID))
.get()
.pipe(Effect.orDie)
const firstSeq = (row?.seq ?? -1) + 1
const finalSeq = firstSeq + payloads.length - 1
const result = new Array<Event.Payload>()
const rows = new Array<typeof EventTable.$inferInsert>()
const ids = new Set<Event.ID>()
for (const [index, item] of payloads.entries()) {
const seq = firstSeq + index
const encoded = Schema.encodeUnknownSync(item.definition.data)(item.event.data) as Record<
string,
unknown
>
if (persist) {
if (ids.has(item.event.id))
yield* Effect.die(
new InvalidDurableEventError({
type: item.event.type,
message: `Event ${item.event.id} appears more than once in the batch`,
}),
)
ids.add(item.event.id)
const stored = yield* db
.select({ aggregateID: EventTable.aggregate_id, seq: EventTable.seq })
.from(EventTable)
.where(eq(EventTable.id, item.event.id))
.get()
.pipe(Effect.orDie)
if (stored)
yield* Effect.die(
new InvalidDurableEventError({
type: item.event.type,
message: `Event ${item.event.id} already exists at aggregate ${stored.aggregateID} sequence ${stored.seq}`,
}),
)
}
const event = {
...item.event,
durable: envelope(aggregateID, seq, item.definition.durable.version),
} as Event.Payload
for (const projector of projectors.get(
versionedType(item.definition.type, item.definition.durable.version),
) ?? []) {
yield* projector(event)
}
if (item.commit) yield* item.commit(seq)
if (persist)
rows.push({
id: event.id,
aggregate_id: aggregateID,
seq,
created: DateTime.toEpochMillis(event.created),
type: versionedType(item.definition.type, item.definition.durable.version),
data: encoded,
})
result.push(event)
}
yield* db
.insert(EventSequenceTable)
.values([{ aggregate_id: aggregateID, seq: finalSeq }])
.onConflictDoUpdate({ target: EventSequenceTable.aggregate_id, set: { seq: finalSeq } })
.run()
.pipe(Effect.orDie)
if (persist) yield* db.insert(EventTable).values(rows).run().pipe(Effect.orDie)
return result
}),
{ behavior: "immediate" },
)
.pipe(Effect.orDie)
yield* Effect.forEach(
pubsub.durable.get(aggregateID) ?? [],
(wake) => PubSub.publish(wake, undefined),
{
discard: true,
},
)
yield* Effect.forEach(committed, (event) => notify(event, true), { discard: true })
return committed as PublishResult<I>
}),
),
)
})
}
function replay(
event: SerializedEvent,
options?: { readonly publish?: boolean; readonly ownerID?: string; readonly strictOwner?: boolean },
@@ -455,27 +619,31 @@ export function configured(options?: Options) {
new InvalidDurableEventError({ type: event.type, message: `Unknown durable event type ${event.type}` }),
)
} else {
const payload = {
id: event.id,
created: event.created ?? DateTime.makeUnsafe(0),
type: definition.type,
data: Schema.decodeUnknownSync(definition.data)(event.data),
} as Event.Payload
const committed = yield* commitDurableEvent(definition, payload, {
seq: event.seq,
aggregateID: event.aggregateID,
ownerID: options?.ownerID,
strictOwner: options?.strictOwner,
})
if (committed && options?.publish) {
yield* notify(
{
...payload,
durable: envelope(committed.aggregateID, committed.seq, definition.durable.version),
},
true,
)
}
yield* durableLocks.withLock(event.aggregateID)(
Effect.gen(function* () {
const payload = {
id: event.id,
created: event.created ?? DateTime.makeUnsafe(0),
type: definition.type,
data: Schema.decodeUnknownSync(definition.data)(event.data),
} as Event.Payload
const committed = yield* commitDurableEvent(definition, payload, {
seq: event.seq,
aggregateID: event.aggregateID,
ownerID: options?.ownerID,
strictOwner: options?.strictOwner,
})
if (committed && options?.publish) {
yield* notify(
{
...payload,
durable: envelope(committed.aggregateID, committed.seq, definition.durable.version),
},
true,
)
}
}),
)
}
})
}
@@ -692,6 +860,7 @@ export function configured(options?: Options) {
return Service.of({
publish,
publishAll,
subscribe,
log,
listen,
+4 -2
View File
@@ -75,7 +75,9 @@ export const create = (
const outputFileParts = outputFiles(content)
if (outputFileParts.length > 0)
yield* Ref.update(files, (items) => [...items, { index, files: outputFileParts }])
return executed.output
if (executed.output !== undefined) return executed.output
const text = content.flatMap((part) => (part.type === "text" ? [part.text] : [])).join("\n")
return text === "" ? null : text
}),
{
onToolCallStart: ({ index, name, input }) => {
@@ -155,7 +157,7 @@ function runtime(
tools[path] = Tool.make({
description: child.description,
input: child.inputSchema,
output: child.outputSchema,
output: child.outputSchema ?? Schema.NullOr(Schema.String),
execute: (input) => executeTool(name, registration, input),
})
}
+9
View File
@@ -65,4 +65,13 @@ export function configured(options?: Options) {
return makeGlobalNode({ service: Service, layer: layer(options), deps: [Global.node] })
}
/** `configured`, but over an injected SqlClient layer instead of a filesystem path. */
export function configuredClient(client: Layer.Layer<SqlClient.SqlClient>) {
return makeGlobalNode({
service: Service,
layer: layerFromClient.pipe(Layer.provide(client)),
deps: [Global.node],
})
}
export const node = configured({ path: ":memory:" })
@@ -28,6 +28,20 @@ export interface EffectSQLiteQueryEffectHKT extends QueryEffectHKTBase {
readonly context: never
}
/**
* A SqlClient whose runtime rejects SQL transaction statements (BEGIN/COMMIT/SAVEPOINT) and
* manages transactions natively instead — Cloudflare Durable Object SQLite. Producers set
* `transactionStatements: false` (see `database/sqlite.workerd.ts`) and the session delegates
* to the client's `withTransaction` rather than issuing transaction statements.
*/
export interface NativeTransactionSqlClient extends SqlClient {
readonly transactionStatements: false
}
export function managesTransactionsNatively(client: SqlClient): client is NativeTransactionSqlClient {
return (client as SqlClient & { readonly transactionStatements?: boolean }).transactionStatements === false
}
export type EffectSQLiteRunResult = readonly never[]
export interface EffectSQLiteSessionOptions {
@@ -120,6 +134,7 @@ export class EffectSQLiteSession<TRelations extends AnyRelations> extends SQLite
}
private withTransaction<A, E, R>(effect: Effect.Effect<A, E, R>, config: SQLiteTransactionConfig | undefined) {
if (managesTransactionsNatively(this.client)) return this.client.withTransaction(effect)
return Effect.uninterruptibleMask((restore) =>
Effect.withFiber<A, E | SqlError, R>((fiber) => {
const services = fiber.context
+4
View File
@@ -41,6 +41,8 @@ import m38 from "./migration/20260804233008_loose_psylocke.js"
import m39 from "./migration/20260805200742_import_legacy_credentials.js"
import m40 from "./migration/20260808023530_workspace_domain.js"
import m41 from "./migration/20260811161259_execution_claim_attempts.js"
import m42 from "./migration/20260812181746_session_inbox.js"
import m43 from "./migration/20260812213948_worktree.js"
export const migrations = [
m00,
@@ -85,4 +87,6 @@ export const migrations = [
m39,
m40,
m41,
m42,
m43,
] satisfies DatabaseMigration.Migration[]
@@ -0,0 +1,30 @@
import { Effect } from "effect"
import type { DatabaseMigration } from "../migration.js"
const migration: DatabaseMigration.Migration = {
id: "20260812181746_session_inbox",
up(tx) {
return Effect.gen(function* () {
yield* tx.run(`
CREATE TABLE \`session_inbox\` (
\`id\` text PRIMARY KEY,
\`session_id\` text NOT NULL,
\`type\` text NOT NULL,
\`payload\` text NOT NULL,
\`delivery\` text NOT NULL,
\`enqueued_seq\` integer NOT NULL,
\`time_created\` integer NOT NULL,
CONSTRAINT \`fk_session_inbox_session_id_session_v2_id_fk\` FOREIGN KEY (\`session_id\`) REFERENCES \`session_v2\`(\`id\`) ON DELETE CASCADE
);
`)
yield* tx.run(
`CREATE INDEX \`session_inbox_session_delivery_seq_idx\` ON \`session_inbox\` (\`session_id\`,\`delivery\`,\`enqueued_seq\`);`,
)
yield* tx.run(
`CREATE UNIQUE INDEX \`session_inbox_session_enqueued_seq_idx\` ON \`session_inbox\` (\`session_id\`,\`enqueued_seq\`);`,
)
})
},
}
export default migration
@@ -0,0 +1,35 @@
import { Effect } from "effect"
import type { DatabaseMigration } from "../migration.js"
const migration: DatabaseMigration.Migration = {
id: "20260812213948_worktree",
up(tx) {
return Effect.gen(function* () {
yield* tx.run(`
CREATE TABLE \`worktree\` (
\`project_id\` text NOT NULL,
\`directory\` text NOT NULL,
\`strategy\` text,
\`time_created\` integer NOT NULL,
CONSTRAINT \`worktree_pk\` PRIMARY KEY(\`project_id\`, \`directory\`),
CONSTRAINT \`fk_worktree_project_id_project_id_fk\` FOREIGN KEY (\`project_id\`) REFERENCES \`project\`(\`id\`) ON DELETE CASCADE
);
`)
yield* tx.run(`
INSERT INTO \`worktree\` (\`project_id\`, \`directory\`, \`strategy\`, \`time_created\`)
SELECT
\`project_id\`,
\`directory\`,
CASE
WHEN \`strategy\` = 'git_worktree' THEN 'git'
WHEN \`strategy\` IS NOT NULL THEN \`strategy\`
WHEN \`type\` = 'git_worktree' THEN 'git'
END,
\`time_created\`
FROM \`project_directory\`;
`)
})
},
}
export default migration
+28
View File
@@ -142,6 +142,18 @@ const schema: Omit<DatabaseMigration.Migration, "id"> = {
CONSTRAINT \`fk_instruction_state_session_id_session_v2_id_fk\` FOREIGN KEY (\`session_id\`) REFERENCES \`session_v2\`(\`id\`) ON DELETE CASCADE
);
`)
yield* tx.run(`
CREATE TABLE \`session_inbox\` (
\`id\` text PRIMARY KEY,
\`session_id\` text NOT NULL,
\`type\` text NOT NULL,
\`payload\` text NOT NULL,
\`delivery\` text NOT NULL,
\`enqueued_seq\` integer NOT NULL,
\`time_created\` integer NOT NULL,
CONSTRAINT \`fk_session_inbox_session_id_session_v2_id_fk\` FOREIGN KEY (\`session_id\`) REFERENCES \`session_v2\`(\`id\`) ON DELETE CASCADE
);
`)
yield* tx.run(`
CREATE TABLE \`session_message\` (
\`id\` text PRIMARY KEY,
@@ -213,11 +225,27 @@ const schema: Omit<DatabaseMigration.Migration, "id"> = {
\`last_used_at\` integer NOT NULL
);
`)
yield* tx.run(`
CREATE TABLE \`worktree\` (
\`project_id\` text NOT NULL,
\`directory\` text NOT NULL,
\`strategy\` text,
\`time_created\` integer NOT NULL,
CONSTRAINT \`worktree_pk\` PRIMARY KEY(\`project_id\`, \`directory\`),
CONSTRAINT \`fk_worktree_project_id_project_id_fk\` FOREIGN KEY (\`project_id\`) REFERENCES \`project\`(\`id\`) ON DELETE CASCADE
);
`)
yield* tx.run(`CREATE UNIQUE INDEX \`event_aggregate_seq_idx\` ON \`event\` (\`aggregate_id\`,\`seq\`);`)
yield* tx.run(`CREATE INDEX \`event_aggregate_type_seq_idx\` ON \`event\` (\`aggregate_id\`,\`type\`,\`seq\`);`)
yield* tx.run(
`CREATE UNIQUE INDEX \`permission_project_action_resource_idx\` ON \`permission\` (\`project_id\`,\`action\`,\`resource\`);`,
)
yield* tx.run(
`CREATE INDEX \`session_inbox_session_delivery_seq_idx\` ON \`session_inbox\` (\`session_id\`,\`delivery\`,\`enqueued_seq\`);`,
)
yield* tx.run(
`CREATE UNIQUE INDEX \`session_inbox_session_enqueued_seq_idx\` ON \`session_inbox\` (\`session_id\`,\`enqueued_seq\`);`,
)
yield* tx.run(
`CREATE UNIQUE INDEX \`session_message_session_seq_idx\` ON \`session_message\` (\`session_id\`,\`seq\`);`,
)
+1 -11
View File
@@ -1,5 +1,4 @@
import { Database, type SQLQueryBindings } from "bun:sqlite"
import { drizzle } from "drizzle-orm/bun-sqlite"
import { Context, Effect, Layer } from "effect"
import { Reactivity } from "effect/unstable/reactivity"
import { SqlClient } from "effect/unstable/sql"
@@ -98,16 +97,7 @@ const nativeLayer = (config: Config) =>
const clientLayer = (config: Config) => Layer.effect(SqlClient.SqlClient, make(config))
const drizzleLayer = Layer.effect(
Sqlite.Drizzle,
Effect.gen(function* () {
return drizzle({ client: (yield* Sqlite.Native) as Database })
}),
)
export const sqliteLayer = (config: Config) => {
const native = nativeLayer(config)
return Layer.merge(native, Layer.merge(clientLayer(config), drizzleLayer).pipe(Layer.provide(native))).pipe(
Layer.provide(Reactivity.layer),
)
return Layer.merge(native, clientLayer(config).pipe(Layer.provide(native))).pipe(Layer.provide(Reactivity.layer))
}
+1 -11
View File
@@ -1,5 +1,4 @@
import { DatabaseSync, type SQLInputValue } from "node:sqlite"
import { drizzle } from "drizzle-orm/node-sqlite"
import { Context, Effect, Layer } from "effect"
import { Reactivity } from "effect/unstable/reactivity"
import { SqlClient } from "effect/unstable/sql"
@@ -95,16 +94,7 @@ const nativeLayer = (config: Config) =>
const clientLayer = (config: Config) => Layer.effect(SqlClient.SqlClient, make(config))
const drizzleLayer = Layer.effect(
Sqlite.Drizzle,
Effect.gen(function* () {
return drizzle({ client: (yield* Sqlite.Native) as DatabaseSync }) as unknown as Sqlite.DrizzleClient
}),
)
export const sqliteLayer = (config: Config) => {
const native = nativeLayer(config)
return Layer.merge(native, Layer.merge(clientLayer(config), drizzleLayer).pipe(Layer.provide(native))).pipe(
Layer.provide(Reactivity.layer),
)
return Layer.merge(native, clientLayer(config).pipe(Layer.provide(native))).pipe(Layer.provide(Reactivity.layer))
}
-3
View File
@@ -5,11 +5,8 @@ import { identity } from "effect/Function"
import { SqlClient, Statement } from "effect/unstable/sql"
import type { Connection } from "effect/unstable/sql/SqlConnection"
import type { SqlError } from "effect/unstable/sql/SqlError"
import type { drizzle } from "drizzle-orm/bun-sqlite"
export type DrizzleClient = ReturnType<typeof drizzle>
export class Native extends Context.Service<Native, unknown>()("@opencode-ai/core/database/SqliteNative") {}
export class Drizzle extends Context.Service<Drizzle, DrizzleClient>()("@opencode-ai/core/database/SqliteDrizzle") {}
export interface ClientConfig {
readonly spanAttributes?: Record<string, unknown>
+5 -15
View File
@@ -1,10 +1,10 @@
import { drizzle } from "drizzle-orm/durable-sqlite"
import { Context, Effect, Exit, Fiber, Layer, Scope, Semaphore, Stream } from "effect"
import { identity } from "effect/Function"
import { Reactivity } from "effect/unstable/reactivity"
import { SqlClient, Statement } from "effect/unstable/sql"
import type { Connection } from "effect/unstable/sql/SqlConnection"
import { classifySqliteError, SqlError, UnknownError } from "effect/unstable/sql/SqlError"
import type { NativeTransactionSqlClient } from "./drizzle/effect-sqlite/session.js"
import { Sqlite } from "./sqlite.js"
const ATTR_DB_SYSTEM_NAME = "db.system.name"
@@ -216,10 +216,10 @@ const make = (options: Config) =>
config: options,
withTransaction: makeWithTransaction(native, connection, semaphore),
// Durable Object SQLite rejects BEGIN/COMMIT/SAVEPOINT; consumers such
// as the drizzle session must route through withTransaction instead.
// as the drizzle session detect this and route through withTransaction.
transactionStatements: false,
},
)
} as const,
) satisfies NativeTransactionSqlClient
return client
})
@@ -238,17 +238,7 @@ const nativeLayer = (config: Config) =>
const clientLayer = (config: Config) => Layer.effect(SqlClient.SqlClient, make(config))
const drizzleLayer = Layer.effect(
Sqlite.Drizzle,
Effect.gen(function* () {
const native = (yield* Sqlite.Native) as DurableObjectStorage
return drizzle(native) as unknown as Sqlite.DrizzleClient
}),
)
export const sqliteLayer = (config: Config) => {
const native = nativeLayer(config)
return Layer.merge(native, Layer.merge(clientLayer(config), drizzleLayer).pipe(Layer.provide(native))).pipe(
Layer.provide(Reactivity.layer),
)
return Layer.merge(native, clientLayer(config).pipe(Layer.provide(native))).pipe(Layer.provide(Reactivity.layer))
}
@@ -0,0 +1,997 @@
export * as V1Migration from "./v1-migration.js"
import { Cause, Effect, Layer, Option, Schema, Semaphore } from "effect"
import { Database } from "./database.js"
import { SessionMessageTable, SessionTable } from "../session/sql.js"
import { SessionV1 } from "@opencode-ai/schema/session-v1"
import { SessionMessage } from "../session/message.js"
import { SessionSchema } from "../session/schema.js"
import { KVTable } from "../kv/sql.js"
import { EventSequenceTable } from "../event/sql.js"
import { eq, sql } from "drizzle-orm"
import { Global } from "@opencode-ai/util/global"
import { existsSync } from "node:fs"
import path from "node:path"
import type { Database as SQLiteDatabase } from "bun:sqlite"
import { Project } from "@opencode-ai/schema/project"
export type SourceMessage = {
readonly id: string
readonly session_id: string
readonly time_created: number
readonly time_updated: number
readonly data: string
}
export type SourcePart = {
readonly id: string
readonly message_id: string
readonly session_id: string
readonly time_created: number
readonly time_updated: number
readonly data: string
}
export type TransformInput = {
readonly session: typeof SessionTable.$inferSelect
readonly messages: ReadonlyArray<SourceMessage>
readonly parts: ReadonlyArray<SourcePart>
}
export type Warning = {
readonly reason: string
readonly sessionID: string
readonly messageID?: string
readonly partID?: string
readonly observedType?: string
}
export type TransformResult = {
readonly messages: ReadonlyArray<{
readonly id: string
readonly session_id: string
readonly type: SessionMessage.Type
readonly seq: number
readonly time_created: number
readonly time_updated: number
readonly data: Record<string, unknown>
}>
readonly session: Pick<
typeof SessionTable.$inferInsert,
| "agent"
| "model"
| "cost"
| "tokens_input"
| "tokens_output"
| "tokens_reasoning"
| "tokens_cache_read"
| "tokens_cache_write"
| "revert"
| "time_compacting"
>
readonly watermark: number
readonly warnings: ReadonlyArray<Warning>
}
type Progress = {
readonly label: string
readonly numerator?: number
readonly denominator?: number
}
export type Status =
| { readonly status: "required" | "completed" }
| { readonly status: "running"; readonly progress: Progress }
| { readonly status: "error"; readonly error: string }
type RunResult = {
readonly status: "completed"
}
type Options = {
readonly nextDatabasePath?: string
}
type MigrationState = { readonly phase: "sessions"; readonly cursor?: string } | { readonly phase: "completed" }
type RuntimeState =
| { readonly status: "idle" }
| { readonly status: "running"; readonly progress: Progress }
| { readonly status: "error"; readonly error: string }
type NextProject = {
readonly id: string
readonly worktree: string
readonly vcs: string | null
readonly name: string | null
readonly icon_url: string | null
readonly icon_url_override: string | null
readonly icon_color: string | null
readonly time_created: number
readonly time_updated: number
readonly time_initialized: number | null
readonly sandboxes: string
readonly commands: string | null
}
type NextSession = {
readonly id: string
readonly project_id: string
readonly workspace_id: string | null
readonly parent_id: string | null
readonly fork_session_id: string | null
readonly fork_boundary: string | null
readonly slug: string
readonly directory: string
readonly path: string | null
readonly title: string | null
readonly version: string
readonly share_url: string | null
readonly summary_additions: number | null
readonly summary_deletions: number | null
readonly summary_files: number | null
readonly summary_diffs: string | null
readonly metadata: string | null
readonly cost: number
readonly tokens_input: number
readonly tokens_output: number
readonly tokens_reasoning: number
readonly tokens_cache_read: number
readonly tokens_cache_write: number
readonly revert: string | null
readonly permission: string | null
readonly agent: string | null
readonly model: string | null
readonly time_created: number
readonly time_updated: number
readonly time_compacting: number | null
readonly time_archived: number | null
readonly time_suspended: number | null
}
type NextMessage = {
readonly id: string
readonly session_id: string
readonly type: string
readonly seq: number
readonly time_created: number
readonly time_updated: number
readonly data: string
}
const lock = Semaphore.makeUnsafe(1)
const MIGRATION_STATE_KEY = "migration.v1-v2"
const EVENT_DELETE_BATCH_SIZE = 1_000
const decodeJson = Schema.decodeUnknownOption(Schema.UnknownFromJsonString)
const decodeMessage = Schema.decodeUnknownOption(SessionV1.Info)
const decodePart = Schema.decodeUnknownOption(SessionV1.Part)
let runtimeState: RuntimeState = { status: "idle" }
export function transformSession(input: TransformInput): TransformResult {
const warnings: Warning[] = []
const messages = input.messages
.map((row) => {
const value = Option.getOrUndefined(decodeJson(row.data))
const decoded =
value && typeof value === "object"
? Option.getOrUndefined(decodeMessage({ ...value, id: row.id, sessionID: row.session_id }))
: undefined
if (decoded) return { row, value: decoded }
warnings.push({ reason: "invalid-message", sessionID: input.session.id, messageID: row.id })
return undefined
})
.filter((item): item is NonNullable<typeof item> => item !== undefined)
.sort((a, b) => a.row.time_created - b.row.time_created || a.row.id.localeCompare(b.row.id))
const messageIDs = new Set(input.messages.map((row) => row.id))
const parts = input.parts
.map((row) => {
const value = Option.getOrUndefined(decodeJson(row.data))
const observedType = value && typeof value === "object" && "type" in value ? String(value.type) : undefined
if (!messageIDs.has(row.message_id)) {
warnings.push({
reason: "orphan-part",
sessionID: input.session.id,
messageID: row.message_id,
partID: row.id,
observedType,
})
return undefined
}
const decoded =
value && typeof value === "object"
? Option.getOrUndefined(
decodePart({ ...value, id: row.id, messageID: row.message_id, sessionID: row.session_id }),
)
: undefined
if (decoded) return { row, value: decoded }
warnings.push({
reason: "invalid-part",
sessionID: input.session.id,
messageID: row.message_id,
partID: row.id,
observedType,
})
return undefined
})
.filter((item): item is NonNullable<typeof item> => item !== undefined)
.sort((a, b) => a.row.id.localeCompare(b.row.id))
const byMessage = Map.groupBy(parts, (item) => item.row.message_id)
const paired = new Set<string>()
const used = new Set(messages.map((item) => item.row.id))
const projected = messages
.flatMap((item) => {
if (paired.has(item.row.id)) return []
const owned = byMessage.get(item.row.id)?.map((part) => part.value) ?? []
if (item.value.role === "user") {
const compaction = owned.find((part) => part.type === "compaction")
if (compaction?.type === "compaction") {
const pairedSummary = messages.find(
(candidate) =>
candidate.value.role === "assistant" &&
candidate.value.parentID === item.row.id &&
candidate.value.summary,
)
if (!pairedSummary || pairedSummary.value.role !== "assistant") return []
paired.add(pairedSummary.row.id)
if (pairedSummary.value.error || pairedSummary.value.time.completed === undefined) return []
const summary = pairedSummary
const summaryText = (byMessage.get(summary.row.id) ?? [])
.map((part) => part.value)
.filter((part) => part.type === "text" && part.text.length > 0)
.map((part) => (part.type === "text" ? part.text : ""))
.join("\n\n")
const tailIndex = compaction.tail_start_id
? messages.findIndex((candidate) => candidate.row.id === compaction.tail_start_id)
: -1
const compactionIndex = messages.findIndex((candidate) => candidate.row.id === item.row.id)
const tail = tailIndex < 0 ? [] : messages.slice(tailIndex, compactionIndex)
return [
row(
{ ...item.row, time_updated: Math.max(item.row.time_updated, summary.row.time_updated) },
{
id: item.row.id,
type: "compaction",
status: "completed",
reason: compaction.auto ? "auto" : "manual",
summary: summaryText,
recent: serializeRecent(tail, byMessage),
time: { created: item.row.time_created },
},
),
]
}
const subtasks = owned.filter((part) => part.type === "subtask")
const visible = owned.filter((part) => part.type === "text" && !part.ignored)
const files = owned.filter((part) => part.type === "file")
const agents = owned.filter((part) => part.type === "agent")
if (subtasks.length > 0 && visible.length === 0 && files.length === 0 && agents.length === 0) return []
const ordinary = visible.filter((part) => part.type === "text" && !part.synthetic)
const synthetic = visible.filter((part) => part.type === "text" && part.synthetic)
const attachments = files.flatMap((part) => (part.type === "file" ? migrateFile(part) : []))
const unavailable = files.flatMap((part) =>
part.type === "file" && !part.url.startsWith("data:") ? [unavailableFile(part)] : [],
)
const text = owned
.flatMap((part) => {
if (part.type === "text" && !part.ignored && !part.synthetic) return [part.text]
if (part.type === "file" && !part.url.startsWith("data:")) return [unavailableFile(part)]
return []
})
.join("\n\n")
const agentAttachments = agents.map((part) =>
part.type === "agent"
? {
name: part.name,
...(part.source
? { mention: { text: part.source.value, start: part.source.start, end: part.source.end } }
: {}),
}
: { name: "" },
)
if (
ordinary.length === 0 &&
unavailable.length === 0 &&
synthetic.length > 0 &&
attachments.length === 0 &&
agentAttachments.length === 0
)
return [
row(item.row, {
id: item.row.id,
type: "synthetic",
text: synthetic.map((part) => (part.type === "text" ? part.text : "")).join("\n\n"),
time: { created: item.row.time_created },
}),
]
const user = row(item.row, {
id: item.row.id,
type: "user",
text,
...(attachments.length ? { files: attachments } : {}),
...(agentAttachments.length ? { agents: agentAttachments } : {}),
time: { created: item.row.time_created },
})
if (synthetic.length === 0) return [user]
return [
user,
row(item.row, {
id: syntheticID(item.row.id, used),
type: "synthetic",
text: synthetic.map((part) => (part.type === "text" ? part.text : "")).join("\n\n"),
time: { created: item.row.time_created },
}),
]
}
if (item.value.role !== "assistant") return []
const assistant = item.value
const parent = messages.find((candidate) => candidate.row.id === assistant.parentID)
const parentParts = parent ? (byMessage.get(parent.row.id)?.map((part) => part.value) ?? []) : []
if (
parentParts.some((part) => part.type === "subtask") &&
owned.some((part) => part.type === "tool" && part.tool === "task")
)
return []
const content = owned.flatMap((part): Array<Record<string, unknown>> => {
if (part.type === "text")
return [{ type: "text", text: part.text, ...(part.metadata ? { state: part.metadata } : {}) }]
if (part.type === "reasoning")
return [
{
type: "reasoning",
text: part.text,
...(part.metadata ? { state: part.metadata } : {}),
time: { created: part.time.start, ...(part.time.end === undefined ? {} : { completed: part.time.end }) },
},
]
if (part.type !== "tool") return []
return [migrateTool(part, item.row.time_created)]
})
const start =
owned.flatMap((part) => (part.type === "step-start" && part.snapshot ? [part.snapshot] : []))[0] ??
owned.flatMap((part) => (part.type === "snapshot" ? [part.snapshot] : []))[0] ??
owned.flatMap((part) => (part.type === "patch" ? [part.hash] : []))[0]
const end = owned.flatMap((part) => (part.type === "step-finish" && part.snapshot ? [part.snapshot] : [])).at(-1)
const snapshotFiles = Array.from(new Set(owned.flatMap((part) => (part.type === "patch" ? part.files : []))))
const finish = normalizeFinish(assistant.finish)
return [
row(item.row, {
id: item.row.id,
type: "assistant",
agent: assistant.agent,
model: {
providerID: assistant.providerID,
id: assistant.modelID,
variant: assistant.variant ?? "default",
},
content,
...(start || end || snapshotFiles.length
? {
snapshot: {
...(start ? { start } : {}),
...(end ? { end } : {}),
...(snapshotFiles.length ? { files: snapshotFiles } : {}),
},
}
: {}),
...(finish ? { finish } : {}),
cost: assistant.cost,
tokens: {
input: assistant.tokens.input,
output: assistant.tokens.output,
reasoning: assistant.tokens.reasoning,
cache: assistant.tokens.cache,
},
...(assistant.error ? { error: migrateError(assistant.error) } : {}),
time: {
created: item.row.time_created,
...(assistant.time.completed === undefined ? {} : { completed: item.row.time_updated }),
},
}),
]
})
.map((item, seq) => ({ ...item, seq }))
const assistants = messages
.filter((item) => item.value.role === "assistant")
.map((item) => item.value)
.filter((item): item is SessionV1.Assistant => item.role === "assistant")
const latestUser = messages.findLast((item) => {
if (item.value.role !== "user") return false
const owned = byMessage.get(item.row.id) ?? []
if (owned.some((part) => part.value.type === "compaction")) return false
return !owned.some((part) => part.value.type === "subtask") || !owned.every((part) => part.value.type === "subtask")
})
return {
messages: projected,
session: {
agent: input.session.agent ?? (latestUser?.value.role === "user" ? latestUser.value.agent : null),
model:
input.session.model ??
(latestUser?.value.role === "user"
? {
id: latestUser.value.model.modelID,
providerID: latestUser.value.model.providerID,
variant: latestUser.value.model.variant ?? "default",
}
: null),
cost: assistants.reduce((total, item) => total + item.cost, 0),
tokens_input: assistants.reduce((total, item) => total + item.tokens.input, 0),
tokens_output: assistants.reduce((total, item) => total + item.tokens.output, 0),
tokens_reasoning: assistants.reduce((total, item) => total + item.tokens.reasoning, 0),
tokens_cache_read: assistants.reduce((total, item) => total + item.tokens.cache.read, 0),
tokens_cache_write: assistants.reduce((total, item) => total + item.tokens.cache.write, 0),
revert: null,
time_compacting: null,
},
watermark: projected.length - 1,
warnings,
}
}
export function status(): Effect.Effect<Status, never, Database.Service> {
return Effect.gen(function* () {
const { db } = yield* Database.Service
if (!(yield* hasLegacySessions(db))) return { status: "completed" as const }
const state = yield* readState(db)
if (runtimeState.status === "running") return runtimeState
if (runtimeState.status === "error") return runtimeState
if (state?.phase === "completed") return { status: "completed" as const }
return { status: "required" as const }
}).pipe(Effect.orDie)
}
export const layer = Layer.effectDiscard(
Effect.gen(function* () {
runtimeState = { status: "running", progress: { label: "Clearing old events" } }
yield* run().pipe(
Effect.matchCauseEffect({
onFailure: (cause) =>
Effect.sync(() => {
runtimeState = { status: "error", error: errorText(Cause.squash(cause)) }
}).pipe(Effect.andThen(Effect.logError("V1 migration failed", { cause }))),
onSuccess: () =>
Effect.sync(() => {
runtimeState = { status: "idle" }
}),
}),
Effect.forkScoped({ startImmediately: true }),
)
}),
)
function errorText(input: unknown): string {
if (!(input instanceof Error)) return String(input)
const cause = input.cause
return cause === undefined ? input.message : `${input.message}\nCaused by: ${errorText(cause)}`
}
function updateProgress(progress: Progress) {
if (runtimeState.status === "running") runtimeState = { status: "running", progress }
}
export function run(options: Options = {}): Effect.Effect<RunResult, never, Database.Service | Global.Service> {
return lock.withPermit(
Effect.gen(function* () {
const { db } = yield* Database.Service
const global = yield* Global.Service
const state = yield* readState(db)
if (state?.phase === "completed") return { status: "completed" as const }
if (!(yield* hasLegacySessions(db))) return { status: "completed" as const }
const migrate = Effect.gen(function* () {
const now = Date.now()
yield* db.run(sql`
INSERT OR IGNORE INTO project (id, worktree, time_created, time_updated, sandboxes)
VALUES (${Project.ID.global}, ${path.parse(global.data).root}, ${now}, ${now}, '[]')
`)
if (state === undefined)
yield* db
.transaction((tx) =>
Effect.gen(function* () {
while (true) {
yield* tx.run(sql`
DELETE FROM event
WHERE rowid IN (SELECT rowid FROM event LIMIT ${EVENT_DELETE_BATCH_SIZE})
`)
const deleted = (yield* tx.get<{ value: number }>(sql`SELECT changes() AS value`))?.value ?? 0
if (deleted < EVENT_DELETE_BATCH_SIZE) break
yield* Effect.yieldNow
}
yield* tx
.insert(KVTable)
.values({ key: MIGRATION_STATE_KEY, value: { phase: "sessions" } })
.run()
}),
)
.pipe(Effect.orDie)
const sourceTotal = yield* countNextSessions(nextPath(options, global.data))
const legacyTotal = (yield* db.get<{ value: number }>(sql`SELECT COUNT(*) AS value FROM session`))?.value ?? 0
const cursor = state?.phase === "sessions" ? state.cursor : undefined
const migrated =
cursor !== undefined
? ((yield* db.get<{ value: number }>(sql`SELECT COUNT(*) AS value FROM session WHERE id >= ${cursor}`))
?.value ?? 0)
: 0
const denominator = sourceTotal + legacyTotal
updateProgress({ label: "Migrating sessions", numerator: migrated, denominator })
yield* importNextDatabase(db, nextPath(options, global.data), (completed) => {
updateProgress({ label: "Migrating sessions", numerator: migrated + completed, denominator })
})
updateProgress({ label: "Migrating sessions", numerator: migrated + sourceTotal, denominator })
const projects = new Set(
(yield* db.all<{ id: string }>(sql`SELECT id FROM project`)).map((project) => project.id),
)
while (true) {
const state = yield* readState(db)
const cursorValue = state?.phase === "sessions" ? state.cursor : undefined
const nextID = yield* db.get<{ id: string; project_id: string }>(
cursorValue === undefined
? sql`SELECT id, project_id FROM session ORDER BY id DESC LIMIT 1`
: sql`SELECT id, project_id FROM session WHERE id < ${cursorValue} ORDER BY id DESC LIMIT 1`,
)
if (!nextID) break
yield* db
.transaction((tx) =>
Effect.gen(function* () {
yield* tx
.insert(KVTable)
.values({ key: MIGRATION_STATE_KEY, value: { phase: "sessions", cursor: nextID.id } })
.onConflictDoUpdate({
target: KVTable.key,
set: { value: { phase: "sessions", cursor: nextID.id }, time_updated: Date.now() },
})
.run()
const projectID = projects.has(nextID.project_id) ? nextID.project_id : Project.ID.global
if (projectID !== nextID.project_id)
yield* Effect.logWarning("Reassigned V1 session with missing project", {
sessionID: nextID.id,
projectID: nextID.project_id,
})
yield* tx.run(sql`
INSERT OR IGNORE INTO session_v2 (
id, project_id, workspace_id, parent_id, slug, directory, path, title, version, share_url,
summary_additions, summary_deletions, summary_files, summary_diffs, metadata, cost,
tokens_input, tokens_output, tokens_reasoning, tokens_cache_read, tokens_cache_write,
revert, permission, agent, model, time_created, time_updated, time_compacting, time_archived
)
SELECT
id, ${projectID}, workspace_id, parent_id, slug, directory, path, title, version, share_url,
summary_additions, summary_deletions, summary_files, summary_diffs, metadata, cost,
tokens_input, tokens_output, tokens_reasoning, tokens_cache_read, tokens_cache_write,
revert, permission, agent, model, time_created, time_updated, time_compacting, time_archived
FROM session
WHERE id = ${nextID.id}
`)
const next = yield* tx
.select()
.from(SessionTable)
.where(eq(SessionTable.id, SessionSchema.ID.make(nextID.id)))
.get()
if (!next) return yield* Effect.die(new Error(`Failed to copy V1 session ${nextID.id}`))
const sourceMessages = yield* tx.all<SourceMessage>(
sql`SELECT id, session_id, time_created, time_updated, data FROM message WHERE session_id = ${next.id}`,
)
const sourceParts = yield* tx.all<SourcePart>(
sql`SELECT id, message_id, session_id, time_created, time_updated, data FROM part WHERE session_id = ${next.id}`,
)
const transformed = transformSession({ session: next, messages: sourceMessages, parts: sourceParts })
yield* Effect.forEach(transformed.warnings, (warning) =>
Effect.logWarning("Skipped V1 migration row", warning),
)
yield* tx.delete(SessionMessageTable).where(eq(SessionMessageTable.session_id, next.id)).run()
yield* Effect.forEach(transformed.messages, (message) =>
tx
.insert(SessionMessageTable)
.values({
id: SessionMessage.ID.make(message.id),
session_id: SessionSchema.ID.make(message.session_id),
type: message.type,
seq: message.seq,
time_created: message.time_created,
time_updated: message.time_updated,
data: sql`${JSON.stringify(message.data)}`,
})
.run(),
)
yield* tx
.update(SessionTable)
.set({ ...transformed.session, time_updated: next.time_updated })
.where(eq(SessionTable.id, next.id))
.run()
yield* tx
.insert(EventSequenceTable)
.values({ aggregate_id: next.id, seq: transformed.watermark })
.onConflictDoUpdate({
target: EventSequenceTable.aggregate_id,
set: { seq: transformed.watermark, owner_id: null },
})
.run()
}),
)
.pipe(Effect.orDie)
if (runtimeState.status === "running")
runtimeState = {
status: "running",
progress: {
label: "Migrating sessions",
numerator: (runtimeState.progress.numerator ?? 0) + 1,
denominator,
},
}
yield* Effect.yieldNow
}
yield* db
.transaction((tx) =>
Effect.gen(function* () {
yield* tx
.insert(KVTable)
.values({ key: MIGRATION_STATE_KEY, value: { phase: "completed" } })
.onConflictDoUpdate({
target: KVTable.key,
set: { value: { phase: "completed" }, time_updated: Date.now() },
})
.run()
}),
)
.pipe(Effect.orDie)
return { status: "completed" as const }
})
return yield* migrate
}).pipe(Effect.orDie),
)
}
function nextPath(options: Options, data: string) {
if (options.nextDatabasePath) return options.nextDatabasePath
if (process.env.OPENCODE_DB === ":memory:") return undefined
return path.join(data, "opencode-next.db")
}
function openNextDatabase(sourcePath: string) {
return Effect.acquireRelease(
Effect.gen(function* () {
const sqlite = yield* Effect.promise(() => import("bun:sqlite"))
return new sqlite.Database(sourcePath, { readonly: true, strict: true })
}),
(source) => Effect.sync(() => source.close()),
)
}
function countNextSessions(sourcePath: string | undefined) {
if (!sourcePath || !existsSync(sourcePath)) return Effect.succeed(0)
return Effect.scoped(
Effect.gen(function* () {
const source = yield* openNextDatabase(sourcePath)
if (!isNextDatabase(source)) return 0
return source.query<{ value: number }, []>("SELECT COUNT(*) AS value FROM session").get()?.value ?? 0
}),
).pipe(Effect.orElseSucceed(() => 0))
}
function importNextDatabase(
db: Database.Interface["db"],
sourcePath: string | undefined,
onProgress: (completed: number) => void,
): Effect.Effect<void, unknown> {
if (!sourcePath || !existsSync(sourcePath)) return Effect.void
return Effect.scoped(
Effect.gen(function* () {
const source = yield* openNextDatabase(sourcePath)
if (!isNextDatabase(source)) {
yield* Effect.logWarning("Skipped incompatible opencode-next.db", { path: sourcePath })
return
}
source.run("BEGIN")
yield* Effect.addFinalizer(() =>
Effect.sync(() => {
if (source.inTransaction) source.run("ROLLBACK")
}),
)
const projects = new Map(
source
.query<NextProject, []>("SELECT * FROM project")
.all()
.map((project) => [project.id, project]),
)
const sessions = source.query<NextSession, []>("SELECT * FROM session ORDER BY id DESC").all()
for (const [index, session] of sessions.entries()) {
const project = projects.get(session.project_id)
const projectID = project ? session.project_id : Project.ID.global
if (!project) {
yield* Effect.logWarning("Reassigned previous V2 session with missing project", {
sessionID: session.id,
projectID: session.project_id,
})
}
const messages = source
.query<
NextMessage,
[string]
>("SELECT id, session_id, type, seq, time_created, time_updated, data FROM session_message WHERE session_id = ? ORDER BY seq")
.all(session.id)
yield* db
.transaction((tx) =>
Effect.gen(function* () {
if (project)
yield* tx.run(sql`
INSERT OR IGNORE INTO project (
id, worktree, vcs, name, icon_url, icon_url_override, icon_color,
time_created, time_updated, time_initialized, sandboxes, commands
) VALUES (
${project.id}, ${project.worktree}, ${project.vcs}, ${project.name}, ${project.icon_url},
${project.icon_url_override}, ${project.icon_color}, ${project.time_created}, ${project.time_updated},
${project.time_initialized}, ${project.sandboxes}, ${project.commands}
)
`)
const existing = yield* tx
.select({ id: SessionTable.id })
.from(SessionTable)
.where(eq(SessionTable.id, SessionSchema.ID.make(session.id)))
.get()
if (existing) return
yield* tx.run(sql`
INSERT INTO session_v2 (
id, project_id, workspace_id, parent_id, fork_session_id, fork_boundary, slug, directory,
path, title, version, share_url, summary_additions, summary_deletions, summary_files,
summary_diffs, metadata, cost, tokens_input, tokens_output, tokens_reasoning, tokens_cache_read,
tokens_cache_write, revert, permission, agent, model, time_created, time_updated, time_compacting,
time_archived, time_suspended
) VALUES (
${session.id}, ${projectID}, ${session.workspace_id}, ${session.parent_id},
${session.fork_session_id}, ${session.fork_boundary}, ${session.slug}, ${session.directory},
${session.path}, ${session.title}, ${session.version}, ${session.share_url},
${session.summary_additions}, ${session.summary_deletions}, ${session.summary_files},
${session.summary_diffs}, ${session.metadata}, ${session.cost}, ${session.tokens_input},
${session.tokens_output}, ${session.tokens_reasoning}, ${session.tokens_cache_read},
${session.tokens_cache_write}, ${session.revert}, ${session.permission}, ${session.agent},
${session.model}, ${session.time_created}, ${session.time_updated}, ${session.time_compacting},
${session.time_archived}, ${session.time_suspended}
)
`)
yield* Effect.forEach(messages, (message) =>
tx
.insert(SessionMessageTable)
.values({
id: SessionMessage.ID.make(message.id),
session_id: SessionSchema.ID.make(message.session_id),
type: message.type as SessionMessage.Type,
seq: message.seq,
time_created: message.time_created,
time_updated: message.time_updated,
data: sql`${message.data}`,
})
.run(),
)
yield* tx
.insert(EventSequenceTable)
.values({ aggregate_id: session.id, seq: messages.at(-1)?.seq ?? -1 })
.onConflictDoUpdate({
target: EventSequenceTable.aggregate_id,
set: { seq: messages.at(-1)?.seq ?? -1, owner_id: null },
})
.run()
}),
)
.pipe(Effect.orDie)
onProgress(index + 1)
yield* Effect.yieldNow
}
source.run("COMMIT")
}),
)
}
function isNextDatabase(source: SQLiteDatabase) {
const tables = new Set(
source
.query<{ name: string }, []>("SELECT name FROM sqlite_master WHERE type = 'table'")
.all()
.map((table) => table.name),
)
return tables.has("project") && tables.has("session") && tables.has("session_message")
}
function row(
source: SourceMessage,
message: {
readonly id: string
readonly type: SessionMessage.Type
readonly time: { readonly created: number }
readonly [key: string]: unknown
},
): TransformResult["messages"][number] {
const { id, type, ...data } = message
return {
id,
session_id: source.session_id,
type,
seq: 0,
time_created: source.time_created,
time_updated: source.time_updated,
data,
}
}
function migrateTool(part: typeof SessionV1.ToolPart.Type, fallback: number) {
const base = {
type: "tool" as const,
id: part.callID,
name: part.tool,
...(part.metadata ? { providerState: part.metadata } : {}),
}
if (part.state.status === "completed")
return {
...base,
state: {
status: "completed",
input: part.state.input,
content:
part.state.time.compacted === undefined
? [
{ type: "text", text: part.state.output },
...(part.state.attachments ?? []).map((file) => ({
type: "file" as const,
uri: file.url,
mime: file.mime,
...(file.filename ? { name: file.filename } : {}),
})),
]
: [{ type: "text", text: "[Old tool result content cleared]" }],
metadata: part.state.metadata,
},
time: { created: part.state.time.start, completed: part.state.time.end },
}
if (part.state.status === "error")
return {
...base,
state: {
status: "error",
input: part.state.input,
error: { type: "tool.execution", message: part.state.error },
...(typeof part.state.metadata?.output === "string"
? { content: [{ type: "text", text: part.state.metadata.output }] }
: {}),
...(part.state.metadata ? { metadata: part.state.metadata } : {}),
},
time: { created: part.state.time.start, completed: part.state.time.end },
}
return {
...base,
state: {
status: "error",
input: part.state.input,
error: { type: "tool.interrupted", message: "Tool execution was interrupted before V2 migration" },
...(part.state.status === "running" && part.state.metadata ? { metadata: part.state.metadata } : {}),
},
time: { created: part.state.status === "running" ? part.state.time.start : fallback },
}
}
function migrateError(error: NonNullable<(typeof SessionV1.Assistant.Type)["error"]>) {
const message =
"message" in error.data
? error.data.message
: error.name === "MessageOutputLengthError"
? "The model exceeded its output limit"
: error.name
const type =
error.name === "ProviderAuthError"
? "provider.auth"
: error.name === "ContentFilterError"
? "provider.content-filter"
: error.name === "ContextOverflowError"
? "provider.invalid-request"
: error.name === "StructuredOutputError" || error.name === "MessageOutputLengthError"
? "provider.invalid-output"
: error.name === "MessageAbortedError"
? "aborted"
: error.name === "APIError"
? "provider.error"
: "unknown"
return { type, message }
}
function normalizeFinish(finish: string | undefined) {
if (!finish) return undefined
return (
(["stop", "length", "tool-calls", "content-filter", "error", "unknown"] as const).find(
(value) => value === finish,
) ?? "unknown"
)
}
function migrateFile(part: SessionV1.FilePart) {
if (!part.url.startsWith("data:")) return []
const comma = part.url.indexOf(",")
if (comma < 0) return []
const header = part.url.slice(0, comma)
const payload = part.url.slice(comma + 1)
const data = header.endsWith(";base64")
? Buffer.from(payload, "base64").toString("base64")
: Buffer.from(decodeURIComponent(payload)).toString("base64")
return [
{
data,
mime: part.mime,
source:
part.source?.type === "resource" ? { type: "uri" as const, uri: part.source.uri } : { type: "inline" as const },
...(part.filename ? { name: part.filename } : {}),
...(part.source
? { mention: { text: part.source.text.value, start: part.source.text.start, end: part.source.text.end } }
: {}),
},
]
}
function unavailableFile(part: SessionV1.FilePart) {
const label = part.filename ?? (part.source?.type === "resource" ? part.source.uri : part.url)
return `[Attachment unavailable after migration: ${label} (${part.mime})]`
}
function syntheticID(source: string, used: Set<string>) {
const prefix = source.slice(0, 16)
const alphabet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
for (let salt = 0; ; salt++) {
const hex = new Bun.CryptoHasher("sha256").update(`v1-synthetic:${source}${salt ? `:${salt}` : ""}`).digest("hex")
let value = BigInt(`0x${hex}`)
let suffix = ""
while (suffix.length < 14) {
suffix = alphabet[Number(value % 62n)] + suffix
value /= 62n
}
const id = prefix + suffix
if (used.has(id)) continue
used.add(id)
return id
}
}
function serializeRecent(
messages: ReadonlyArray<{ row: SourceMessage; value: typeof SessionV1.Info.Type }>,
parts: Map<string, Array<{ row: SourcePart; value: typeof SessionV1.Part.Type }>>,
) {
return messages
.flatMap((message) => {
const owned = parts.get(message.row.id)?.map((part) => part.value) ?? []
if (message.value.role === "user")
return [
`[User]: ${owned
.filter((part) => part.type === "text" && !part.ignored)
.map((part) => (part.type === "text" ? part.text : ""))
.join("\n\n")}`,
]
return owned.flatMap((part) =>
part.type === "text"
? [`[Assistant]: ${part.text}`]
: part.type === "reasoning" && part.text
? [`[Assistant reasoning]: ${part.text}`]
: [],
)
})
.join("\n\n")
}
function readState(db: Database.Interface["db"]): Effect.Effect<MigrationState | undefined> {
return db
.select({ value: KVTable.value })
.from(KVTable)
.where(eq(KVTable.key, MIGRATION_STATE_KEY))
.get()
.pipe(
Effect.map((row) => parseState(row?.value)),
Effect.orDie,
)
}
function parseState(input: unknown): MigrationState | undefined {
if (!input || typeof input !== "object" || !("phase" in input)) return
if (input.phase === "completed") return { phase: "completed" }
if (input.phase !== "sessions") return
if (!("cursor" in input) || input.cursor === undefined) return { phase: "sessions" }
if (typeof input.cursor === "string") return { phase: "sessions", cursor: input.cursor }
}
function hasLegacySessions(db: Database.Interface["db"]) {
return db.get(sql`SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'session'`).pipe(
Effect.map((row) => row !== undefined),
Effect.orDie,
)
}
@@ -0,0 +1,11 @@
import { Effect, Layer } from "effect"
export type Status = { readonly status: "completed" }
export const layer = Layer.empty
export function status() {
return Effect.succeed({ status: "completed" } as const)
}
export function run() {
return Effect.succeed({ status: "completed" } as const)
}
+2 -997
View File
@@ -1,997 +1,2 @@
export * as V1Migration from "./v1-migration.js"
import { Cause, Effect, Layer, Option, Schema, Semaphore } from "effect"
import { Database } from "./database.js"
import { SessionMessageTable, SessionTable } from "../session/sql.js"
import { SessionV1 } from "@opencode-ai/schema/session-v1"
import { SessionMessage } from "../session/message.js"
import { SessionSchema } from "../session/schema.js"
import { KVTable } from "../kv/sql.js"
import { EventSequenceTable } from "../event/sql.js"
import { eq, sql } from "drizzle-orm"
import { Global } from "@opencode-ai/util/global"
import { existsSync } from "node:fs"
import path from "node:path"
import type { Database as SQLiteDatabase } from "bun:sqlite"
import { Project } from "@opencode-ai/schema/project"
export type SourceMessage = {
readonly id: string
readonly session_id: string
readonly time_created: number
readonly time_updated: number
readonly data: string
}
export type SourcePart = {
readonly id: string
readonly message_id: string
readonly session_id: string
readonly time_created: number
readonly time_updated: number
readonly data: string
}
export type TransformInput = {
readonly session: typeof SessionTable.$inferSelect
readonly messages: ReadonlyArray<SourceMessage>
readonly parts: ReadonlyArray<SourcePart>
}
export type Warning = {
readonly reason: string
readonly sessionID: string
readonly messageID?: string
readonly partID?: string
readonly observedType?: string
}
export type TransformResult = {
readonly messages: ReadonlyArray<{
readonly id: string
readonly session_id: string
readonly type: SessionMessage.Type
readonly seq: number
readonly time_created: number
readonly time_updated: number
readonly data: Record<string, unknown>
}>
readonly session: Pick<
typeof SessionTable.$inferInsert,
| "agent"
| "model"
| "cost"
| "tokens_input"
| "tokens_output"
| "tokens_reasoning"
| "tokens_cache_read"
| "tokens_cache_write"
| "revert"
| "time_compacting"
>
readonly watermark: number
readonly warnings: ReadonlyArray<Warning>
}
type Progress = {
readonly label: string
readonly numerator?: number
readonly denominator?: number
}
export type Status =
| { readonly status: "required" | "completed" }
| { readonly status: "running"; readonly progress: Progress }
| { readonly status: "error"; readonly error: string }
type RunResult = {
readonly status: "completed"
}
type Options = {
readonly nextDatabasePath?: string
}
type MigrationState = { readonly phase: "sessions"; readonly cursor?: string } | { readonly phase: "completed" }
type RuntimeState =
| { readonly status: "idle" }
| { readonly status: "running"; readonly progress: Progress }
| { readonly status: "error"; readonly error: string }
type NextProject = {
readonly id: string
readonly worktree: string
readonly vcs: string | null
readonly name: string | null
readonly icon_url: string | null
readonly icon_url_override: string | null
readonly icon_color: string | null
readonly time_created: number
readonly time_updated: number
readonly time_initialized: number | null
readonly sandboxes: string
readonly commands: string | null
}
type NextSession = {
readonly id: string
readonly project_id: string
readonly workspace_id: string | null
readonly parent_id: string | null
readonly fork_session_id: string | null
readonly fork_boundary: string | null
readonly slug: string
readonly directory: string
readonly path: string | null
readonly title: string | null
readonly version: string
readonly share_url: string | null
readonly summary_additions: number | null
readonly summary_deletions: number | null
readonly summary_files: number | null
readonly summary_diffs: string | null
readonly metadata: string | null
readonly cost: number
readonly tokens_input: number
readonly tokens_output: number
readonly tokens_reasoning: number
readonly tokens_cache_read: number
readonly tokens_cache_write: number
readonly revert: string | null
readonly permission: string | null
readonly agent: string | null
readonly model: string | null
readonly time_created: number
readonly time_updated: number
readonly time_compacting: number | null
readonly time_archived: number | null
readonly time_suspended: number | null
}
type NextMessage = {
readonly id: string
readonly session_id: string
readonly type: string
readonly seq: number
readonly time_created: number
readonly time_updated: number
readonly data: string
}
const lock = Semaphore.makeUnsafe(1)
const MIGRATION_STATE_KEY = "migration.v1-v2"
const EVENT_DELETE_BATCH_SIZE = 1_000
const decodeJson = Schema.decodeUnknownOption(Schema.UnknownFromJsonString)
const decodeMessage = Schema.decodeUnknownOption(SessionV1.Info)
const decodePart = Schema.decodeUnknownOption(SessionV1.Part)
let runtimeState: RuntimeState = { status: "idle" }
export function transformSession(input: TransformInput): TransformResult {
const warnings: Warning[] = []
const messages = input.messages
.map((row) => {
const value = Option.getOrUndefined(decodeJson(row.data))
const decoded =
value && typeof value === "object"
? Option.getOrUndefined(decodeMessage({ ...value, id: row.id, sessionID: row.session_id }))
: undefined
if (decoded) return { row, value: decoded }
warnings.push({ reason: "invalid-message", sessionID: input.session.id, messageID: row.id })
return undefined
})
.filter((item): item is NonNullable<typeof item> => item !== undefined)
.sort((a, b) => a.row.time_created - b.row.time_created || a.row.id.localeCompare(b.row.id))
const messageIDs = new Set(input.messages.map((row) => row.id))
const parts = input.parts
.map((row) => {
const value = Option.getOrUndefined(decodeJson(row.data))
const observedType = value && typeof value === "object" && "type" in value ? String(value.type) : undefined
if (!messageIDs.has(row.message_id)) {
warnings.push({
reason: "orphan-part",
sessionID: input.session.id,
messageID: row.message_id,
partID: row.id,
observedType,
})
return undefined
}
const decoded =
value && typeof value === "object"
? Option.getOrUndefined(
decodePart({ ...value, id: row.id, messageID: row.message_id, sessionID: row.session_id }),
)
: undefined
if (decoded) return { row, value: decoded }
warnings.push({
reason: "invalid-part",
sessionID: input.session.id,
messageID: row.message_id,
partID: row.id,
observedType,
})
return undefined
})
.filter((item): item is NonNullable<typeof item> => item !== undefined)
.sort((a, b) => a.row.id.localeCompare(b.row.id))
const byMessage = Map.groupBy(parts, (item) => item.row.message_id)
const paired = new Set<string>()
const used = new Set(messages.map((item) => item.row.id))
const projected = messages
.flatMap((item) => {
if (paired.has(item.row.id)) return []
const owned = byMessage.get(item.row.id)?.map((part) => part.value) ?? []
if (item.value.role === "user") {
const compaction = owned.find((part) => part.type === "compaction")
if (compaction?.type === "compaction") {
const pairedSummary = messages.find(
(candidate) =>
candidate.value.role === "assistant" &&
candidate.value.parentID === item.row.id &&
candidate.value.summary,
)
if (!pairedSummary || pairedSummary.value.role !== "assistant") return []
paired.add(pairedSummary.row.id)
if (pairedSummary.value.error || pairedSummary.value.time.completed === undefined) return []
const summary = pairedSummary
const summaryText = (byMessage.get(summary.row.id) ?? [])
.map((part) => part.value)
.filter((part) => part.type === "text" && part.text.length > 0)
.map((part) => (part.type === "text" ? part.text : ""))
.join("\n\n")
const tailIndex = compaction.tail_start_id
? messages.findIndex((candidate) => candidate.row.id === compaction.tail_start_id)
: -1
const compactionIndex = messages.findIndex((candidate) => candidate.row.id === item.row.id)
const tail = tailIndex < 0 ? [] : messages.slice(tailIndex, compactionIndex)
return [
row(
{ ...item.row, time_updated: Math.max(item.row.time_updated, summary.row.time_updated) },
{
id: item.row.id,
type: "compaction",
status: "completed",
reason: compaction.auto ? "auto" : "manual",
summary: summaryText,
recent: serializeRecent(tail, byMessage),
time: { created: item.row.time_created },
},
),
]
}
const subtasks = owned.filter((part) => part.type === "subtask")
const visible = owned.filter((part) => part.type === "text" && !part.ignored)
const files = owned.filter((part) => part.type === "file")
const agents = owned.filter((part) => part.type === "agent")
if (subtasks.length > 0 && visible.length === 0 && files.length === 0 && agents.length === 0) return []
const ordinary = visible.filter((part) => part.type === "text" && !part.synthetic)
const synthetic = visible.filter((part) => part.type === "text" && part.synthetic)
const attachments = files.flatMap((part) => (part.type === "file" ? migrateFile(part) : []))
const unavailable = files.flatMap((part) =>
part.type === "file" && !part.url.startsWith("data:") ? [unavailableFile(part)] : [],
)
const text = owned
.flatMap((part) => {
if (part.type === "text" && !part.ignored && !part.synthetic) return [part.text]
if (part.type === "file" && !part.url.startsWith("data:")) return [unavailableFile(part)]
return []
})
.join("\n\n")
const agentAttachments = agents.map((part) =>
part.type === "agent"
? {
name: part.name,
...(part.source
? { mention: { text: part.source.value, start: part.source.start, end: part.source.end } }
: {}),
}
: { name: "" },
)
if (
ordinary.length === 0 &&
unavailable.length === 0 &&
synthetic.length > 0 &&
attachments.length === 0 &&
agentAttachments.length === 0
)
return [
row(item.row, {
id: item.row.id,
type: "synthetic",
text: synthetic.map((part) => (part.type === "text" ? part.text : "")).join("\n\n"),
time: { created: item.row.time_created },
}),
]
const user = row(item.row, {
id: item.row.id,
type: "user",
text,
...(attachments.length ? { files: attachments } : {}),
...(agentAttachments.length ? { agents: agentAttachments } : {}),
time: { created: item.row.time_created },
})
if (synthetic.length === 0) return [user]
return [
user,
row(item.row, {
id: syntheticID(item.row.id, used),
type: "synthetic",
text: synthetic.map((part) => (part.type === "text" ? part.text : "")).join("\n\n"),
time: { created: item.row.time_created },
}),
]
}
if (item.value.role !== "assistant") return []
const assistant = item.value
const parent = messages.find((candidate) => candidate.row.id === assistant.parentID)
const parentParts = parent ? (byMessage.get(parent.row.id)?.map((part) => part.value) ?? []) : []
if (
parentParts.some((part) => part.type === "subtask") &&
owned.some((part) => part.type === "tool" && part.tool === "task")
)
return []
const content = owned.flatMap((part): Array<Record<string, unknown>> => {
if (part.type === "text")
return [{ type: "text", text: part.text, ...(part.metadata ? { state: part.metadata } : {}) }]
if (part.type === "reasoning")
return [
{
type: "reasoning",
text: part.text,
...(part.metadata ? { state: part.metadata } : {}),
time: { created: part.time.start, ...(part.time.end === undefined ? {} : { completed: part.time.end }) },
},
]
if (part.type !== "tool") return []
return [migrateTool(part, item.row.time_created)]
})
const start =
owned.flatMap((part) => (part.type === "step-start" && part.snapshot ? [part.snapshot] : []))[0] ??
owned.flatMap((part) => (part.type === "snapshot" ? [part.snapshot] : []))[0] ??
owned.flatMap((part) => (part.type === "patch" ? [part.hash] : []))[0]
const end = owned.flatMap((part) => (part.type === "step-finish" && part.snapshot ? [part.snapshot] : [])).at(-1)
const snapshotFiles = Array.from(new Set(owned.flatMap((part) => (part.type === "patch" ? part.files : []))))
const finish = normalizeFinish(assistant.finish)
return [
row(item.row, {
id: item.row.id,
type: "assistant",
agent: assistant.agent,
model: {
providerID: assistant.providerID,
id: assistant.modelID,
variant: assistant.variant ?? "default",
},
content,
...(start || end || snapshotFiles.length
? {
snapshot: {
...(start ? { start } : {}),
...(end ? { end } : {}),
...(snapshotFiles.length ? { files: snapshotFiles } : {}),
},
}
: {}),
...(finish ? { finish } : {}),
cost: assistant.cost,
tokens: {
input: assistant.tokens.input,
output: assistant.tokens.output,
reasoning: assistant.tokens.reasoning,
cache: assistant.tokens.cache,
},
...(assistant.error ? { error: migrateError(assistant.error) } : {}),
time: {
created: item.row.time_created,
...(assistant.time.completed === undefined ? {} : { completed: item.row.time_updated }),
},
}),
]
})
.map((item, seq) => ({ ...item, seq }))
const assistants = messages
.filter((item) => item.value.role === "assistant")
.map((item) => item.value)
.filter((item): item is SessionV1.Assistant => item.role === "assistant")
const latestUser = messages.findLast((item) => {
if (item.value.role !== "user") return false
const owned = byMessage.get(item.row.id) ?? []
if (owned.some((part) => part.value.type === "compaction")) return false
return !owned.some((part) => part.value.type === "subtask") || !owned.every((part) => part.value.type === "subtask")
})
return {
messages: projected,
session: {
agent: input.session.agent ?? (latestUser?.value.role === "user" ? latestUser.value.agent : null),
model:
input.session.model ??
(latestUser?.value.role === "user"
? {
id: latestUser.value.model.modelID,
providerID: latestUser.value.model.providerID,
variant: latestUser.value.model.variant ?? "default",
}
: null),
cost: assistants.reduce((total, item) => total + item.cost, 0),
tokens_input: assistants.reduce((total, item) => total + item.tokens.input, 0),
tokens_output: assistants.reduce((total, item) => total + item.tokens.output, 0),
tokens_reasoning: assistants.reduce((total, item) => total + item.tokens.reasoning, 0),
tokens_cache_read: assistants.reduce((total, item) => total + item.tokens.cache.read, 0),
tokens_cache_write: assistants.reduce((total, item) => total + item.tokens.cache.write, 0),
revert: null,
time_compacting: null,
},
watermark: projected.length - 1,
warnings,
}
}
export function status(): Effect.Effect<Status, never, Database.Service> {
return Effect.gen(function* () {
const { db } = yield* Database.Service
if (!(yield* hasLegacySessions(db))) return { status: "completed" as const }
const state = yield* readState(db)
if (runtimeState.status === "running") return runtimeState
if (runtimeState.status === "error") return runtimeState
if (state?.phase === "completed") return { status: "completed" as const }
return { status: "required" as const }
}).pipe(Effect.orDie)
}
export const layer = Layer.effectDiscard(
Effect.gen(function* () {
runtimeState = { status: "running", progress: { label: "Clearing old events" } }
yield* run().pipe(
Effect.matchCauseEffect({
onFailure: (cause) =>
Effect.sync(() => {
runtimeState = { status: "error", error: errorText(Cause.squash(cause)) }
}).pipe(Effect.andThen(Effect.logError("V1 migration failed", { cause }))),
onSuccess: () =>
Effect.sync(() => {
runtimeState = { status: "idle" }
}),
}),
Effect.forkScoped({ startImmediately: true }),
)
}),
)
function errorText(input: unknown): string {
if (!(input instanceof Error)) return String(input)
const cause = input.cause
return cause === undefined ? input.message : `${input.message}\nCaused by: ${errorText(cause)}`
}
function updateProgress(progress: Progress) {
if (runtimeState.status === "running") runtimeState = { status: "running", progress }
}
export function run(options: Options = {}): Effect.Effect<RunResult, never, Database.Service | Global.Service> {
return lock.withPermit(
Effect.gen(function* () {
const { db } = yield* Database.Service
const global = yield* Global.Service
const state = yield* readState(db)
if (state?.phase === "completed") return { status: "completed" as const }
if (!(yield* hasLegacySessions(db))) return { status: "completed" as const }
const migrate = Effect.gen(function* () {
const now = Date.now()
yield* db.run(sql`
INSERT OR IGNORE INTO project (id, worktree, time_created, time_updated, sandboxes)
VALUES (${Project.ID.global}, ${path.parse(global.data).root}, ${now}, ${now}, '[]')
`)
if (state === undefined)
yield* db
.transaction((tx) =>
Effect.gen(function* () {
while (true) {
yield* tx.run(sql`
DELETE FROM event
WHERE rowid IN (SELECT rowid FROM event LIMIT ${EVENT_DELETE_BATCH_SIZE})
`)
const deleted = (yield* tx.get<{ value: number }>(sql`SELECT changes() AS value`))?.value ?? 0
if (deleted < EVENT_DELETE_BATCH_SIZE) break
yield* Effect.yieldNow
}
yield* tx
.insert(KVTable)
.values({ key: MIGRATION_STATE_KEY, value: { phase: "sessions" } })
.run()
}),
)
.pipe(Effect.orDie)
const sourceTotal = yield* countNextSessions(nextPath(options, global.data))
const legacyTotal = (yield* db.get<{ value: number }>(sql`SELECT COUNT(*) AS value FROM session`))?.value ?? 0
const cursor = state?.phase === "sessions" ? state.cursor : undefined
const migrated =
cursor !== undefined
? ((yield* db.get<{ value: number }>(sql`SELECT COUNT(*) AS value FROM session WHERE id >= ${cursor}`))
?.value ?? 0)
: 0
const denominator = sourceTotal + legacyTotal
updateProgress({ label: "Migrating sessions", numerator: migrated, denominator })
yield* importNextDatabase(db, nextPath(options, global.data), (completed) => {
updateProgress({ label: "Migrating sessions", numerator: migrated + completed, denominator })
})
updateProgress({ label: "Migrating sessions", numerator: migrated + sourceTotal, denominator })
const projects = new Set(
(yield* db.all<{ id: string }>(sql`SELECT id FROM project`)).map((project) => project.id),
)
while (true) {
const state = yield* readState(db)
const cursorValue = state?.phase === "sessions" ? state.cursor : undefined
const nextID = yield* db.get<{ id: string; project_id: string }>(
cursorValue === undefined
? sql`SELECT id, project_id FROM session ORDER BY id DESC LIMIT 1`
: sql`SELECT id, project_id FROM session WHERE id < ${cursorValue} ORDER BY id DESC LIMIT 1`,
)
if (!nextID) break
yield* db
.transaction((tx) =>
Effect.gen(function* () {
yield* tx
.insert(KVTable)
.values({ key: MIGRATION_STATE_KEY, value: { phase: "sessions", cursor: nextID.id } })
.onConflictDoUpdate({
target: KVTable.key,
set: { value: { phase: "sessions", cursor: nextID.id }, time_updated: Date.now() },
})
.run()
const projectID = projects.has(nextID.project_id) ? nextID.project_id : Project.ID.global
if (projectID !== nextID.project_id)
yield* Effect.logWarning("Reassigned V1 session with missing project", {
sessionID: nextID.id,
projectID: nextID.project_id,
})
yield* tx.run(sql`
INSERT OR IGNORE INTO session_v2 (
id, project_id, workspace_id, parent_id, slug, directory, path, title, version, share_url,
summary_additions, summary_deletions, summary_files, summary_diffs, metadata, cost,
tokens_input, tokens_output, tokens_reasoning, tokens_cache_read, tokens_cache_write,
revert, permission, agent, model, time_created, time_updated, time_compacting, time_archived
)
SELECT
id, ${projectID}, workspace_id, parent_id, slug, directory, path, title, version, share_url,
summary_additions, summary_deletions, summary_files, summary_diffs, metadata, cost,
tokens_input, tokens_output, tokens_reasoning, tokens_cache_read, tokens_cache_write,
revert, permission, agent, model, time_created, time_updated, time_compacting, time_archived
FROM session
WHERE id = ${nextID.id}
`)
const next = yield* tx
.select()
.from(SessionTable)
.where(eq(SessionTable.id, SessionSchema.ID.make(nextID.id)))
.get()
if (!next) return yield* Effect.die(new Error(`Failed to copy V1 session ${nextID.id}`))
const sourceMessages = yield* tx.all<SourceMessage>(
sql`SELECT id, session_id, time_created, time_updated, data FROM message WHERE session_id = ${next.id}`,
)
const sourceParts = yield* tx.all<SourcePart>(
sql`SELECT id, message_id, session_id, time_created, time_updated, data FROM part WHERE session_id = ${next.id}`,
)
const transformed = transformSession({ session: next, messages: sourceMessages, parts: sourceParts })
yield* Effect.forEach(transformed.warnings, (warning) =>
Effect.logWarning("Skipped V1 migration row", warning),
)
yield* tx.delete(SessionMessageTable).where(eq(SessionMessageTable.session_id, next.id)).run()
yield* Effect.forEach(transformed.messages, (message) =>
tx
.insert(SessionMessageTable)
.values({
id: SessionMessage.ID.make(message.id),
session_id: SessionSchema.ID.make(message.session_id),
type: message.type,
seq: message.seq,
time_created: message.time_created,
time_updated: message.time_updated,
data: sql`${JSON.stringify(message.data)}`,
})
.run(),
)
yield* tx
.update(SessionTable)
.set({ ...transformed.session, time_updated: next.time_updated })
.where(eq(SessionTable.id, next.id))
.run()
yield* tx
.insert(EventSequenceTable)
.values({ aggregate_id: next.id, seq: transformed.watermark })
.onConflictDoUpdate({
target: EventSequenceTable.aggregate_id,
set: { seq: transformed.watermark, owner_id: null },
})
.run()
}),
)
.pipe(Effect.orDie)
if (runtimeState.status === "running")
runtimeState = {
status: "running",
progress: {
label: "Migrating sessions",
numerator: (runtimeState.progress.numerator ?? 0) + 1,
denominator,
},
}
yield* Effect.yieldNow
}
yield* db
.transaction((tx) =>
Effect.gen(function* () {
yield* tx
.insert(KVTable)
.values({ key: MIGRATION_STATE_KEY, value: { phase: "completed" } })
.onConflictDoUpdate({
target: KVTable.key,
set: { value: { phase: "completed" }, time_updated: Date.now() },
})
.run()
}),
)
.pipe(Effect.orDie)
return { status: "completed" as const }
})
return yield* migrate
}).pipe(Effect.orDie),
)
}
function nextPath(options: Options, data: string) {
if (options.nextDatabasePath) return options.nextDatabasePath
if (process.env.OPENCODE_DB === ":memory:") return undefined
return path.join(data, "opencode-next.db")
}
function openNextDatabase(sourcePath: string) {
return Effect.acquireRelease(
Effect.gen(function* () {
const sqlite = yield* Effect.promise(() => import("bun:sqlite"))
return new sqlite.Database(sourcePath, { readonly: true, strict: true })
}),
(source) => Effect.sync(() => source.close()),
)
}
function countNextSessions(sourcePath: string | undefined) {
if (!sourcePath || !existsSync(sourcePath)) return Effect.succeed(0)
return Effect.scoped(
Effect.gen(function* () {
const source = yield* openNextDatabase(sourcePath)
if (!isNextDatabase(source)) return 0
return source.query<{ value: number }, []>("SELECT COUNT(*) AS value FROM session").get()?.value ?? 0
}),
).pipe(Effect.orElseSucceed(() => 0))
}
function importNextDatabase(
db: Database.Interface["db"],
sourcePath: string | undefined,
onProgress: (completed: number) => void,
): Effect.Effect<void, unknown> {
if (!sourcePath || !existsSync(sourcePath)) return Effect.void
return Effect.scoped(
Effect.gen(function* () {
const source = yield* openNextDatabase(sourcePath)
if (!isNextDatabase(source)) {
yield* Effect.logWarning("Skipped incompatible opencode-next.db", { path: sourcePath })
return
}
source.run("BEGIN")
yield* Effect.addFinalizer(() =>
Effect.sync(() => {
if (source.inTransaction) source.run("ROLLBACK")
}),
)
const projects = new Map(
source
.query<NextProject, []>("SELECT * FROM project")
.all()
.map((project) => [project.id, project]),
)
const sessions = source.query<NextSession, []>("SELECT * FROM session ORDER BY id DESC").all()
for (const [index, session] of sessions.entries()) {
const project = projects.get(session.project_id)
const projectID = project ? session.project_id : Project.ID.global
if (!project) {
yield* Effect.logWarning("Reassigned previous V2 session with missing project", {
sessionID: session.id,
projectID: session.project_id,
})
}
const messages = source
.query<
NextMessage,
[string]
>("SELECT id, session_id, type, seq, time_created, time_updated, data FROM session_message WHERE session_id = ? ORDER BY seq")
.all(session.id)
yield* db
.transaction((tx) =>
Effect.gen(function* () {
if (project)
yield* tx.run(sql`
INSERT OR IGNORE INTO project (
id, worktree, vcs, name, icon_url, icon_url_override, icon_color,
time_created, time_updated, time_initialized, sandboxes, commands
) VALUES (
${project.id}, ${project.worktree}, ${project.vcs}, ${project.name}, ${project.icon_url},
${project.icon_url_override}, ${project.icon_color}, ${project.time_created}, ${project.time_updated},
${project.time_initialized}, ${project.sandboxes}, ${project.commands}
)
`)
const existing = yield* tx
.select({ id: SessionTable.id })
.from(SessionTable)
.where(eq(SessionTable.id, SessionSchema.ID.make(session.id)))
.get()
if (existing) return
yield* tx.run(sql`
INSERT INTO session_v2 (
id, project_id, workspace_id, parent_id, fork_session_id, fork_boundary, slug, directory,
path, title, version, share_url, summary_additions, summary_deletions, summary_files,
summary_diffs, metadata, cost, tokens_input, tokens_output, tokens_reasoning, tokens_cache_read,
tokens_cache_write, revert, permission, agent, model, time_created, time_updated, time_compacting,
time_archived, time_suspended
) VALUES (
${session.id}, ${projectID}, ${session.workspace_id}, ${session.parent_id},
${session.fork_session_id}, ${session.fork_boundary}, ${session.slug}, ${session.directory},
${session.path}, ${session.title}, ${session.version}, ${session.share_url},
${session.summary_additions}, ${session.summary_deletions}, ${session.summary_files},
${session.summary_diffs}, ${session.metadata}, ${session.cost}, ${session.tokens_input},
${session.tokens_output}, ${session.tokens_reasoning}, ${session.tokens_cache_read},
${session.tokens_cache_write}, ${session.revert}, ${session.permission}, ${session.agent},
${session.model}, ${session.time_created}, ${session.time_updated}, ${session.time_compacting},
${session.time_archived}, ${session.time_suspended}
)
`)
yield* Effect.forEach(messages, (message) =>
tx
.insert(SessionMessageTable)
.values({
id: SessionMessage.ID.make(message.id),
session_id: SessionSchema.ID.make(message.session_id),
type: message.type as SessionMessage.Type,
seq: message.seq,
time_created: message.time_created,
time_updated: message.time_updated,
data: sql`${message.data}`,
})
.run(),
)
yield* tx
.insert(EventSequenceTable)
.values({ aggregate_id: session.id, seq: messages.at(-1)?.seq ?? -1 })
.onConflictDoUpdate({
target: EventSequenceTable.aggregate_id,
set: { seq: messages.at(-1)?.seq ?? -1, owner_id: null },
})
.run()
}),
)
.pipe(Effect.orDie)
onProgress(index + 1)
yield* Effect.yieldNow
}
source.run("COMMIT")
}),
)
}
function isNextDatabase(source: SQLiteDatabase) {
const tables = new Set(
source
.query<{ name: string }, []>("SELECT name FROM sqlite_master WHERE type = 'table'")
.all()
.map((table) => table.name),
)
return tables.has("project") && tables.has("session") && tables.has("session_message")
}
function row(
source: SourceMessage,
message: {
readonly id: string
readonly type: SessionMessage.Type
readonly time: { readonly created: number }
readonly [key: string]: unknown
},
): TransformResult["messages"][number] {
const { id, type, ...data } = message
return {
id,
session_id: source.session_id,
type,
seq: 0,
time_created: source.time_created,
time_updated: source.time_updated,
data,
}
}
function migrateTool(part: typeof SessionV1.ToolPart.Type, fallback: number) {
const base = {
type: "tool" as const,
id: part.callID,
name: part.tool,
...(part.metadata ? { providerState: part.metadata } : {}),
}
if (part.state.status === "completed")
return {
...base,
state: {
status: "completed",
input: part.state.input,
content:
part.state.time.compacted === undefined
? [
{ type: "text", text: part.state.output },
...(part.state.attachments ?? []).map((file) => ({
type: "file" as const,
uri: file.url,
mime: file.mime,
...(file.filename ? { name: file.filename } : {}),
})),
]
: [{ type: "text", text: "[Old tool result content cleared]" }],
metadata: part.state.metadata,
},
time: { created: part.state.time.start, completed: part.state.time.end },
}
if (part.state.status === "error")
return {
...base,
state: {
status: "error",
input: part.state.input,
error: { type: "tool.execution", message: part.state.error },
...(typeof part.state.metadata?.output === "string"
? { content: [{ type: "text", text: part.state.metadata.output }] }
: {}),
...(part.state.metadata ? { metadata: part.state.metadata } : {}),
},
time: { created: part.state.time.start, completed: part.state.time.end },
}
return {
...base,
state: {
status: "error",
input: part.state.input,
error: { type: "tool.interrupted", message: "Tool execution was interrupted before V2 migration" },
...(part.state.status === "running" && part.state.metadata ? { metadata: part.state.metadata } : {}),
},
time: { created: part.state.status === "running" ? part.state.time.start : fallback },
}
}
function migrateError(error: NonNullable<(typeof SessionV1.Assistant.Type)["error"]>) {
const message =
"message" in error.data
? error.data.message
: error.name === "MessageOutputLengthError"
? "The model exceeded its output limit"
: error.name
const type =
error.name === "ProviderAuthError"
? "provider.auth"
: error.name === "ContentFilterError"
? "provider.content-filter"
: error.name === "ContextOverflowError"
? "provider.invalid-request"
: error.name === "StructuredOutputError" || error.name === "MessageOutputLengthError"
? "provider.invalid-output"
: error.name === "MessageAbortedError"
? "aborted"
: error.name === "APIError"
? "provider.error"
: "unknown"
return { type, message }
}
function normalizeFinish(finish: string | undefined) {
if (!finish) return undefined
return (
(["stop", "length", "tool-calls", "content-filter", "error", "unknown"] as const).find(
(value) => value === finish,
) ?? "unknown"
)
}
function migrateFile(part: SessionV1.FilePart) {
if (!part.url.startsWith("data:")) return []
const comma = part.url.indexOf(",")
if (comma < 0) return []
const header = part.url.slice(0, comma)
const payload = part.url.slice(comma + 1)
const data = header.endsWith(";base64")
? Buffer.from(payload, "base64").toString("base64")
: Buffer.from(decodeURIComponent(payload)).toString("base64")
return [
{
data,
mime: part.mime,
source:
part.source?.type === "resource" ? { type: "uri" as const, uri: part.source.uri } : { type: "inline" as const },
...(part.filename ? { name: part.filename } : {}),
...(part.source
? { mention: { text: part.source.text.value, start: part.source.text.start, end: part.source.text.end } }
: {}),
},
]
}
function unavailableFile(part: SessionV1.FilePart) {
const label = part.filename ?? (part.source?.type === "resource" ? part.source.uri : part.url)
return `[Attachment unavailable after migration: ${label} (${part.mime})]`
}
function syntheticID(source: string, used: Set<string>) {
const prefix = source.slice(0, 16)
const alphabet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
for (let salt = 0; ; salt++) {
const hex = new Bun.CryptoHasher("sha256").update(`v1-synthetic:${source}${salt ? `:${salt}` : ""}`).digest("hex")
let value = BigInt(`0x${hex}`)
let suffix = ""
while (suffix.length < 14) {
suffix = alphabet[Number(value % 62n)] + suffix
value /= 62n
}
const id = prefix + suffix
if (used.has(id)) continue
used.add(id)
return id
}
}
function serializeRecent(
messages: ReadonlyArray<{ row: SourceMessage; value: typeof SessionV1.Info.Type }>,
parts: Map<string, Array<{ row: SourcePart; value: typeof SessionV1.Part.Type }>>,
) {
return messages
.flatMap((message) => {
const owned = parts.get(message.row.id)?.map((part) => part.value) ?? []
if (message.value.role === "user")
return [
`[User]: ${owned
.filter((part) => part.type === "text" && !part.ignored)
.map((part) => (part.type === "text" ? part.text : ""))
.join("\n\n")}`,
]
return owned.flatMap((part) =>
part.type === "text"
? [`[Assistant]: ${part.text}`]
: part.type === "reasoning" && part.text
? [`[Assistant reasoning]: ${part.text}`]
: [],
)
})
.join("\n\n")
}
function readState(db: Database.Interface["db"]): Effect.Effect<MigrationState | undefined> {
return db
.select({ value: KVTable.value })
.from(KVTable)
.where(eq(KVTable.key, MIGRATION_STATE_KEY))
.get()
.pipe(
Effect.map((row) => parseState(row?.value)),
Effect.orDie,
)
}
function parseState(input: unknown): MigrationState | undefined {
if (!input || typeof input !== "object" || !("phase" in input)) return
if (input.phase === "completed") return { phase: "completed" }
if (input.phase !== "sessions") return
if (!("cursor" in input) || input.cursor === undefined) return { phase: "sessions" }
if (typeof input.cursor === "string") return { phase: "sessions", cursor: input.cursor }
}
function hasLegacySessions(db: Database.Interface["db"]) {
return db.get(sql`SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'session'`).pipe(
Effect.map((row) => row !== undefined),
Effect.orDie,
)
}
export * as V1Migration from "#v1-migration"
export * from "#v1-migration"
@@ -0,0 +1,17 @@
import { Effect, Layer, PlatformError } from "effect"
import { ChildProcessSpawner, make } from "effect/unstable/process/ChildProcessSpawner"
export const spawner = make(() =>
Effect.fail(
PlatformError.systemError({
_tag: "Unknown",
module: "Environment",
method: "spawn",
description: "This location has no execution plane: no workspace is attached and the host cannot spawn processes",
}),
),
)
export const layer = Layer.succeed(ChildProcessSpawner, spawner)
export * as EnvironmentUnavailable from "./unavailable.js"
@@ -0,0 +1,12 @@
import { bind } from "./fff.js"
export type { Directory, DirSearch, File, Init, Mixed, MixedSearch, Picker, Result, Search } from "./fff.js"
// No fff backend exists on workerd; every create reports unavailability and
// FileSystemSearch degrades the same way it does on a runtime without the native module.
const adapter = bind(undefined, "fff unavailable on workerd runtime")
export const available = adapter.available
export const create = adapter.create
export * as Fff from "./fff.workerd.js"
@@ -2,9 +2,10 @@ import { createRequire } from "node:module"
declare const OPENCODE_LIBC: string | undefined
const require = createRequire(import.meta.url)
// Lazy: on workerd import.meta.url is undefined and the watcher is never
// loaded, so createRequire must not run at module scope.
export default function load() {
const require = createRequire(import.meta.url)
const libc = typeof OPENCODE_LIBC === "undefined" ? undefined : OPENCODE_LIBC
return require(
process.env.OPENCODE_PARCEL_WATCHER_PATH ??
@@ -0,0 +1,4 @@
// workerd has no filesystem path to a photon wasm artifact. Image.Photon only
// reads this lazily and surfaces a typed ResizerUnavailableError when loading
// fails, so an empty path degrades cleanly instead of breaking module load.
export default ""
+2 -3
View File
@@ -25,7 +25,7 @@ import { MCP } from "./mcp/index.js"
import { Permission } from "./permission.js"
import { Plugin } from "./plugin.js"
import { PluginSupervisor } from "./plugin/supervisor.js"
import { ProjectCopy } from "./project/copy.js"
import { Worktree } from "./worktree.js"
import { Pty } from "./pty.js"
import { Question } from "./question.js"
import { Shell } from "./shell.js"
@@ -66,8 +66,7 @@ const locationServiceNodes = [
AISDK.node,
Plugin.node,
PluginSupervisor.node,
ProjectCopy.node,
ProjectCopy.refreshNode,
Worktree.refreshNode,
FileSystemSearch.node,
FileSystem.node,
Pty.node,
+15 -59
View File
@@ -1,11 +1,10 @@
export * as MCPClient from "./client.js"
import path from "node:path"
import { execFile } from "node:child_process"
import { pathToFileURL } from "node:url"
import { Client } from "@modelcontextprotocol/sdk/client/index.js"
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"
import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js"
import { UnauthorizedError, type OAuthClientProvider } from "@modelcontextprotocol/sdk/client/auth.js"
import {
CallToolResultSchema,
@@ -30,13 +29,12 @@ import {
} from "@modelcontextprotocol/sdk/types.js"
import { Cause, Effect, Exit, Schema } from "effect"
import { ConfigMCP } from "@opencode-ai/schema/config/mcp"
import { MCPStdio } from "./stdio.js"
const DEFAULT_STARTUP_TIMEOUT = 30_000
const DEFAULT_CATALOG_TIMEOUT = 30_000
const DEFAULT_EXECUTION_TIMEOUT = 12 * 60 * 60 * 1_000 // 12 hours
type Transport = StdioClientTransport | StreamableHTTPClientTransport
// Some servers advertise tool outputSchemas the SDK's strict validator can't resolve; this drops
// only that field so a single bad schema doesn't blank out the whole tool list.
const TolerantListToolsResult = ListToolsResultSchema.extend({
@@ -176,7 +174,12 @@ export interface Connection {
readonly onResourcesChanged: (callback: () => void) => void
}
/** Connects an MCP server; closing the calling scope tears down the transport and any spawned process. */
/**
* Connects an MCP server; closing the calling scope tears down the transport and any spawned process.
*
* A stdio server is spawned through the location's `Environment`, so it runs on the same execution
* plane as the location's shell commands rather than always on the host.
*/
export const connect = Effect.fnUntraced(function* (
server: string,
config: typeof ConfigMCP.Server.Type,
@@ -190,13 +193,12 @@ export const connect = Effect.fnUntraced(function* (
const transport: Transport = yield* Effect.gen(function* () {
if (config.type === "local") {
const [command, ...args] = config.command
return new StdioClientTransport({
return yield* MCPStdio.make({
server,
command,
args,
cwd: config.cwd ? path.resolve(directory, config.cwd) : directory,
stderr: "pipe",
env: {
...(process.env as Record<string, string>),
environment: {
...(command === "opencode" ? { BUN_BE_BUN: "1" } : {}),
...config.environment,
},
@@ -233,9 +235,9 @@ export const connect = Effect.fnUntraced(function* (
catch: (error) => error,
}).pipe(Effect.exit)
if (Exit.isSuccess(exit)) {
yield* Effect.addFinalizer(() =>
cleanupStdioDescendants(transport).pipe(Effect.andThen(Effect.promise(() => client.close())), Effect.ignore),
)
// Closing the client closes the transport, which ends stdin and then kills through the spawner
// handle if the server does not exit cleanly. The process scope remains a final backstop.
yield* Effect.addFinalizer(() => Effect.promise(() => client.close()).pipe(Effect.ignore))
const catalogTimeout = config.timeout?.catalog ?? DEFAULT_CATALOG_TIMEOUT
const executionTimeout = config.timeout?.execution ?? DEFAULT_EXECUTION_TIMEOUT
return {
@@ -434,58 +436,12 @@ export const connect = Effect.fnUntraced(function* (
} satisfies Connection
}
yield* cleanupStdioDescendants(transport).pipe(Effect.andThen(Effect.promise(() => transport.close())), Effect.ignore)
yield* Effect.promise(() => transport.close()).pipe(Effect.ignore)
const error = Cause.squash(exit.cause)
if (error instanceof UnauthorizedError) return yield* new NeedsAuthError({ server })
return yield* new ConnectError({ server, message: error instanceof Error ? error.message : String(error) })
})
// SDK close stops the MCP process, but not child processes it spawned.
const cleanupStdioDescendants = (transport: Transport) =>
Effect.gen(function* () {
if (!(transport instanceof StdioClientTransport)) return
const pid = transport.pid
if (typeof pid !== "number") return
yield* Effect.forEach(
yield* descendantPids(pid),
(pid) =>
Effect.try({
try: () => process.kill(pid, "SIGTERM"),
catch: () => undefined,
}).pipe(Effect.ignore),
{ discard: true },
)
})
const descendantPids = Effect.fnUntraced(function* (root: number) {
if (process.platform === "win32") return []
const result: number[] = []
const queue = [root]
for (let index = 0; index < queue.length; index++) {
const parent = queue[index]
if (parent === undefined) return result
const children = (yield* childPids(parent)).filter((pid) => !result.includes(pid))
result.push(...children)
queue.push(...children)
}
return result
})
const childPids = (pid: number) =>
Effect.promise(
() =>
new Promise<number[]>((resolve) => {
execFile("pgrep", ["-P", String(pid)], { encoding: "utf8" }, (_error, stdout) => {
resolve(
stdout
.split("\n")
.map((line) => Number.parseInt(line, 10))
.filter((pid) => Number.isInteger(pid)),
)
})
}),
)
async function paginate<R extends { nextCursor?: string }, T>(
list: (cursor: string | undefined) => Promise<R>,
items: (result: R) => T[],
+7 -9
View File
@@ -12,6 +12,7 @@ import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
import { Config } from "../config.js"
import { Credential } from "../credential.js"
import { Bus } from "../bus.js"
import { Environment } from "../environment/index.js"
import { Form } from "../form.js"
import { Integration } from "../integration.js"
import { KeyedMutex } from "../effect/keyed-mutex.js"
@@ -173,13 +174,13 @@ export const layer = (options?: Options) =>
Effect.gen(function* () {
const config = yield* Config.Service
const location = yield* Location.Service
const environment = yield* Environment.Service
const bus = yield* Bus.Service
const forms = yield* Form.Service
const integration = yield* Integration.Service
const credentials = yield* Credential.Service
const root = yield* Scope.make()
const root = yield* Effect.scope
const fork = yield* FiberSet.makeRuntime<never, void, never>()
yield* Effect.addFinalizer((exit) => Scope.close(root, exit))
const loadConfig = (entries: readonly Entry[]) => {
const documents = entries.filter((entry): entry is Document => entry.type === "document")
@@ -459,13 +460,8 @@ export const layer = (options?: Options) =>
connection.onClose(() =>
live(
Effect.gen(function* () {
entry.client = undefined
entry.tools = undefined
entry.prompts = undefined
entry.status = { status: "failed", error: "Connection closed" }
yield* bus.publish(McpEvent.ToolsChanged, { server: name }).pipe(Effect.ignore)
yield* bus.publish(McpEvent.ResourcesChanged, { server: name }).pipe(Effect.ignore)
yield* bus.publish(Command.Event.Updated, {}).pipe(Effect.ignore)
yield* stopServer(name, entry)
yield* bus.publish(McpEvent.StatusChanged, { server: name }).pipe(Effect.ignore)
}),
),
@@ -520,6 +516,8 @@ export const layer = (options?: Options) =>
options?.clientInfo,
).pipe(
Effect.flatMap((connection) => connection.tools().pipe(Effect.map((tools) => ({ connection, tools })))),
// A stdio server is spawned on this location's execution plane, not the host's.
Effect.provideService(Environment.Service, environment),
Scope.provide(scope),
Effect.exit,
)
@@ -828,7 +826,7 @@ export function configured(options?: Options) {
return makeLocationNode({
service: Service,
layer: layer(options),
deps: [Config.node, Location.node, Bus.node, Form.node, Integration.node, Credential.node],
deps: [Config.node, Location.node, Environment.node, Bus.node, Form.node, Integration.node, Credential.node],
})
}
+2 -1
View File
@@ -2,7 +2,6 @@ export * as MCPOAuth from "./oauth.js"
import { auth, type OAuthClientProvider } from "@modelcontextprotocol/sdk/client/auth.js"
import type { OAuthClientInformationMixed, OAuthTokens } from "@modelcontextprotocol/sdk/shared/auth.js"
import { createServer } from "node:http"
import { Deferred, Effect } from "effect"
import { Credential } from "@opencode-ai/schema/credential"
import { ConfigMCP } from "@opencode-ai/schema/config/mcp"
@@ -152,6 +151,8 @@ export const authorize = (input: {
const redirectPath = oauth?.redirect_uri ? new URL(oauth.redirect_uri).pathname : "/callback"
const state = Buffer.from(crypto.getRandomValues(new Uint8Array(32))).toString("base64url")
// Lazy so runtimes without a loopback listener (workerd) never evaluate node:http.
const { createServer } = yield* Effect.promise(() => import("node:http"))
const server = createServer((request, response) => {
const url = new URL(request.url ?? "/", "http://127.0.0.1")
if (url.pathname !== redirectPath) {
+181
View File
@@ -0,0 +1,181 @@
export * as MCPStdio from "./stdio.js"
import { ReadBuffer, serializeMessage } from "@modelcontextprotocol/sdk/shared/stdio.js"
import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js"
import type { JSONRPCMessage } from "@modelcontextprotocol/sdk/types.js"
import { Cause, Duration, Effect, Queue, Scope, Stream } from "effect"
import { ChildProcess } from "effect/unstable/process"
import type { ChildProcessHandle } from "effect/unstable/process/ChildProcessSpawner"
import { Environment } from "../environment/index.js"
/** Mirrors StdioClientTransport: wait this long for a graceful exit after stdin closes. */
const CLOSE_GRACE = Duration.seconds(2)
/** Mirrors StdioClientTransport: escalate SIGTERM to SIGKILL after this long. */
const FORCE_KILL_AFTER = Duration.seconds(2)
const OUTGOING_CAPACITY = 64
const MAX_FRAME_BYTES = 16 * 1024 * 1024
export interface Options {
/** Server name; only used to attribute logs. */
readonly server: string
readonly command: string
readonly args: ReadonlyArray<string>
readonly cwd: string
/**
* Environment declared by the server config, and nothing else.
*
* The host environment is merged in by the spawner via `extendEnv`, which keeps the merge on the
* side that actually runs the process: the local driver extends with the host's `process.env`
* (what the MCP SDK's transport did), while a workspace driver extends with the sandbox's own
* environment. Host variables therefore never cross the seam into a remote workspace.
*/
readonly environment: Record<string, string>
}
/**
* MCP stdio transport that spawns its server through the location's `Environment` instead of the
* SDK's host-bound `StdioClientTransport`, so a workspace-backed location runs its MCP servers
* wherever the rest of its execution happens.
*
* The process is acquired in the calling scope: closing the scope kills it (the spawner kills the
* whole process group, so descendants go too) regardless of whether the transport was closed.
*/
export const make = Effect.fnUntraced(function* (options: Options) {
const environment = yield* Environment.Service
const scope = yield* Effect.scope
// Outgoing frames are queued rather than written to `handle.stdin` directly: the sink closes the
// stream it is run with, and stdin must stay open across the whole session.
const outgoing = yield* Queue.bounded<string, Cause.Done>(OUTGOING_CAPACITY)
const buffer = new ReadBuffer()
const state: { phase: "ready" | "starting" | "open" | "closed"; handle?: ChildProcessHandle } = { phase: "ready" }
let startup: Promise<void> | undefined
let closing: Promise<void> | undefined
let trailingBytes = 0
const stop = (handle: ChildProcessHandle) =>
Effect.gen(function* () {
const exit = yield* Effect.timeoutOption(handle.exitCode, CLOSE_GRACE)
if (exit._tag === "Some") return
const terminated = yield* Effect.timeoutOption(handle.kill({ killSignal: "SIGTERM" }), FORCE_KILL_AFTER)
if (terminated._tag === "None") yield* handle.kill({ killSignal: "SIGKILL" })
}).pipe(Effect.ignore)
const close = () =>
(closing ??= Effect.runPromise(
Effect.gen(function* () {
state.phase = "closed"
Queue.endUnsafe(outgoing)
if (startup) yield* Effect.promise(() => startup!.catch(() => undefined))
const handle = state.handle
if (!handle) return
state.handle = undefined
yield* stop(handle)
}).pipe(Effect.ensuring(Queue.shutdown(outgoing)), Effect.ensuring(Effect.sync(() => buffer.clear()))),
))
const transport: Transport = {
start: () => {
if (state.phase !== "ready") return Promise.reject(new Error("Stdio transport already started"))
state.phase = "starting"
startup = Effect.runPromise(
Effect.gen(function* () {
const handle = yield* environment.spawner.spawn(
ChildProcess.make(options.command, [...options.args], {
cwd: options.cwd,
env: options.environment,
extendEnv: true,
stdin: { stream: Stream.encodeText(Stream.fromQueue(outgoing)), endOnDone: true },
stdout: "pipe",
stderr: "pipe",
forceKillAfter: FORCE_KILL_AFTER,
}),
)
state.handle = handle
if (state.phase === "closed") {
state.handle = undefined
return yield* stop(handle)
}
state.phase = "open"
yield* startOutput(handle)
}).pipe(Scope.provide(scope)),
)
return startup
},
send: (message: JSONRPCMessage) =>
state.phase !== "open"
? Promise.reject(new Error("Not connected"))
: Effect.runPromise(
Queue.offer(outgoing, serializeMessage(message)).pipe(
Effect.flatMap((offered) => (offered ? Effect.void : Effect.fail(new Error("Not connected")))),
),
),
close,
}
const deliver = (chunk: Uint8Array) =>
Effect.gen(function* () {
for (const byte of chunk) {
trailingBytes = byte === 10 ? 0 : trailingBytes + 1
if (trailingBytes > MAX_FRAME_BYTES) return yield* Effect.fail(new Error("MCP stdio frame exceeded 16 MiB"))
}
buffer.append(Buffer.from(chunk.buffer, chunk.byteOffset, chunk.byteLength))
while (true) {
// `undefined` means the frame failed to parse: the buffer has already advanced past it, so
// keep draining. `null` means the buffer holds no complete frame yet.
const message = yield* Effect.try({
try: () => buffer.readMessage(),
catch: (cause) => (cause instanceof Error ? cause : new Error(String(cause))),
}).pipe(
Effect.catch((error) =>
Effect.sync(() => {
transport.onerror?.(error)
return undefined
}),
),
)
if (message === undefined) continue
if (message === null) return
transport.onmessage?.(message)
}
})
const startOutput = (handle: ChildProcessHandle) =>
Effect.gen(function* () {
yield* Effect.forkScoped(
Stream.runForEach(handle.stdout, deliver).pipe(
Effect.tapCause((cause) =>
Effect.sync(() => {
const error = Cause.squash(cause)
transport.onerror?.(error instanceof Error ? error : new Error(String(error)))
}),
),
Effect.ignore,
// stdout ending means the server is gone; the SDK transport reports that the same way.
Effect.ensuring(
Effect.gen(function* () {
const unexpected = state.phase !== "closed"
if (unexpected) yield* Effect.promise(close)
transport.onclose?.()
}),
),
),
)
// StdioClientTransport pipes stderr into a stream nobody reads. Drain chunks into the debug
// log so chatty servers cannot stall and newline-free output is not buffered without bound.
yield* Effect.forkScoped(
handle.stderr.pipe(
Stream.decodeText(),
Stream.runForEach((output) =>
output.trim() === ""
? Effect.void
: Effect.logDebug("mcp server stderr", { server: options.server, output }),
),
Effect.ignore,
),
)
})
return transport
})
+22 -6
View File
@@ -544,6 +544,24 @@ const Cache = Schema.Struct({
})
const defaultSource = "https://models.opencode.ai"
// Bundled snapshot of https://models.opencode.ai/api.json, committed at
// packages/core/src/models-dev/snapshot.txt and refreshed via
// `bun run script/update-models-snapshot.ts`. Decoded and normalized once per
// isolate: the snapshot is a multi-MB module-level constant and one isolate can
// host many runtimes (Cloudflare colocates Durable Object instances), so
// per-runtime decoding would multiply the cost.
let bundledCache: readonly Snapshot[] | undefined
const bundledSnapshot = Effect.suspend(() =>
bundledCache
? Effect.succeed(bundledCache)
: decodeCatalog(snapshotText).pipe(
Effect.map((catalog) => {
bundledCache = normalize(catalog)
return bundledCache
}),
),
)
function cacheKey(source: string) {
if (source === defaultSource) return "models-dev:catalog"
return `models-dev:catalog:${Hash.fast(source)}`
@@ -607,11 +625,9 @@ export const layer = (options?: Options) =>
)
: Effect.succeed(undefined)
// Bundled snapshot of https://models.opencode.ai/api.json, committed at
// packages/core/src/models-dev/snapshot.txt and refreshed via
// `bun run script/update-models-snapshot.ts`. It is the boot-time floor
// for the catalog; the periodic fetch below still refreshes on top.
const loadSnapshot = options?.snapshot === false ? Effect.succeed(undefined) : decodeCatalog(snapshotText)
// The bundled snapshot is the boot-time floor for the catalog; the
// periodic fetch below still refreshes on top.
const loadSnapshot = options?.snapshot === false ? Effect.succeed(undefined) : bundledSnapshot
const fetchAndWrite = Effect.fn("ModelsDev.fetchAndWrite")(function* () {
const text = yield* fetchApi()
@@ -635,7 +651,7 @@ export const layer = (options?: Options) =>
const cached = options?.file ? undefined : yield* loadFromCache()
if (cached) return normalize(cached.catalog)
const bundled = yield* loadSnapshot
if (bundled) return normalize(bundled)
if (bundled) return bundled
if (!fetch) return []
const catalog = yield* lock.withPermit(
Effect.gen(function* () {
@@ -204,10 +204,11 @@ export const GithubCopilotPlugin = define({
for (const [id, model] of loaded.models) {
evt.model.update(item.provider.id, id, (draft) => Object.assign(draft, structuredClone(model)))
}
} else if (loaded.baseURL) {
} else {
for (const id of item.models.keys()) {
evt.model.update(item.provider.id, id, (model) => {
model.settings = Provider.mergeOverlay(model.settings, { baseURL: loaded.baseURL })
model.package = "@ai-sdk/github-copilot"
if (loaded.baseURL) model.settings = Provider.mergeOverlay(model.settings, { baseURL: loaded.baseURL })
})
}
}
+2 -1
View File
@@ -1,4 +1,3 @@
import { createServer } from "node:http"
import type { IntegrationOAuthMethodRegistration } from "@opencode-ai/plugin/effect/integration"
import { define } from "@opencode-ai/plugin/effect/plugin"
import { Deferred, Effect, Option, Schema, Semaphore, Stream } from "effect"
@@ -58,6 +57,8 @@ const browser = (app: App.Info) =>
const state = base64UrlEncode(crypto.getRandomValues(new Uint8Array(32)).buffer)
const code = yield* Deferred.make<string, Error>()
const redirect = `http://localhost:${callbackPort}/auth/callback`
// Lazy so runtimes without a loopback listener (workerd) never evaluate node:http.
const { createServer } = yield* Effect.promise(() => import("node:http"))
const server = createServer((request, response) => {
const url = new URL(request.url ?? "/", `http://localhost:${callbackPort}`)
if (url.pathname !== "/auth/callback") {
+5 -1
View File
@@ -23,7 +23,11 @@ export const GooglePlugin = make("google", (id) => (id.includes("gemini-") ? PRO
export const AnthropicPlugin = make("anthropic", (id) => (id.includes("claude") ? PROMPT_ANTHROPIC : undefined))
export const KimiPlugin = make("kimi", (id) => (id.includes("kimi") ? PROMPT_KIMI : undefined))
export const ArceePlugin = make("arcee", (id) => (id.includes("trinity") ? PROMPT_TRINITY : undefined))
export const MetaPlugin = make("meta", (id) => (id.includes("muse-spark") ? PROMPT_META : undefined))
export const MetaPlugin = make("meta", (id) => {
if (!id.includes("muse")) return
const name = id.includes("muse-glimmer") ? "Muse Glimmer" : "Muse Spark"
return PROMPT_META.replaceAll("{{MODEL_NAME}}", name)
})
export const Plugins = [OpenAIPlugin, GooglePlugin, AnthropicPlugin, KimiPlugin, ArceePlugin, MetaPlugin] as const
@@ -1,4 +1,4 @@
You are OpenCode, a coding agent that helps users with software engineering tasks. You are powered by Muse Spark, a large language model trained by Meta MSL.
You are OpenCode, a coding agent that helps users with software engineering tasks. You are powered by {{MODEL_NAME}}, a large language model trained by Meta MSL.
Use the instructions below and the tools available to assist the user.
@@ -55,5 +55,5 @@ Use the instructions below and the tools available to assist the user.
- NEVER use comments as a place for long-winded chain-of-thought. Long thinking texts must be generated as private reasoning. Comments in code must be appropriately concise.
# User Help & Feedback
- Users can give feedback or report issues at https://github.com/anomalyco/opencode and mention that they are using Meta Muse Spark.
- Users can give feedback or report issues at https://github.com/anomalyco/opencode and mention that they are using Meta {{MODEL_NAME}}.
- When users ask directly about OpenCode (eg. "can OpenCode do...", "are you able to do...") or its features (eg. implement a hook, write a slash command, or install an MCP server), use the `webfetch` tool to gather information to answer the question from the V2 OpenCode docs at https://opencode.ai/v2/docs/.
+52 -37
View File
@@ -2,18 +2,20 @@ export * as Project from "./project.js"
import { Context, Effect, Layer, Schema } from "effect"
import { ChildProcess } from "effect/unstable/process"
import { asc, desc } from "drizzle-orm"
import { and, asc, desc, eq } from "drizzle-orm"
import path from "path"
import { AbsolutePath } from "./schema.js"
import { Bus } from "./bus.js"
import { Database } from "./database/database.js"
import { Worktree } from "@opencode-ai/schema/worktree"
import { FSUtil } from "@opencode-ai/util/fs-util"
import { Git } from "./git.js"
import { AppProcess } from "@opencode-ai/util/process"
import { makeGlobalNode } from "@opencode-ai/util/effect/app-node"
import { Hash } from "@opencode-ai/util/hash"
import { ProjectDirectories } from "./project/directories.js"
import { ProjectSchema } from "./project/schema.js"
import { ProjectTable, upsertProject } from "./project/sql.js"
import { WorktreeTable } from "./worktree/sql.js"
export const ID = ProjectSchema.ID
export type ID = ProjectSchema.ID
@@ -24,18 +26,9 @@ export type Vcs = ProjectSchema.Vcs
export const Current = ProjectSchema.Current
export type Current = ProjectSchema.Current
export const Directory = ProjectSchema.Directory
export type Directory = ProjectSchema.Directory
export const Info = ProjectSchema.Info
export interface Info extends Schema.Schema.Type<typeof Info> {}
export const DirectoriesInput = ProjectSchema.DirectoriesInput
export type DirectoriesInput = typeof DirectoriesInput.Type
export const Directories = ProjectSchema.Directories
export type Directories = typeof Directories.Type
export interface Resolved {
readonly previous?: ID
readonly id: ID
@@ -54,7 +47,6 @@ export const root = Effect.fn("Project.root")(function* (fs: FSUtil.Interface, i
export interface Interface {
readonly list: () => Effect.Effect<ReadonlyArray<Info>>
readonly directories: (input: DirectoriesInput) => Effect.Effect<Directories>
readonly resolve: (input: AbsolutePath) => Effect.Effect<Resolved>
}
@@ -91,28 +83,55 @@ const layer = Layer.effect(
const fs = yield* FSUtil.Service
const git = yield* Git.Service
const proc = yield* AppProcess.Service
const bus = yield* Bus.Service
const db = (yield* Database.Service).db
const projectDirectories = yield* ProjectDirectories.Service
const announcing = new Set<string>()
const persist = Effect.fnUntraced(function* (project: Resolved) {
yield* db
.transaction((tx) =>
Effect.gen(function* () {
yield* upsertProject(tx, project)
if (!project.vcs) return
yield* projectDirectories.create({ projectID: project.id, directory: project.canonical }, tx)
if (project.directory === project.canonical) return
yield* projectDirectories.create(
{
projectID: project.id,
directory: project.directory,
strategy: project.vcs.type === "git" ? "git_worktree" : undefined,
},
tx,
)
}),
)
.pipe(Effect.orDie)
yield* upsertProject(db, project).pipe(Effect.orDie)
if (!project.vcs) return project
const directories: Array<{ projectID: ID; directory: AbsolutePath; strategy?: string }> = [
{ projectID: project.id, directory: project.canonical },
]
if (project.directory !== project.canonical)
directories.push({
projectID: project.id,
directory: project.directory,
strategy: project.vcs.type === "git" ? "git" : undefined,
})
// A missing directory row means this directory's resolution is a new durable
// fact (copy.ts registers copy directories directly; those never strand
// sessions and never announce). The row insert commits atomically with the
// event, so a crash between checks retries on the next resolve instead of
// stranding the announcement. The in-flight set keeps concurrent resolves
// from publishing the same fact twice.
for (const item of directories) {
const key = item.projectID + "\u0000" + item.directory
if (announcing.has(key)) continue
announcing.add(key)
yield* Effect.gen(function* () {
const stored = yield* db
.select({ directory: WorktreeTable.directory })
.from(WorktreeTable)
.where(and(eq(WorktreeTable.project_id, item.projectID), eq(WorktreeTable.directory, item.directory)))
.get()
.pipe(Effect.orDie)
if (stored) return
yield* bus.publish(
Worktree.Event.Resolved,
{ projectID: item.projectID, directory: item.directory, previous: project.previous ?? ID.global },
{
commit: () =>
db
.insert(WorktreeTable)
.values({ project_id: item.projectID, directory: item.directory, strategy: item.strategy })
.onConflictDoNothing()
.run()
.pipe(Effect.orDie, Effect.asVoid),
},
)
}).pipe(Effect.ensuring(Effect.sync(() => announcing.delete(key))))
}
return project
})
@@ -126,10 +145,6 @@ const layer = Layer.effect(
return rows.map(fromRow)
})
const directories = Effect.fn("Project.directories")(function* (input: DirectoriesInput) {
return yield* projectDirectories.list(input.projectID)
})
const cached = Effect.fnUntraced(function* (dir: string) {
return yield* fs.readFileString(path.join(dir, "opencode")).pipe(
Effect.map((value) => value.trim()),
@@ -243,12 +258,12 @@ const layer = Layer.effect(
return yield* persist({ id: ID.global, directory, canonical: directory, vcs: undefined })
})
return Service.of({ list, directories, resolve })
return Service.of({ list, resolve })
}),
)
export const node = makeGlobalNode({
service: Service,
layer: layer,
deps: [Database.node, FSUtil.node, Git.node, AppProcess.node, ProjectDirectories.node],
deps: [Bus.node, Database.node, FSUtil.node, Git.node, AppProcess.node],
})
@@ -1,35 +0,0 @@
import { Effect } from "effect"
import { AbsolutePath } from "../schema.js"
import { Git } from "../git.js"
import { DirectoryUnavailableError, StrategyID, type ListEntry, type Strategy } from "./copy.js"
export function makeGitWorktreeStrategy(input: {
git: Git.Interface
canonical: (directory: AbsolutePath) => Effect.Effect<AbsolutePath, DirectoryUnavailableError>
}) {
return {
id: StrategyID.make("git_worktree"),
create: Effect.fn("ProjectCopy.GitWorktree.create")(function* (options) {
const repository = yield* input.git.repo.discover(options.sourceDirectory)
if (!repository) return yield* new DirectoryUnavailableError({ directory: options.sourceDirectory })
yield* input.git.worktree.create({ repository, directory: options.directory })
return { directory: yield* input.canonical(options.directory) }
}),
remove: Effect.fn("ProjectCopy.GitWorktree.remove")(function* (options) {
const found = yield* input.git.repo.discover(options.directory)
if (!found) return yield* new DirectoryUnavailableError({ directory: options.directory })
yield* input.git.worktree.remove({ repository: found, directory: options.directory, force: options.force })
}),
list: Effect.fn("ProjectCopy.GitWorktree.list")(function* (directory) {
const found = yield* input.git.repo.discover(directory)
if (!found) return yield* new DirectoryUnavailableError({ directory })
const entries = yield* input.git.worktree.list(found)
return yield* Effect.forEach(entries, (entry) =>
input.canonical(entry.directory).pipe(
Effect.map((directory) => ({ directory, type: entry.kind === "main" ? "root" : "copy" }) as const),
Effect.catchTag("ProjectCopy.DirectoryUnavailableError", () => Effect.succeed(undefined)),
),
).pipe(Effect.map((items) => items.filter((item): item is ListEntry => item !== undefined)))
}),
} satisfies Strategy
}
-272
View File
@@ -1,272 +0,0 @@
export * as ProjectCopy from "./copy.js"
import { Context, Effect, Layer, Schema } from "effect"
import path from "path"
import { AbsolutePath } from "../schema.js"
import { FSUtil } from "@opencode-ai/util/fs-util"
import { Git } from "../git.js"
import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
import { Project } from "../project.js"
import { ProjectDirectories } from "./directories.js"
import { makeGitWorktreeStrategy } from "./copy-strategies.js"
import { Slug } from "../util/slug.js"
import { Bus } from "../bus.js"
import { Database } from "../database/database.js"
import { Location } from "../location.js"
import { Event } from "@opencode-ai/schema/project-directories"
import { ProjectCopy } from "@opencode-ai/schema/project-copy"
export const StrategyID = ProjectCopy.StrategyID
export type StrategyID = typeof StrategyID.Type
export const CreateInput = ProjectCopy.CreateInput
export type CreateInput = typeof CreateInput.Type
export const RemoveInput = ProjectCopy.RemoveInput
export type RemoveInput = typeof RemoveInput.Type
export const RefreshInput = Schema.Struct({
projectID: Project.ID,
}).annotate({ identifier: "ProjectCopy.RefreshInput" })
export type RefreshInput = typeof RefreshInput.Type
export const RefreshResult = Schema.Struct({
updated: Schema.Array(AbsolutePath),
removed: Schema.Array(AbsolutePath),
}).annotate({ identifier: "ProjectCopy.RefreshResult" })
export type RefreshResult = typeof RefreshResult.Type
export const Copy = ProjectCopy.Copy
export type Copy = typeof Copy.Type
export const ListEntry = Schema.Struct({
directory: AbsolutePath,
type: Schema.Literals(["root", "copy"]),
}).annotate({ identifier: "ProjectCopy.ListEntry" })
export type ListEntry = typeof ListEntry.Type
export class SourceDirectoryNotFoundError extends Schema.TaggedErrorClass<SourceDirectoryNotFoundError>()(
"ProjectCopy.SourceDirectoryNotFoundError",
{ directory: AbsolutePath },
) {}
export class DestinationExistsError extends Schema.TaggedErrorClass<DestinationExistsError>()(
"ProjectCopy.DestinationExistsError",
{ directory: AbsolutePath },
) {}
export class DirectoryUnavailableError extends Schema.TaggedErrorClass<DirectoryUnavailableError>()(
"ProjectCopy.DirectoryUnavailableError",
{ directory: AbsolutePath },
) {}
export class InvalidDirectoryError extends Schema.TaggedErrorClass<InvalidDirectoryError>()(
"ProjectCopy.InvalidDirectoryError",
{ directory: AbsolutePath },
) {}
export class StrategyUnavailableError extends Schema.TaggedErrorClass<StrategyUnavailableError>()(
"ProjectCopy.StrategyUnavailableError",
{ strategy: StrategyID },
) {}
export type Error =
| SourceDirectoryNotFoundError
| DestinationExistsError
| DirectoryUnavailableError
| InvalidDirectoryError
| StrategyUnavailableError
| Git.WorktreeError
export interface Strategy {
readonly id: StrategyID
readonly create: (input: {
sourceDirectory: AbsolutePath
directory: AbsolutePath
}) => Effect.Effect<Copy, Git.WorktreeError | DirectoryUnavailableError>
readonly remove: (input: {
directory: AbsolutePath
force: boolean
}) => Effect.Effect<void, Git.WorktreeError | DirectoryUnavailableError>
readonly list: (directory: AbsolutePath) => Effect.Effect<ListEntry[], Git.WorktreeError | DirectoryUnavailableError>
}
export { Event }
export interface Interface {
readonly create: (input: CreateInput) => Effect.Effect<Copy, Error>
readonly remove: (input: RemoveInput) => Effect.Effect<void, Error>
readonly refresh: (input: RefreshInput) => Effect.Effect<RefreshResult, Error>
}
export class Service extends Context.Service<Service, Interface>()("@opencode/ProjectCopy") {}
export const refreshAfterBoot = Effect.gen(function* () {
const location = yield* Location.Service
const copies = yield* Service
yield* Effect.gen(function* () {
yield* Effect.logInfo("project copy refresh started", { projectID: location.project.id })
const result = yield* copies.refresh({ projectID: location.project.id })
yield* Effect.logInfo("project copy refresh done", {
projectID: location.project.id,
updated: result.updated,
removed: result.removed,
})
}).pipe(
Effect.catchCause((cause) => Effect.logWarning("project copy refresh failed", { cause })),
Effect.forkScoped,
Effect.asVoid,
)
})
const layer = Layer.effect(
Service,
Effect.gen(function* () {
const fs = yield* FSUtil.Service
const git = yield* Git.Service
const directories = yield* ProjectDirectories.Service
const db = (yield* Database.Service).db
const bus = yield* Bus.Service
const changed = Effect.fnUntraced(function* (projectID: Project.ID, update: boolean) {
if (update) yield* bus.publish(Event.Updated, { projectID })
})
const canonical = Effect.fnUntraced(function* (input: AbsolutePath) {
const resolved = AbsolutePath.make(yield* fs.resolve(input))
if (!(yield* fs.isDir(resolved))) return yield* new DirectoryUnavailableError({ directory: input })
return resolved
})
const strategy = makeGitWorktreeStrategy({ git, canonical })
const source = Effect.fnUntraced(function* (input: AbsolutePath, projectID: Project.ID) {
const sourceDirectory = yield* canonical(input)
if ((yield* directories.get({ projectID, directory: sourceDirectory })) === undefined)
return yield* new SourceDirectoryNotFoundError({ directory: sourceDirectory })
return sourceDirectory
})
const getStrategy = Effect.fnUntraced(function* (id: StrategyID) {
if (id !== strategy.id) return yield* new StrategyUnavailableError({ strategy: id })
return strategy
})
const create = Effect.fn("ProjectCopy.create")(function* (input: CreateInput) {
const selected = yield* getStrategy(input.strategy)
const sourceDirectory = yield* source(input.sourceDirectory, input.projectID)
yield* fs.makeDirectory(input.directory, { recursive: true }).pipe(Effect.orDie)
const name = input.name ?? Slug.create()
let suffix = 1
let copyDirectory = AbsolutePath.make(path.join(input.directory, name))
while (yield* fs.existsSafe(copyDirectory)) {
suffix++
if (suffix > 10) return yield* new DestinationExistsError({ directory: copyDirectory })
copyDirectory = AbsolutePath.make(path.join(input.directory, `${name}-${suffix}`))
}
const result = yield* selected.create({
directory: copyDirectory,
sourceDirectory,
})
yield* changed(
input.projectID,
yield* directories.create({
projectID: input.projectID,
directory: result.directory,
strategy: input.strategy,
behavior: "replace",
}),
)
return result
})
const remove = Effect.fn("ProjectCopy.remove")(function* (input: RemoveInput) {
const copyDirectory = yield* canonical(input.directory)
const stored = yield* directories.get({ projectID: input.projectID, directory: copyDirectory })
if (!stored?.strategy) return yield* new InvalidDirectoryError({ directory: copyDirectory })
const strategy = yield* getStrategy(StrategyID.make(stored.strategy))
yield* strategy.remove({
directory: copyDirectory,
force: input.force,
})
yield* changed(
input.projectID,
yield* directories.remove({ projectID: input.projectID, directory: copyDirectory }),
)
})
const refresh = Effect.fn("ProjectCopy.refresh")(function* (input: RefreshInput) {
const stored = yield* directories.list(input.projectID)
const checked = yield* Effect.forEach(
stored,
(item) => fs.isDir(item.directory).pipe(Effect.map((exists) => ({ ...item, exists }))),
{ concurrency: "unbounded" },
)
const sourceDirectories = checked
.filter((item) => item.strategy === undefined && item.exists)
.map((item) => item.directory)
const discovered = yield* Effect.forEach(
sourceDirectories,
(sourceDirectory) =>
strategy.list(sourceDirectory).pipe(
Effect.catchTag("ProjectCopy.DirectoryUnavailableError", () => Effect.succeed([])),
Effect.map((items) =>
items.map((item) => ({
directory: item.directory,
strategy: item.type === "copy" ? strategy.id : undefined,
})),
),
),
{ concurrency: "unbounded" },
).pipe(
Effect.map((sets) => new Map(sets.flat().map((item) => [item.directory, item] as const)).values().toArray()),
)
const removed = checked.filter((item) => !item.exists).map((item) => item.directory)
const result = yield* db
.transaction((tx) =>
Effect.all({
updated: Effect.forEach(discovered, (item) =>
directories.create(
{
projectID: input.projectID,
directory: item.directory,
strategy: item.strategy,
behavior: "replace",
},
tx,
),
),
removed: Effect.forEach(removed, (directory) =>
directories.remove({ projectID: input.projectID, directory }, tx),
),
}),
)
.pipe(Effect.orDie)
const changes = {
updated: discovered.filter((_, index) => result.updated[index]).map((item) => item.directory),
removed: removed.filter((_, index) => result.removed[index]),
}
yield* changed(input.projectID, changes.updated.length > 0 || changes.removed.length > 0)
return changes
})
return Service.of({
create,
remove,
refresh,
})
}),
)
export const node = makeLocationNode({
service: Service,
layer: layer,
deps: [FSUtil.node, Git.node, ProjectDirectories.node, Bus.node, Database.node],
})
export const refreshNode = makeLocationNode({
name: "project-copy-refresh",
layer: Layer.effectDiscard(refreshAfterBoot),
deps: [node, Location.node],
})
-128
View File
@@ -1,128 +0,0 @@
export * as ProjectDirectories from "./directories.js"
import { and, asc, desc, eq, isNotNull, isNull, ne, or } from "drizzle-orm"
import { Context, Effect, Layer, Schema } from "effect"
import { Database } from "../database/database.js"
import { makeGlobalNode } from "@opencode-ai/util/effect/app-node"
import { AbsolutePath } from "../schema.js"
import { ProjectSchema } from "./schema.js"
import { ProjectDirectoryTable } from "./sql.js"
import type { EffectDrizzleSqlite } from "../database/drizzle.js"
import type { Project } from "../project.js"
export type Directory = Project.Directory
export const CreateInput = Schema.Struct({
projectID: ProjectSchema.ID,
directory: AbsolutePath,
strategy: Schema.optional(Schema.String),
behavior: Schema.Literals(["ignore", "replace"]).pipe(Schema.optional),
})
export type CreateInput = typeof CreateInput.Type
export const RemoveInput = Schema.Struct({
projectID: ProjectSchema.ID,
directory: AbsolutePath,
})
export type RemoveInput = typeof RemoveInput.Type
type DatabaseClient = EffectDrizzleSqlite.EffectSQLiteDatabase
export type Transaction = Parameters<Parameters<DatabaseClient["transaction"]>[0]>[0]
export const ListInput = ProjectSchema.DirectoriesInput
export type ListInput = typeof ListInput.Type
export const ListOutput = ProjectSchema.Directories
export type ListOutput = typeof ListOutput.Type
export interface Interface {
readonly list: (projectID: ProjectSchema.ID) => Effect.Effect<ReadonlyArray<Directory>>
readonly get: (input: {
projectID: ProjectSchema.ID
directory: AbsolutePath
}) => Effect.Effect<Directory | undefined>
readonly create: (input: CreateInput, tx?: Transaction) => Effect.Effect<boolean>
readonly remove: (input: RemoveInput, tx?: Transaction) => Effect.Effect<boolean>
}
export class Service extends Context.Service<Service, Interface>()("@opencode/ProjectDirectories") {}
const layer = Layer.effect(
Service,
Effect.gen(function* () {
const db = (yield* Database.Service).db
const create = Effect.fn("ProjectDirectories.create")(function* (input: CreateInput, tx?: Transaction) {
const insert = (tx ?? db)
.insert(ProjectDirectoryTable)
.values({ project_id: input.projectID, directory: input.directory, strategy: input.strategy })
const query =
input.behavior === "replace"
? insert.onConflictDoUpdate({
target: [ProjectDirectoryTable.project_id, ProjectDirectoryTable.directory],
set: { strategy: input.strategy ?? null },
setWhere: input.strategy
? or(isNull(ProjectDirectoryTable.strategy), ne(ProjectDirectoryTable.strategy, input.strategy))
: isNotNull(ProjectDirectoryTable.strategy),
})
: insert.onConflictDoNothing()
return (
(yield* query.returning({ directory: ProjectDirectoryTable.directory }).get().pipe(Effect.orDie)) !== undefined
)
})
const remove = Effect.fn("ProjectDirectories.remove")(function* (input: RemoveInput, tx?: Transaction) {
return (
(yield* (tx ?? db)
.delete(ProjectDirectoryTable)
.where(
and(
eq(ProjectDirectoryTable.project_id, input.projectID),
eq(ProjectDirectoryTable.directory, input.directory),
),
)
.returning({ directory: ProjectDirectoryTable.directory })
.get()
.pipe(Effect.orDie)) !== undefined
)
})
const list = Effect.fn("ProjectDirectories.list")(function* (projectID: ProjectSchema.ID) {
const rows = yield* db
.select({ directory: ProjectDirectoryTable.directory, strategy: ProjectDirectoryTable.strategy })
.from(ProjectDirectoryTable)
.where(eq(ProjectDirectoryTable.project_id, projectID))
.orderBy(desc(ProjectDirectoryTable.time_created), asc(ProjectDirectoryTable.directory))
.all()
.pipe(Effect.orDie)
return rows.map((row) => ({ directory: row.directory, strategy: row.strategy ?? undefined }))
})
const get = Effect.fn("ProjectDirectories.get")(function* (input: {
projectID: ProjectSchema.ID
directory: AbsolutePath
}) {
const row = yield* db
.select({ directory: ProjectDirectoryTable.directory, strategy: ProjectDirectoryTable.strategy })
.from(ProjectDirectoryTable)
.where(
and(
eq(ProjectDirectoryTable.project_id, input.projectID),
eq(ProjectDirectoryTable.directory, input.directory),
),
)
.get()
.pipe(Effect.orDie)
return row ? { directory: row.directory, strategy: row.strategy ?? undefined } : undefined
})
return Service.of({
list,
get,
create,
remove,
})
}),
)
export const node = makeGlobalNode({ service: Service, layer: layer, deps: [Database.node] })
-9
View File
@@ -10,18 +10,9 @@ export type ID = typeof ID.Type
export const Current = Project.Current
export type Current = typeof Current.Type
export const Directory = Project.Directory
export type Directory = typeof Directory.Type
export const Info = Project.Info
export interface Info extends Schema.Schema.Type<typeof Info> {}
export const DirectoriesInput = Project.DirectoriesInput
export type DirectoriesInput = typeof DirectoriesInput.Type
export const Directories = Project.Directories
export type Directories = typeof Directories.Type
export const Vcs = Schema.Union([
Schema.Struct({
type: Schema.Literal("git"),
+1
View File
@@ -23,6 +23,7 @@ export const ProjectTable = sqliteTable("project", {
commands: text({ mode: "json" }).$type<{ start?: string }>(),
})
/** @deprecated Use WorktreeTable from worktree/sql instead. */
export const ProjectDirectoryTable = sqliteTable(
"project_directory",
{
+9
View File
@@ -0,0 +1,9 @@
import type { Proc } from "./pty.js"
export type { Disp, Exit, Opts, Proc } from "./pty.js"
// workerd cannot spawn processes; the Pty service surfaces this as a defect if
// a terminal is ever requested on this runtime.
export function spawn(): Proc {
throw new Error("Pseudo-terminals are unavailable on the workerd runtime")
}
+60 -55
View File
@@ -31,7 +31,7 @@ import { ForkEmptyError, MessageDecodeError, NotFoundError } from "./session/err
import { makeGlobalNode } from "@opencode-ai/util/effect/app-node"
import { LocationServiceMap } from "./location-service-map.js"
import { SessionEvent } from "./session/event.js"
import { SessionPending } from "./session/pending.js"
import { SessionInbox } from "./session/inbox.js"
import { InstructionState } from "./session/instruction-state.js"
import { SessionGenerate } from "./session/generate.js"
import { Snapshot } from "./snapshot.js"
@@ -99,6 +99,7 @@ type CreateInput = CreateBaseInput &
type CompactInput = {
id?: SessionMessage.ID
sessionID: SessionSchema.ID
delivery?: SessionInbox.Delivery
}
type ForkInput = {
@@ -133,14 +134,11 @@ export class CompactionConflictError extends Schema.TaggedErrorClass<CompactionC
export class BusyError extends Schema.TaggedErrorClass<BusyError>()("Session.BusyError", {
sessionID: SessionSchema.ID,
}) {}
export class PendingInputConflictError extends Schema.TaggedErrorClass<PendingInputConflictError>()(
"Session.PendingInputConflictError",
{
sessionID: SessionSchema.ID,
inputID: SessionMessage.ID,
},
) {}
type PendingInputRef = { readonly sessionID: SessionSchema.ID; readonly inputID: SessionMessage.ID }
export class InboxConflictError extends Schema.TaggedErrorClass<InboxConflictError>()("Session.InboxConflictError", {
sessionID: SessionSchema.ID,
inboxID: SessionMessage.ID,
}) {}
type InboxItemRef = { readonly sessionID: SessionSchema.ID; readonly inboxID: SessionMessage.ID }
export class SkillNotFoundError extends Schema.TaggedErrorClass<SkillNotFoundError>()("Session.SkillNotFoundError", {
skill: Skill.ID,
}) {}
@@ -185,13 +183,13 @@ export interface Interface {
) => Effect.Effect<SessionMessage.Info[], NotFoundError | MessageDecodeError>
/**
* Durable admitted session work not yet visible in projected history,
* ordered by admission. Includes unpromoted user and synthetic inputs,
* unhandled compaction barriers, and deferred moves.
* ordered by admission. Includes unpromoted user and synthetic inputs and
* unhandled compaction barriers.
*/
readonly pending: (sessionID: SessionSchema.ID) => Effect.Effect<SessionPending.Info[], NotFoundError>
readonly cancelPending: (input: PendingInputRef) => Effect.Effect<void, NotFoundError | PendingInputConflictError>
readonly steerPending: (input: PendingInputRef) => Effect.Effect<void, NotFoundError | PendingInputConflictError>
readonly queuePending: (input: PendingInputRef) => Effect.Effect<void, NotFoundError | PendingInputConflictError>
readonly inbox: (sessionID: SessionSchema.ID) => Effect.Effect<SessionInbox.Info[], NotFoundError>
readonly cancelInbox: (input: InboxItemRef) => Effect.Effect<void, NotFoundError | InboxConflictError>
readonly steerInbox: (input: InboxItemRef) => Effect.Effect<void, NotFoundError | InboxConflictError>
readonly queueInbox: (input: InboxItemRef) => Effect.Effect<void, NotFoundError | InboxConflictError>
/**
* Durable, ordered session log read. Replays durable session bus after
* the exclusive `after` cursor, emits a `Synced` marker at the captured
@@ -211,6 +209,7 @@ export interface Interface {
sessionID: SessionSchema.ID
directory: AbsolutePath
workspaceID?: Location.Ref["workspaceID"]
delivery?: SessionInbox.Delivery
}) => Effect.Effect<void, NotFoundError | DestinationNotFoundError | DestinationNotDirectoryError>
readonly prompt: (input: {
id?: SessionMessage.ID
@@ -220,9 +219,9 @@ export interface Interface {
agents?: PromptInput.Prompt["agents"]
skills?: PromptInput.Prompt["skills"]
metadata?: Record<string, unknown>
delivery?: SessionPending.Delivery
delivery?: SessionInbox.Delivery
resume?: boolean
}) => Effect.Effect<SessionPending.User, NotFoundError | PromptConflictError | AttachmentError | SkillNotFoundError>
}) => Effect.Effect<SessionInbox.User, NotFoundError | PromptConflictError | AttachmentError | SkillNotFoundError>
/** Generates text from current Session context without admitting input or mutating history. */
readonly generate: (input: {
sessionID: SessionSchema.ID
@@ -238,10 +237,10 @@ export interface Interface {
files?: PromptInput.Prompt["files"]
agents?: PromptInput.Prompt["agents"]
skills?: PromptInput.Prompt["skills"]
delivery?: SessionPending.Delivery
delivery?: SessionInbox.Delivery
resume?: boolean
}) => Effect.Effect<
SessionPending.User,
SessionInbox.User,
| NotFoundError
| PromptConflictError
| AttachmentError
@@ -262,7 +261,7 @@ export interface Interface {
}) => Effect.Effect<void, NotFoundError | SkillNotFoundError>
readonly compact: (
input: CompactInput,
) => Effect.Effect<SessionPending.Compaction, NotFoundError | CompactionConflictError>
) => Effect.Effect<SessionInbox.Compaction, NotFoundError | CompactionConflictError>
readonly wait: (id: SessionSchema.ID) => Effect.Effect<void, NotFoundError>
readonly active: Effect.Effect<ReadonlySet<SessionSchema.ID>>
readonly background: (sessionID: SessionSchema.ID) => Effect.Effect<void, NotFoundError>
@@ -274,9 +273,9 @@ export interface Interface {
text: string
description?: string
metadata?: Record<string, unknown>
delivery?: SessionPending.Delivery
delivery?: SessionInbox.Delivery
resume?: boolean
}) => Effect.Effect<SessionPending.Synthetic, NotFoundError | SyntheticConflictError>
}) => Effect.Effect<SessionInbox.Synthetic, NotFoundError | SyntheticConflictError>
readonly revert: {
readonly stage: (input: {
sessionID: SessionSchema.ID
@@ -321,12 +320,12 @@ const layer = Layer.effect(
),
)
const pendingConflict = Effect.fn("Session.pendingConflict")(function* (input: PendingInputRef) {
const pendingConflict = Effect.fn("Session.pendingConflict")(function* (input: InboxItemRef) {
yield* result.get(input.sessionID)
return yield* new PendingInputConflictError(input)
return yield* new InboxConflictError(input)
})
const mutatePending = (
input: PendingInputRef,
input: InboxItemRef,
mutation: (
bus: Bus.Interface,
input: { readonly id: SessionMessage.ID; readonly sessionID: SessionSchema.ID },
@@ -335,9 +334,9 @@ const layer = Layer.effect(
) =>
Effect.uninterruptible(
Effect.gen(function* () {
yield* mutation(bus, { sessionID: input.sessionID, id: input.inputID }).pipe(
yield* mutation(bus, { sessionID: input.sessionID, id: input.inboxID }).pipe(
Effect.catchDefect((defect) =>
defect instanceof SessionPending.LifecycleConflict ? pendingConflict(input) : Effect.die(defect),
defect instanceof SessionInbox.LifecycleConflict ? pendingConflict(input) : Effect.die(defect),
),
)
if (wake) yield* execution.wake(input.sessionID)
@@ -529,13 +528,13 @@ const layer = Layer.effect(
yield* result.get(sessionID)
return yield* store.context(sessionID)
}),
pending: Effect.fn("Session.pending")(function* (sessionID) {
inbox: Effect.fn("Session.inbox")(function* (sessionID) {
yield* result.get(sessionID)
return yield* SessionPending.list(db, sessionID)
return yield* SessionInbox.list(db, sessionID)
}),
cancelPending: Effect.fn("Session.cancelPending")((input) => mutatePending(input, SessionPending.cancel)),
steerPending: Effect.fn("Session.steerPending")((input) => mutatePending(input, SessionPending.steer, true)),
queuePending: Effect.fn("Session.queuePending")((input) => mutatePending(input, SessionPending.queue)),
cancelInbox: Effect.fn("Session.cancelInbox")((input) => mutatePending(input, SessionInbox.cancel)),
steerInbox: Effect.fn("Session.steerInbox")((input) => mutatePending(input, SessionInbox.steer, true)),
queueInbox: Effect.fn("Session.queueInbox")((input) => mutatePending(input, SessionInbox.queue)),
log: (input) =>
Stream.unwrap(
result
@@ -564,25 +563,25 @@ const layer = Layer.effect(
skills,
).pipe(Effect.provideService(FSUtil.Service, fs))
const messageID = input.id ?? SessionMessage.ID.create()
const admittedInput = SessionPending.Message.make({
const admittedInput = SessionInbox.Item.make({
type: "user",
data: { ...prompt, metadata: input.metadata },
payload: { ...prompt, metadata: input.metadata },
delivery: input.delivery ?? "steer",
})
const admitted = yield* SessionPending.admit(db, bus, {
const admitted = yield* SessionInbox.admit(db, bus, {
id: messageID,
sessionID: input.sessionID,
input: admittedInput,
item: admittedInput,
}).pipe(
Effect.catchDefect((defect) =>
defect instanceof SessionPending.LifecycleConflict
defect instanceof SessionInbox.LifecycleConflict
? new PromptConflictError({ sessionID: input.sessionID, messageID })
: Effect.die(defect),
),
)
if (
admitted.type !== "user" ||
!SessionPending.equivalent(admitted, { sessionID: input.sessionID, input: admittedInput })
!SessionInbox.equivalent(admitted, { sessionID: input.sessionID, item: admittedInput })
)
return yield* new PromptConflictError({ sessionID: input.sessionID, messageID })
if (input.resume !== false) {
@@ -640,7 +639,9 @@ const layer = Layer.effect(
yield* execution.awaitIdle(input.sessionID)
const started = yield* Effect.gen(function* () {
const shell = yield* Shell.Service
return yield* shell.create({ command: input.command, cwd: session.location.directory, timeout: 0 })
return yield* shell
.create({ command: input.command, cwd: session.location.directory, timeout: 0 })
.pipe(Effect.orDie)
}).pipe(Effect.provide(locations.get(session.location)))
yield* bus.publish(
SessionEvent.Shell.Started,
@@ -738,31 +739,35 @@ const layer = Layer.effect(
const info = yield* fs.stat(directory).pipe(Effect.catch(() => Effect.succeed(undefined)))
if (!info) return yield* new DestinationNotFoundError({ directory })
if (info.type !== "Directory") return yield* new DestinationNotDirectoryError({ directory })
const pending = yield* SessionPending.move(db, input.sessionID)
if (!pending && current.location.directory === directory && current.location.workspaceID === input.workspaceID)
return
const project = yield* projects.resolve(directory)
yield* persistProject(project)
yield* SessionPending.admitMove(db, bus, {
sessionID: input.sessionID,
source: current.location,
data: {
const item = SessionInbox.Item.make({
type: "move",
payload: {
location: Location.Ref.make({ directory, workspaceID: input.workspaceID }),
projectID: project.id,
subpath: RelativePath.make(path.relative(project.directory, directory).replaceAll("\\", "/")),
},
delivery: input.delivery ?? "steer",
})
const inboxID = SessionMessage.ID.create()
yield* SessionInbox.admit(db, bus, {
id: inboxID,
sessionID: input.sessionID,
item,
})
yield* execution.wake(input.sessionID)
}),
compact: Effect.fn("Session.compact")(function* (input) {
yield* result.get(input.sessionID)
const inputID = input.id ?? SessionMessage.ID.create()
const admitted = yield* SessionPending.admitCompaction(db, bus, {
const admitted = yield* SessionInbox.admitCompaction(db, bus, {
id: inputID,
sessionID: input.sessionID,
delivery: input.delivery ?? "queue",
}).pipe(
Effect.catchDefect((defect) =>
defect instanceof SessionPending.LifecycleConflict
defect instanceof SessionInbox.LifecycleConflict
? new CompactionConflictError({ sessionID: input.sessionID, inputID })
: Effect.die(defect),
),
@@ -802,29 +807,29 @@ const layer = Layer.effect(
Effect.gen(function* () {
yield* result.get(input.sessionID)
const inputID = input.id ?? SessionMessage.ID.create()
const admittedInput = SessionPending.Message.make({
const admittedInput = SessionInbox.Item.make({
type: "synthetic",
data: {
payload: {
text: input.text,
description: input.description,
metadata: input.metadata,
},
delivery: input.delivery ?? "steer",
})
const admitted = yield* SessionPending.admit(db, bus, {
const admitted = yield* SessionInbox.admit(db, bus, {
id: inputID,
sessionID: input.sessionID,
input: admittedInput,
item: admittedInput,
}).pipe(
Effect.catchDefect((defect) =>
defect instanceof SessionPending.LifecycleConflict
defect instanceof SessionInbox.LifecycleConflict
? new SyntheticConflictError({ sessionID: input.sessionID, inputID })
: Effect.die(defect),
),
)
if (
admitted.type !== "synthetic" ||
!SessionPending.equivalent(admitted, { sessionID: input.sessionID, input: admittedInput })
!SessionInbox.equivalent(admitted, { sessionID: input.sessionID, item: admittedInput })
)
return yield* new SyntheticConflictError({ sessionID: input.sessionID, inputID })
if (input.resume !== false && !(yield* result.get(input.sessionID)).revert)
@@ -837,7 +842,7 @@ const layer = Layer.effect(
Effect.uninterruptible(
Effect.gen(function* () {
yield* execution.interrupt(sessionID)
if (options?.continue && (yield* SessionPending.has(db, sessionID, "any"))) yield* execution.wake(sessionID)
if (options?.continue && (yield* SessionInbox.has(db, sessionID, "any"))) yield* execution.wake(sessionID)
}),
),
),
+10 -6
View File
@@ -89,6 +89,7 @@ export type ManualInput = {
readonly session: SessionSchema.Info
readonly messages: readonly SessionMessage.Info[]
readonly inputID: SessionMessage.ID
readonly started?: boolean
}
type RequiredInput = Omit<AutoInput, "ref">
@@ -102,6 +103,7 @@ type Plan = {
readonly prompt: string
readonly recent: string
readonly inputID?: SessionMessage.ID
readonly started?: boolean
}
export type Outcome =
@@ -248,12 +250,13 @@ const make = (dependencies: Dependencies) => {
return { status: "failed" as const, error: input.error }
})
const execute = Effect.fn("SessionCompaction.execute")(function* (plan: Plan) {
yield* dependencies.bus.publish(SessionEvent.Compaction.Started, {
sessionID: plan.session.id,
reason: plan.reason,
recent: plan.recent,
inputID: plan.inputID,
})
if (!plan.started)
yield* dependencies.bus.publish(SessionEvent.Compaction.Started, {
sessionID: plan.session.id,
reason: plan.reason,
recent: plan.recent,
inputID: plan.inputID,
})
const chunks: string[] = []
let failure: SessionError.Error | undefined
@@ -408,6 +411,7 @@ const make = (dependencies: Dependencies) => {
cost: resolved.cost,
reason: "manual",
inputID: input.inputID,
started: input.started,
...content,
})
})
+39 -35
View File
@@ -1,6 +1,6 @@
export * as SessionExecution from "./execution.js"
import { Cause, Context, Effect, Exit, Layer } from "effect"
import { Cause, Context, Effect, Exit, Layer, Stream } from "effect"
import { Bus } from "../bus.js"
import { LocationServiceMap } from "../location-service-map.js"
import { makeGlobalNode } from "@opencode-ai/util/effect/app-node"
@@ -11,8 +11,6 @@ import { SessionSchema } from "./schema.js"
import { SessionStore } from "./store.js"
import { toSessionError } from "./to-session-error.js"
import { UserInterruptedError } from "./error.js"
import { Database } from "../database/database.js"
import { SessionPending } from "./pending.js"
export interface Interface {
/** Snapshots active execution owned by this process. */
@@ -47,7 +45,6 @@ export const layer = Layer.effect(
const store = yield* SessionStore.Service
const locations = yield* LocationServiceMap.Service
const bus = yield* Bus.Service
const db = (yield* Database.Service).db
const reportLifecycle = <A>(sessionID: SessionSchema.ID, effect: Effect.Effect<A>) =>
effect.pipe(
Effect.tapCause((cause) =>
@@ -57,6 +54,7 @@ export const layer = Layer.effect(
Effect.annotateLogs({ sessionID }),
),
),
Effect.asVoid,
)
// Write-ahead claim: starting records the durable intent that a turn is in flight, in the same
// transaction as the started event. Terminals release it — except shutdown interruption, which
@@ -69,16 +67,17 @@ export const layer = Layer.effect(
const releaseOnCommit = (sessionID: SessionSchema.ID) => ({
commit: () => store.release(sessionID),
})
const coordinator = yield* SessionRunCoordinator.make<SessionSchema.ID, SessionRunner.RunError, InterruptReason>({
started: (sessionID) =>
reportLifecycle(
sessionID,
bus.publish(SessionEvent.Execution.Started, { sessionID }, claimOnCommit(sessionID)),
).pipe(Effect.asVoid),
drain: Effect.fnUntraced(function* (sessionID: SessionSchema.ID, force) {
function drain(
sessionID: SessionSchema.ID,
force: boolean,
continuation?: SessionRunner.Continuation,
): Effect.Effect<void, SessionRunner.RunError> {
return Effect.gen(function* () {
const session = yield* store.get(sessionID)
if (!session) return yield* Effect.die(new Error(`Session not found: ${sessionID}`))
return yield* SessionRunner.Service.use((runner) => runner.drain({ sessionID, force })).pipe(
const result = yield* SessionRunner.Service.use((runner) =>
runner.drain({ sessionID, force, continuation }),
).pipe(
Effect.provide(locations.get(session.location)),
Effect.tapCause((cause) =>
Cause.hasInterruptsOnly(cause)
@@ -86,16 +85,28 @@ export const layer = Layer.effect(
: Effect.logError("Failed to drain Session", cause).pipe(Effect.annotateLogs({ sessionID })),
),
)
}),
if (result.type === "complete") return
return yield* drain(sessionID, false, result.continuation)
})
}
const coordinator = yield* SessionRunCoordinator.make<SessionSchema.ID, SessionRunner.RunError, InterruptReason>({
started: (sessionID) =>
reportLifecycle(
sessionID,
bus.publish(SessionEvent.Execution.Started, { sessionID }, claimOnCommit(sessionID)),
),
drain: (sessionID, force) => drain(sessionID, force),
// One terminal observation per busy period, covering every coalesced drain.
settled: (sessionID, exit, reason) =>
reportLifecycle(
sessionID,
Effect.gen(function* () {
const outcome = terminal(exit, reason)
if (outcome.type === "succeeded")
if (outcome.type === "succeeded") {
yield* bus.publish(SessionEvent.Execution.Succeeded, { sessionID }, releaseOnCommit(sessionID))
if (outcome.type === "interrupted")
return
}
if (outcome.type === "interrupted") {
// A user cancel (or a superseding execution) releases the claim: the turn must not
// resurrect at the next boot. Shutdown interruption keeps it for restart continuity.
yield* bus.publish(
@@ -103,30 +114,23 @@ export const layer = Layer.effect(
{ sessionID, reason: outcome.reason },
outcome.reason === "shutdown" ? undefined : releaseOnCommit(sessionID),
)
if (outcome.type === "failed")
yield* bus.publish(
SessionEvent.Execution.Failed,
{
sessionID,
error: outcome.error,
},
releaseOnCommit(sessionID),
)
if (outcome.type === "interrupted" && outcome.reason === "shutdown") return false
const pending = yield* SessionPending.move(db, sessionID)
if (!pending) return false
const session = yield* store.get(sessionID)
if (!session) return yield* Effect.die(new Error(`Session not found: ${sessionID}`))
return
}
yield* bus.publish(
SessionEvent.Moved,
{ sessionID, moveID: pending.id, ...pending.data },
{ location: session.location },
SessionEvent.Execution.Failed,
{
sessionID,
error: outcome.error,
},
releaseOnCommit(sessionID),
)
return yield* SessionPending.has(db, sessionID, "any")
}),
),
})
yield* bus.subscribe(SessionEvent.Moved).pipe(
Stream.runForEach((event) => coordinator.wake(event.data.sessionID)),
Effect.forkScoped,
)
return Service.of({
active: coordinator.active,
@@ -141,7 +145,7 @@ export const layer = Layer.effect(
export const node = makeGlobalNode({
service: Service,
layer,
deps: [SessionStore.node, LocationServiceMap.node, Bus.node, Database.node],
deps: [SessionStore.node, LocationServiceMap.node, Bus.node],
})
/** Low-level compatibility layer for callers that only need durable Session recording. */
+1 -12
View File
@@ -7,8 +7,6 @@ import { SessionEvent } from "../event.js"
import { SessionExecution } from "../execution.js"
import { SessionSchema } from "../schema.js"
import { SessionStore } from "../store.js"
import { Database } from "../../database/database.js"
import { SessionPending } from "../pending.js"
const CONTINUE_AFTER_SERVER_RESTART =
"The server restarted while you were working. Continue from where you left off without repeating completed work."
@@ -64,7 +62,6 @@ export const layer = (options?: Options) =>
const store = yield* SessionStore.Service
const execution = yield* SessionExecution.Service
const bus = yield* Bus.Service
const db = (yield* Database.Service).db
const scope = yield* Effect.scope
const maxAttempts = options?.maxAttempts ?? DEFAULT_MAX_ATTEMPTS
@@ -106,14 +103,6 @@ export const layer = (options?: Options) =>
// them would only inject a stray continuation into a live turn.
const orphaned = (yield* store.listSuspended()).filter((sessionID) => !active.has(sessionID))
yield* Effect.forEach(orphaned, resumeOne, { concurrency: "unbounded", discard: true })
const claimed = new Set(orphaned)
yield* Effect.forEach(
(yield* SessionPending.moveSessions(db)).filter(
(sessionID) => !active.has(sessionID) && !claimed.has(sessionID),
),
execution.wake,
{ concurrency: "unbounded", discard: true },
)
}),
})
}),
@@ -122,5 +111,5 @@ export const layer = (options?: Options) =>
export const node = makeGlobalNode({
service: Service,
layer: layer(),
deps: [SessionStore.node, SessionExecution.node, Bus.node, Database.node],
deps: [SessionStore.node, SessionExecution.node, Bus.node],
})
+505
View File
@@ -0,0 +1,505 @@
export * as SessionInbox from "./inbox.js"
import { and, asc, eq, or } from "drizzle-orm"
import { DateTime, Effect, Schema } from "effect"
import {
Compaction,
CompactionPayload,
Delivery,
Info,
Item,
Move,
MovePayload,
Synthetic,
SyntheticPayload,
User,
UserPayload,
} from "@opencode-ai/schema/session-inbox"
import type { Database } from "../database/database.js"
import { Bus } from "../bus.js"
import { KeyedMutex } from "../effect/keyed-mutex.js"
import { SessionEvent } from "./event.js"
import { SessionMessage } from "./message.js"
import { SessionSchema } from "./schema.js"
import { SessionInboxTable, SessionMessageTable } from "./sql.js"
type DatabaseService = Database.Interface["db"]
export {
Compaction,
CompactionPayload,
Delivery,
Info,
Item,
Move,
MovePayload,
Synthetic,
SyntheticPayload,
User,
UserPayload,
}
/**
* Which pending input `promote` may consume: "steer" promotes steers only (a step
* boundary mid-work), while "input" also allows one queued input when no steers are
* waiting (the idle boundary, where the Session picks up fresh work).
*/
export type Promotable = "input" | "steer"
const decodeUser = Schema.decodeUnknownSync(UserPayload)
const encodeUser = Schema.encodeSync(UserPayload)
const decodeSynthetic = Schema.decodeUnknownSync(SyntheticPayload)
const encodeSynthetic = Schema.encodeSync(SyntheticPayload)
const decodeCompaction = Schema.decodeUnknownSync(CompactionPayload)
const encodeCompaction = Schema.encodeSync(CompactionPayload)
const decodeMove = Schema.decodeUnknownSync(MovePayload)
const encodeMove = Schema.encodeSync(MovePayload)
const decodeMessage = Schema.decodeUnknownSync(SessionMessage.Info)
const inboxLocks = KeyedMutex.makeUnsafe<SessionSchema.ID>()
type PendingRef = { readonly id: SessionMessage.ID; readonly sessionID: SessionSchema.ID }
export const serialized = <A, E, R>(sessionID: SessionSchema.ID, effect: Effect.Effect<A, E, R>) =>
inboxLocks.withLock(sessionID)(effect)
export class LifecycleConflict extends Schema.TaggedErrorClass<LifecycleConflict>()("SessionInbox.LifecycleConflict", {
id: SessionMessage.ID,
}) {}
const fromRow = (row: typeof SessionInboxTable.$inferSelect): Info => {
const base = {
id: SessionMessage.ID.make(row.id),
sessionID: SessionSchema.ID.make(row.session_id),
timeCreated: DateTime.makeUnsafe(row.time_created),
}
if (row.type === "compaction")
return Compaction.make({
...base,
type: "compaction",
payload: decodeCompaction(row.payload),
delivery: row.delivery,
})
if (row.type === "move")
return Move.make({ ...base, type: "move", payload: decodeMove(row.payload), delivery: row.delivery })
if (row.type === "user")
return User.make({
...base,
type: "user",
payload: decodeUser(row.payload),
delivery: row.delivery,
})
if (row.type === "synthetic")
return Synthetic.make({
...base,
type: "synthetic",
payload: decodeSynthetic(row.payload),
delivery: row.delivery,
})
throw new LifecycleConflict({ id: base.id })
}
export const find = Effect.fn("SessionInbox.find")(function* (db: DatabaseService, id: SessionMessage.ID) {
const row = yield* db.select().from(SessionInboxTable).where(eq(SessionInboxTable.id, id)).get().pipe(Effect.orDie)
return row === undefined ? undefined : fromRow(row)
})
const promotedFromMessage = Effect.fn("SessionInbox.promotedFromMessage")(function* (
db: DatabaseService,
sessionID: SessionSchema.ID,
id: SessionMessage.ID,
delivery: Delivery,
) {
const row = yield* db
.select()
.from(SessionMessageTable)
.where(eq(SessionMessageTable.id, id))
.get()
.pipe(Effect.orDie)
if (row === undefined) return undefined
if (row.session_id !== sessionID || (row.type !== "user" && row.type !== "synthetic"))
return yield* Effect.die(new LifecycleConflict({ id }))
const message = decodeMessage({ ...row.data, id: row.id, type: row.type })
const base = { id, sessionID, timeCreated: message.time.created, delivery }
if (message.type === "user")
return User.make({
...base,
type: "user",
payload: decodeUser(message),
})
if (message.type === "synthetic")
return Synthetic.make({
...base,
type: "synthetic",
payload: decodeSynthetic(message),
})
return yield* Effect.die(new LifecycleConflict({ id }))
})
export const admit = Effect.fn("SessionInbox.admit")(function* (
db: DatabaseService,
bus: Bus.Interface,
request: {
readonly id: SessionMessage.ID
readonly sessionID: SessionSchema.ID
readonly item: Item
},
) {
const existing = yield* find(db, request.id)
if (existing !== undefined) {
if (existing.type === "compaction") return yield* Effect.die(new LifecycleConflict({ id: request.id }))
return existing
}
const promoted = yield* promotedFromMessage(db, request.sessionID, request.id, request.item.delivery)
if (promoted !== undefined) return promoted
return yield* bus
.publish(SessionEvent.InboxEnqueued, {
inboxID: request.id,
sessionID: request.sessionID,
item: request.item,
})
.pipe(
Effect.flatMap((event) => {
const base = {
id: request.id,
sessionID: request.sessionID,
timeCreated: event.created,
}
return Effect.succeed(Info.make({ ...base, ...request.item }))
}),
Effect.catchDefect((defect) =>
find(db, request.id).pipe(
Effect.flatMap((stored) =>
stored?.type === request.item.type ? Effect.succeed(stored) : Effect.die(defect),
),
),
),
)
})
export const admitCompaction = Effect.fn("SessionInbox.admitCompaction")(function* (
db: DatabaseService,
bus: Bus.Interface,
input: { readonly id: SessionMessage.ID; readonly sessionID: SessionSchema.ID; readonly delivery: Delivery },
) {
const admitted = yield* admit(db, bus, {
id: input.id,
sessionID: input.sessionID,
item: Item.make({ type: "compaction", payload: {}, delivery: input.delivery }),
})
if (admitted.type === "compaction") return admitted
return yield* Effect.die(new LifecycleConflict({ id: input.id }))
})
export const projectAdmitted = Effect.fn("SessionInbox.projectAdmitted")(function* (
db: DatabaseService,
request: {
readonly enqueuedSeq: number
readonly id: SessionMessage.ID
readonly sessionID: SessionSchema.ID
readonly item: Item
readonly timeCreated: DateTime.Utc
},
) {
const message = yield* db
.select({ id: SessionMessageTable.id })
.from(SessionMessageTable)
.where(eq(SessionMessageTable.id, request.id))
.get()
.pipe(Effect.orDie)
if (message !== undefined) return yield* Effect.die(new LifecycleConflict({ id: request.id }))
const stored = yield* db
.insert(SessionInboxTable)
.values({
id: request.id,
session_id: request.sessionID,
type: request.item.type,
payload:
request.item.type === "user"
? encodeUser(request.item.payload)
: request.item.type === "synthetic"
? encodeSynthetic(request.item.payload)
: request.item.type === "compaction"
? encodeCompaction(request.item.payload)
: encodeMove(request.item.payload),
delivery: request.item.delivery,
enqueued_seq: request.enqueuedSeq,
time_created: DateTime.toEpochMillis(request.timeCreated),
})
.onConflictDoNothing()
.returning({ id: SessionInboxTable.id })
.get()
.pipe(Effect.orDie)
if (!stored) return yield* Effect.die(new LifecycleConflict({ id: request.id }))
})
/**
* Consume one pending row at promotion. The row's content feeds the projected
* message insert inside the same event transaction; the deleted row is what
* makes the table pending-only.
*/
export const projectDelivered = Effect.fn("SessionInbox.projectDelivered")(function* (
db: DatabaseService,
input: PendingRef,
) {
const deleted = yield* db
.delete(SessionInboxTable)
.where(and(eq(SessionInboxTable.id, input.id), eq(SessionInboxTable.session_id, input.sessionID)))
.returning()
.get()
.pipe(Effect.orDie)
if (!deleted) return yield* Effect.die(new LifecycleConflict({ id: input.id }))
return fromRow(deleted)
})
export const projectCancelled = Effect.fn("SessionInbox.projectCancelled")(function* (
db: DatabaseService,
input: PendingRef,
) {
const deleted = yield* db
.delete(SessionInboxTable)
.where(
and(
eq(SessionInboxTable.id, input.id),
eq(SessionInboxTable.session_id, input.sessionID),
or(eq(SessionInboxTable.delivery, "queue"), eq(SessionInboxTable.delivery, "steer")),
),
)
.returning({ id: SessionInboxTable.id })
.get()
.pipe(Effect.orDie)
if (!deleted) return yield* Effect.die(new LifecycleConflict({ id: input.id }))
})
const projectDelivery = Effect.fn("SessionInbox.projectDelivery")(function* (
db: DatabaseService,
input: PendingRef & { readonly from: Delivery; readonly to: Delivery },
) {
const updated = yield* db
.update(SessionInboxTable)
.set({ delivery: input.to })
.where(
and(
eq(SessionInboxTable.id, input.id),
eq(SessionInboxTable.session_id, input.sessionID),
eq(SessionInboxTable.delivery, input.from),
),
)
.returning({ id: SessionInboxTable.id })
.get()
.pipe(Effect.orDie)
if (!updated) return yield* Effect.die(new LifecycleConflict({ id: input.id }))
})
export const projectDeliveryChanged = Effect.fn("SessionInbox.projectDeliveryChanged")(
(db: DatabaseService, input: PendingRef & { readonly delivery: Delivery }) =>
projectDelivery(db, {
...input,
from: input.delivery === "steer" ? "queue" : "steer",
to: input.delivery,
}),
)
export const list = Effect.fn("SessionInbox.list")(function* (db: DatabaseService, sessionID: SessionSchema.ID) {
const rows = yield* db
.select()
.from(SessionInboxTable)
.where(eq(SessionInboxTable.session_id, sessionID))
.orderBy(asc(SessionInboxTable.enqueued_seq))
.all()
.pipe(Effect.orDie)
return rows.map(fromRow)
})
export const nextQueued = Effect.fn("SessionInbox.nextQueued")(function* (
db: DatabaseService,
sessionID: SessionSchema.ID,
) {
const row = yield* db
.select()
.from(SessionInboxTable)
.where(and(eq(SessionInboxTable.session_id, sessionID), eq(SessionInboxTable.delivery, "queue")))
.orderBy(asc(SessionInboxTable.enqueued_seq))
.limit(1)
.get()
.pipe(Effect.orDie)
return row ? fromRow(row) : undefined
})
export const nextSteer = Effect.fn("SessionInbox.nextSteer")(function* (
db: DatabaseService,
sessionID: SessionSchema.ID,
) {
const row = yield* db
.select()
.from(SessionInboxTable)
.where(and(eq(SessionInboxTable.session_id, sessionID), eq(SessionInboxTable.delivery, "steer")))
.orderBy(asc(SessionInboxTable.enqueued_seq))
.limit(1)
.get()
.pipe(Effect.orDie)
return row ? fromRow(row) : undefined
})
/**
* Which pending rows count: "any" counts every row, while "input" means any
* item in either delivery mode.
*/
export type Scope = "any" | "input" | Delivery
export const has = Effect.fn("SessionInbox.has")(function* (
db: DatabaseService,
sessionID: SessionSchema.ID,
scope: Scope,
) {
const row = yield* db
.select({ id: SessionInboxTable.id })
.from(SessionInboxTable)
.where(
and(
eq(SessionInboxTable.session_id, sessionID),
scope === "any"
? undefined
: scope === "input"
? or(eq(SessionInboxTable.delivery, "steer"), eq(SessionInboxTable.delivery, "queue"))
: eq(SessionInboxTable.delivery, scope),
),
)
.limit(1)
.get()
.pipe(Effect.orDie)
return row !== undefined
})
export const equivalent = (input: Info, expected: { readonly sessionID: SessionSchema.ID; readonly item: Item }) => {
if (
input.type !== expected.item.type ||
input.delivery !== expected.item.delivery ||
input.sessionID !== expected.sessionID
)
return false
if (input.type === "user" && expected.item.type === "user")
return JSON.stringify(encodeUser(input.payload)) === JSON.stringify(encodeUser(expected.item.payload))
if (input.type === "synthetic" && expected.item.type === "synthetic")
return JSON.stringify(encodeSynthetic(input.payload)) === JSON.stringify(encodeSynthetic(expected.item.payload))
if (input.type === "compaction" && expected.item.type === "compaction") return true
if (input.type === "move" && expected.item.type === "move")
return JSON.stringify(encodeMove(input.payload)) === JSON.stringify(encodeMove(expected.item.payload))
return false
}
const publishMutation = <A, E, R>(input: PendingRef, effect: Effect.Effect<A, E, R>) =>
serialized(input.sessionID, effect).pipe(Effect.asVoid)
export const cancel = Effect.fn("SessionInbox.cancel")((bus: Bus.Interface, input: PendingRef) =>
publishMutation(
input,
bus.publish(SessionEvent.InboxCancelled, {
sessionID: input.sessionID,
inboxID: input.id,
}),
),
)
export const steer = Effect.fn("SessionInbox.steer")((bus: Bus.Interface, input: PendingRef) =>
publishMutation(
input,
bus.publish(SessionEvent.InboxDeliveryChanged, {
sessionID: input.sessionID,
inboxID: input.id,
delivery: "steer",
}),
),
)
export const queue = Effect.fn("SessionInbox.queue")((bus: Bus.Interface, input: PendingRef) =>
publishMutation(
input,
bus.publish(SessionEvent.InboxDeliveryChanged, {
sessionID: input.sessionID,
inboxID: input.id,
delivery: "queue",
}),
),
)
const publish = Effect.fn("SessionInbox.publish")(function* (
db: DatabaseService,
bus: Bus.Interface,
sessionID: SessionSchema.ID,
rows: ReadonlyArray<typeof SessionInboxTable.$inferSelect>,
) {
yield* Effect.forEach(
rows,
(row) => {
const entry = fromRow(row)
if (entry.type === "compaction") return Effect.die(new LifecycleConflict({ id: entry.id }))
return bus
.publish(SessionEvent.InboxDelivered, {
sessionID,
inboxID: entry.id,
})
.pipe(
Effect.catchDefect((defect) =>
defect instanceof LifecycleConflict
? promotedFromMessage(db, sessionID, entry.id, entry.delivery).pipe(
Effect.flatMap((stored) => (stored !== undefined ? Effect.void : Effect.die(defect))),
)
: Effect.die(defect),
),
)
},
{ discard: true },
)
return rows.length
})
/**
* Promotes pending input into visible messages and returns the promoted count.
* Steers always go first; only the "input" scope may fall through to one queued
* input, and it then collects steers that arrived during promotion.
*/
export const promote = Effect.fn("SessionInbox.promote")(function* (
db: DatabaseService,
bus: Bus.Interface,
sessionID: SessionSchema.ID,
scope: Promotable,
) {
return yield* serialized(
sessionID,
Effect.gen(function* () {
const steers = yield* db
.select()
.from(SessionInboxTable)
.where(and(eq(SessionInboxTable.session_id, sessionID), eq(SessionInboxTable.delivery, "steer")))
.orderBy(asc(SessionInboxTable.enqueued_seq))
.all()
.pipe(Effect.orDie)
if (steers.length > 0 || scope === "steer") {
const control = steers.findIndex((row) => row.type === "compaction" || row.type === "move")
return yield* publish(db, bus, sessionID, control === -1 ? steers : steers.slice(0, control))
}
const queued = yield* db
.select()
.from(SessionInboxTable)
.where(and(eq(SessionInboxTable.session_id, sessionID), eq(SessionInboxTable.delivery, "queue")))
.orderBy(asc(SessionInboxTable.enqueued_seq))
.limit(1)
.get()
.pipe(Effect.orDie)
if (!queued) return 0
const promoted = yield* publish(db, bus, sessionID, [queued])
const arrivedSteers = yield* db
.select()
.from(SessionInboxTable)
.where(and(eq(SessionInboxTable.session_id, sessionID), eq(SessionInboxTable.delivery, "steer")))
.orderBy(asc(SessionInboxTable.enqueued_seq))
.all()
.pipe(Effect.orDie)
const control = arrivedSteers.findIndex((row) => row.type === "compaction" || row.type === "move")
return (
promoted +
(yield* publish(db, bus, sessionID, control === -1 ? arrivedSteers : arrivedSteers.slice(0, control)))
)
}),
)
})
+4 -7
View File
@@ -106,15 +106,13 @@ export function update(adapter: Adapter, event: SessionEvent.DurableEvent) {
)
})
},
"session.move.admitted": () => Effect.void,
"session.renamed": () => Effect.void,
"session.deleted": () => Effect.void,
"session.forked": () => Effect.void,
"session.input.promoted": () => Effect.void,
"session.input.admitted": () => Effect.void,
"session.input.cancelled": () => Effect.void,
"session.input.steered": () => Effect.void,
"session.input.queued": () => Effect.void,
"session.inbox.delivered": () => Effect.void,
"session.inbox.enqueued": () => Effect.void,
"session.inbox.cancelled": () => Effect.void,
"session.inbox.delivery.changed": () => Effect.void,
"session.execution.started": () => Effect.void,
"session.execution.succeeded": () => clearCurrentRetry,
"session.execution.failed": () => clearCurrentRetry,
@@ -381,7 +379,6 @@ export function update(adapter: Adapter, event: SessionEvent.DurableEvent) {
}
})
},
"session.compaction.admitted": () => Effect.void,
"session.compaction.started": (event) =>
adapter.appendMessage(
SessionMessage.CompactionRunning.make({
-651
View File
@@ -1,651 +0,0 @@
export * as SessionPending from "./pending.js"
import { and, asc, eq, or } from "drizzle-orm"
import { DateTime, Effect, Schema } from "effect"
import {
Compaction,
Delivery,
Info,
Message,
Move,
MoveData,
Synthetic,
SyntheticData,
User,
UserData,
} from "@opencode-ai/schema/session-pending"
import type { Database } from "../database/database.js"
import { Bus } from "../bus.js"
import { KeyedMutex } from "../effect/keyed-mutex.js"
import { SessionEvent } from "./event.js"
import { SessionMessage } from "./message.js"
import { SessionSchema } from "./schema.js"
import { SessionMessageTable, SessionPendingTable } from "./sql.js"
import { Event } from "@opencode-ai/schema/event"
type DatabaseService = Database.Interface["db"]
export { Compaction, Delivery, Info, Message, Move, MoveData, Synthetic, SyntheticData, User, UserData }
/**
* Which pending input `promote` may consume: "steer" promotes steers only (a step
* boundary mid-work), while "input" also allows one queued input when no steers are
* waiting (the idle boundary, where the Session picks up fresh work).
*/
export type Promotable = "input" | "steer"
const decodeUser = Schema.decodeUnknownSync(UserData)
const encodeUser = Schema.encodeSync(UserData)
const decodeSynthetic = Schema.decodeUnknownSync(SyntheticData)
const encodeSynthetic = Schema.encodeSync(SyntheticData)
const decodeMove = Schema.decodeUnknownSync(MoveData)
const encodeMove = Schema.encodeSync(MoveData)
const decodeMessage = Schema.decodeUnknownSync(SessionMessage.Info)
const inboxLocks = KeyedMutex.makeUnsafe<SessionSchema.ID>()
type PendingRef = { readonly id: SessionMessage.ID; readonly sessionID: SessionSchema.ID }
export class LifecycleConflict extends Schema.TaggedErrorClass<LifecycleConflict>()(
"SessionPending.LifecycleConflict",
{
id: Schema.Union([SessionMessage.ID, Event.ID]),
},
) {}
const fromRow = (row: typeof SessionPendingTable.$inferSelect): Info => {
const base = {
sessionID: SessionSchema.ID.make(row.session_id),
timeCreated: DateTime.makeUnsafe(row.time_created),
}
if (row.type === "move")
return Move.make({ ...base, id: Event.ID.make(row.id), type: "move", data: decodeMove(row.data) })
const id = SessionMessage.ID.make(row.id)
if (row.type === "compaction") return Compaction.make({ ...base, id, type: "compaction" })
if (!row.delivery) throw new LifecycleConflict({ id })
if (row.type === "user")
return User.make({
...base,
id,
type: "user",
data: decodeUser(row.data),
delivery: row.delivery,
})
if (row.type === "synthetic")
return Synthetic.make({
...base,
id,
type: "synthetic",
data: decodeSynthetic(row.data),
delivery: row.delivery,
})
throw new LifecycleConflict({ id })
}
export const find = Effect.fn("SessionPending.find")(function* (db: DatabaseService, id: SessionMessage.ID) {
const row = yield* db
.select()
.from(SessionPendingTable)
.where(eq(SessionPendingTable.id, id))
.get()
.pipe(Effect.orDie)
return row === undefined ? undefined : fromRow(row)
})
export const compaction = Effect.fn("SessionPending.compaction")(function* (
db: DatabaseService,
sessionID: SessionSchema.ID,
) {
const row = yield* db
.select()
.from(SessionPendingTable)
.where(and(eq(SessionPendingTable.session_id, sessionID), eq(SessionPendingTable.type, "compaction")))
.orderBy(asc(SessionPendingTable.admitted_seq))
.limit(1)
.get()
.pipe(Effect.orDie)
if (!row) return
const entry = fromRow(row)
return entry.type === "compaction" ? entry : undefined
})
export const move = Effect.fn("SessionPending.move")(function* (db: DatabaseService, sessionID: SessionSchema.ID) {
const row = yield* db
.select()
.from(SessionPendingTable)
.where(and(eq(SessionPendingTable.session_id, sessionID), eq(SessionPendingTable.type, "move")))
.orderBy(asc(SessionPendingTable.admitted_seq))
.limit(1)
.get()
.pipe(Effect.orDie)
if (!row) return
const entry = fromRow(row)
return entry.type === "move" ? entry : undefined
})
export const admitMove = Effect.fn("SessionPending.admitMove")(function* (
db: DatabaseService,
bus: Bus.Interface,
input: { readonly sessionID: SessionSchema.ID; readonly data: MoveData; readonly source: MoveData["location"] },
) {
return yield* inboxLocks.withLock(input.sessionID)(
Effect.gen(function* () {
const pending = yield* move(db, input.sessionID)
if (pending && JSON.stringify(encodeMove(pending.data)) === JSON.stringify(encodeMove(input.data))) return pending
const event = yield* bus.publish(
SessionEvent.MoveAdmitted,
{
sessionID: input.sessionID,
move: input.data,
},
{ location: input.source },
)
const stored = yield* move(db, input.sessionID)
if (stored) return stored
return yield* Effect.die(new LifecycleConflict({ id: event.id }))
}),
)
})
const promotedFromMessage = Effect.fn("SessionPending.promotedFromMessage")(function* (
db: DatabaseService,
sessionID: SessionSchema.ID,
id: SessionMessage.ID,
delivery: Delivery,
) {
const row = yield* db
.select()
.from(SessionMessageTable)
.where(eq(SessionMessageTable.id, id))
.get()
.pipe(Effect.orDie)
if (row === undefined) return undefined
if (row.session_id !== sessionID || (row.type !== "user" && row.type !== "synthetic"))
return yield* Effect.die(new LifecycleConflict({ id }))
const message = decodeMessage({ ...row.data, id: row.id, type: row.type })
const base = { id, sessionID, timeCreated: message.time.created, delivery }
if (message.type === "user")
return User.make({
...base,
type: "user",
data: decodeUser(message),
})
if (message.type === "synthetic")
return Synthetic.make({
...base,
type: "synthetic",
data: decodeSynthetic(message),
})
return yield* Effect.die(new LifecycleConflict({ id }))
})
export const admit = Effect.fn("SessionPending.admit")(function* (
db: DatabaseService,
bus: Bus.Interface,
request: {
readonly id: SessionMessage.ID
readonly sessionID: SessionSchema.ID
readonly input: Message
},
) {
const existing = yield* find(db, request.id)
if (existing !== undefined) {
if (existing.type === "compaction") return yield* Effect.die(new LifecycleConflict({ id: request.id }))
return existing
}
const promoted = yield* promotedFromMessage(db, request.sessionID, request.id, request.input.delivery)
if (promoted !== undefined) return promoted
return yield* bus
.publish(SessionEvent.InputAdmitted, {
inputID: request.id,
sessionID: request.sessionID,
input: request.input,
})
.pipe(
Effect.flatMap((event) => {
const base = {
id: request.id,
sessionID: request.sessionID,
timeCreated: event.created,
}
return Effect.succeed(
request.input.type === "user"
? User.make({ ...base, ...request.input })
: Synthetic.make({ ...base, ...request.input }),
)
}),
Effect.catchDefect((defect) =>
find(db, request.id).pipe(
Effect.flatMap((stored) =>
stored?.type === request.input.type ? Effect.succeed(stored) : Effect.die(defect),
),
),
),
)
})
export const admitCompaction = Effect.fn("SessionPending.admitCompaction")(function* (
db: DatabaseService,
bus: Bus.Interface,
input: { readonly id: SessionMessage.ID; readonly sessionID: SessionSchema.ID },
) {
return yield* inboxLocks.withLock(input.sessionID)(
Effect.gen(function* () {
const exact = yield* find(db, input.id)
if (exact) {
if (exact.type === "compaction" && exact.sessionID === input.sessionID) return exact
return yield* Effect.die(new LifecycleConflict({ id: input.id }))
}
const pending = yield* compaction(db, input.sessionID)
if (pending) return pending
return yield* bus
.publish(SessionEvent.Compaction.Admitted, {
inputID: input.id,
sessionID: input.sessionID,
})
.pipe(
Effect.flatMap((event) => {
if (event.durable === undefined)
return Effect.die(new Error("Compaction admission event is missing aggregate sequence"))
return compaction(db, input.sessionID).pipe(
Effect.flatMap((stored) =>
stored ? Effect.succeed(stored) : Effect.die(new LifecycleConflict({ id: input.id })),
),
)
}),
Effect.catchDefect((defect) =>
compaction(db, input.sessionID).pipe(
Effect.flatMap((stored) => (stored ? Effect.succeed(stored) : Effect.die(defect))),
),
),
)
}),
)
})
export const projectAdmitted = Effect.fn("SessionPending.projectAdmitted")(function* (
db: DatabaseService,
request: {
readonly admittedSeq: number
readonly id: SessionMessage.ID
readonly sessionID: SessionSchema.ID
readonly input: Message
readonly timeCreated: DateTime.Utc
},
) {
const message = yield* db
.select({ id: SessionMessageTable.id })
.from(SessionMessageTable)
.where(eq(SessionMessageTable.id, request.id))
.get()
.pipe(Effect.orDie)
if (message !== undefined) return yield* Effect.die(new LifecycleConflict({ id: request.id }))
const stored = yield* db
.insert(SessionPendingTable)
.values({
id: request.id,
session_id: request.sessionID,
type: request.input.type,
data: request.input.type === "user" ? encodeUser(request.input.data) : encodeSynthetic(request.input.data),
delivery: request.input.delivery,
admitted_seq: request.admittedSeq,
time_created: DateTime.toEpochMillis(request.timeCreated),
})
.onConflictDoNothing()
.returning({ id: SessionPendingTable.id })
.get()
.pipe(Effect.orDie)
if (!stored) return yield* Effect.die(new LifecycleConflict({ id: request.id }))
})
export const projectCompactionAdmitted = Effect.fn("SessionPending.projectCompactionAdmitted")(function* (
db: DatabaseService,
input: {
readonly admittedSeq: number
readonly id: SessionMessage.ID
readonly sessionID: SessionSchema.ID
readonly timeCreated: DateTime.Utc
},
) {
const message = yield* db
.select({ id: SessionMessageTable.id })
.from(SessionMessageTable)
.where(eq(SessionMessageTable.id, input.id))
.get()
.pipe(Effect.orDie)
if (message !== undefined) return yield* Effect.die(new LifecycleConflict({ id: input.id }))
const stored = yield* db
.insert(SessionPendingTable)
.values({
id: input.id,
session_id: input.sessionID,
type: "compaction",
data: {},
admitted_seq: input.admittedSeq,
time_created: DateTime.toEpochMillis(input.timeCreated),
})
.onConflictDoNothing()
.returning()
.get()
.pipe(Effect.orDie)
if (stored) {
const entry = fromRow(stored)
return entry.type === "compaction" ? entry : yield* Effect.die(new LifecycleConflict({ id: entry.id }))
}
const pending = yield* compaction(db, input.sessionID)
if (pending) return pending
return yield* Effect.die(new LifecycleConflict({ id: input.id }))
})
export const projectMoveAdmitted = Effect.fn("SessionPending.projectMoveAdmitted")(function* (
db: DatabaseService,
input: {
readonly admittedSeq: number
readonly id: Event.ID
readonly sessionID: SessionSchema.ID
readonly data: MoveData
readonly timeCreated: DateTime.Utc
},
) {
yield* db
.delete(SessionPendingTable)
.where(and(eq(SessionPendingTable.session_id, input.sessionID), eq(SessionPendingTable.type, "move")))
.run()
.pipe(Effect.orDie)
yield* db
.insert(SessionPendingTable)
.values({
id: input.id,
session_id: input.sessionID,
type: "move",
data: input.data,
admitted_seq: input.admittedSeq,
time_created: DateTime.toEpochMillis(input.timeCreated),
})
.run()
.pipe(Effect.orDie)
})
/**
* Consume one pending row at promotion. The row's content feeds the projected
* message insert inside the same event transaction; the deleted row is what
* makes the table pending-only.
*/
export const projectPromoted = Effect.fn("SessionPending.projectPromoted")(function* (
db: DatabaseService,
input: PendingRef,
) {
if ((yield* compaction(db, input.sessionID)) || (yield* move(db, input.sessionID)))
return yield* Effect.die(new LifecycleConflict({ id: input.id }))
const deleted = yield* db
.delete(SessionPendingTable)
.where(and(eq(SessionPendingTable.id, input.id), eq(SessionPendingTable.session_id, input.sessionID)))
.returning()
.get()
.pipe(Effect.orDie)
if (!deleted) return yield* Effect.die(new LifecycleConflict({ id: input.id }))
const stored = fromRow(deleted)
if (stored.type === "compaction" || stored.type === "move")
return yield* Effect.die(new LifecycleConflict({ id: input.id }))
return stored
})
export const projectCancelled = Effect.fn("SessionPending.projectCancelled")(function* (
db: DatabaseService,
input: PendingRef,
) {
const deleted = yield* db
.delete(SessionPendingTable)
.where(
and(
eq(SessionPendingTable.id, input.id),
eq(SessionPendingTable.session_id, input.sessionID),
or(eq(SessionPendingTable.delivery, "queue"), eq(SessionPendingTable.delivery, "steer")),
),
)
.returning({ id: SessionPendingTable.id })
.get()
.pipe(Effect.orDie)
if (!deleted) return yield* Effect.die(new LifecycleConflict({ id: input.id }))
})
const projectDelivery = Effect.fn("SessionPending.projectDelivery")(function* (
db: DatabaseService,
input: PendingRef & { readonly from: Delivery; readonly to: Delivery },
) {
const updated = yield* db
.update(SessionPendingTable)
.set({ delivery: input.to })
.where(
and(
eq(SessionPendingTable.id, input.id),
eq(SessionPendingTable.session_id, input.sessionID),
eq(SessionPendingTable.delivery, input.from),
),
)
.returning({ id: SessionPendingTable.id })
.get()
.pipe(Effect.orDie)
if (!updated) return yield* Effect.die(new LifecycleConflict({ id: input.id }))
})
export const projectSteered = Effect.fn("SessionPending.projectSteered")((db: DatabaseService, input: PendingRef) =>
projectDelivery(db, { ...input, from: "queue", to: "steer" }),
)
export const projectQueued = Effect.fn("SessionPending.projectQueued")((db: DatabaseService, input: PendingRef) =>
projectDelivery(db, { ...input, from: "steer", to: "queue" }),
)
export const settleCompaction = Effect.fn("SessionPending.settleCompaction")(function* (
db: DatabaseService,
input: { readonly sessionID: SessionSchema.ID },
) {
const deleted = yield* db
.delete(SessionPendingTable)
.where(and(eq(SessionPendingTable.session_id, input.sessionID), eq(SessionPendingTable.type, "compaction")))
.returning()
.get()
.pipe(Effect.orDie)
if (deleted) {
const stored = fromRow(deleted)
return stored.type === "compaction" ? stored : yield* Effect.die(new LifecycleConflict({ id: stored.id }))
}
return undefined
})
export const settleMove = Effect.fn("SessionPending.settleMove")(function* (
db: DatabaseService,
input: { readonly sessionID: SessionSchema.ID; readonly id: Event.ID },
) {
yield* db
.delete(SessionPendingTable)
.where(
and(
eq(SessionPendingTable.id, input.id),
eq(SessionPendingTable.session_id, input.sessionID),
eq(SessionPendingTable.type, "move"),
),
)
.run()
.pipe(Effect.orDie)
})
export const moveSessions = Effect.fn("SessionPending.moveSessions")(function* (db: DatabaseService) {
const rows = yield* db
.select({ sessionID: SessionPendingTable.session_id })
.from(SessionPendingTable)
.where(eq(SessionPendingTable.type, "move"))
.all()
.pipe(Effect.orDie)
return [...new Set(rows.map((row) => row.sessionID))]
})
export const list = Effect.fn("SessionPending.list")(function* (db: DatabaseService, sessionID: SessionSchema.ID) {
const rows = yield* db
.select()
.from(SessionPendingTable)
.where(eq(SessionPendingTable.session_id, sessionID))
.orderBy(asc(SessionPendingTable.admitted_seq))
.all()
.pipe(Effect.orDie)
return rows.map(fromRow)
})
/**
* Which pending rows count: "any" counts every row including compaction, while
* delivery scopes are blocked behind a pending compaction barrier. "input" means
* any model-facing input, steered or queued.
*/
export type Scope = "any" | "input" | Delivery
export const has = Effect.fn("SessionPending.has")(function* (
db: DatabaseService,
sessionID: SessionSchema.ID,
scope: Scope,
) {
if (scope !== "any" && ((yield* compaction(db, sessionID)) || (yield* move(db, sessionID)))) return false
const row = yield* db
.select({ id: SessionPendingTable.id })
.from(SessionPendingTable)
.where(
and(
eq(SessionPendingTable.session_id, sessionID),
scope === "any"
? undefined
: scope === "input"
? or(eq(SessionPendingTable.delivery, "steer"), eq(SessionPendingTable.delivery, "queue"))
: eq(SessionPendingTable.delivery, scope),
),
)
.limit(1)
.get()
.pipe(Effect.orDie)
return row !== undefined
})
export const equivalent = (
input: User | Synthetic,
expected: { readonly sessionID: SessionSchema.ID; readonly input: Message },
) => {
if (
input.type !== expected.input.type ||
input.delivery !== expected.input.delivery ||
input.sessionID !== expected.sessionID
)
return false
if (input.type === "user" && expected.input.type === "user")
return JSON.stringify(encodeUser(input.data)) === JSON.stringify(encodeUser(expected.input.data))
if (input.type === "synthetic" && expected.input.type === "synthetic")
return JSON.stringify(encodeSynthetic(input.data)) === JSON.stringify(encodeSynthetic(expected.input.data))
return false
}
const publishMutation = <A, E, R>(input: PendingRef, effect: Effect.Effect<A, E, R>) =>
inboxLocks.withLock(input.sessionID)(effect).pipe(Effect.asVoid)
export const cancel = Effect.fn("SessionPending.cancel")((bus: Bus.Interface, input: PendingRef) =>
publishMutation(
input,
bus.publish(SessionEvent.InputCancelled, {
sessionID: input.sessionID,
inputID: input.id,
}),
),
)
export const steer = Effect.fn("SessionPending.steer")((bus: Bus.Interface, input: PendingRef) =>
publishMutation(
input,
bus.publish(SessionEvent.InputSteered, {
sessionID: input.sessionID,
inputID: input.id,
}),
),
)
export const queue = Effect.fn("SessionPending.queue")((bus: Bus.Interface, input: PendingRef) =>
publishMutation(
input,
bus.publish(SessionEvent.InputQueued, {
sessionID: input.sessionID,
inputID: input.id,
}),
),
)
const publish = Effect.fn("SessionPending.publish")(function* (
db: DatabaseService,
bus: Bus.Interface,
sessionID: SessionSchema.ID,
rows: ReadonlyArray<typeof SessionPendingTable.$inferSelect>,
) {
if ((yield* compaction(db, sessionID)) || (yield* move(db, sessionID))) return 0
yield* Effect.forEach(
rows,
(row) => {
const entry = fromRow(row)
if (entry.type === "compaction" || entry.type === "move")
return Effect.die(new LifecycleConflict({ id: entry.id }))
return bus
.publish(SessionEvent.InputPromoted, {
sessionID,
inputID: entry.id,
})
.pipe(
Effect.catchDefect((defect) =>
defect instanceof LifecycleConflict
? promotedFromMessage(db, sessionID, entry.id, entry.delivery).pipe(
Effect.flatMap((stored) => (stored !== undefined ? Effect.void : Effect.die(defect))),
)
: Effect.die(defect),
),
)
},
{ discard: true },
)
return rows.length
})
/**
* Promotes pending input into visible messages and returns the promoted count.
* Steers always go first; only the "input" scope may fall through to one queued
* input, and it then collects steers that arrived during promotion.
*/
export const promote = Effect.fn("SessionPending.promote")(function* (
db: DatabaseService,
bus: Bus.Interface,
sessionID: SessionSchema.ID,
scope: Promotable,
) {
return yield* inboxLocks.withLock(sessionID)(
Effect.gen(function* () {
if ((yield* compaction(db, sessionID)) || (yield* move(db, sessionID))) return 0
const steers = yield* db
.select()
.from(SessionPendingTable)
.where(and(eq(SessionPendingTable.session_id, sessionID), eq(SessionPendingTable.delivery, "steer")))
.orderBy(asc(SessionPendingTable.admitted_seq))
.all()
.pipe(Effect.orDie)
if (steers.length > 0 || scope === "steer") return yield* publish(db, bus, sessionID, steers)
const queued = yield* db
.select()
.from(SessionPendingTable)
.where(and(eq(SessionPendingTable.session_id, sessionID), eq(SessionPendingTable.delivery, "queue")))
.orderBy(asc(SessionPendingTable.admitted_seq))
.limit(1)
.get()
.pipe(Effect.orDie)
if (!queued) return 0
const promoted = yield* publish(db, bus, sessionID, [queued])
const arrivedSteers = yield* db
.select()
.from(SessionPendingTable)
.where(and(eq(SessionPendingTable.session_id, sessionID), eq(SessionPendingTable.delivery, "steer")))
.orderBy(asc(SessionPendingTable.admitted_seq))
.all()
.pipe(Effect.orDie)
return promoted + (yield* publish(db, bus, sessionID, arrivedSteers))
}),
)
})
+75 -65
View File
@@ -1,7 +1,8 @@
export * as SessionProjector from "./projector.js"
import { and, asc, desc, eq, gt, gte, lt, lte, sql } from "drizzle-orm"
import { and, asc, desc, eq, gt, gte, inArray, lt, lte, sql } from "drizzle-orm"
import { DateTime, Effect, Layer, Schema, Stream } from "effect"
import path from "path"
import { Database } from "../database/database.js"
import { Bus } from "../bus.js"
import { makeGlobalNode } from "@opencode-ai/util/effect/app-node"
@@ -10,12 +11,15 @@ import { Model } from "../model.js"
import { SessionEvent } from "./event.js"
import { SessionMessage } from "./message.js"
import { SessionMessageUpdater } from "./message-updater.js"
import { SessionPending } from "./pending.js"
import { SessionInbox } from "./inbox.js"
import { Workspace } from "../workspace.js"
import { InstructionState } from "./instruction-state.js"
import { SessionPendingTable, SessionMessageTable, SessionTable } from "./sql.js"
import { SessionInboxTable, SessionMessageTable, SessionTable } from "./sql.js"
import { Slug } from "../util/slug.js"
import { FSUtil } from "@opencode-ai/util/fs-util"
import { Money } from "@opencode-ai/schema/money"
import { Worktree } from "@opencode-ai/schema/worktree"
import { Project } from "@opencode-ai/schema/project"
import { AbsolutePath, RelativePath } from "../schema.js"
import type { SessionSchema } from "./schema.js"
@@ -433,8 +437,47 @@ const layer = Layer.effectDiscard(
.run()
.pipe(Effect.orDie)
yield* InstructionState.reset(db, event.data.sessionID)
if (event.data.moveID)
yield* SessionPending.settleMove(db, { sessionID: event.data.sessionID, id: event.data.moveID })
}),
)
// Sessions whose ownership came from the directory's previous resolution
// follow its new identity. Location, transcript, instructions, and recency
// are untouched: the session did not move, its directory got identified.
yield* bus.project(Worktree.Event.Resolved, (event) =>
Effect.gen(function* () {
const stale = [event.data.previous, Project.ID.global].filter((id) => id !== event.data.projectID)
if (stale.length === 0) return
const rows = yield* db
.select({ id: SessionTable.id, directory: SessionTable.directory })
.from(SessionTable)
.where(
and(
inArray(SessionTable.project_id, stale),
// Lexicographic range narrows the scan to prefix neighbors without
// LIKE escaping; FSUtil.contains below decides containment exactly.
gte(SessionTable.directory, event.data.directory),
lte(SessionTable.directory, AbsolutePath.make(event.data.directory + "\uffff")),
),
)
.all()
.pipe(Effect.orDie)
yield* Effect.forEach(
rows,
(row) => {
if (!FSUtil.contains(event.data.directory, row.directory)) return Effect.void
return db
.update(SessionTable)
.set({
project_id: event.data.projectID,
path: RelativePath.make(path.relative(event.data.directory, row.directory).replaceAll("\\", "/")),
// Self-assignment suppresses the column's $onUpdate: adoption is not activity.
time_updated: sql`${SessionTable.time_updated}`,
})
.where(eq(SessionTable.id, row.id))
.run()
.pipe(Effect.orDie)
},
{ discard: true },
)
}),
)
yield* bus.project(SessionEvent.Deleted, (event) =>
@@ -472,14 +515,15 @@ const layer = Layer.effectDiscard(
)
yield* bus.project(SessionEvent.UsageRecorded, (event) => applyUsage(db, event.data.sessionID, event.data))
yield* bus.project(SessionEvent.Forked, (event) => projectFork(db, event))
yield* bus.project(SessionEvent.InputPromoted, (event) =>
yield* bus.project(SessionEvent.InboxDelivered, (event) =>
Effect.gen(function* () {
if (event.durable === undefined)
return yield* Effect.die(new Error("Durable Session event is missing aggregate sequence"))
const input = yield* SessionPending.projectPromoted(db, {
id: event.data.inputID,
const input = yield* SessionInbox.projectDelivered(db, {
id: event.data.inboxID,
sessionID: event.data.sessionID,
})
if (input.type === "compaction" || input.type === "move") return
yield* insertMessage(
db,
event,
@@ -487,33 +531,33 @@ const layer = Layer.effectDiscard(
? {
id: input.id,
type: "user",
metadata: input.data.metadata,
text: input.data.text,
files: input.data.files,
agents: input.data.agents,
skills: input.data.skills,
metadata: input.payload.metadata,
text: input.payload.text,
files: input.payload.files,
agents: input.payload.agents,
skills: input.payload.skills,
time: { created: event.created },
}
: {
id: input.id,
type: "synthetic",
text: input.data.text,
description: input.data.description,
metadata: input.data.metadata,
text: input.payload.text,
description: input.payload.description,
metadata: input.payload.metadata,
time: { created: event.created },
},
)
}),
)
yield* bus.project(SessionEvent.InputAdmitted, (event) =>
yield* bus.project(SessionEvent.InboxEnqueued, (event) =>
Effect.gen(function* () {
if (event.durable === undefined)
return yield* Effect.die(new Error("Durable Session event is missing aggregate sequence"))
yield* SessionPending.projectAdmitted(db, {
admittedSeq: event.durable.seq,
id: event.data.inputID,
yield* SessionInbox.projectAdmitted(db, {
enqueuedSeq: event.durable.seq,
id: event.data.inboxID,
sessionID: event.data.sessionID,
input: event.data.input,
item: event.data.item,
timeCreated: event.created,
})
yield* db
@@ -524,47 +568,17 @@ const layer = Layer.effectDiscard(
.pipe(Effect.orDie)
}),
)
yield* bus.project(SessionEvent.MoveAdmitted, (event) =>
Effect.gen(function* () {
if (event.durable === undefined)
return yield* Effect.die(new Error("Durable Session event is missing aggregate sequence"))
yield* SessionPending.projectMoveAdmitted(db, {
admittedSeq: event.durable.seq,
id: event.id,
sessionID: event.data.sessionID,
data: event.data.move,
timeCreated: event.created,
})
}),
)
yield* bus.project(SessionEvent.InputCancelled, (event) =>
SessionPending.projectCancelled(db, {
id: event.data.inputID,
yield* bus.project(SessionEvent.InboxCancelled, (event) =>
SessionInbox.projectCancelled(db, {
id: event.data.inboxID,
sessionID: event.data.sessionID,
}),
)
yield* bus.project(SessionEvent.InputSteered, (event) =>
SessionPending.projectSteered(db, {
id: event.data.inputID,
yield* bus.project(SessionEvent.InboxDeliveryChanged, (event) =>
SessionInbox.projectDeliveryChanged(db, {
id: event.data.inboxID,
sessionID: event.data.sessionID,
}),
)
yield* bus.project(SessionEvent.InputQueued, (event) =>
SessionPending.projectQueued(db, {
id: event.data.inputID,
sessionID: event.data.sessionID,
}),
)
yield* bus.project(SessionEvent.Compaction.Admitted, (event) =>
Effect.gen(function* () {
if (event.durable === undefined)
return yield* Effect.die(new Error("Durable Session event is missing aggregate sequence"))
yield* SessionPending.projectCompactionAdmitted(db, {
admittedSeq: event.durable.seq,
id: event.data.inputID,
sessionID: event.data.sessionID,
timeCreated: event.created,
})
delivery: event.data.delivery,
}),
)
yield* bus.project(SessionEvent.Execution.Succeeded, (event) => run(db, event))
@@ -611,8 +625,6 @@ const layer = Layer.effectDiscard(
yield* InstructionState.advanceEpoch(db, event.data.sessionID, event.durable.seq)
if (event.durable === undefined)
return yield* Effect.die(new Error("Durable Session event is missing aggregate sequence"))
if (event.data.reason === "manual")
yield* SessionPending.settleCompaction(db, { sessionID: event.data.sessionID })
}),
)
yield* bus.project(SessionEvent.Compaction.Failed, (event) =>
@@ -620,8 +632,6 @@ const layer = Layer.effectDiscard(
yield* run(db, event)
if (event.durable === undefined)
return yield* Effect.die(new Error("Durable Session event is missing aggregate sequence"))
if (event.data.reason === "manual")
yield* SessionPending.settleCompaction(db, { sessionID: event.data.sessionID })
}),
)
yield* bus.project(SessionEvent.RevertEvent.Staged, (event) =>
@@ -665,11 +675,11 @@ const layer = Layer.effectDiscard(
.run()
.pipe(Effect.orDie)
yield* db
.delete(SessionPendingTable)
.delete(SessionInboxTable)
.where(
and(
eq(SessionPendingTable.session_id, event.data.sessionID),
gte(SessionPendingTable.admitted_seq, boundary.seq),
eq(SessionInboxTable.session_id, event.data.sessionID),
gte(SessionInboxTable.enqueued_seq, boundary.seq),
),
)
.run()
+3 -24
View File
@@ -1,6 +1,6 @@
export * as SessionRunCoordinator from "./run-coordinator.js"
import { Cause, Deferred, Effect, Exit, Fiber, FiberSet, Scope } from "effect"
import { Deferred, Effect, Exit, Fiber, FiberSet, Scope } from "effect"
/** Serializes execution for each key while allowing different keys to run concurrently. */
export interface Coordinator<Key, E, Reason = never> {
@@ -50,17 +50,11 @@ export const make = <Key, E, Reason = never>(options: {
* Runs in the execution fiber for every exit, including interruption, after the final
* drain and before the execution settles (waiters resolve after it completes).
*/
readonly settled?: (key: Key, exit: Exit.Exit<void, E>, reason?: Reason) => Effect.Effect<boolean | void>
readonly settled?: (key: Key, exit: Exit.Exit<void, E>, reason?: Reason) => Effect.Effect<void>
}): Effect.Effect<Coordinator<Key, E, Reason>, never, Scope.Scope> =>
Effect.gen(function* () {
const executions = new Map<Key, Execution<E, Reason>>()
const fork = yield* FiberSet.makeRuntime<never, void, never>()
const closing = { value: false }
yield* Effect.addFinalizer(() =>
Effect.sync(() => {
closing.value = true
}),
)
const loop = (key: Key, execution: Execution<E, Reason>, force: boolean): Effect.Effect<void, E> =>
Effect.suspend(() => options.drain(key, force)).pipe(
@@ -91,22 +85,7 @@ export const make = <Key, E, Reason = never>(options: {
Effect.onExit((exit) =>
Effect.sync(() => {
execution.owner = undefined
if (closing.value && Exit.isFailure(exit) && Cause.hasInterrupts(exit.cause)) {
execution.stopping = true
execution.pendingWake = false
}
}).pipe(
Effect.andThen(options.settled?.(key, exit, execution.interruptionReason) ?? Effect.void),
Effect.map(Boolean),
Effect.tap((restart) =>
restart && !execution.stopping
? Effect.sync(() => {
execution.pendingWake = true
})
: Effect.void,
),
Effect.asVoid,
),
}).pipe(Effect.andThen(options.settled?.(key, exit, execution.interruptionReason) ?? Effect.void)),
),
Effect.onExit((exit) => Effect.sync(() => settle(key, execution, exit))),
Effect.exit,
+9 -2
View File
@@ -16,13 +16,20 @@ export type RunError =
| UserInterruptedError
| Instructions.InitializationBlocked
export type Continuation = { readonly step: number }
export type DrainResult =
| { readonly type: "complete" }
| { readonly type: "moved"; readonly continuation?: Continuation }
/** Runs one local continuation from already-recorded Session history. */
export interface Interface {
/** Drains eligible durable work. Explicit runs make one model call even when no work is eligible. */
/** Drains eligible durable work, returning transient state when execution must continue at a new Location. */
readonly drain: (input: {
readonly sessionID: SessionSchema.ID
readonly force: boolean
}) => Effect.Effect<void, RunError>
readonly continuation?: Continuation
}) => Effect.Effect<DrainResult, RunError>
}
export class Service extends Context.Service<Service, Interface>()("@opencode/SessionRunner") {}
+93 -39
View File
@@ -17,13 +17,13 @@ import { InstructionState } from "../instruction-state.js"
import { SessionCompaction } from "../compaction.js"
import { SessionContext } from "../context.js"
import { SessionEvent } from "../event.js"
import { SessionPending } from "../pending.js"
import { SessionInbox } from "../inbox.js"
import { SessionModelRequest } from "../model-request.js"
import { SessionMessage } from "../message.js"
import { SessionSchema } from "../schema.js"
import { SessionStore } from "../store.js"
import { SessionTitle } from "../title.js"
import { Service } from "./index.js"
import { Service, type Continuation } from "./index.js"
import { createLLMEventPublisher, type StepRecord } from "./publish-llm-event.js"
import { Snapshot } from "../../snapshot.js"
import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
@@ -113,8 +113,8 @@ const layer = Layer.effect(
const compaction = yield* SessionCompaction.Service
const title = yield* SessionTitle.Service
const toolOutput = yield* ToolOutput.Service
// Title generation is a side effect of a successful step; it must not delay continuation.
// The in-flight set coalesces overlapping steps while title presence records success durably.
// Title generation starts once input is visible and must not delay model execution.
// The in-flight set coalesces overlapping prompts while title presence records success durably.
const titlesRunning = new Set<SessionSchema.ID>()
const forkTitle = yield* FiberSet.makeRuntime<never, void, never>()
/**
@@ -124,30 +124,47 @@ const layer = Layer.effect(
const drain = Effect.fn("SessionRunner.drain")(function* (input: {
readonly sessionID: SessionSchema.ID
readonly force: boolean
readonly continuation?: Continuation
}) {
if (!input.force && !(yield* SessionPending.has(db, input.sessionID, "any"))) return
let force = input.force
let continuation = input.continuation
if (!force && !continuation && !(yield* SessionInbox.has(db, input.sessionID, "any")))
return { type: "complete" as const }
yield* settleStaleToolCalls(input.sessionID)
yield* runPendingCompaction(input.sessionID)
if (!input.force && !(yield* SessionPending.has(db, input.sessionID, "input"))) return
do {
yield* runSteps(input.sessionID)
} while (yield* SessionPending.has(db, input.sessionID, "input"))
while (true) {
if (yield* runPendingCompaction(input.sessionID)) {
force = false
continue
}
if (yield* runPendingMove(input.sessionID, "input")) return { type: "moved" as const }
if (!force && !continuation && !(yield* SessionInbox.has(db, input.sessionID, "input")))
return { type: "complete" as const }
const result = yield* runSteps(input.sessionID, continuation)
if (result.type === "moved") return result
force = false
continuation = undefined
}
})
/**
* Runs logical steps until no tool result or newly admitted steer requires another
* model call. Queued inputs remain pending until the current model work reaches idle.
*/
const runSteps = Effect.fn("SessionRunner.runSteps")(function* (sessionID: SessionSchema.ID) {
const runSteps = Effect.fn("SessionRunner.runSteps")(function* (
sessionID: SessionSchema.ID,
continuation?: Continuation,
) {
// Fresh work may promote queued input; later steps absorb steers only.
let promotable: SessionPending.Promotable = "input"
let step = 1
let promotable: SessionInbox.Promotable = continuation ? "steer" : "input"
let step = continuation?.step ?? 1
let next = continuation
while (true) {
if (yield* SessionPending.move(db, sessionID)) return
if (yield* runPendingCompaction(sessionID)) continue
if (yield* runPendingMove(sessionID, "steer")) return { type: "moved" as const, continuation: next }
const result = yield* runStep(sessionID, promotable, step)
if (step === 1) yield* startTitle(sessionID)
yield* runPendingCompaction(sessionID)
if (!result.needsContinuation && !(yield* SessionPending.has(db, sessionID, "steer"))) return
next = result.needsContinuation ? { step: result.step + 1 } : undefined
if (!result.needsContinuation && !(yield* SessionInbox.has(db, sessionID, "steer")))
return { type: "complete" as const }
promotable = "steer"
step = result.step + 1
}
@@ -156,7 +173,7 @@ const layer = Layer.effect(
/** Completes one logical model step, transparently retrying or rebuilding after compaction. */
const runStep = Effect.fnUntraced(function* (
sessionID: SessionSchema.ID,
promotable: SessionPending.Promotable,
promotable: SessionInbox.Promotable,
step: number,
) {
// Minting message identity before any attempt lets retries resume the same durable
@@ -184,7 +201,7 @@ const layer = Layer.effect(
.pipe(Effect.andThen(Effect.fail(failure.cause))),
),
)
let currentPromotable: SessionPending.Promotable | undefined = promotable
let currentPromotable: SessionInbox.Promotable | undefined = promotable
let currentStep = step
// Overflow recovery is one-shot: a call after recovery must not recover another overflow.
let recoverOverflow = true
@@ -227,7 +244,7 @@ const layer = Layer.effect(
*/
const callModel = Effect.fn("SessionRunner.callModel")(function* (
sessionID: SessionSchema.ID,
promotable: SessionPending.Promotable | undefined,
promotable: SessionInbox.Promotable | undefined,
step: number,
recoverOverflow: boolean,
assistantMessageID: SessionMessage.ID,
@@ -236,9 +253,8 @@ const layer = Layer.effect(
// Establish what the model knows before admitting what the user said, so
// a blocked first step leaves pending inputs untouched.
yield* InstructionState.prepare(db, bus, selected.instructions, selected.session.id)
const promoted = promotable ? yield* SessionPending.promote(db, bus, selected.session.id, promotable) : 0
if (promotable && promoted === 0 && (yield* SessionPending.move(db, sessionID)))
return CallOutcome.Completed({ needsContinuation: false, step })
const promoted = promotable ? yield* SessionInbox.promote(db, bus, selected.session.id, promotable) : 0
if (promoted > 0) yield* startTitle(sessionID)
// Promoted input opens a fresh step allowance.
const currentStep = promoted > 0 ? 1 : step
const loaded = yield* context.load(selected)
@@ -248,7 +264,7 @@ const layer = Layer.effect(
// Make room: history must fit the context window before the call. A pending manual
// compaction owns this instead; the runner executes it between steps.
const compactionInput = { session, messages: loaded.messages, model, ref: resolved.ref, cost: resolved.cost }
if (compaction.required(compactionInput) && !(yield* SessionPending.compaction(db, session.id))) {
if (compaction.required(compactionInput)) {
const compacted = yield* compaction.compact(compactionInput)
if (compacted.status === "completed")
return CallOutcome.Restart({ step: currentStep, recoveredOverflow: false })
@@ -485,37 +501,75 @@ const layer = Layer.effect(
const runPendingCompaction = Effect.fn("SessionRunner.runPendingCompaction")(function* (
sessionID: SessionSchema.ID,
) {
if (yield* SessionPending.move(db, sessionID)) return
const pending = yield* SessionPending.compaction(db, sessionID)
if (!pending) return
const session = yield* getSession(sessionID)
return yield* Effect.uninterruptibleMask((restore) =>
Effect.gen(function* () {
const pending = yield* SessionInbox.serialized(
sessionID,
Effect.gen(function* () {
const selected =
(yield* SessionInbox.nextSteer(db, sessionID)) ?? (yield* SessionInbox.nextQueued(db, sessionID))
if (selected?.type !== "compaction") return
yield* bus.publishAll([
[SessionEvent.InboxDelivered, { sessionID, inboxID: selected.id }],
[SessionEvent.Compaction.Started, { sessionID, reason: "manual", recent: "", inputID: selected.id }],
])
return selected
}),
)
if (pending?.type !== "compaction") return false
const session = yield* getSession(sessionID)
const compacted = yield* restore(
Effect.gen(function* () {
return yield* compaction.compactManual({
session,
messages: yield* store.context(sessionID),
inputID: pending.id,
started: true,
})
}),
).pipe(Effect.exit)
if (Exit.isSuccess(compacted)) return
const unsettled = yield* SessionPending.compaction(db, sessionID)
if (unsettled)
yield* bus.publish(SessionEvent.Compaction.Failed, {
sessionID,
reason: "manual",
error: Cause.hasInterruptsOnly(compacted.cause)
? { type: "aborted", message: "Compaction cancelled" }
: { type: "compaction.failed", message: Cause.pretty(compacted.cause) },
inputID: unsettled.id,
})
if (Exit.isSuccess(compacted)) return true
yield* bus.publish(SessionEvent.Compaction.Failed, {
sessionID,
reason: "manual",
error: Cause.hasInterruptsOnly(compacted.cause)
? { type: "aborted", message: "Compaction cancelled" }
: { type: "compaction.failed", message: Cause.pretty(compacted.cause) },
inputID: pending.id,
})
return yield* Effect.failCause(compacted.cause)
}),
)
})
const runPendingMove = Effect.fn("SessionRunner.runPendingMove")(function* (
sessionID: SessionSchema.ID,
promotable: SessionInbox.Promotable,
) {
return yield* SessionInbox.serialized(
sessionID,
Effect.gen(function* () {
const pending =
(yield* SessionInbox.nextSteer(db, sessionID)) ??
(promotable === "input" ? yield* SessionInbox.nextQueued(db, sessionID) : undefined)
if (pending?.type !== "move") return false
yield* bus.publishAll([
[SessionEvent.InboxDelivered, { sessionID, inboxID: pending.id }],
[
SessionEvent.Moved,
{
sessionID,
location: pending.payload.location,
projectID: pending.payload.projectID,
subpath: pending.payload.subpath,
},
],
])
return true
}),
)
})
/** Closes stale tool calls left active by an earlier interrupted drain. */
const settleStaleToolCalls = Effect.fn("SessionRunner.settleStaleToolCalls")(function* (
sessionID: SessionSchema.ID,
+28 -8
View File
@@ -3,7 +3,7 @@ import { sql } from "drizzle-orm"
import { directoryColumn, pathColumn } from "../database/path.js"
import { ProjectTable } from "../project/sql.js"
import type { SessionMessage } from "./message.js"
import type { SessionPending } from "./pending.js"
import type { SessionInbox } from "./inbox.js"
import type { FileDiff } from "@opencode-ai/schema/file-diff"
import { PermissionV1 } from "../v1/permission.js"
import { Project } from "../project.js"
@@ -12,7 +12,7 @@ import { Workspace } from "../workspace.js"
import { Timestamps } from "../database/schema.sql.js"
import type { Instruction } from "@opencode-ai/schema/instruction"
import type { Session } from "@opencode-ai/schema/session"
import type { SyntheticData, UserData } from "@opencode-ai/schema/session-pending"
import type { CompactionPayload, MovePayload, SyntheticPayload, UserPayload } from "@opencode-ai/schema/session-inbox"
import type { RevertV1 } from "@opencode-ai/schema/session-revert"
import type { Schema } from "effect"
@@ -96,16 +96,14 @@ export const SessionMessageTable = sqliteTable(
export const SessionPendingTable = sqliteTable(
"session_pending",
{
id: text().$type<SessionPending.Info["id"]>().primaryKey(),
id: text().$type<SessionMessage.ID>().primaryKey(),
session_id: text()
.$type<SessionSchema.ID>()
.notNull()
.references(() => SessionTable.id, { onDelete: "cascade" }),
type: text().$type<SessionPending.Info["type"]>().notNull(),
data: text({ mode: "json" })
.$type<UserData | SyntheticData | SessionPending.MoveData | Record<string, never>>()
.notNull(),
delivery: text().$type<SessionPending.Delivery>(),
type: text().$type<SessionInbox.Info["type"]>().notNull(),
data: text({ mode: "json" }).$type<UserPayload | SyntheticPayload | Record<string, never>>().notNull(),
delivery: text().$type<SessionInbox.Delivery>(),
admitted_seq: integer().notNull(),
time_created: integer()
.notNull()
@@ -120,6 +118,28 @@ export const SessionPendingTable = sqliteTable(
],
)
export const SessionInboxTable = sqliteTable(
"session_inbox",
{
id: text().$type<SessionMessage.ID>().primaryKey(),
session_id: text()
.$type<SessionSchema.ID>()
.notNull()
.references(() => SessionTable.id, { onDelete: "cascade" }),
type: text().$type<SessionInbox.Info["type"]>().notNull(),
payload: text({ mode: "json" }).$type<UserPayload | SyntheticPayload | CompactionPayload | MovePayload>().notNull(),
delivery: text().$type<SessionInbox.Delivery>().notNull(),
enqueued_seq: integer().notNull(),
time_created: integer()
.notNull()
.$default(() => Date.now()),
},
(table) => [
index("session_inbox_session_delivery_seq_idx").on(table.session_id, table.delivery, table.enqueued_seq),
uniqueIndex("session_inbox_session_enqueued_seq_idx").on(table.session_id, table.enqueued_seq),
],
)
export const InstructionEntryTable = sqliteTable(
"instruction_entry",
{
+17 -12
View File
@@ -5,6 +5,7 @@ import { Context, Deferred, Duration, Effect, Fiber, Layer, Schema, Stream } fro
import { ChildProcess } from "effect/unstable/process"
import { produce } from "immer"
import { Shell } from "@opencode-ai/schema/shell"
import { AppProcess } from "@opencode-ai/util/process"
import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
import { Config } from "./config.js"
import { Bus } from "./bus.js"
@@ -50,7 +51,7 @@ export interface Interface {
readonly create: <E = never, R = never>(
input: Shell.CreateInput,
before?: (input: ShellCreateBefore) => Effect.Effect<void, E, R>,
) => Effect.Effect<Shell.Info, E, R>
) => Effect.Effect<Shell.Info, E | AppProcess.AppProcessError, R>
// Currently running commands only; exited shells are retained for get/output but excluded here.
readonly list: () => Effect.Effect<Shell.Info[]>
readonly get: (id: Shell.ID) => Effect.Effect<Shell.Info, NotFoundError>
@@ -215,19 +216,23 @@ export const layer = (options?: ShellSelect.Options) =>
// Spawn through the Environment and stream combined output to the file. The handle is scope-bound, so
// the managing fiber keeps its scope open until the command terminates (it awaits `done` at the
// end). `create` returns once `ready` resolves with the registered session.
const ready = Deferred.makeUnsafe<Active>()
const ready = Deferred.makeUnsafe<Active, AppProcess.AppProcessError>()
runFork(
Effect.scoped(
Effect.gen(function* () {
const handle = yield* environment.spawner.spawn(
ChildProcess.make(invocation.shell, args, {
cwd: invocation.cwd,
env: invocation.env,
stdin: "ignore",
detached: process.platform !== "win32",
forceKillAfter: Duration.seconds(3),
}),
)
const handle = yield* environment.spawner
.spawn(
ChildProcess.make(invocation.shell, args, {
cwd: invocation.cwd,
env: invocation.env,
stdin: "ignore",
detached: process.platform !== "win32",
forceKillAfter: Duration.seconds(3),
}),
)
.pipe(
Effect.mapError((cause) => new AppProcess.AppProcessError({ command: invocation.command, cause })),
)
const session: Active = {
info: produce(info, (draft) => {
draft.pid = handle.pid
@@ -329,7 +334,7 @@ export const layer = (options?: ShellSelect.Options) =>
// release (kill) the process before its exit is observed.
yield* Deferred.await(session.done).pipe(Effect.catch(() => Effect.void))
}),
).pipe(Effect.catch(() => Effect.void)),
).pipe(Effect.catchTag("AppProcessError", (error) => Deferred.fail(ready, error))),
)
const session = yield* Deferred.await(ready)
@@ -0,0 +1,4 @@
// workerd has no filesystem paths to tree-sitter wasm artifacts. ShellParse
// loads these lazily and degrades when initialization fails, so empty paths
// keep module load side-effect free instead of resolving from disk.
export const shellParserWasm = { runtime: "", bash: "", powershell: "" }
+14 -1
View File
@@ -2,7 +2,7 @@ export * as Tool from "./tool.js"
export { CallID, Content, Error, FileContent, TextContent } from "@opencode-ai/schema/tool"
export type { Context, Metadata, Options, Result } from "@opencode-ai/schema/tool"
import type { ToolCall, ToolDefinition } from "@opencode-ai/ai"
import { ToolDefinition, type ToolCall } from "@opencode-ai/ai"
import { Tool } from "@opencode-ai/schema/tool"
import { Context, Effect, Layer, Schema, Scope, Semaphore } from "effect"
import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
@@ -165,6 +165,19 @@ const layer = Layer.effect(
}),
)
if (entries.length === 0) return
yield* Effect.forEach(
entries,
(entry) =>
Effect.try({
try: () => ToolDefinition.make(definition(entry.tool)),
catch: (error) =>
new RegistrationError({
name: entry.key,
message: `Invalid tool definition ${entry.key}: ${error instanceof Error ? error.message : String(error)}`,
}),
}),
{ discard: true },
)
yield* Effect.uninterruptible(
lock.withPermit(
Effect.gen(function* () {
@@ -0,0 +1,14 @@
export type LockResult =
| { readonly acquired: true }
| { readonly acquired: false; readonly held: true }
| { readonly acquired: false; readonly held: false; readonly code: number }
// workerd has no FFI and no cross-process file locking; a Durable Object is
// already single-threaded per instance, so nothing on this runtime should
// reach these.
const unavailable = (_fd: number): LockResult => {
throw new Error("Process locks are unavailable on the workerd runtime")
}
export const lockDarwin = unavailable
export const lockLinux = unavailable
+349
View File
@@ -0,0 +1,349 @@
export * as Worktree from "./worktree.js"
import { Context, Effect, Layer, Schema } from "effect"
import { and, asc, desc, eq, isNotNull, isNull, ne, or } from "drizzle-orm"
import path from "path"
import { AbsolutePath } from "./schema.js"
import { FSUtil } from "@opencode-ai/util/fs-util"
import { Git } from "./git.js"
import { makeGlobalNode, makeLocationNode } from "@opencode-ai/util/effect/app-node"
import { ProjectSchema } from "./project/schema.js"
import { Slug } from "./util/slug.js"
import { Bus } from "./bus.js"
import { Database } from "./database/database.js"
import { Location } from "./location.js"
import { Worktree } from "@opencode-ai/schema/worktree"
import { WorktreeTable } from "./worktree/sql.js"
import { canonical, DirectoryUnavailableError } from "./worktree/directory.js"
import { WorktreeGit } from "./worktree/git.js"
import type { EffectDrizzleSqlite } from "./database/drizzle.js"
export { DirectoryUnavailableError } from "./worktree/directory.js"
export const StrategyID = Worktree.StrategyID
export type StrategyID = typeof StrategyID.Type
export const CreateInput = Worktree.CreateInput
export type CreateInput = typeof CreateInput.Type
export const RemoveInput = Worktree.RemoveInput
export type RemoveInput = typeof RemoveInput.Type
export const RefreshInput = Schema.Struct({
projectID: ProjectSchema.ID,
}).annotate({ identifier: "Worktree.RefreshInput" })
export type RefreshInput = typeof RefreshInput.Type
export const RefreshResult = Schema.Struct({
updated: Schema.Array(AbsolutePath),
removed: Schema.Array(AbsolutePath),
}).annotate({ identifier: "Worktree.RefreshResult" })
export type RefreshResult = typeof RefreshResult.Type
export const Info = Worktree.Info
export type Info = typeof Info.Type
export const ListInput = Worktree.ListInput
export type ListInput = typeof ListInput.Type
export const List = Worktree.List
export type List = typeof List.Type
export const ListEntry = Schema.Struct({
directory: AbsolutePath,
type: Schema.Literals(["root", "worktree"]),
}).annotate({ identifier: "Worktree.ListEntry" })
export type ListEntry = typeof ListEntry.Type
export class SourceDirectoryNotFoundError extends Schema.TaggedErrorClass<SourceDirectoryNotFoundError>()(
"Worktree.SourceDirectoryNotFoundError",
{ projectID: ProjectSchema.ID },
) {}
export class DestinationExistsError extends Schema.TaggedErrorClass<DestinationExistsError>()(
"Worktree.DestinationExistsError",
{ directory: AbsolutePath },
) {}
export class InvalidDirectoryError extends Schema.TaggedErrorClass<InvalidDirectoryError>()(
"Worktree.InvalidDirectoryError",
{ directory: AbsolutePath },
) {}
export class StrategyUnavailableError extends Schema.TaggedErrorClass<StrategyUnavailableError>()(
"Worktree.StrategyUnavailableError",
{ strategy: StrategyID },
) {}
export class DuplicateStrategyError extends Schema.TaggedErrorClass<DuplicateStrategyError>()(
"Worktree.DuplicateStrategyError",
{ strategy: StrategyID },
) {}
export type Error =
| SourceDirectoryNotFoundError
| DestinationExistsError
| DirectoryUnavailableError
| InvalidDirectoryError
| StrategyUnavailableError
| Git.WorktreeError
export interface Strategy {
readonly id: StrategyID
readonly create: (input: {
sourceDirectory: AbsolutePath
directory: AbsolutePath
}) => Effect.Effect<Info, Git.WorktreeError | DirectoryUnavailableError>
readonly remove: (input: {
directory: AbsolutePath
force: boolean
}) => Effect.Effect<void, Git.WorktreeError | DirectoryUnavailableError>
readonly list: (directory: AbsolutePath) => Effect.Effect<ListEntry[], Git.WorktreeError | DirectoryUnavailableError>
}
export const Event = Worktree.Event
interface StoredInput {
readonly projectID: ProjectSchema.ID
readonly directory: AbsolutePath
readonly strategy?: string
}
type DatabaseClient = EffectDrizzleSqlite.EffectSQLiteDatabase
type Transaction = Parameters<Parameters<DatabaseClient["transaction"]>[0]>[0]
export interface Interface {
readonly register: (strategy: Strategy) => Effect.Effect<void, DuplicateStrategyError>
readonly list: (projectID: ProjectSchema.ID) => Effect.Effect<List>
readonly create: (input: CreateInput) => Effect.Effect<Info, Error>
readonly remove: (input: RemoveInput) => Effect.Effect<void, Error>
readonly refresh: (input: RefreshInput) => Effect.Effect<RefreshResult, Error>
}
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/Worktree") {}
export const refreshAfterBoot = Effect.gen(function* () {
const location = yield* Location.Service
const worktrees = yield* Service
yield* Effect.gen(function* () {
yield* Effect.logInfo("worktree refresh started", { projectID: location.project.id })
const result = yield* worktrees.refresh({ projectID: location.project.id })
yield* Effect.logInfo("worktree refresh done", {
projectID: location.project.id,
updated: result.updated,
removed: result.removed,
})
}).pipe(
Effect.catchCause((cause) => Effect.logWarning("worktree refresh failed", { cause })),
Effect.forkScoped,
Effect.asVoid,
)
})
const layer = Layer.effect(
Service,
Effect.gen(function* () {
const fs = yield* FSUtil.Service
const db = (yield* Database.Service).db
const bus = yield* Bus.Service
const changed = Effect.fnUntraced(function* (projectID: ProjectSchema.ID, update: boolean) {
if (update) yield* bus.publish(Event.Updated, { projectID })
})
const ops = {
list: Effect.fn("Worktree.list")(function* (projectID: ProjectSchema.ID) {
const rows = yield* db
.select({ directory: WorktreeTable.directory, strategy: WorktreeTable.strategy })
.from(WorktreeTable)
.where(eq(WorktreeTable.project_id, projectID))
.orderBy(desc(WorktreeTable.time_created), asc(WorktreeTable.directory))
.all()
.pipe(Effect.orDie)
return rows.map((row) => ({ directory: row.directory, strategy: row.strategy ?? undefined }))
}),
find: Effect.fnUntraced(function* (projectID: ProjectSchema.ID, directory: AbsolutePath) {
const row = yield* db
.select({ directory: WorktreeTable.directory, strategy: WorktreeTable.strategy })
.from(WorktreeTable)
.where(and(eq(WorktreeTable.project_id, projectID), eq(WorktreeTable.directory, directory)))
.get()
.pipe(Effect.orDie)
return row ? { directory: row.directory, strategy: row.strategy ?? undefined } : undefined
}),
create: Effect.fnUntraced(function* (input: StoredInput, tx?: Transaction) {
return (
(yield* (tx ?? db)
.insert(WorktreeTable)
.values({ project_id: input.projectID, directory: input.directory, strategy: input.strategy })
.onConflictDoUpdate({
target: [WorktreeTable.project_id, WorktreeTable.directory],
set: { strategy: input.strategy ?? null },
setWhere: input.strategy
? or(isNull(WorktreeTable.strategy), ne(WorktreeTable.strategy, input.strategy))
: isNotNull(WorktreeTable.strategy),
})
.returning({ directory: WorktreeTable.directory })
.get()
.pipe(Effect.orDie)) !== undefined
)
}),
remove: Effect.fnUntraced(function* (projectID: ProjectSchema.ID, directory: AbsolutePath, tx?: Transaction) {
return (
(yield* (tx ?? db)
.delete(WorktreeTable)
.where(and(eq(WorktreeTable.project_id, projectID), eq(WorktreeTable.directory, directory)))
.returning({ directory: WorktreeTable.directory })
.get()
.pipe(Effect.orDie)) !== undefined
)
}),
}
const registry = new Map<StrategyID, Strategy>()
const register = Effect.fn("Worktree.register")(function* (strategy: Strategy) {
if (registry.has(strategy.id)) return yield* new DuplicateStrategyError({ strategy: strategy.id })
registry.set(strategy.id, strategy)
})
// Register default strategies
const gitStrategy = yield* WorktreeGit.make
yield* register(gitStrategy).pipe(Effect.orDie)
const strategies = () => Array.from(registry.values())
const source = Effect.fnUntraced(function* (projectID: ProjectSchema.ID) {
const checked = yield* Effect.forEach(
yield* ops.list(projectID),
(item) =>
canonical(fs, item.directory).pipe(
Effect.catchTag("Worktree.DirectoryUnavailableError", () => Effect.succeed(undefined)),
),
{ concurrency: "unbounded" },
)
const found = checked.find((directory) => directory !== undefined)
if (!found) return yield* new SourceDirectoryNotFoundError({ projectID })
return found
})
const getStrategy = Effect.fnUntraced(function* (id: StrategyID) {
const found = registry.get(id)
if (!found) return yield* new StrategyUnavailableError({ strategy: id })
return found
})
const create = Effect.fn("Worktree.create")(function* (input: CreateInput) {
const selected = yield* getStrategy(input.strategy)
const sourceDirectory = yield* source(input.projectID)
yield* fs.makeDirectory(input.directory, { recursive: true }).pipe(Effect.orDie)
const name = input.name ?? Slug.create()
let suffix = 1
let worktreeDirectory = AbsolutePath.make(path.join(input.directory, name))
while (yield* fs.existsSafe(worktreeDirectory)) {
suffix++
if (suffix > 10) return yield* new DestinationExistsError({ directory: worktreeDirectory })
worktreeDirectory = AbsolutePath.make(path.join(input.directory, `${name}-${suffix}`))
}
const result = yield* selected.create({
directory: worktreeDirectory,
sourceDirectory,
})
yield* changed(
input.projectID,
yield* ops.create({
projectID: input.projectID,
directory: result.directory,
strategy: input.strategy,
}),
)
return result
})
const remove = Effect.fn("Worktree.remove")(function* (input: RemoveInput) {
const worktreeDirectory = yield* canonical(fs, input.directory)
const stored = yield* ops.find(input.projectID, worktreeDirectory)
if (!stored?.strategy) return yield* new InvalidDirectoryError({ directory: worktreeDirectory })
yield* (yield* getStrategy(StrategyID.make(stored.strategy))).remove({
directory: worktreeDirectory,
force: input.force,
})
yield* changed(input.projectID, yield* ops.remove(input.projectID, worktreeDirectory))
})
const refresh = Effect.fn("Worktree.refresh")(function* (input: RefreshInput) {
const stored = yield* ops.list(input.projectID)
const checked = yield* Effect.forEach(
stored,
(item) => fs.isDir(item.directory).pipe(Effect.map((exists) => ({ ...item, exists }))),
{ concurrency: "unbounded" },
)
const sourceDirectories = checked
.filter((item) => item.strategy === undefined && item.exists)
.map((item) => item.directory)
const discovered = yield* Effect.forEach(
sourceDirectories,
(sourceDirectory) =>
Effect.forEach(strategies(), (strategy) =>
strategy.list(sourceDirectory).pipe(
Effect.catchTag("Worktree.DirectoryUnavailableError", () => Effect.succeed([])),
Effect.map((items) =>
items.map((item) => ({
directory: item.directory,
strategy: item.type === "worktree" ? strategy.id : undefined,
})),
),
),
),
{ concurrency: "unbounded" },
).pipe(
Effect.map((sets) => new Map(sets.flat(2).map((item) => [item.directory, item] as const)).values().toArray()),
)
const removed = checked.filter((item) => !item.exists).map((item) => item.directory)
const result = yield* db
.transaction((tx) =>
Effect.all({
updated: Effect.forEach(discovered, (item) =>
ops.create(
{
projectID: input.projectID,
directory: item.directory,
strategy: item.strategy,
},
tx,
),
),
removed: Effect.forEach(removed, (directory) => ops.remove(input.projectID, directory, tx)),
}),
)
.pipe(Effect.orDie)
const changes = {
updated: discovered.filter((_, index) => result.updated[index]).map((item) => item.directory),
removed: removed.filter((_, index) => result.removed[index]),
}
yield* changed(input.projectID, changes.updated.length > 0 || changes.removed.length > 0)
return changes
})
return Service.of({
register,
list: ops.list,
create,
remove,
refresh,
})
}),
)
export const node = makeGlobalNode({
service: Service,
layer: layer,
deps: [FSUtil.node, Git.node, Bus.node, Database.node],
})
export const refreshNode = makeLocationNode({
name: "worktree-refresh",
layer: Layer.effectDiscard(refreshAfterBoot),
deps: [node, Location.node],
})
+16
View File
@@ -0,0 +1,16 @@
export * as WorktreeDirectory from "./directory.js"
import { Effect, Schema } from "effect"
import { FSUtil } from "@opencode-ai/util/fs-util"
import { AbsolutePath } from "../schema.js"
export class DirectoryUnavailableError extends Schema.TaggedErrorClass<DirectoryUnavailableError>()(
"Worktree.DirectoryUnavailableError",
{ directory: AbsolutePath },
) {}
export const canonical = Effect.fnUntraced(function* (fs: FSUtil.Interface, input: AbsolutePath) {
const resolved = AbsolutePath.make(yield* fs.resolve(input))
if (!(yield* fs.isDir(resolved))) return yield* new DirectoryUnavailableError({ directory: input })
return resolved
})
+39
View File
@@ -0,0 +1,39 @@
export * as WorktreeGit from "./git.js"
import { Effect } from "effect"
import { Worktree } from "@opencode-ai/schema/worktree"
import { FSUtil } from "@opencode-ai/util/fs-util"
import { Git } from "../git.js"
import { canonical, DirectoryUnavailableError } from "./directory.js"
import type { ListEntry, Strategy } from "../worktree.js"
export const make = Effect.gen(function* () {
const fs = yield* FSUtil.Service
const git = yield* Git.Service
return {
id: Worktree.StrategyID.make("git"),
create: Effect.fn("Worktree.Git.create")(function* (input) {
const repository = yield* git.repo.discover(input.sourceDirectory)
if (!repository) return yield* new DirectoryUnavailableError({ directory: input.sourceDirectory })
yield* git.worktree.create({ repository, directory: input.directory })
return { directory: yield* canonical(fs, input.directory) }
}),
remove: Effect.fn("Worktree.Git.remove")(function* (input) {
const repository = yield* git.repo.discover(input.directory)
if (!repository) return yield* new DirectoryUnavailableError({ directory: input.directory })
yield* git.worktree.remove({ repository, directory: input.directory, force: input.force })
}),
list: Effect.fn("Worktree.Git.list")(function* (directory) {
const repository = yield* git.repo.discover(directory)
if (!repository) return yield* new DirectoryUnavailableError({ directory })
const entries = yield* git.worktree.list(repository)
return yield* Effect.forEach(entries, (entry) =>
canonical(fs, entry.directory).pipe(
Effect.map((directory) => ({ directory, type: entry.kind === "main" ? "root" : "worktree" }) as const),
Effect.catchTag("Worktree.DirectoryUnavailableError", () => Effect.succeed(undefined)),
),
).pipe(Effect.map((items) => items.filter((item): item is ListEntry => item !== undefined)))
}),
} satisfies Strategy
})
+20
View File
@@ -0,0 +1,20 @@
import { integer, primaryKey, sqliteTable, text } from "drizzle-orm/sqlite-core"
import { absoluteColumn } from "../database/path.js"
import { ProjectSchema } from "../project/schema.js"
import { ProjectTable } from "../project/sql.js"
export const WorktreeTable = sqliteTable(
"worktree",
{
project_id: text()
.$type<ProjectSchema.ID>()
.notNull()
.references(() => ProjectTable.id, { onDelete: "cascade" }),
directory: absoluteColumn().notNull(),
strategy: text(),
time_created: integer()
.notNull()
.$default(() => Date.now()),
},
(table) => [primaryKey({ columns: [table.project_id, table.directory] })],
)
+114
View File
@@ -455,6 +455,120 @@ describe("Bus", () => {
}),
)
it.effect("publishes a durable batch atomically in provided order", () =>
Effect.gen(function* () {
const bus = yield* Bus.Service
const { db } = yield* Database.Service
const aggregateID = Event.ID.create()
const observed = new Array<string>()
yield* bus.project(SyncMessage, (event) =>
Effect.sync(() => {
observed.push(`project:${event.data.text}`)
}),
)
yield* bus.listen((event) =>
event.type === SyncMessage.type
? Effect.gen(function* () {
const text = (event.data as { readonly text: string }).text
const row = yield* db
.select({ seq: EventSequenceTable.seq })
.from(EventSequenceTable)
.where(eq(EventSequenceTable.aggregate_id, aggregateID))
.get()
.pipe(Effect.orDie)
observed.push(`notify:${text}:${row?.seq}`)
})
: Effect.void,
)
const events = yield* bus.publishAll([
[SyncMessage, { id: aggregateID, text: "first" }],
[SyncMessage, { id: aggregateID, text: "second" }],
])
expect(events.map((event) => event.durable.seq)).toEqual([Event.Seq.make(0), Event.Seq.make(1)])
expect(observed).toEqual(["project:first", "project:second", "notify:first:1", "notify:second:1"])
expect(
(yield* db.select().from(EventTable).where(eq(EventTable.aggregate_id, aggregateID)).all()).map(
(row) => row.seq,
),
).toEqual([0, 1])
}),
)
it.effect("rolls back every batch event when a projector fails", () =>
Effect.gen(function* () {
const bus = yield* Bus.Service
const { db } = yield* Database.Service
const aggregateID = Event.ID.create()
const notifications = new Array<string>()
yield* db.run("CREATE TABLE IF NOT EXISTS event_batch_probe (value text NOT NULL)")
yield* db.run("DELETE FROM event_batch_probe")
yield* bus.project(SyncMessage, (event) =>
db
.run(`INSERT INTO event_batch_probe (value) VALUES ('${event.data.text}')`)
.pipe(
Effect.orDie,
Effect.andThen(event.data.text === "second" ? Effect.die("projector failed") : Effect.void),
),
)
yield* bus.listen((event) =>
Effect.sync(() => {
notifications.push(event.type)
}),
)
const exit = yield* bus
.publishAll([
[SyncMessage, { id: aggregateID, text: "first" }],
[SyncMessage, { id: aggregateID, text: "second" }],
])
.pipe(Effect.exit)
expect(String(exit)).toContain("projector failed")
expect(yield* db.all("SELECT value FROM event_batch_probe")).toEqual([])
expect(yield* db.select().from(EventTable).where(eq(EventTable.aggregate_id, aggregateID)).all()).toEqual([])
expect(
yield* db.select().from(EventSequenceTable).where(eq(EventSequenceTable.aggregate_id, aggregateID)).all(),
).toEqual([])
expect(notifications).toEqual([])
}),
)
it.effect("does not interleave a concurrent publish with batch notifications", () =>
Effect.gen(function* () {
const bus = yield* Bus.Service
const aggregateID = Event.ID.create()
const firstObserved = yield* Deferred.make<void>()
const continueNotifications = yield* Deferred.make<void>()
const observed = new Array<string>()
yield* bus.listen((event) => {
if (event.type !== SyncMessage.type) return Effect.void
const text = (event.data as { readonly text: string }).text
return Effect.sync(() => observed.push(text)).pipe(
Effect.andThen(text === "first" ? Deferred.succeed(firstObserved, undefined) : Effect.void),
Effect.andThen(text === "first" ? Deferred.await(continueNotifications) : Effect.void),
)
})
const batch = yield* bus
.publishAll([
[SyncMessage, { id: aggregateID, text: "first" }],
[SyncMessage, { id: aggregateID, text: "second" }],
])
.pipe(Effect.forkScoped)
yield* Deferred.await(firstObserved)
const single = yield* bus.publish(SyncMessage, { id: aggregateID, text: "third" }).pipe(Effect.forkScoped)
yield* Effect.yieldNow
expect(observed).toEqual(["first"])
yield* Deferred.succeed(continueNotifications, undefined)
yield* Fiber.join(batch)
yield* Fiber.join(single)
expect(observed).toEqual(["first", "second", "third"])
}),
)
it.effect("replays durable aggregate events after a sequence and tails new events", () =>
Effect.gen(function* () {
const bus = yield* Bus.Service
@@ -12,6 +12,7 @@ import { Database } from "@opencode-ai/core/database/database"
import { tmpdir } from "./fixture/tmpdir"
import type { SqlClient } from "effect/unstable/sql/SqlClient"
import legacyCredentialsMigration from "@opencode-ai/core/database/migration/20260805200742_import_legacy_credentials"
import worktreeMigration from "@opencode-ai/core/database/migration/20260812213948_worktree"
import { Global } from "@opencode-ai/util/global"
const run = <A, E>(
@@ -127,6 +128,31 @@ describe("DatabaseMigration", () => {
)
})
test("copies project directories into worktrees without removing the old table", async () => {
await run(
Effect.gen(function* () {
const db = yield* makeDb
yield* db.run(sql`CREATE TABLE project (id text PRIMARY KEY)`)
yield* db.run(
sql`CREATE TABLE project_directory (project_id text NOT NULL, directory text NOT NULL, type text, strategy text, time_created integer NOT NULL, PRIMARY KEY (project_id, directory))`,
)
yield* db.run(
sql`INSERT INTO project_directory (project_id, directory, type, strategy, time_created) VALUES ('project', '/root', 'main', NULL, 1), ('project', '/legacy', 'git_worktree', NULL, 2), ('project', '/strategy', NULL, 'git_worktree', 3), ('project', '/custom', NULL, 'acme/snapshot', 4)`,
)
yield* DatabaseMigration.applyOnly(db, [worktreeMigration])
expect(yield* db.all(sql`SELECT directory, strategy FROM worktree ORDER BY directory`)).toEqual([
{ directory: "/custom", strategy: "acme/snapshot" },
{ directory: "/legacy", strategy: "git" },
{ directory: "/root", strategy: null },
{ directory: "/strategy", strategy: "git" },
])
expect(yield* db.get(sql`SELECT count(*) AS count FROM project_directory`)).toEqual({ count: 4 })
}),
)
})
test("imports legacy JSON credentials without changing the source file or existing credentials", async () => {
await using tmp = await tmpdir()
const source = path.join(tmp.path, "auth.json")

Some files were not shown because too many files have changed in this diff Show More