James Long 3 hari lalu
induk
melakukan
ba499fb405
28 mengubah file dengan 2830 tambahan dan 406 penghapusan
  1. 1 0
      .opencode/opencode.jsonc
  2. 121 0
      WORKSPACE_SESSION_DELETE_NOTES.md
  3. 244 6
      bun.lock
  4. 43 0
      daytonaWorkspaceBootstrap.sh
  5. 206 0
      daytonaWorkspacePlugin.ts
  6. 23 0
      debugWorkspacePlugin.ts
  7. 1 0
      package.json
  8. 101 0
      packages/opencode/src/cli/cmd/tui/component/dialog-session-delete-failed.tsx
  9. 91 5
      packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx
  10. 131 4
      packages/opencode/src/cli/cmd/tui/component/dialog-workspace-create.tsx
  11. 3 0
      packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx
  12. 8 4
      packages/opencode/src/cli/cmd/tui/context/local.tsx
  13. 40 2
      packages/opencode/src/cli/cmd/tui/context/sdk.tsx
  14. 117 36
      packages/opencode/src/cli/cmd/tui/context/sync.tsx
  15. 247 23
      packages/opencode/src/control-plane/workspace.ts
  16. 1 0
      packages/opencode/src/plugin/loader.ts
  17. 79 18
      packages/opencode/src/server/instance/middleware.ts
  18. 118 0
      packages/opencode/src/server/instance/sync.ts
  19. 66 0
      packages/opencode/src/server/instance/workspace.ts
  20. 1 1
      packages/opencode/src/server/middleware.ts
  21. 44 11
      packages/opencode/src/server/proxy.ts
  22. 19 0
      packages/opencode/src/sync/index.ts
  23. 89 0
      packages/opencode/test/cli/tui/workspace-restore.test.ts
  24. 268 0
      packages/opencode/test/server/workspace-restore.test.ts
  25. 70 0
      packages/opencode/test/sync/index.test.ts
  26. 149 2
      packages/sdk/js/src/v2/gen/sdk.gen.ts
  27. 360 294
      packages/sdk/js/src/v2/gen/types.gen.ts
  28. 189 0
      sync-routes.ts

+ 1 - 0
.opencode/opencode.jsonc

@@ -11,6 +11,7 @@
     },
   },
   "mcp": {},
+  "plugin": ["../daytonaWorkspacePlugin.ts", "../debugWorkspacePlugin.ts"],
   "tools": {
     "github-triage": false,
     "github-pr-search": false,

+ 121 - 0
WORKSPACE_SESSION_DELETE_NOTES.md

@@ -0,0 +1,121 @@
+# Workspace Session Delete Notes
+
+## Current state
+
+- Deleting a workspace-backed session now removes the session record correctly.
+- The UI still goes stale in some cases because the delete is handled locally and the expected event propagation is incomplete.
+
+## Important behavior discovered
+
+### 1. `DELETE /session/:id` is currently being handled locally
+
+This is because workspace routing was changed so path handling happens earlier, and we now always let `DELETE /session/:id` through the local route path.
+
+That is a little weird semantically, because for workspace-backed sessions the delete action conceptually belongs to the remote workspace session too.
+
+Relevant code:
+
+- `packages/opencode/src/server/router.ts`
+- special-case for `DELETE /session/:id`
+
+### 2. Local delete works, but UI does not fully update from events
+
+The delete operation works server-side, but because we do not have an active instance in this local-delete path, we are not publishing the same events the TUI expects for immediate sync/UI updates.
+
+That means:
+
+- delete succeeds
+- persistence updates
+- but event-driven UI refresh may not happen
+
+## Root issue
+
+We need event publication for local handling of workspace session deletes, but in this code path we may not have an instance context.
+
+So the current system is in an awkward middle state:
+
+- delete is handled locally
+- remote session/workspace semantics still matter
+- but local event publishing is instance-dependent in places
+
+## Key design question
+
+Where should the "delete last session in workspace -> delete workspace" logic live?
+
+### Option A: handle it remotely
+
+If the remote workspace handles session deletion and also decides whether the workspace should be deleted, then the result can sync back naturally.
+
+Pros:
+
+- cleaner ownership model
+- remote workspace remains source of truth for workspace-backed session lifecycle
+- sync/event flow stays more consistent
+
+Cons:
+
+- requires remote delete path to be used reliably
+- local special-casing in router becomes more suspect
+
+### Option B: handle it locally
+
+If local server deletes the session and then checks whether any sessions remain for that `workspaceID`, local can also delete the workspace.
+
+Pros:
+
+- straightforward to implement
+- does not depend on remote behavior
+
+Cons:
+
+- local path now owns workspace lifecycle decisions for remote workspaces
+- still has event propagation problems unless we explicitly publish/update correctly
+
+## Current leaning
+
+The workspace cleanup logic probably belongs on the remote side if workspace-backed sessions are supposed to behave as remote-owned state.
+
+Reason:
+
+- if remote handles it, the result can sync back
+- avoids local special-case ownership drift
+
+But this depends on whether `DELETE /session/:id` should actually be routed remotely for workspace sessions instead of always being forced local.
+
+## Things to inspect next
+
+1. `packages/opencode/src/server/router.ts`
+
+- Revisit why `DELETE /session/:id` is forced local.
+- Decide whether workspace-backed session deletes should proxy to remote instead.
+
+2. `packages/opencode/src/session/index.ts`
+
+- Current local cleanup logic removes workspace if no sessions remain for its `workspaceID`.
+- Re-evaluate whether this should stay here or move to remote handling.
+
+3. Event publication path
+
+- Figure out what event(s) the TUI actually needs to update correctly after delete.
+- Check whether local delete without instance can still publish enough global/sync events.
+
+4. TUI refresh path
+
+- `packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx`
+- `packages/opencode/src/cli/cmd/tui/context/sync.tsx`
+
+We added an explicit session-list refresh to work around stale UI, but that is treating the symptom.
+
+## Summary
+
+The real unresolved issue is ownership:
+
+- local currently handles `DELETE /session/:id`
+- remote workspace semantics still matter
+- event propagation is incomplete when delete is handled without an instance
+
+Next session should start by deciding:
+
+1. Should workspace session delete be handled locally or remotely?
+2. Where should "delete workspace if no sessions remain" live?
+3. What event must be published so the UI updates without manual refresh?

+ 244 - 6
bun.lock

@@ -6,6 +6,7 @@
       "name": "opencode",
       "dependencies": {
         "@aws-sdk/client-s3": "3.933.0",
+        "@daytona/sdk": "0.164.0",
         "@opencode-ai/plugin": "workspace:*",
         "@opencode-ai/script": "workspace:*",
         "@opencode-ai/sdk": "workspace:*",
@@ -628,6 +629,7 @@
   "trustedDependencies": [
     "esbuild",
     "tree-sitter-powershell",
+    "protobufjs",
     "electron",
     "web-tree-sitter",
     "tree-sitter-bash",
@@ -836,6 +838,8 @@
 
     "@aws-sdk/credential-providers": ["@aws-sdk/[email protected]", "", { "dependencies": { "@aws-sdk/client-cognito-identity": "3.993.0", "@aws-sdk/core": "^3.973.11", "@aws-sdk/credential-provider-cognito-identity": "^3.972.3", "@aws-sdk/credential-provider-env": "^3.972.9", "@aws-sdk/credential-provider-http": "^3.972.11", "@aws-sdk/credential-provider-ini": "^3.972.9", "@aws-sdk/credential-provider-login": "^3.972.9", "@aws-sdk/credential-provider-node": "^3.972.10", "@aws-sdk/credential-provider-process": "^3.972.9", "@aws-sdk/credential-provider-sso": "^3.972.9", "@aws-sdk/credential-provider-web-identity": "^3.972.9", "@aws-sdk/nested-clients": "3.993.0", "@aws-sdk/types": "^3.973.1", "@smithy/config-resolver": "^4.4.6", "@smithy/core": "^3.23.2", "@smithy/credential-provider-imds": "^4.2.8", "@smithy/node-config-provider": "^4.3.8", "@smithy/property-provider": "^4.2.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-1M/nukgPSLqe9krzOKHnE8OylUaKAiokAV3xRLdeExVHcRE7WG5uzCTKWTj1imKvPjDqXq/FWhlbbdWIn7xIwA=="],
 
+    "@aws-sdk/lib-storage": ["@aws-sdk/[email protected]", "", { "dependencies": { "@smithy/middleware-endpoint": "^4.4.29", "@smithy/protocol-http": "^5.3.13", "@smithy/smithy-client": "^4.12.9", "@smithy/types": "^4.14.0", "buffer": "5.6.0", "events": "3.3.0", "stream-browserify": "3.0.0", "tslib": "^2.6.2" }, "peerDependencies": { "@aws-sdk/client-s3": "^3.1028.0" } }, "sha512-AT937nfpMDW/8oDiWPBP/BdGJ6943ALMWTBpUi0fD0qelA3lyZgErSnX7yp9j3t/enzyHdlyBOPq9kGFBt0Xcg=="],
+
     "@aws-sdk/middleware-bucket-endpoint": ["@aws-sdk/[email protected]", "", { "dependencies": { "@aws-sdk/types": "3.930.0", "@aws-sdk/util-arn-parser": "3.893.0", "@smithy/node-config-provider": "^4.3.5", "@smithy/protocol-http": "^5.3.5", "@smithy/types": "^4.9.0", "@smithy/util-config-provider": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-cnCLWeKPYgvV4yRYPFH6pWMdUByvu2cy2BAlfsPpvnm4RaVioztyvxmQj5PmVN5fvWs5w/2d6U7le8X9iye2sA=="],
 
     "@aws-sdk/middleware-expect-continue": ["@aws-sdk/[email protected]", "", { "dependencies": { "@aws-sdk/types": "3.930.0", "@smithy/protocol-http": "^5.3.5", "@smithy/types": "^4.9.0", "tslib": "^2.6.2" } }, "sha512-5HEQ+JU4DrLNWeY27wKg/jeVa8Suy62ivJHOSUf6e6hZdVIMx0h/kXS1fHEQNNiLu2IzSEP/bFXsKBaW7x7s0g=="],
@@ -1020,6 +1024,12 @@
 
     "@ctrl/tinycolor": ["@ctrl/[email protected]", "", {}, "sha512-kzyuwOAQnXJNLS9PSyrk0CWk35nWJW/zl/6KvnTBMFK65gm7U1/Z5BqjxeapjZCIhQcM/DsrEmcbRwDyXyXK4A=="],
 
+    "@daytona/api-client": ["@daytona/[email protected]", "", { "dependencies": { "axios": "^1.6.1" } }, "sha512-G1cV6gC0mTOMSkGy4C5FFkxEytNItOzNCWXtnYVGepLqcLsnXuv1+vpIJFiIsfKEjrZKax8bu2sdSA8lQHTyOw=="],
+
+    "@daytona/sdk": ["@daytona/[email protected]", "", { "dependencies": { "@aws-sdk/client-s3": "^3.787.0", "@aws-sdk/lib-storage": "^3.798.0", "@daytona/api-client": "0.164.0", "@daytona/toolbox-api-client": "0.164.0", "@iarna/toml": "^2.2.5", "@opentelemetry/api": "^1.9.0", "@opentelemetry/exporter-trace-otlp-http": "^0.207.0", "@opentelemetry/instrumentation-http": "^0.207.0", "@opentelemetry/otlp-exporter-base": "0.207.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-node": "^0.207.0", "@opentelemetry/sdk-trace-base": "^2.2.0", "@opentelemetry/semantic-conventions": "^1.37.0", "axios": "^1.13.5", "busboy": "^1.0.0", "dotenv": "^17.0.1", "expand-tilde": "^2.0.2", "fast-glob": "^3.3.0", "form-data": "^4.0.4", "isomorphic-ws": "^5.0.0", "pathe": "^2.0.3", "shell-quote": "^1.8.2", "tar": "^7.5.11" } }, "sha512-EBxKyWjvbMMR7ZpcHp+QvtJHPeCQ7SVO08okEWMLiKeJ0bqSyXgzYW5gbXUyhhogOkcDptD+j89yK1EnTTWjSg=="],
+
+    "@daytona/toolbox-api-client": ["@daytona/[email protected]", "", { "dependencies": { "axios": "^1.6.1" } }, "sha512-7LU6mxPuobkgDIg24MHP7kf36Wm1kSSvorM/8ODGTkbLfUdXk7DSTGBs9Lqoy0cM4/62LJ9kPmjw607pPSTXbQ=="],
+
     "@develar/schema-utils": ["@develar/[email protected]", "", { "dependencies": { "ajv": "^6.12.0", "ajv-keywords": "^3.4.1" } }, "sha512-0cp4PsWQ/9avqTVMCtZ+GirikIA36ikvjtHweU4/j8yLtgObI0+JUPhYFScgwlteveGB1rt3Cm8UhN04XayDig=="],
 
     "@dimforge/rapier2d-simd-compat": ["@dimforge/[email protected]", "", {}, "sha512-bijvwWz6NHsNj5e5i1vtd3dU2pDhthSaTUZSh14DUGGKJfw8eMnlWZsxwHBxB/a3AXVNDjL9abuHw1k9FGR+jg=="],
@@ -1168,6 +1178,10 @@
 
     "@graphql-typed-document-node/core": ["@graphql-typed-document-node/[email protected]", "", { "peerDependencies": { "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ=="],
 
+    "@grpc/grpc-js": ["@grpc/[email protected]", "", { "dependencies": { "@grpc/proto-loader": "^0.8.0", "@js-sdsl/ordered-map": "^4.4.2" } }, "sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA=="],
+
+    "@grpc/proto-loader": ["@grpc/[email protected]", "", { "dependencies": { "lodash.camelcase": "^4.3.0", "long": "^5.0.0", "protobufjs": "^7.5.3", "yargs": "^17.7.2" }, "bin": { "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" } }, "sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ=="],
+
     "@happy-dom/global-registrator": ["@happy-dom/[email protected]", "", { "dependencies": { "@types/node": "^20.0.0", "happy-dom": "^20.0.11" } }, "sha512-GqNqiShBT/lzkHTMC/slKBrvN0DsD4Di8ssBk4aDaVgEn+2WMzE6DXxq701ndSXj7/0cJ8mNT71pM7Bnrr6JRw=="],
 
     "@hey-api/codegen-core": ["@hey-api/[email protected]", "", { "dependencies": { "@hey-api/types": "0.1.2", "ansi-colors": "4.1.3", "c12": "3.3.3", "color-support": "1.1.3" }, "peerDependencies": { "typescript": ">=5.5.3" } }, "sha512-f2ZHucnA2wBGAY8ipB4wn/mrEYW+WUxU2huJmUvfDO6AE2vfILSHeF3wCO39Pz4wUYPoAWZByaauftLrOfC12Q=="],
@@ -1186,6 +1200,8 @@
 
     "@hono/zod-validator": ["@hono/[email protected]", "", { "peerDependencies": { "hono": ">=3.9.0", "zod": "^3.19.1" } }, "sha512-1rrlBg+EpDPhzOV4hT9pxr5+xDVmKuz6YJl+la7VCwK6ass5ldyKm5fD+umJdV2zhHD6jROoCCv8NbTwyfhT0g=="],
 
+    "@iarna/toml": ["@iarna/[email protected]", "", {}, "sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg=="],
+
     "@ibm/plex": ["@ibm/[email protected]", "", { "dependencies": { "@ibm/telemetry-js": "^1.5.1" } }, "sha512-fnsipQywHt3zWvsnlyYKMikcVI7E2fEwpiPnIHFqlbByXVfQfANAAeJk1IV4mNnxhppUIDlhU0TzwYwL++Rn2g=="],
 
     "@ibm/telemetry-js": ["@ibm/[email protected]", "", { "bin": { "ibmtelemetry": "dist/collect.js" } }, "sha512-RO/9j+URJnSfseWg9ZkEX9p+a3Ousd33DBU7rOafoZB08RqdzxFVYJ2/iM50dkBuD0o7WX7GYt1sLbNgCoE+pA=="],
@@ -1316,6 +1332,8 @@
 
     "@js-joda/core": ["@js-joda/[email protected]", "", {}, "sha512-WBu4ULVVxySLLzK1Ppq+OdfP+adRS4ntmDQT915rzDJ++i95gc2jZkM5B6LWEAwN3lGXpfie3yPABozdD3K3Vg=="],
 
+    "@js-sdsl/ordered-map": ["@js-sdsl/[email protected]", "", {}, "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw=="],
+
     "@js-temporal/polyfill": ["@js-temporal/[email protected]", "", { "dependencies": { "jsbi": "^4.3.0" } }, "sha512-hloP58zRVCRSpgDxmqCWJNlizAlUgJFqG2ypq79DCvyv9tHjRYMDOcPFjzfl/A1/YxDvRCZz8wvZvmapQnKwFQ=="],
 
     "@jsdevtools/ono": ["@jsdevtools/[email protected]", "", {}, "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg=="],
@@ -1544,6 +1562,62 @@
 
     "@opentelemetry/api": ["@opentelemetry/[email protected]", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
 
+    "@opentelemetry/api-logs": ["@opentelemetry/[email protected]", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-lAb0jQRVyleQQGiuuvCOTDVspc14nx6XJjP4FspJ1sNARo3Regq4ZZbrc3rN4b1TYSuUCvgH+UXUPug4SLOqEQ=="],
+
+    "@opentelemetry/context-async-hooks": ["@opentelemetry/[email protected]", "", { "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-qRkLWiUEZNAmYapZ7KGS5C4OmBLcP/H2foXeOEaowYCR0wi89fHejrfYfbuLVCMLp/dWZXKvQusdbUEZjERfwQ=="],
+
+    "@opentelemetry/core": ["@opentelemetry/[email protected]", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw=="],
+
+    "@opentelemetry/exporter-logs-otlp-grpc": ["@opentelemetry/[email protected]", "", { "dependencies": { "@grpc/grpc-js": "^1.7.1", "@opentelemetry/core": "2.2.0", "@opentelemetry/otlp-exporter-base": "0.207.0", "@opentelemetry/otlp-grpc-exporter-base": "0.207.0", "@opentelemetry/otlp-transformer": "0.207.0", "@opentelemetry/sdk-logs": "0.207.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-K92RN+kQGTMzFDsCzsYNGqOsXRUnko/Ckk+t/yPJao72MewOLgBUTWVHhebgkNfRCYqDz1v3K0aPT9OJkemvgg=="],
+
+    "@opentelemetry/exporter-logs-otlp-http": ["@opentelemetry/[email protected]", "", { "dependencies": { "@opentelemetry/api-logs": "0.207.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/otlp-exporter-base": "0.207.0", "@opentelemetry/otlp-transformer": "0.207.0", "@opentelemetry/sdk-logs": "0.207.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-JpOh7MguEUls8eRfkVVW3yRhClo5b9LqwWTOg8+i4gjr/+8eiCtquJnC7whvpTIGyff06cLZ2NsEj+CVP3Mjeg=="],
+
+    "@opentelemetry/exporter-logs-otlp-proto": ["@opentelemetry/[email protected]", "", { "dependencies": { "@opentelemetry/api-logs": "0.207.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/otlp-exporter-base": "0.207.0", "@opentelemetry/otlp-transformer": "0.207.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-logs": "0.207.0", "@opentelemetry/sdk-trace-base": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-RQJEV/K6KPbQrIUbsrRkEe0ufks1o5OGLHy6jbDD8tRjeCsbFHWfg99lYBRqBV33PYZJXsigqMaAbjWGTFYzLw=="],
+
+    "@opentelemetry/exporter-metrics-otlp-grpc": ["@opentelemetry/[email protected]", "", { "dependencies": { "@grpc/grpc-js": "^1.7.1", "@opentelemetry/core": "2.2.0", "@opentelemetry/exporter-metrics-otlp-http": "0.207.0", "@opentelemetry/otlp-exporter-base": "0.207.0", "@opentelemetry/otlp-grpc-exporter-base": "0.207.0", "@opentelemetry/otlp-transformer": "0.207.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-metrics": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-6flX89W54gkwmqYShdcTBR1AEF5C1Ob0O8pDgmLPikTKyEv27lByr9yBmO5WrP0+5qJuNPHrLfgFQFYi6npDGA=="],
+
+    "@opentelemetry/exporter-metrics-otlp-http": ["@opentelemetry/[email protected]", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/otlp-exporter-base": "0.207.0", "@opentelemetry/otlp-transformer": "0.207.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-metrics": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-fG8FAJmvXOrKXGIRN8+y41U41IfVXxPRVwyB05LoMqYSjugx/FSBkMZUZXUT/wclTdmBKtS5MKoi0bEKkmRhSw=="],
+
+    "@opentelemetry/exporter-metrics-otlp-proto": ["@opentelemetry/[email protected]", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/exporter-metrics-otlp-http": "0.207.0", "@opentelemetry/otlp-exporter-base": "0.207.0", "@opentelemetry/otlp-transformer": "0.207.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-metrics": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-kDBxiTeQjaRlUQzS1COT9ic+et174toZH6jxaVuVAvGqmxOkgjpLOjrI5ff8SMMQE69r03L3Ll3nPKekLopLwg=="],
+
+    "@opentelemetry/exporter-prometheus": ["@opentelemetry/[email protected]", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-metrics": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-Y5p1s39FvIRmU+F1++j7ly8/KSqhMmn6cMfpQqiDCqDjdDHwUtSq0XI0WwL3HYGnZeaR/VV4BNmsYQJ7GAPrhw=="],
+
+    "@opentelemetry/exporter-trace-otlp-grpc": ["@opentelemetry/[email protected]", "", { "dependencies": { "@grpc/grpc-js": "^1.7.1", "@opentelemetry/core": "2.2.0", "@opentelemetry/otlp-exporter-base": "0.207.0", "@opentelemetry/otlp-grpc-exporter-base": "0.207.0", "@opentelemetry/otlp-transformer": "0.207.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-trace-base": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-7u2ZmcIx6D4KG/+5np4X2qA0o+O0K8cnUDhR4WI/vr5ZZ0la9J9RG+tkSjC7Yz+2XgL6760gSIM7/nyd3yaBLA=="],
+
+    "@opentelemetry/exporter-trace-otlp-http": ["@opentelemetry/[email protected]", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/otlp-exporter-base": "0.207.0", "@opentelemetry/otlp-transformer": "0.207.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-trace-base": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-HSRBzXHIC7C8UfPQdu15zEEoBGv0yWkhEwxqgPCHVUKUQ9NLHVGXkVrf65Uaj7UwmAkC1gQfkuVYvLlD//AnUQ=="],
+
+    "@opentelemetry/exporter-trace-otlp-proto": ["@opentelemetry/[email protected]", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/otlp-exporter-base": "0.207.0", "@opentelemetry/otlp-transformer": "0.207.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-trace-base": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-ruUQB4FkWtxHjNmSXjrhmJZFvyMm+tBzHyMm7YPQshApy4wvZUTcrpPyP/A/rCl/8M4BwoVIZdiwijMdbZaq4w=="],
+
+    "@opentelemetry/exporter-zipkin": ["@opentelemetry/[email protected]", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-trace-base": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": "^1.0.0" } }, "sha512-VV4QzhGCT7cWrGasBWxelBjqbNBbyHicWWS/66KoZoe9BzYwFB72SH2/kkc4uAviQlO8iwv2okIJy+/jqqEHTg=="],
+
+    "@opentelemetry/instrumentation": ["@opentelemetry/[email protected]", "", { "dependencies": { "@opentelemetry/api-logs": "0.207.0", "import-in-the-middle": "^2.0.0", "require-in-the-middle": "^8.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-y6eeli9+TLKnznrR8AZlQMSJT7wILpXH+6EYq5Vf/4Ao+huI7EedxQHwRgVUOMLFbe7VFDvHJrX9/f4lcwnJsA=="],
+
+    "@opentelemetry/instrumentation-http": ["@opentelemetry/[email protected]", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/instrumentation": "0.207.0", "@opentelemetry/semantic-conventions": "^1.29.0", "forwarded-parse": "2.1.2" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-FC4i5hVixTzuhg4SV2ycTEAYx+0E2hm+GwbdoVPSA6kna0pPVI4etzaA9UkpJ9ussumQheFXP6rkGIaFJjMxsw=="],
+
+    "@opentelemetry/otlp-exporter-base": ["@opentelemetry/[email protected]", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/otlp-transformer": "0.207.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-4RQluMVVGMrHok/3SVeSJ6EnRNkA2MINcX88sh+d/7DjGUrewW/WT88IsMEci0wUM+5ykTpPPNbEOoW+jwHnbw=="],
+
+    "@opentelemetry/otlp-grpc-exporter-base": ["@opentelemetry/[email protected]", "", { "dependencies": { "@grpc/grpc-js": "^1.7.1", "@opentelemetry/core": "2.2.0", "@opentelemetry/otlp-exporter-base": "0.207.0", "@opentelemetry/otlp-transformer": "0.207.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-eKFjKNdsPed4q9yYqeI5gBTLjXxDM/8jwhiC0icw3zKxHVGBySoDsed5J5q/PGY/3quzenTr3FiTxA3NiNT+nw=="],
+
+    "@opentelemetry/otlp-transformer": ["@opentelemetry/[email protected]", "", { "dependencies": { "@opentelemetry/api-logs": "0.207.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-logs": "0.207.0", "@opentelemetry/sdk-metrics": "2.2.0", "@opentelemetry/sdk-trace-base": "2.2.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-+6DRZLqM02uTIY5GASMZWUwr52sLfNiEe20+OEaZKhztCs3+2LxoTjb6JxFRd9q1qNqckXKYlUKjbH/AhG8/ZA=="],
+
+    "@opentelemetry/propagator-b3": ["@opentelemetry/[email protected]", "", { "dependencies": { "@opentelemetry/core": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-9CrbTLFi5Ee4uepxg2qlpQIozoJuoAZU5sKMx0Mn7Oh+p7UrgCiEV6C02FOxxdYVRRFQVCinYR8Kf6eMSQsIsw=="],
+
+    "@opentelemetry/propagator-jaeger": ["@opentelemetry/[email protected]", "", { "dependencies": { "@opentelemetry/core": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-FfeOHOrdhiNzecoB1jZKp2fybqmqMPJUXe2ZOydP7QzmTPYcfPeuaclTLYVhK3HyJf71kt8sTl92nV4YIaLaKA=="],
+
+    "@opentelemetry/resources": ["@opentelemetry/[email protected]", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A=="],
+
+    "@opentelemetry/sdk-logs": ["@opentelemetry/[email protected]", "", { "dependencies": { "@opentelemetry/api-logs": "0.207.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, "sha512-4MEQmn04y+WFe6cyzdrXf58hZxilvY59lzZj2AccuHW/+BxLn/rGVN/Irsi/F0qfBOpMOrrCLKTExoSL2zoQmg=="],
+
+    "@opentelemetry/sdk-metrics": ["@opentelemetry/[email protected]", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-G5KYP6+VJMZzpGipQw7Giif48h6SGQ2PFKEYCybeXJsOCB4fp8azqMAAzE5lnnHK3ZVwYQrgmFbsUJO/zOnwGw=="],
+
+    "@opentelemetry/sdk-node": ["@opentelemetry/[email protected]", "", { "dependencies": { "@opentelemetry/api-logs": "0.207.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/exporter-logs-otlp-grpc": "0.207.0", "@opentelemetry/exporter-logs-otlp-http": "0.207.0", "@opentelemetry/exporter-logs-otlp-proto": "0.207.0", "@opentelemetry/exporter-metrics-otlp-grpc": "0.207.0", "@opentelemetry/exporter-metrics-otlp-http": "0.207.0", "@opentelemetry/exporter-metrics-otlp-proto": "0.207.0", "@opentelemetry/exporter-prometheus": "0.207.0", "@opentelemetry/exporter-trace-otlp-grpc": "0.207.0", "@opentelemetry/exporter-trace-otlp-http": "0.207.0", "@opentelemetry/exporter-trace-otlp-proto": "0.207.0", "@opentelemetry/exporter-zipkin": "2.2.0", "@opentelemetry/instrumentation": "0.207.0", "@opentelemetry/propagator-b3": "2.2.0", "@opentelemetry/propagator-jaeger": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-logs": "0.207.0", "@opentelemetry/sdk-metrics": "2.2.0", "@opentelemetry/sdk-trace-base": "2.2.0", "@opentelemetry/sdk-trace-node": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-hnRsX/M8uj0WaXOBvFenQ8XsE8FLVh2uSnn1rkWu4mx+qu7EKGUZvZng6y/95cyzsqOfiaDDr08Ek4jppkIDNg=="],
+
+    "@opentelemetry/sdk-trace-base": ["@opentelemetry/[email protected]", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/resources": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-r86ut4T1e8vNwB35CqCcKd45yzqH6/6Wzvpk2/cZB8PsPLlZFTvrh8yfOS3CYZYcUmAx4hHTZJ8AO8Dj8nrdhw=="],
+
+    "@opentelemetry/sdk-trace-node": ["@opentelemetry/[email protected]", "", { "dependencies": { "@opentelemetry/context-async-hooks": "2.2.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/sdk-trace-base": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-+OaRja3f0IqGG2kptVeYsrZQK9nKRSpfFrKtRBq4uh6nIB8bTBgaGvYQrQoRrQWQMA5dK5yLhDMDc0dvYvCOIQ=="],
+
+    "@opentelemetry/semantic-conventions": ["@opentelemetry/[email protected]", "", {}, "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw=="],
+
     "@opentui/core": ["@opentui/[email protected]", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "marked": "17.0.1", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.97", "@opentui/core-darwin-x64": "0.1.97", "@opentui/core-linux-arm64": "0.1.97", "@opentui/core-linux-x64": "0.1.97", "@opentui/core-win32-arm64": "0.1.97", "@opentui/core-win32-x64": "0.1.97", "bun-webgpu": "0.1.5", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-2ENH0Dc4NUAeHeeQCQhF1lg68RuyntOUP68UvortvDqTz/hqLG0tIwF+DboCKtWi8Nmao4SAQEJ7lfmyQNEDOQ=="],
 
     "@opentui/core-darwin-arm64": ["@opentui/[email protected]", "", { "os": "darwin", "cpu": "arm64" }, "sha512-t7oMGEfMPQsqLEx7/rPqv/UGJ+vqhe4RWHRRQRYcuHuLKssZ2S8P9mSS7MBPtDqGcxg4PosCrh5nHYeZ94EXUw=="],
@@ -1698,6 +1772,26 @@
 
     "@protobuf-ts/runtime-rpc": ["@protobuf-ts/[email protected]", "", { "dependencies": { "@protobuf-ts/runtime": "^2.11.1" } }, "sha512-4CqqUmNA+/uMz00+d3CYKgElXO9VrEbucjnBFEjqI4GuDrEQ32MaI3q+9qPBvIGOlL4PmHXrzM32vBPWRhQKWQ=="],
 
+    "@protobufjs/aspromise": ["@protobufjs/[email protected]", "", {}, "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="],
+
+    "@protobufjs/base64": ["@protobufjs/[email protected]", "", {}, "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="],
+
+    "@protobufjs/codegen": ["@protobufjs/[email protected]", "", {}, "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg=="],
+
+    "@protobufjs/eventemitter": ["@protobufjs/[email protected]", "", {}, "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q=="],
+
+    "@protobufjs/fetch": ["@protobufjs/[email protected]", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.1", "@protobufjs/inquire": "^1.1.0" } }, "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ=="],
+
+    "@protobufjs/float": ["@protobufjs/[email protected]", "", {}, "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ=="],
+
+    "@protobufjs/inquire": ["@protobufjs/[email protected]", "", {}, "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q=="],
+
+    "@protobufjs/path": ["@protobufjs/[email protected]", "", {}, "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA=="],
+
+    "@protobufjs/pool": ["@protobufjs/[email protected]", "", {}, "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw=="],
+
+    "@protobufjs/utf8": ["@protobufjs/[email protected]", "", {}, "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="],
+
     "@radix-ui/colors": ["@radix-ui/[email protected]", "", {}, "sha512-xySw8f0ZVsAEP+e7iLl3EvcBXX7gsIlC1Zso/sPBW9gIWerBTgz6axrjU+MZ39wD+WFi5h5zdWpsg3+hwt2Qsg=="],
 
     "@radix-ui/primitive": ["@radix-ui/[email protected]", "", { "dependencies": { "@babel/runtime": "^7.13.10" } }, "sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw=="],
@@ -2378,6 +2472,8 @@
 
     "acorn": ["[email protected]", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="],
 
+    "acorn-import-attributes": ["[email protected]", "", { "peerDependencies": { "acorn": "^8" } }, "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ=="],
+
     "acorn-jsx": ["[email protected]", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
 
     "acorn-walk": ["[email protected]", "", {}, "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A=="],
@@ -2600,6 +2696,8 @@
 
     "bundle-name": ["[email protected]", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="],
 
+    "busboy": ["[email protected]", "", { "dependencies": { "streamsearch": "^1.1.0" } }, "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA=="],
+
     "bytes": ["[email protected]", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
 
     "c12": ["[email protected]", "", { "dependencies": { "chokidar": "^5.0.0", "confbox": "^0.2.2", "defu": "^6.1.4", "dotenv": "^17.2.3", "exsolve": "^1.0.8", "giget": "^2.0.0", "jiti": "^2.6.1", "ohash": "^2.0.11", "pathe": "^2.0.3", "perfect-debounce": "^2.0.0", "pkg-types": "^2.3.0", "rc9": "^2.1.2" }, "peerDependencies": { "magicast": "*" }, "optionalPeers": ["magicast"] }, "sha512-750hTRvgBy5kcMNPdh95Qo+XUBeGo8C7nsKSmedDmaQI+E0r82DwHeM6vBewDe4rGFbnxoa4V9pw+sPh5+Iz8Q=="],
@@ -2660,6 +2758,8 @@
 
     "citty": ["[email protected]", "", { "dependencies": { "consola": "^3.2.3" } }, "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ=="],
 
+    "cjs-module-lexer": ["[email protected]", "", {}, "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ=="],
+
     "classnames": ["[email protected]", "", {}, "sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw=="],
 
     "clean-css": ["[email protected]", "", { "dependencies": { "source-map": "~0.6.0" } }, "sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg=="],
@@ -2870,7 +2970,7 @@
 
     "dot-prop": ["[email protected]", "", { "dependencies": { "type-fest": "^3.8.0" } }, "sha512-xaBe6ZT4DHPkg0k4Ytbvn5xoxgpG0jOS1dYxSOwAHPuNLjP3/OzN0gH55SrLqpx8cBfSaVt91lXYkApjb+nYdQ=="],
 
-    "dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="],
+    "dotenv": ["dotenv@17.3.1", "", {}, "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA=="],
 
     "dotenv-expand": ["[email protected]", "", { "dependencies": { "dotenv": "^16.4.5" } }, "sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA=="],
 
@@ -3022,6 +3122,8 @@
 
     "exit-hook": ["[email protected]", "", {}, "sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw=="],
 
+    "expand-tilde": ["[email protected]", "", { "dependencies": { "homedir-polyfill": "^1.0.1" } }, "sha512-A5EmesHW6rfnZ9ysHQjPdJRni0SRar0tjtG5MNtm9n5TUvsYU8oozprtRD4AqHxcZWWlVuAmQo2nWKfN9oyjTw=="],
+
     "expect-type": ["[email protected]", "", {}, "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA=="],
 
     "exponential-backoff": ["[email protected]", "", {}, "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA=="],
@@ -3122,6 +3224,8 @@
 
     "forwarded": ["[email protected]", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="],
 
+    "forwarded-parse": ["[email protected]", "", {}, "sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw=="],
+
     "fraction.js": ["[email protected]", "", {}, "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ=="],
 
     "framer-motion": ["[email protected]", "", { "dependencies": { "@motionone/dom": "^10.15.3", "hey-listen": "^1.0.8", "tslib": "^2.4.0" }, "optionalDependencies": { "@emotion/is-prop-valid": "^0.8.2" }, "peerDependencies": { "react": "^18.0.0", "react-dom": "^18.0.0" } }, "sha512-5IDx5bxkjWHWUF3CVJoSyUVOtrbAxtzYBBowRE2uYI/6VYhkEBD+rbTHEGuUmbGHRj6YqqSfoG7Aa1cLyWCrBA=="],
@@ -3278,6 +3382,8 @@
 
     "hey-listen": ["[email protected]", "", {}, "sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q=="],
 
+    "homedir-polyfill": ["[email protected]", "", { "dependencies": { "parse-passwd": "^1.0.0" } }, "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA=="],
+
     "hono": ["[email protected]", "", {}, "sha512-icXIITfw/07Q88nLSkB9aiUrd8rYzSweK681Kjo/TSggaGbOX4RRyxxm71v+3PC8C/j+4rlxGeoTRxQDkaJkUw=="],
 
     "hono-openapi": ["[email protected]", "", { "peerDependencies": { "@hono/standard-validator": "^0.2.0", "@standard-community/standard-json": "^0.3.5", "@standard-community/standard-openapi": "^0.2.9", "@types/json-schema": "^7.0.15", "hono": "^4.8.3", "openapi-types": "^12.1.3" }, "optionalPeers": ["@hono/standard-validator", "hono"] }, "sha512-toUcO60MftRBxqcVyxsHNYs2m4vf4xkQaiARAucQx3TiBPDtMNNkoh+C4I1vAretQZiGyaLOZNWn1YxfSyUA5g=="],
@@ -3330,6 +3436,8 @@
 
     "image-q": ["[email protected]", "", { "dependencies": { "@types/node": "16.9.1" } }, "sha512-PfJGVgIfKQJuq3s0tTDOKtztksibuUEbJQIYT3by6wctQo+Rdlh7ef4evJ5NCdxY4CfMbvFkocEwbl4BF8RlJw=="],
 
+    "import-in-the-middle": ["[email protected]", "", { "dependencies": { "acorn": "^8.15.0", "acorn-import-attributes": "^1.9.5", "cjs-module-lexer": "^2.2.0", "module-details-from-path": "^1.0.4" } }, "sha512-3vZV3jX0XRFW3EJDTwzWoZa+RH1b8eTTx6YOCjglrLyPuepwoBti1k3L2dKwdCUrnVEfc5CuRuGstaC/uQJJaw=="],
+
     "import-local": ["[email protected]", "", { "dependencies": { "pkg-dir": "^4.2.0", "resolve-cwd": "^3.0.0" }, "bin": { "import-local-fixture": "fixtures/cli.js" } }, "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA=="],
 
     "import-meta-resolve": ["[email protected]", "", {}, "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg=="],
@@ -3592,6 +3700,8 @@
 
     "lodash": ["[email protected]", "", {}, "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w=="],
 
+    "lodash.camelcase": ["[email protected]", "", {}, "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA=="],
+
     "lodash.defaults": ["[email protected]", "", {}, "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ=="],
 
     "lodash.escaperegexp": ["[email protected]", "", {}, "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw=="],
@@ -3822,6 +3932,8 @@
 
     "mkdirp": ["[email protected]", "", { "dependencies": { "minimist": "^1.2.6" }, "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw=="],
 
+    "module-details-from-path": ["[email protected]", "", {}, "sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w=="],
+
     "morphdom": ["[email protected]", "", {}, "sha512-D/fR4xgGUyVRbdMGU6Nejea1RFzYxYtyurG4Fbv2Fi/daKlWKuXGLOdXtl+3eIwL110cI2hz1ZojGICjjFLgTg=="],
 
     "motion": ["[email protected]", "", { "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=="],
@@ -4024,6 +4136,8 @@
 
     "parse-latin": ["[email protected]", "", { "dependencies": { "@types/nlcst": "^2.0.0", "@types/unist": "^3.0.0", "nlcst-to-string": "^4.0.0", "unist-util-modify-children": "^4.0.0", "unist-util-visit-children": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-mhHgobPPua5kZ98EF4HWiH167JWBfl4pvAIXXdbaVohtK7a6YBOy56kvhCqduqyo/f3yrHFWmqmiMg/BkBkYYQ=="],
 
+    "parse-passwd": ["[email protected]", "", {}, "sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q=="],
+
     "parse5": ["[email protected]", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="],
 
     "parse5-htmlparser2-tree-adapter": ["[email protected]", "", { "dependencies": { "domhandler": "^5.0.3", "parse5": "^7.0.0" } }, "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g=="],
@@ -4166,6 +4280,8 @@
 
     "proto-list": ["[email protected]", "", {}, "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA=="],
 
+    "protobufjs": ["[email protected]", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg=="],
+
     "proxy-addr": ["[email protected]", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="],
 
     "proxy-from-env": ["[email protected]", "", {}, "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA=="],
@@ -4302,6 +4418,8 @@
 
     "require-from-string": ["[email protected]", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
 
+    "require-in-the-middle": ["[email protected]", "", { "dependencies": { "debug": "^4.3.5", "module-details-from-path": "^1.0.3" } }, "sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ=="],
+
     "resedit": ["[email protected]", "", { "dependencies": { "pe-library": "^0.4.1" } }, "sha512-vHjcY2MlAITJhC0eRD/Vv8Vlgmu9Sd3LX9zZvtGzU5ZImdTN3+d6e/4mnTyV8vEbyf1sgNIrWxhWlrys52OkEA=="],
 
     "reselect": ["[email protected]", "", {}, "sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ=="],
@@ -4412,6 +4530,8 @@
 
     "shebang-regex": ["[email protected]", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
 
+    "shell-quote": ["[email protected]", "", {}, "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw=="],
+
     "shiki": ["[email protected]", "", { "dependencies": { "@shikijs/core": "3.20.0", "@shikijs/engine-javascript": "3.20.0", "@shikijs/engine-oniguruma": "3.20.0", "@shikijs/langs": "3.20.0", "@shikijs/themes": "3.20.0", "@shikijs/types": "3.20.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-kgCOlsnyWb+p0WU+01RjkCH+eBVsjL1jOwUYWv0YDWkM2/A46+LDKVs5yZCUXjJG6bj4ndFoAg5iLIIue6dulg=="],
 
     "shikiji": ["[email protected]", "", { "dependencies": { "hast-util-to-html": "^9.0.0" } }, "sha512-4T7X39csvhT0p7GDnq9vysWddf2b6BeioiN3Ymhnt3xcy9tXmDcnsEFVxX18Z4YcQgEE/w48dLJ4pPPUcG9KkA=="],
@@ -4540,8 +4660,12 @@
 
     "storybook-solidjs-vite": ["[email protected]", "", { "dependencies": { "@joshwooding/vite-plugin-react-docgen-typescript": "^0.6.4", "@storybook/builder-vite": "^10.3.1", "@storybook/global": "^5.0.0", "vite-plugin-solid": "^2.11.11" }, "peerDependencies": { "solid-js": "^1.9.0", "storybook": "^0.0.0-0 || ^10.0.0", "typescript": "^4.0.0 || ^5.0.0 || ^6.0.0", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["typescript"] }, "sha512-aDe2ipD0HAH54zsQbbCUI9AcBPiLwljtdx+GRmyHsHaPyij1KmGVKo5N004qXZfQaoK0uru9DsdnWon5rRkjfg=="],
 
+    "stream-browserify": ["[email protected]", "", { "dependencies": { "inherits": "~2.0.4", "readable-stream": "^3.5.0" } }, "sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA=="],
+
     "stream-replace-string": ["[email protected]", "", {}, "sha512-TlnjJ1C0QrmxRNrON00JvaFFlNh5TTG00APw23j74ET7gkQpTASi6/L2fuiav8pzK715HXtUeClpBTw2NPSn6w=="],
 
+    "streamsearch": ["[email protected]", "", {}, "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg=="],
+
     "streamx": ["[email protected]", "", { "dependencies": { "events-universal": "^1.0.0", "fast-fifo": "^1.3.2", "text-decoder": "^1.1.0" } }, "sha512-0nQuG6jf1w+wddNEEXCF4nTg3LtufWINB5eFEN+5TNZW7KWJp6x87+JFL43vaAUPyCfH1wID+mNVyW6OHtFamg=="],
 
     "string-width": ["[email protected]", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
@@ -4956,7 +5080,7 @@
 
     "y18n": ["[email protected]", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="],
 
-    "yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
+    "yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="],
 
     "yaml": ["[email protected]", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg=="],
 
@@ -5192,6 +5316,16 @@
 
     "@aws-sdk/credential-providers/@aws-sdk/types": ["@aws-sdk/[email protected]", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-Atfcy4E++beKtwJHiDln2Nby8W/mam64opFPTiHEqgsthqeydFS1pY+OUlN1ouNOmf8ArPU/6cDS65anOP3KQw=="],
 
+    "@aws-sdk/lib-storage/@smithy/middleware-endpoint": ["@smithy/[email protected]", "", { "dependencies": { "@smithy/core": "^3.23.14", "@smithy/middleware-serde": "^4.2.17", "@smithy/node-config-provider": "^4.3.13", "@smithy/shared-ini-file-loader": "^4.4.8", "@smithy/types": "^4.14.0", "@smithy/url-parser": "^4.2.13", "@smithy/util-middleware": "^4.2.13", "tslib": "^2.6.2" } }, "sha512-R9Q/58U+qBiSARGWbAbFLczECg/RmysRksX6Q8BaQEpt75I7LI6WGDZnjuC9GXSGKljEbA7N118LhGaMbfrTXw=="],
+
+    "@aws-sdk/lib-storage/@smithy/protocol-http": ["@smithy/[email protected]", "", { "dependencies": { "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-+HsmuJUF4u8POo6s8/a2Yb/AQ5t/YgLovCuHF9oxbocqv+SZ6gd8lC2duBFiCA/vFHoHQhoq7QjqJqZC6xOxxg=="],
+
+    "@aws-sdk/lib-storage/@smithy/smithy-client": ["@smithy/[email protected]", "", { "dependencies": { "@smithy/core": "^3.23.14", "@smithy/middleware-endpoint": "^4.4.29", "@smithy/middleware-stack": "^4.2.13", "@smithy/protocol-http": "^5.3.13", "@smithy/types": "^4.14.0", "@smithy/util-stream": "^4.5.22", "tslib": "^2.6.2" } }, "sha512-ovaLEcTU5olSeHcRXcxV6viaKtpkHZumn6Ps0yn7dRf2rRSfy794vpjOtrWDO0d1auDSvAqxO+lyhERSXQ03EQ=="],
+
+    "@aws-sdk/lib-storage/@smithy/types": ["@smithy/[email protected]", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-OWgntFLW88kx2qvf/c/67Vno1yuXm/f9M7QFAtVkkO29IJXGBIg0ycEaBTH0kvCtwmvZxRujrgP5a86RvsXJAQ=="],
+
+    "@aws-sdk/lib-storage/buffer": ["[email protected]", "", { "dependencies": { "base64-js": "^1.0.2", "ieee754": "^1.1.4" } }, "sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw=="],
+
     "@aws-sdk/nested-clients/@aws-sdk/core": ["@aws-sdk/[email protected]", "", { "dependencies": { "@aws-sdk/types": "^3.973.6", "@aws-sdk/xml-builder": "^3.972.16", "@smithy/core": "^3.23.13", "@smithy/node-config-provider": "^4.3.12", "@smithy/property-provider": "^4.2.12", "@smithy/protocol-http": "^5.3.12", "@smithy/signature-v4": "^5.3.12", "@smithy/smithy-client": "^4.12.8", "@smithy/types": "^4.13.1", "@smithy/util-base64": "^4.3.2", "@smithy/util-middleware": "^4.2.12", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-A/E6n2W42ruU+sfWk+mMUOyVXbsSgGrY3MJ9/0Az5qUdG67y8I6HYzzoAa+e/lzxxl1uCYmEL6BTMi9ZiZnplQ=="],
 
     "@aws-sdk/nested-clients/@aws-sdk/middleware-host-header": ["@aws-sdk/[email protected]", "", { "dependencies": { "@aws-sdk/types": "^3.973.6", "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-wAr2REfKsqoKQ+OkNqvOShnBoh+nkPurDKW7uAeVSu6kUECnWlSJiPvnoqxGlfousEY/v9LfS9sNc46hjSYDIQ=="],
@@ -5290,6 +5424,8 @@
 
     "@gitlab/opencode-gitlab-auth/open": ["[email protected]", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="],
 
+    "@grpc/proto-loader/yargs": ["[email protected]", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="],
+
     "@hey-api/openapi-ts/open": ["[email protected]", "", { "dependencies": { "default-browser": "^5.4.0", "define-lazy-prop": "^3.0.0", "is-in-ssh": "^1.0.0", "is-inside-container": "^1.0.0", "powershell-utils": "^0.1.0", "wsl-utils": "^0.3.0" } }, "sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw=="],
 
     "@hey-api/openapi-ts/semver": ["[email protected]", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
@@ -5442,6 +5578,26 @@
 
     "@opencode-ai/web/@shikijs/transformers": ["@shikijs/[email protected]", "", { "dependencies": { "@shikijs/core": "3.20.0", "@shikijs/types": "3.20.0" } }, "sha512-PrHHMRr3Q5W1qB/42kJW6laqFyWdhrPF2hNR9qjOm1xcSiAO3hAHo7HaVyHE6pMyevmy3i51O8kuGGXC78uK3g=="],
 
+    "@opentelemetry/exporter-logs-otlp-proto/@opentelemetry/sdk-trace-base": ["@opentelemetry/[email protected]", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw=="],
+
+    "@opentelemetry/exporter-trace-otlp-grpc/@opentelemetry/sdk-trace-base": ["@opentelemetry/[email protected]", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw=="],
+
+    "@opentelemetry/exporter-trace-otlp-http/@opentelemetry/sdk-trace-base": ["@opentelemetry/[email protected]", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw=="],
+
+    "@opentelemetry/exporter-trace-otlp-proto/@opentelemetry/sdk-trace-base": ["@opentelemetry/[email protected]", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw=="],
+
+    "@opentelemetry/exporter-zipkin/@opentelemetry/sdk-trace-base": ["@opentelemetry/[email protected]", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw=="],
+
+    "@opentelemetry/otlp-transformer/@opentelemetry/sdk-trace-base": ["@opentelemetry/[email protected]", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw=="],
+
+    "@opentelemetry/sdk-node/@opentelemetry/sdk-trace-base": ["@opentelemetry/[email protected]", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw=="],
+
+    "@opentelemetry/sdk-trace-base/@opentelemetry/core": ["@opentelemetry/[email protected]", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-8xHSGWpJP9wBxgBpnqGL0R3PbdWQndL1Qp50qrg71+B28zK5OQmUgcDKLJgzyAAV38t4tOyLMGDD60LneR5W8g=="],
+
+    "@opentelemetry/sdk-trace-base/@opentelemetry/resources": ["@opentelemetry/[email protected]", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA=="],
+
+    "@opentelemetry/sdk-trace-node/@opentelemetry/sdk-trace-base": ["@opentelemetry/[email protected]", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw=="],
+
     "@opentui/solid/@babel/core": ["@babel/[email protected]", "", { "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.0", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.27.3", "@babel/helpers": "^7.27.6", "@babel/parser": "^7.28.0", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.0", "@babel/types": "^7.28.0", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ=="],
 
     "@opentui/solid/babel-preset-solid": ["[email protected]", "", { "dependencies": { "babel-plugin-jsx-dom-expressions": "^0.40.3" }, "peerDependencies": { "@babel/core": "^7.0.0", "solid-js": "^1.9.10" }, "optionalPeers": ["solid-js"] }, "sha512-HCelrgua/Y+kqO8RyL04JBWS/cVdrtUv/h45GntgQY+cJl4eBcKkCDV3TdMjtKx1nXwRaR9QXslM/Npm1dxdZQ=="],
@@ -5572,6 +5728,8 @@
 
     "app-builder-lib/ci-info": ["[email protected]", "", {}, "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA=="],
 
+    "app-builder-lib/dotenv": ["[email protected]", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="],
+
     "app-builder-lib/hosted-git-info": ["[email protected]", "", { "dependencies": { "lru-cache": "^6.0.0" } }, "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA=="],
 
     "app-builder-lib/minimatch": ["[email protected]", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="],
@@ -5616,8 +5774,6 @@
 
     "c12/chokidar": ["[email protected]", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="],
 
-    "c12/dotenv": ["[email protected]", "", {}, "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA=="],
-
     "clone-response/mimic-response": ["[email protected]", "", {}, "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ=="],
 
     "compress-commons/is-stream": ["[email protected]", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="],
@@ -5648,6 +5804,8 @@
 
     "dot-prop/type-fest": ["[email protected]", "", {}, "sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g=="],
 
+    "dotenv-expand/dotenv": ["[email protected]", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="],
+
     "editorconfig/commander": ["[email protected]", "", {}, "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug=="],
 
     "editorconfig/minimatch": ["[email protected]", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="],
@@ -5878,6 +6036,8 @@
 
     "storybook-solidjs-vite/vite-plugin-solid": ["[email protected]", "", { "dependencies": { "@babel/core": "^7.23.3", "@types/babel__core": "^7.20.4", "babel-preset-solid": "^1.8.4", "merge-anything": "^5.1.7", "solid-refresh": "^0.6.3", "vitefu": "^1.0.4" }, "peerDependencies": { "@testing-library/jest-dom": "^5.16.6 || ^5.17.0 || ^6.*", "solid-js": "^1.7.2", "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["@testing-library/jest-dom"] }, "sha512-YMZCXsLw9kyuvQFEdwLP27fuTQJLmjNoHy90AOJnbRuJ6DwShUxKFo38gdFrWn9v11hnGicKCZEaeI/TFs6JKw=="],
 
+    "stream-browserify/readable-stream": ["[email protected]", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
+
     "string-width-cjs/emoji-regex": ["[email protected]", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
 
     "string-width-cjs/strip-ansi": ["[email protected]", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
@@ -5886,8 +6046,6 @@
 
     "sucrase/commander": ["[email protected]", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="],
 
-    "tar/yallist": ["[email protected]", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="],
-
     "tedious/iconv-lite": ["[email protected]", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
 
     "terser/commander": ["[email protected]", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="],
@@ -6162,6 +6320,26 @@
 
     "@aws-sdk/credential-providers/@aws-sdk/core/@smithy/util-utf8": ["@smithy/[email protected]", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="],
 
+    "@aws-sdk/lib-storage/@smithy/middleware-endpoint/@smithy/core": ["@smithy/[email protected]", "", { "dependencies": { "@smithy/protocol-http": "^5.3.13", "@smithy/types": "^4.14.0", "@smithy/url-parser": "^4.2.13", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-middleware": "^4.2.13", "@smithy/util-stream": "^4.5.22", "@smithy/util-utf8": "^4.2.2", "@smithy/uuid": "^1.1.2", "tslib": "^2.6.2" } }, "sha512-vJ0IhpZxZAkFYOegMKSrxw7ujhhT2pass/1UEcZ4kfl5srTAqtPU5I7MdYQoreVas3204ykCiNhY1o7Xlz6Yyg=="],
+
+    "@aws-sdk/lib-storage/@smithy/middleware-endpoint/@smithy/middleware-serde": ["@smithy/[email protected]", "", { "dependencies": { "@smithy/core": "^3.23.14", "@smithy/protocol-http": "^5.3.13", "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-0T2mcaM6v9W1xku86Dk0bEW7aEseG6KenFkPK98XNw0ZhOqOiD1MrMsdnQw9QsL3/Oa85T53iSMlm0SZdSuIEQ=="],
+
+    "@aws-sdk/lib-storage/@smithy/middleware-endpoint/@smithy/node-config-provider": ["@smithy/[email protected]", "", { "dependencies": { "@smithy/property-provider": "^4.2.13", "@smithy/shared-ini-file-loader": "^4.4.8", "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-iGxQ04DsKXLckbgnX4ipElrOTk+IHgTyu0q0WssZfYhDm9CQWHmu6cOeI5wmWRxpXbBDhIIfXMWz5tPEtcVqbw=="],
+
+    "@aws-sdk/lib-storage/@smithy/middleware-endpoint/@smithy/shared-ini-file-loader": ["@smithy/[email protected]", "", { "dependencies": { "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-VZCZx2bZasxdqxVgEAhREvDSlkatTPnkdWy1+Kiy8w7kYPBosW0V5IeDwzDUMvWBt56zpK658rx1cOBFOYaPaw=="],
+
+    "@aws-sdk/lib-storage/@smithy/middleware-endpoint/@smithy/url-parser": ["@smithy/[email protected]", "", { "dependencies": { "@smithy/querystring-parser": "^4.2.13", "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-2G03yoboIRZlZze2+PT4GZEjgwQsJjUgn6iTsvxA02bVceHR6vp4Cuk7TUnPFWKF+ffNUk3kj4COwkENS2K3vw=="],
+
+    "@aws-sdk/lib-storage/@smithy/middleware-endpoint/@smithy/util-middleware": ["@smithy/[email protected]", "", { "dependencies": { "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-GTooyrlmRTqvUen4eK7/K1p6kryF7bnDfq6XsAbIsf2mo51B/utaH+XThY6dKgNCWzMAaH/+OLmqaBuLhLWRow=="],
+
+    "@aws-sdk/lib-storage/@smithy/smithy-client/@smithy/core": ["@smithy/[email protected]", "", { "dependencies": { "@smithy/protocol-http": "^5.3.13", "@smithy/types": "^4.14.0", "@smithy/url-parser": "^4.2.13", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-middleware": "^4.2.13", "@smithy/util-stream": "^4.5.22", "@smithy/util-utf8": "^4.2.2", "@smithy/uuid": "^1.1.2", "tslib": "^2.6.2" } }, "sha512-vJ0IhpZxZAkFYOegMKSrxw7ujhhT2pass/1UEcZ4kfl5srTAqtPU5I7MdYQoreVas3204ykCiNhY1o7Xlz6Yyg=="],
+
+    "@aws-sdk/lib-storage/@smithy/smithy-client/@smithy/middleware-stack": ["@smithy/[email protected]", "", { "dependencies": { "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-g72jN/sGDLyTanrCLH9fhg3oysO3f7tQa6eWWsMyn2BiYNCgjF24n4/I9wff/5XidFvjj9ilipAoQrurTUrLvw=="],
+
+    "@aws-sdk/lib-storage/@smithy/smithy-client/@smithy/util-stream": ["@smithy/[email protected]", "", { "dependencies": { "@smithy/fetch-http-handler": "^5.3.16", "@smithy/node-http-handler": "^4.5.2", "@smithy/types": "^4.14.0", "@smithy/util-base64": "^4.3.2", "@smithy/util-buffer-from": "^4.2.2", "@smithy/util-hex-encoding": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-3H8iq/0BfQjUs2/4fbHZ9aG9yNzcuZs24LPkcX1Q7Z+qpqaGM8+qbGmE8zo9m2nCRgamyvS98cHdcWvR6YUsew=="],
+
+    "@aws-sdk/lib-storage/buffer/ieee754": ["[email protected]", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
+
     "@aws-sdk/nested-clients/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/[email protected]", "", { "dependencies": { "@smithy/types": "^4.13.1", "fast-xml-parser": "5.5.8", "tslib": "^2.6.2" } }, "sha512-iu2pyvaqmeatIJLURLqx9D+4jKAdTH20ntzB6BFwjyN7V960r4jK32mx0Zf7YbtOYAbmbtQfDNuL60ONinyw7A=="],
 
     "@aws-sdk/nested-clients/@aws-sdk/core/@smithy/util-utf8": ["@smithy/[email protected]", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="],
@@ -6242,6 +6420,10 @@
 
     "@gitlab/opencode-gitlab-auth/open/wsl-utils": ["[email protected]", "", { "dependencies": { "is-wsl": "^3.1.0" } }, "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw=="],
 
+    "@grpc/proto-loader/yargs/cliui": ["[email protected]", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="],
+
+    "@grpc/proto-loader/yargs/string-width": ["[email protected]", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
+
     "@jsx-email/cli/esbuild/@esbuild/aix-ppc64": ["@esbuild/[email protected]", "", { "os": "aix", "cpu": "ppc64" }, "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA=="],
 
     "@jsx-email/cli/esbuild/@esbuild/android-arm": ["@esbuild/[email protected]", "", { "os": "android", "cpu": "arm" }, "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w=="],
@@ -6540,6 +6722,10 @@
 
     "lazystream/readable-stream/string_decoder": ["[email protected]", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="],
 
+    "minipass-flush/minipass/yallist": ["[email protected]", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
+
+    "minipass-pipeline/minipass/yallist": ["[email protected]", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
+
     "motion/framer-motion/motion-dom": ["[email protected]", "", { "dependencies": { "motion-utils": "^12.36.0" } }, "sha512-pdkHLD8QYRp8VfiNLb8xIBJis1byQ9gPT3Jnh2jqfFtAsWUA3dEepDlsWe/xMpO8McV+VdpKVcp+E+TGJEtOoA=="],
 
     "motion/framer-motion/motion-utils": ["[email protected]", "", {}, "sha512-eHWisygbiwVvf6PZ1vhaHCLamvkSbPIeAYxWUuL3a2PD/TROgE7FvfHWTIH4vMl798QLfMw15nRqIaRDXTlYRg=="],
@@ -6556,6 +6742,8 @@
 
     "opencontrol/@modelcontextprotocol/sdk/zod-to-json-schema": ["[email protected]", "", { "peerDependencies": { "zod": "^3.25.28 || ^4" } }, "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA=="],
 
+    "openid-client/lru-cache/yallist": ["[email protected]", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
+
     "ora/bl/buffer": ["[email protected]", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="],
 
     "ora/bl/readable-stream": ["[email protected]", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
@@ -6720,6 +6908,26 @@
 
     "@aws-sdk/credential-providers/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser": ["[email protected]", "", { "dependencies": { "fast-xml-builder": "^1.1.4", "path-expression-matcher": "^1.2.0", "strnum": "^2.2.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-Z7Fh2nVQSb2d+poDViM063ix2ZGt9jmY1nWhPfHBOK2Hgnb/OW3P4Et3P/81SEej0J7QbWtJqxO05h8QYfK7LQ=="],
 
+    "@aws-sdk/lib-storage/@smithy/middleware-endpoint/@smithy/core/@smithy/util-stream": ["@smithy/[email protected]", "", { "dependencies": { "@smithy/fetch-http-handler": "^5.3.16", "@smithy/node-http-handler": "^4.5.2", "@smithy/types": "^4.14.0", "@smithy/util-base64": "^4.3.2", "@smithy/util-buffer-from": "^4.2.2", "@smithy/util-hex-encoding": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-3H8iq/0BfQjUs2/4fbHZ9aG9yNzcuZs24LPkcX1Q7Z+qpqaGM8+qbGmE8zo9m2nCRgamyvS98cHdcWvR6YUsew=="],
+
+    "@aws-sdk/lib-storage/@smithy/middleware-endpoint/@smithy/core/@smithy/util-utf8": ["@smithy/[email protected]", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="],
+
+    "@aws-sdk/lib-storage/@smithy/middleware-endpoint/@smithy/node-config-provider/@smithy/property-provider": ["@smithy/[email protected]", "", { "dependencies": { "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-bGzUCthxRmezuxkbu9wD33wWg9KX3hJpCXpQ93vVkPrHn9ZW6KNNdY5xAUWNuRCwQ+VyboFuWirG1lZhhkcyRQ=="],
+
+    "@aws-sdk/lib-storage/@smithy/middleware-endpoint/@smithy/url-parser/@smithy/querystring-parser": ["@smithy/[email protected]", "", { "dependencies": { "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-hqW3Q4P+CDzUyQ87GrboGMeD7XYNMOF+CuTwu936UQRB/zeYn3jys8C3w+wMkDfY7CyyyVwZQ5cNFoG0x1pYmA=="],
+
+    "@aws-sdk/lib-storage/@smithy/smithy-client/@smithy/core/@smithy/url-parser": ["@smithy/[email protected]", "", { "dependencies": { "@smithy/querystring-parser": "^4.2.13", "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-2G03yoboIRZlZze2+PT4GZEjgwQsJjUgn6iTsvxA02bVceHR6vp4Cuk7TUnPFWKF+ffNUk3kj4COwkENS2K3vw=="],
+
+    "@aws-sdk/lib-storage/@smithy/smithy-client/@smithy/core/@smithy/util-middleware": ["@smithy/[email protected]", "", { "dependencies": { "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-GTooyrlmRTqvUen4eK7/K1p6kryF7bnDfq6XsAbIsf2mo51B/utaH+XThY6dKgNCWzMAaH/+OLmqaBuLhLWRow=="],
+
+    "@aws-sdk/lib-storage/@smithy/smithy-client/@smithy/core/@smithy/util-utf8": ["@smithy/[email protected]", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="],
+
+    "@aws-sdk/lib-storage/@smithy/smithy-client/@smithy/util-stream/@smithy/fetch-http-handler": ["@smithy/[email protected]", "", { "dependencies": { "@smithy/protocol-http": "^5.3.13", "@smithy/querystring-builder": "^4.2.13", "@smithy/types": "^4.14.0", "@smithy/util-base64": "^4.3.2", "tslib": "^2.6.2" } }, "sha512-nYDRUIvNd4mFmuXraRWt6w5UsZTNqtj4hXJA/iiOD4tuseIdLP9Lq38teH/SZTcIFCa2f+27o7hYpIsWktJKEQ=="],
+
+    "@aws-sdk/lib-storage/@smithy/smithy-client/@smithy/util-stream/@smithy/node-http-handler": ["@smithy/[email protected]", "", { "dependencies": { "@smithy/protocol-http": "^5.3.13", "@smithy/querystring-builder": "^4.2.13", "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-/oD7u8M0oj2ZTFw7GkuuHWpIxtWdLlnyNkbrWcyVYhd5RJNDuczdkb0wfnQICyNFrVPlr8YHOhamjNy3zidhmA=="],
+
+    "@aws-sdk/lib-storage/@smithy/smithy-client/@smithy/util-stream/@smithy/util-utf8": ["@smithy/[email protected]", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="],
+
     "@aws-sdk/nested-clients/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser": ["[email protected]", "", { "dependencies": { "fast-xml-builder": "^1.1.4", "path-expression-matcher": "^1.2.0", "strnum": "^2.2.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-Z7Fh2nVQSb2d+poDViM063ix2ZGt9jmY1nWhPfHBOK2Hgnb/OW3P4Et3P/81SEej0J7QbWtJqxO05h8QYfK7LQ=="],
 
     "@aws-sdk/token-providers/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser": ["[email protected]", "", { "dependencies": { "fast-xml-builder": "^1.1.4", "path-expression-matcher": "^1.2.0", "strnum": "^2.2.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-Z7Fh2nVQSb2d+poDViM063ix2ZGt9jmY1nWhPfHBOK2Hgnb/OW3P4Et3P/81SEej0J7QbWtJqxO05h8QYfK7LQ=="],
@@ -6750,6 +6958,14 @@
 
     "@electron/universal/minimatch/brace-expansion/balanced-match": ["[email protected]", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
 
+    "@grpc/proto-loader/yargs/cliui/strip-ansi": ["[email protected]", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
+
+    "@grpc/proto-loader/yargs/cliui/wrap-ansi": ["[email protected]", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
+
+    "@grpc/proto-loader/yargs/string-width/emoji-regex": ["[email protected]", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
+
+    "@grpc/proto-loader/yargs/string-width/strip-ansi": ["[email protected]", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
+
     "@jsx-email/cli/tailwindcss/chokidar/glob-parent": ["[email protected]", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
 
     "@jsx-email/cli/tailwindcss/chokidar/readdirp": ["[email protected]", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="],
@@ -6834,6 +7050,8 @@
 
     "app-builder-lib/@electron/get/fs-extra/universalify": ["[email protected]", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="],
 
+    "app-builder-lib/hosted-git-info/lru-cache/yallist": ["[email protected]", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
+
     "archiver-utils/glob/jackspeak/@isaacs/cliui": ["@isaacs/[email protected]", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="],
 
     "archiver-utils/glob/minimatch/brace-expansion": ["[email protected]", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA=="],
@@ -6950,6 +7168,16 @@
 
     "@aws-sdk/credential-providers/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser/strnum": ["[email protected]", "", {}, "sha512-DnR90I+jtXNSTXWdwrEy9FakW7UX+qUZg28gj5fk2vxxl7uS/3bpI4fjFYVmdK9etptYBPNkpahuQnEwhwECqA=="],
 
+    "@aws-sdk/lib-storage/@smithy/middleware-endpoint/@smithy/core/@smithy/util-stream/@smithy/fetch-http-handler": ["@smithy/[email protected]", "", { "dependencies": { "@smithy/protocol-http": "^5.3.13", "@smithy/querystring-builder": "^4.2.13", "@smithy/types": "^4.14.0", "@smithy/util-base64": "^4.3.2", "tslib": "^2.6.2" } }, "sha512-nYDRUIvNd4mFmuXraRWt6w5UsZTNqtj4hXJA/iiOD4tuseIdLP9Lq38teH/SZTcIFCa2f+27o7hYpIsWktJKEQ=="],
+
+    "@aws-sdk/lib-storage/@smithy/middleware-endpoint/@smithy/core/@smithy/util-stream/@smithy/node-http-handler": ["@smithy/[email protected]", "", { "dependencies": { "@smithy/protocol-http": "^5.3.13", "@smithy/querystring-builder": "^4.2.13", "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-/oD7u8M0oj2ZTFw7GkuuHWpIxtWdLlnyNkbrWcyVYhd5RJNDuczdkb0wfnQICyNFrVPlr8YHOhamjNy3zidhmA=="],
+
+    "@aws-sdk/lib-storage/@smithy/smithy-client/@smithy/core/@smithy/url-parser/@smithy/querystring-parser": ["@smithy/[email protected]", "", { "dependencies": { "@smithy/types": "^4.14.0", "tslib": "^2.6.2" } }, "sha512-hqW3Q4P+CDzUyQ87GrboGMeD7XYNMOF+CuTwu936UQRB/zeYn3jys8C3w+wMkDfY7CyyyVwZQ5cNFoG0x1pYmA=="],
+
+    "@aws-sdk/lib-storage/@smithy/smithy-client/@smithy/util-stream/@smithy/fetch-http-handler/@smithy/querystring-builder": ["@smithy/[email protected]", "", { "dependencies": { "@smithy/types": "^4.14.0", "@smithy/util-uri-escape": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-tG4aOYFCZdPMjbgfhnIQ322H//ojujldp1SrHPHpBSb3NqgUp3dwiUGRJzie87hS1DYwWGqDuPaowoDF+rYCbQ=="],
+
+    "@aws-sdk/lib-storage/@smithy/smithy-client/@smithy/util-stream/@smithy/node-http-handler/@smithy/querystring-builder": ["@smithy/[email protected]", "", { "dependencies": { "@smithy/types": "^4.14.0", "@smithy/util-uri-escape": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-tG4aOYFCZdPMjbgfhnIQ322H//ojujldp1SrHPHpBSb3NqgUp3dwiUGRJzie87hS1DYwWGqDuPaowoDF+rYCbQ=="],
+
     "@aws-sdk/nested-clients/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser/strnum": ["[email protected]", "", {}, "sha512-DnR90I+jtXNSTXWdwrEy9FakW7UX+qUZg28gj5fk2vxxl7uS/3bpI4fjFYVmdK9etptYBPNkpahuQnEwhwECqA=="],
 
     "@aws-sdk/token-providers/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser/strnum": ["[email protected]", "", {}, "sha512-DnR90I+jtXNSTXWdwrEy9FakW7UX+qUZg28gj5fk2vxxl7uS/3bpI4fjFYVmdK9etptYBPNkpahuQnEwhwECqA=="],
@@ -6968,6 +7196,10 @@
 
     "@electron/rebuild/yargs/string-width/strip-ansi/ansi-regex": ["[email protected]", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
 
+    "@grpc/proto-loader/yargs/cliui/strip-ansi/ansi-regex": ["[email protected]", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
+
+    "@grpc/proto-loader/yargs/string-width/strip-ansi/ansi-regex": ["[email protected]", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
+
     "@jsx-email/cli/tailwindcss/chokidar/readdirp/picomatch": ["[email protected]", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="],
 
     "@solidjs/start/shiki/@shikijs/engine-javascript/oniguruma-to-es/regex": ["[email protected]", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-dN5I359AVGPnwzJm2jN1k0W9LPZ+ePvoOeVMMfqIMFz53sSwXkxaJoxr50ptnsC771lK95BnTrVSZxq0b9yCGw=="],
@@ -7008,6 +7240,10 @@
 
     "@aws-sdk/credential-provider-cognito-identity/@aws-sdk/nested-clients/@aws-sdk/core/@aws-sdk/xml-builder/fast-xml-parser/strnum": ["[email protected]", "", {}, "sha512-DnR90I+jtXNSTXWdwrEy9FakW7UX+qUZg28gj5fk2vxxl7uS/3bpI4fjFYVmdK9etptYBPNkpahuQnEwhwECqA=="],
 
+    "@aws-sdk/lib-storage/@smithy/middleware-endpoint/@smithy/core/@smithy/util-stream/@smithy/fetch-http-handler/@smithy/querystring-builder": ["@smithy/[email protected]", "", { "dependencies": { "@smithy/types": "^4.14.0", "@smithy/util-uri-escape": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-tG4aOYFCZdPMjbgfhnIQ322H//ojujldp1SrHPHpBSb3NqgUp3dwiUGRJzie87hS1DYwWGqDuPaowoDF+rYCbQ=="],
+
+    "@aws-sdk/lib-storage/@smithy/middleware-endpoint/@smithy/core/@smithy/util-stream/@smithy/node-http-handler/@smithy/querystring-builder": ["@smithy/[email protected]", "", { "dependencies": { "@smithy/types": "^4.14.0", "@smithy/util-uri-escape": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-tG4aOYFCZdPMjbgfhnIQ322H//ojujldp1SrHPHpBSb3NqgUp3dwiUGRJzie87hS1DYwWGqDuPaowoDF+rYCbQ=="],
+
     "@electron/rebuild/node-gyp/make-fetch-happen/cacache/glob/jackspeak": ["[email protected]", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="],
 
     "@electron/rebuild/node-gyp/make-fetch-happen/cacache/glob/minimatch": ["[email protected]", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="],
@@ -7028,6 +7264,8 @@
 
     "@electron/rebuild/node-gyp/make-fetch-happen/cacache/glob/minimatch/brace-expansion": ["[email protected]", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA=="],
 
+    "@electron/rebuild/node-gyp/make-fetch-happen/minipass-fetch/minipass-sized/minipass/yallist": ["[email protected]", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
+
     "@electron/rebuild/node-gyp/make-fetch-happen/cacache/glob/jackspeak/@isaacs/cliui/string-width": ["[email protected]", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="],
 
     "@electron/rebuild/node-gyp/make-fetch-happen/cacache/glob/jackspeak/@isaacs/cliui/wrap-ansi": ["[email protected]", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="],

+ 43 - 0
daytonaWorkspaceBootstrap.sh

@@ -0,0 +1,43 @@
+#!/usr/bin/env bash
+set -euox pipefail
+
+project="$1"
+
+root="/home/daytona/workspace"
+repo="$root/repo"
+localbin="/home/daytona/opencode"
+installbin="/home/daytona/.opencode/bin/opencode"
+
+printf "%s\n" "ipv4" > "$HOME/.curlrc"
+rm -rf "$repo"
+mkdir -p "$root"
+tar -xzf "$HOME/repo.tgz" -C "$HOME/workspace"
+
+ls -last "$HOME"
+
+if [ -f "$HOME/opencode" ]; then
+  chmod +x "$HOME/opencode"
+  exe="$localbin"
+else
+  mkdir -p "$HOME/.opencode/bin"
+  OPENCODE_INSTALL_DIR="$HOME/.opencode/bin" curl -4 -fsSL https://opencode.ai/install | bash
+  exe="$installbin"
+fi
+
+echo "opencode: $exe"
+printf "%s\n" "$project" > "$repo/.git/opencode"
+
+cd "$repo"
+OPENCODE_WORKSPACE=true OPENCODE_EXPERIMENTAL_WORKSPACES=true nohup "$exe" serve --hostname 0.0.0.0 --port 3096 --print-logs > /tmp/opencode-server.log 2>&1 &
+
+for i in $(seq 1 60); do
+  if curl -4 -fsS http://127.0.0.1:3096/global/health >/dev/null; then
+    echo "ready"
+    exit 0
+  fi
+  echo "waiting for server ($i/60)"
+  sleep 1
+done
+
+echo "daytona workspace server did not become ready in time" >&2
+exit 1

+ 206 - 0
daytonaWorkspacePlugin.ts

@@ -0,0 +1,206 @@
+import type { Daytona, Sandbox } from "@daytonaio/sdk"
+import type { Plugin } from "@opencode-ai/plugin"
+import { join } from "node:path"
+import { fileURLToPath } from "node:url"
+import { tmpdir } from "node:os"
+import { access, mkdir } from "node:fs/promises"
+import { randomUUID } from "node:crypto"
+
+let client: Promise<Daytona> | undefined
+
+let daytona = function daytona(): Promise<Daytona> {
+  if (client == null) {
+    client = import("@daytonaio/sdk").then(
+      ({ Daytona }) =>
+        new Daytona({
+          apiKey: "dtn_2ffe19d27837953f1a46cc297d8a5331d4c46b00856eb5f4a4afded3f3426038",
+        }),
+    )
+  }
+  return client
+}
+
+const preview = new Map<string, { url: string; token: string }>()
+const repo = "/home/daytona/workspace/repo"
+
+const local = fileURLToPath(
+  new URL("./packages/opencode/dist/opencode-linux-x64-baseline/bin/opencode", import.meta.url),
+)
+const bootstrap = fileURLToPath(new URL("./daytonaWorkspaceBootstrap.sh", import.meta.url))
+
+async function exists(file: string) {
+  return access(file)
+    .then(() => true)
+    .catch(() => false)
+}
+
+function sh(value: string) {
+  return `'${value.replace(/'/g, `'"'"'`)}'`
+}
+
+async function boot() {
+  return Bun.file(bootstrap).text()
+}
+
+// Internally Daytona uses axios, which tries to overwrite stack
+// traces when a failure happens. That path fails in Bun, however, so
+// when something goes wrong you only see a very obscure error.
+async function withSandbox<T>(name: string, fn: (sandbox: Sandbox) => Promise<T>) {
+  const stack = Error.captureStackTrace
+  // @ts-expect-error temporary compatibility hack for Daytona's axios stack handling in Bun
+  Error.captureStackTrace = undefined
+  try {
+    return await fn(await (await daytona()).get(name))
+  } finally {
+    Error.captureStackTrace = stack
+  }
+}
+
+export const DaytonaWorkspacePlugin: Plugin = async ({ experimental_workspace, worktree, project }) => {
+  experimental_workspace.register("daytona", {
+    name: "Daytona",
+    description: "Create a remote Daytona workspace",
+    configure(config) {
+      return config
+    },
+    async create(config) {
+      const temp = join(tmpdir(), `opencode-daytona-${randomUUID()}`)
+
+      console.log("creating sandbox...")
+
+      const sandbox = await (
+        await daytona()
+      ).create({
+        name: config.name,
+        envVars: {
+          foo: "bar",
+        },
+      })
+
+      const sid = `setup-${randomUUID()}`
+      await sandbox.process.createSession(sid)
+
+      try {
+        console.log("creating ssh...")
+
+        const ssh = await withSandbox(config.name, (sandbox) => sandbox.createSshAccess())
+        console.log("daytona:", ssh.sshCommand)
+
+        const run = async (command: string, opts?: { stream?: boolean }) => {
+          if (!opts?.stream) {
+            const result = await sandbox.process.executeCommand(command)
+            if (result.exitCode === 0) return result
+            throw new Error(result.result || `sandbox command failed: ${command}`)
+          }
+
+          const res = await sandbox.process.executeSessionCommand(sid, { command, runAsync: true })
+          if (!res.cmdId) throw new Error(`sandbox command failed to start: ${command}`)
+
+          let out = ""
+          let err = ""
+          await sandbox.process.getSessionCommandLogs(
+            sid,
+            res.cmdId,
+            (chunk) => {
+              out += chunk
+              process.stdout.write(chunk)
+            },
+            (chunk) => {
+              err += chunk
+              process.stderr.write(chunk)
+            },
+          )
+
+          for (let i = 0; i < 120; i++) {
+            const cmd = await sandbox.process.getSessionCommand(sid, res.cmdId)
+            if (typeof cmd.exitCode !== "number") {
+              await Bun.sleep(500)
+              continue
+            }
+            if (cmd.exitCode === 0) return cmd
+            throw new Error(err || out || `sandbox command failed: ${command}`)
+          }
+
+          throw new Error(`sandbox command timed out waiting for exit code: ${command}`)
+        }
+
+        const dir = join(temp, "repo")
+        const tar = join(temp, "repo.tgz")
+        const scr = join(temp, "bootstrap.sh")
+        const source = `file://${worktree}`
+        await mkdir(temp, { recursive: true })
+        const args = ["clone", "--depth", "1", "--no-local"]
+        if (config.branch) args.push("--branch", config.branch)
+        args.push(source, dir)
+
+        console.log("git cloning...")
+
+        const clone = Bun.spawn(["git", ...args], {
+          cwd: tmpdir(),
+          stdout: "pipe",
+          stderr: "pipe",
+        })
+        const code = await clone.exited
+        if (code !== 0) throw new Error(await new Response(clone.stderr).text())
+
+        console.log("tarring...")
+
+        const packed = Bun.spawn(["tar", "-czf", tar, "-C", temp, "repo"], {
+          stdout: "ignore",
+          stderr: "pipe",
+        })
+        if ((await packed.exited) !== 0) throw new Error(await new Response(packed.stderr).text())
+
+        console.log("writing bootstrap script...")
+
+        await Bun.write(scr, await boot())
+
+        console.log("uploading files...")
+
+        await sandbox.fs.uploadFile(tar, "repo.tgz")
+        await sandbox.fs.uploadFile(scr, "bootstrap.sh")
+
+        console.log("local", local)
+        if (await exists(local)) {
+          console.log("uploading local binary...")
+          await sandbox.fs.uploadFile(local, "opencode")
+        }
+
+        console.log("bootstrapping workspace...")
+
+        await run(`bash bootstrap.sh ${sh(project.id)}`, {
+          stream: true,
+        })
+        return
+      } finally {
+        await sandbox.process.deleteSession(sid).catch(() => undefined)
+      }
+    },
+    async remove(config) {
+      const sandbox = await (await daytona()).get(config.name).catch(() => undefined)
+      if (!sandbox) return
+      await (await daytona()).delete(sandbox)
+      preview.delete(config.name)
+    },
+    async target(config) {
+      let link = preview.get(config.name)
+      if (!link) {
+        link = await withSandbox(config.name, (sandbox) => sandbox.getPreviewLink(3096))
+        preview.set(config.name, link)
+      }
+      return {
+        type: "remote",
+        url: link.url,
+        headers: {
+          "x-daytona-preview-token": link.token,
+          "x-daytona-skip-preview-warning": "true",
+          "x-opencode-directory": repo,
+        },
+      }
+    },
+  })
+
+  return {}
+}
+
+export default DaytonaWorkspacePlugin

+ 23 - 0
debugWorkspacePlugin.ts

@@ -0,0 +1,23 @@
+import type { Plugin } from "@opencode-ai/plugin"
+
+export const DebugWorkspacePlugin: Plugin = async ({ experimental_workspace }) => {
+  experimental_workspace.register("debug", {
+    name: "Debug",
+    description: "Create a debugging server",
+    configure(config) {
+      return config
+    },
+    async create(_config) {},
+    async remove(_config) {},
+    target(_config) {
+      return {
+        type: "remote",
+        url: "http://localhost:5096/",
+      }
+    },
+  })
+
+  return {}
+}
+
+export default DebugWorkspacePlugin

+ 1 - 0
package.json

@@ -89,6 +89,7 @@
   },
   "dependencies": {
     "@aws-sdk/client-s3": "3.933.0",
+    "@daytona/sdk": "0.164.0",
     "@opencode-ai/plugin": "workspace:*",
     "@opencode-ai/script": "workspace:*",
     "@opencode-ai/sdk": "workspace:*",

+ 101 - 0
packages/opencode/src/cli/cmd/tui/component/dialog-session-delete-failed.tsx

@@ -0,0 +1,101 @@
+import { TextAttributes } from "@opentui/core"
+import { useTheme } from "../context/theme"
+import { useDialog } from "../ui/dialog"
+import { createStore } from "solid-js/store"
+import { For } from "solid-js"
+import { useKeyboard } from "@opentui/solid"
+
+export function DialogSessionDeleteFailed(props: {
+  session: string
+  workspace: string
+  onDelete?: () => boolean | void | Promise<boolean | void>
+  onRestore?: () => boolean | void | Promise<boolean | void>
+  onDone?: () => void
+}) {
+  const dialog = useDialog()
+  const { theme } = useTheme()
+  const [store, setStore] = createStore({
+    active: "delete" as "delete" | "restore",
+  })
+
+  const options = [
+    {
+      id: "delete" as const,
+      title: "Delete workspace",
+      description: "Delete the workspace and all sessions attached to it.",
+      run: props.onDelete,
+    },
+    {
+      id: "restore" as const,
+      title: "Restore to new workspace",
+      description: "Try to restore this session into a new workspace.",
+      run: props.onRestore,
+    },
+  ]
+
+  async function confirm() {
+    const result = await options.find((item) => item.id === store.active)?.run?.()
+    if (result === false) return
+    props.onDone?.()
+    if (!props.onDone) dialog.clear()
+  }
+
+  useKeyboard((evt) => {
+    if (evt.name === "return") {
+      void confirm()
+    }
+    if (evt.name === "left" || evt.name === "up") {
+      setStore("active", "delete")
+    }
+    if (evt.name === "right" || evt.name === "down") {
+      setStore("active", "restore")
+    }
+  })
+
+  return (
+    <box paddingLeft={2} paddingRight={2} gap={1}>
+      <box flexDirection="row" justifyContent="space-between">
+        <text attributes={TextAttributes.BOLD} fg={theme.text}>
+          Failed to Delete Session
+        </text>
+        <text fg={theme.textMuted} onMouseUp={() => dialog.clear()}>
+          esc
+        </text>
+      </box>
+      <text fg={theme.textMuted} wrapMode="word">
+        {`The session "${props.session}" could not be deleted because the workspace "${props.workspace}" is not available.`}
+      </text>
+      <text fg={theme.textMuted} wrapMode="word">
+        Choose how you want to recover this broken workspace session.
+      </text>
+      <box flexDirection="column" paddingBottom={1} gap={1}>
+        <For each={options}>
+          {(item) => (
+            <box
+              flexDirection="column"
+              paddingLeft={1}
+              paddingRight={1}
+              paddingTop={1}
+              paddingBottom={1}
+              backgroundColor={item.id === store.active ? theme.primary : undefined}
+              onMouseUp={() => {
+                setStore("active", item.id)
+                void confirm()
+              }}
+            >
+              <text
+                attributes={TextAttributes.BOLD}
+                fg={item.id === store.active ? theme.selectedListItemText : theme.text}
+              >
+                {item.title}
+              </text>
+              <text fg={item.id === store.active ? theme.selectedListItemText : theme.textMuted} wrapMode="word">
+                {item.description}
+              </text>
+            </box>
+          )}
+        </For>
+      </box>
+    </box>
+  )
+}

+ 91 - 5
packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx

@@ -13,8 +13,10 @@ import { DialogSessionRename } from "./dialog-session-rename"
 import { Keybind } from "@/util/keybind"
 import { createDebouncedSignal } from "../util/signal"
 import { useToast } from "../ui/toast"
-import { DialogWorkspaceCreate, openWorkspaceSession } from "./dialog-workspace-create"
+import { DialogWorkspaceCreate, openWorkspaceSession, restoreWorkspaceSession } from "./dialog-workspace-create"
 import { Spinner } from "./spinner"
+import { errorMessage } from "@/util/error"
+import { DialogSessionDeleteFailed } from "./dialog-session-delete-failed"
 
 type WorkspaceStatus = "connected" | "connecting" | "disconnected" | "error"
 
@@ -30,7 +32,7 @@ export function DialogSessionList() {
   const [toDelete, setToDelete] = createSignal<string>()
   const [search, setSearch] = createDebouncedSignal("", 150)
 
-  const [searchResults] = createResource(search, async (query) => {
+  const [searchResults, { refetch }] = createResource(search, async (query) => {
     if (!query) return undefined
     const result = await sdk.client.session.list({ search: query, limit: 30 })
     return result.data ?? []
@@ -56,6 +58,57 @@ export function DialogSessionList() {
     ))
   }
 
+  function recover(session: NonNullable<ReturnType<typeof sessions>[number]>) {
+    const workspace = project.workspace.get(session.workspaceID!)
+    const list = () => dialog.replace(() => <DialogSessionList />)
+    dialog.replace(() => (
+      <DialogSessionDeleteFailed
+        session={session.title}
+        workspace={workspace?.name ?? session.workspaceID!}
+        onDone={list}
+        onDelete={async () => {
+          const current = currentSessionID()
+          const info = current ? sync.data.session.find((item) => item.id === current) : undefined
+          const result = await sdk.client.experimental.workspace.remove({ id: session.workspaceID! })
+          if (result.error) {
+            toast.show({
+              variant: "error",
+              title: "Failed to delete workspace",
+              message: errorMessage(result.error),
+            })
+            return false
+          }
+          await project.workspace.sync()
+          await sync.session.refresh()
+          if (search()) await refetch()
+          if (info?.workspaceID === session.workspaceID) {
+            route.navigate({ type: "home" })
+          }
+          return true
+        }}
+        onRestore={() => {
+          dialog.replace(() => (
+            <DialogWorkspaceCreate
+              onSelect={(workspaceID) =>
+                restoreWorkspaceSession({
+                  dialog,
+                  sdk,
+                  sync,
+                  project,
+                  toast,
+                  workspaceID,
+                  sessionID: session.id,
+                  done: list,
+                })
+              }
+            />
+          ))
+          return false
+        }}
+      />
+    ))
+  }
+
   const options = createMemo(() => {
     const today = new Date().toDateString()
     return sessions()
@@ -145,9 +198,42 @@ export function DialogSessionList() {
           title: "delete",
           onTrigger: async (option) => {
             if (toDelete() === option.value) {
-              sdk.client.session.delete({
-                sessionID: option.value,
-              })
+              const session = sessions().find((item) => item.id === option.value)
+              const status = session?.workspaceID ? project.workspace.status(session.workspaceID) : undefined
+              try {
+                const result = await sdk.client.session.delete({
+                  sessionID: option.value,
+                })
+                if (result.error) {
+                  if (session?.workspaceID) {
+                    recover(session)
+                  } else {
+                    toast.show({
+                      variant: "error",
+                      title: "Failed to delete session",
+                      message: errorMessage(result.error),
+                    })
+                  }
+                  setToDelete(undefined)
+                  return
+                }
+              } catch (err) {
+                if (session?.workspaceID) {
+                  recover(session)
+                } else {
+                  toast.show({
+                    variant: "error",
+                    title: "Failed to delete session",
+                    message: errorMessage(err),
+                  })
+                }
+                setToDelete(undefined)
+                return
+              }
+              if (status && status !== "connected") {
+                await sync.session.refresh()
+              }
+              if (search()) await refetch()
               setToDelete(undefined)
               return
             }

+ 131 - 4
packages/opencode/src/cli/cmd/tui/component/dialog-workspace-create.tsx

@@ -6,6 +6,8 @@ import { useSync } from "@tui/context/sync"
 import { useProject } from "@tui/context/project"
 import { createMemo, createSignal, onMount } from "solid-js"
 import { setTimeout as sleep } from "node:timers/promises"
+import { errorData, errorMessage } from "@/util/error"
+import { Log } from "@/util/log"
 import { useSDK } from "../context/sdk"
 import { useToast } from "../ui/toast"
 
@@ -15,6 +17,8 @@ type Adaptor = {
   description: string
 }
 
+const log = Log.Default.clone().tag("service", "tui-workspace")
+
 function scoped(sdk: ReturnType<typeof useSDK>, sync: ReturnType<typeof useSync>, workspaceID: string) {
   return createOpencodeClient({
     baseUrl: sdk.url,
@@ -33,8 +37,19 @@ export async function openWorkspaceSession(input: {
   workspaceID: string
 }) {
   const client = scoped(input.sdk, input.sync, input.workspaceID)
+  log.info("workspace session create requested", {
+    workspaceID: input.workspaceID,
+  })
   while (true) {
-    const result = await client.session.create({ workspaceID: input.workspaceID }).catch(() => undefined)
+    const result = await client.session
+      .create({ workspaceID: input.workspaceID, workspace: input.workspaceID })
+      .catch((err) => {
+        log.error("workspace session create request failed", {
+          workspaceID: input.workspaceID,
+          error: errorData(err),
+        })
+        return undefined
+      })
     if (!result) {
       input.toast.show({
         message: "Failed to create workspace session",
@@ -42,26 +57,115 @@ export async function openWorkspaceSession(input: {
       })
       return
     }
-    if (result.response.status >= 500 && result.response.status < 600) {
+    log.info("workspace session create response", {
+      workspaceID: input.workspaceID,
+      status: result.response?.status,
+      sessionID: result.data?.id,
+    })
+    if (result.response?.status && result.response.status >= 500 && result.response.status < 600) {
+      log.warn("workspace session create retrying after server error", {
+        workspaceID: input.workspaceID,
+        status: result.response.status,
+      })
       await sleep(1000)
       continue
     }
     if (!result.data) {
+      log.error("workspace session create returned no data", {
+        workspaceID: input.workspaceID,
+        status: result.response?.status,
+      })
       input.toast.show({
         message: "Failed to create workspace session",
         variant: "error",
       })
       return
     }
+
+    await sleep(5000)
+
     input.route.navigate({
       type: "session",
       sessionID: result.data.id,
     })
+    log.info("workspace session create complete", {
+      workspaceID: input.workspaceID,
+      sessionID: result.data.id,
+    })
     input.dialog.clear()
     return
   }
 }
 
+export async function restoreWorkspaceSession(input: {
+  dialog: ReturnType<typeof useDialog>
+  sdk: ReturnType<typeof useSDK>
+  sync: ReturnType<typeof useSync>
+  project: ReturnType<typeof useProject>
+  toast: ReturnType<typeof useToast>
+  workspaceID: string
+  sessionID: string
+  done?: () => void
+}) {
+  log.info("session restore requested", {
+    workspaceID: input.workspaceID,
+    sessionID: input.sessionID,
+  })
+  const result = await input.sdk.client.experimental.workspace
+    .sessionRestore({ id: input.workspaceID, sessionID: input.sessionID })
+    .catch((err) => {
+      log.error("session restore request failed", {
+        workspaceID: input.workspaceID,
+        sessionID: input.sessionID,
+        error: errorData(err),
+      })
+      return undefined
+    })
+  if (!result?.data) {
+    log.error("session restore failed", {
+      workspaceID: input.workspaceID,
+      sessionID: input.sessionID,
+      status: result?.response?.status,
+      error: result?.error ? errorData(result.error) : undefined,
+    })
+    input.toast.show({
+      message: `Failed to restore session: ${errorMessage(result?.error ?? "no response")}`,
+      variant: "error",
+    })
+    return
+  }
+
+  log.info("session restore response", {
+    workspaceID: input.workspaceID,
+    sessionID: input.sessionID,
+    status: result.response?.status,
+    total: result.data.total,
+  })
+
+  await Promise.all([input.project.workspace.sync(), input.sync.session.refresh()]).catch((err) => {
+    log.error("session restore refresh failed", {
+      workspaceID: input.workspaceID,
+      sessionID: input.sessionID,
+      error: errorData(err),
+    })
+    throw err
+  })
+
+  log.info("session restore complete", {
+    workspaceID: input.workspaceID,
+    sessionID: input.sessionID,
+    total: result.data.total,
+  })
+
+  input.toast.show({
+    message: "Session restored into the new workspace",
+    variant: "success",
+  })
+  input.done?.()
+  if (input.done) return
+  input.dialog.clear()
+}
+
 export function DialogWorkspaceCreate(props: { onSelect: (workspaceID: string) => Promise<void> | void }) {
   const dialog = useDialog()
   const sync = useSync()
@@ -123,18 +227,41 @@ export function DialogWorkspaceCreate(props: { onSelect: (workspaceID: string) =
   const create = async (type: string) => {
     if (creating()) return
     setCreating(type)
+    log.info("workspace create requested", {
+      type,
+    })
 
-    const result = await sdk.client.experimental.workspace.create({ type, branch: null }).catch(() => undefined)
+    const result = await sdk.client.experimental.workspace.create({ type, branch: null }).catch((err) => {
+      log.error("workspace create request failed", {
+        type,
+        error: errorData(err),
+      })
+      return undefined
+    })
     const workspace = result?.data
     if (!workspace) {
       setCreating(undefined)
+      log.error("workspace create failed", {
+        type,
+        status: result?.response.status,
+        error: result?.error ? errorData(result.error) : undefined,
+      })
       toast.show({
-        message: "Failed to create workspace",
+        message: `Failed to create workspace: ${errorMessage(result?.error ?? "no response")}`,
         variant: "error",
       })
       return
     }
+    log.info("workspace create response", {
+      type,
+      workspaceID: workspace.id,
+      status: result.response?.status,
+    })
     await project.workspace.sync()
+    log.info("workspace create synced", {
+      type,
+      workspaceID: workspace.id,
+    })
     await props.onSelect(workspace.id)
     setCreating(undefined)
   }

+ 3 - 0
packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx

@@ -611,8 +611,11 @@ export function Prompt(props: PromptProps) {
     }
 
     let sessionID = props.sessionID
+    console.log("jwl", sessionID)
     if (sessionID == null) {
+      console.log("creating")
       const res = await sdk.client.session.create({
+        workspace: props.workspaceID,
         workspaceID: props.workspaceID,
       })
 

+ 8 - 4
packages/opencode/src/cli/cmd/tui/context/local.tsx

@@ -21,8 +21,12 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
     const sdk = useSDK()
     const toast = useToast()
 
+    function providers() {
+      return sync.data?.provider ?? []
+    }
+
     function isModelValid(model: { providerID: string; modelID: string }) {
-      const provider = sync.data.provider.find((x) => x.id === model.providerID)
+      const provider = providers().find((x) => x.id === model.providerID)
       return !!provider?.models[model.modelID]
     }
 
@@ -178,7 +182,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
           }
         }
 
-        const provider = sync.data.provider[0]
+        const provider = providers()[0]
         if (!provider) return undefined
         const defaultModel = sync.data.provider_default[provider.id]
         const firstModel = Object.values(provider.models)[0]
@@ -221,7 +225,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
               reasoning: false,
             }
           }
-          const provider = sync.data.provider.find((x) => x.id === value.providerID)
+          const provider = providers().find((x) => x.id === value.providerID)
           const info = provider?.models[value.modelID]
           return {
             provider: provider?.name ?? value.providerID,
@@ -336,7 +340,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
           list() {
             const m = currentModel()
             if (!m) return []
-            const provider = sync.data.provider.find((x) => x.id === m.providerID)
+            const provider = providers().find((x) => x.id === m.providerID)
             const info = provider?.models[m.modelID]
             if (!info?.variants) return []
             return Object.keys(info.variants)

+ 40 - 2
packages/opencode/src/cli/cmd/tui/context/sdk.tsx

@@ -3,6 +3,8 @@ import type { GlobalEvent, Event } from "@opencode-ai/sdk/v2"
 import { createSimpleContext } from "./helper"
 import { createGlobalEmitter } from "@solid-primitives/event-bus"
 import { batch, onCleanup, onMount } from "solid-js"
+import { Log } from "@/util/log"
+import { errorData } from "@/util/error"
 
 export type EventSource = {
   subscribe: (handler: (event: GlobalEvent) => void) => Promise<() => void>
@@ -19,13 +21,49 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
   }) => {
     const abort = new AbortController()
     let sse: AbortController | undefined
+    const log = Log.Default.clone().tag("service", "tui-sdk")
+
+    const raw = props.fetch ?? fetch
+
+    const traced: typeof fetch = async (input, init) => {
+      const req = input instanceof Request ? input : new Request(input, init)
+      const start = Date.now()
+      try {
+        const res = await raw(req)
+        const url = new URL(res.url || req.url)
+        if (!res.ok || url.searchParams.get("workspace")) {
+          const body = await res
+            .clone()
+            .text()
+            .catch(() => "")
+          log.info("sdk fetch", {
+            method: req.method,
+            request: req.url,
+            response: res.url || req.url,
+            status: res.status,
+            duration: Date.now() - start,
+            workspace: url.searchParams.get("workspace"),
+            body: body.slice(0, 1000),
+          })
+        }
+        return res
+      } catch (error) {
+        log.error("sdk fetch failed", {
+          method: req.method,
+          request: req.url,
+          duration: Date.now() - start,
+          error: errorData(error),
+        })
+        throw error
+      }
+    }
 
     function createSDK() {
       return createOpencodeClient({
         baseUrl: props.url,
         signal: abort.signal,
         directory: props.directory,
-        fetch: props.fetch,
+        fetch: traced,
         headers: props.headers,
       })
     }
@@ -109,7 +147,7 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
       },
       directory: props.directory,
       event: emitter,
-      fetch: props.fetch ?? fetch,
+      fetch: traced,
       url: props.url,
     }
   },

+ 117 - 36
packages/opencode/src/cli/cmd/tui/context/sync.tsx

@@ -29,6 +29,7 @@ import { useExit } from "./exit"
 import { useArgs } from "./args"
 import { batch, createEffect, on } from "solid-js"
 import { Log } from "@/util/log"
+import { errorData } from "@/util/error"
 import { ConsoleState, emptyConsoleState, type ConsoleState as ConsoleStateType } from "@/config/console-state"
 
 export const { use: useSync, provider: SyncProvider } = createSimpleContext({
@@ -107,11 +108,20 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
     const event = useEvent()
     const project = useProject()
     const sdk = useSDK()
+    const log = Log.Default.clone().tag("service", "tui-sync")
 
     event.subscribe((event) => {
       switch (event.type) {
         case "server.instance.disposed":
-          bootstrap()
+          log.info("bootstrap triggered by instance disposal", {
+            workspace: project.workspace.current(),
+          })
+          void bootstrap().catch((error) => {
+            log.error("bootstrap after disposal failed", {
+              workspace: project.workspace.current(),
+              error: errorData(error),
+            })
+          })
           break
         case "permission.replied": {
           const requests = store.permission[event.properties.sessionID]
@@ -351,30 +361,63 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
     const args = useArgs()
 
     async function bootstrap() {
-      console.log("bootstrapping")
       const workspace = project.workspace.current()
       const start = Date.now() - 30 * 24 * 60 * 60 * 1000
+      log.info("bootstrap started", { workspace, path: project.instance.path() })
+
+      function track<T>(name: string, promise: Promise<T>) {
+        return promise
+          .then((value) => {
+            log.debug("bootstrap request ok", { name, workspace })
+            return value
+          })
+          .catch((error) => {
+            log.error("bootstrap request failed", {
+              name,
+              workspace,
+              error: errorData(error),
+            })
+            throw error
+          })
+      }
+
       const sessionListPromise = sdk.client.session
         .list({ start: start })
         .then((x) => (x.data ?? []).toSorted((a, b) => a.id.localeCompare(b.id)))
 
       // blocking - include session.list when continuing a session
-      const providersPromise = sdk.client.config.providers({ workspace }, { throwOnError: true })
-      const providerListPromise = sdk.client.provider.list({ workspace }, { throwOnError: true })
-      const consoleStatePromise = sdk.client.experimental.console
-        .get({ workspace }, { throwOnError: true })
-        .then((x) => ConsoleState.parse(x.data))
-        .catch(() => emptyConsoleState)
-      const agentsPromise = sdk.client.app.agents({ workspace }, { throwOnError: true })
-      const configPromise = sdk.client.config.get({ workspace }, { throwOnError: true })
-      const projectPromise = project.sync()
+      const providersPromise = track(
+        "config.providers",
+        sdk.client.config.providers({ workspace }, { throwOnError: true }),
+      )
+      const providerListPromise = track(
+        "provider.list",
+        sdk.client.provider.list({ workspace }, { throwOnError: true }),
+      )
+      const consoleStatePromise = track(
+        "experimental.console.get",
+        sdk.client.experimental.console
+          .get({ workspace }, { throwOnError: true })
+          .then((x) => ConsoleState.parse(x.data))
+          .catch((error) => {
+            log.warn("console state unavailable", {
+              workspace,
+              error: errorData(error),
+            })
+            return emptyConsoleState
+          }),
+      )
+      const agentsPromise = track("app.agents", sdk.client.app.agents({ workspace }, { throwOnError: true }))
+      const configPromise = track("config.get", sdk.client.config.get({ workspace }, { throwOnError: true }))
+      const projectPromise = track("project.sync", project.sync())
+      const sessionSyncPromise = track("session.list", sessionListPromise)
       const blockingRequests: Promise<unknown>[] = [
         providersPromise,
         providerListPromise,
         agentsPromise,
         configPromise,
         projectPromise,
-        ...(args.continue ? [sessionListPromise] : []),
+        ...(args.continue ? [sessionSyncPromise] : []),
       ]
 
       await Promise.all(blockingRequests)
@@ -384,7 +427,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
           const consoleStateResponse = consoleStatePromise
           const agentsResponse = agentsPromise.then((x) => x.data ?? [])
           const configResponse = configPromise.then((x) => x.data!)
-          const sessionListResponse = args.continue ? sessionListPromise : undefined
+          const sessionListResponse = args.continue ? sessionSyncPromise : undefined
 
           return Promise.all([
             providersResponse,
@@ -414,34 +457,57 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
         })
         .then(() => {
           if (store.status !== "complete") setStore("status", "partial")
+          log.info("bootstrap partial", { workspace })
           // non-blocking
           Promise.all([
-            ...(args.continue ? [] : [sessionListPromise.then((sessions) => setStore("session", reconcile(sessions)))]),
-            consoleStatePromise.then((consoleState) => setStore("console_state", reconcile(consoleState))),
-            sdk.client.command.list({ workspace }).then((x) => setStore("command", reconcile(x.data ?? []))),
-            sdk.client.lsp.status({ workspace }).then((x) => setStore("lsp", reconcile(x.data!))),
-            sdk.client.mcp.status({ workspace }).then((x) => setStore("mcp", reconcile(x.data!))),
-            sdk.client.experimental.resource
-              .list({ workspace })
-              .then((x) => setStore("mcp_resource", reconcile(x.data ?? {}))),
-            sdk.client.formatter.status({ workspace }).then((x) => setStore("formatter", reconcile(x.data!))),
-            sdk.client.session.status({ workspace }).then((x) => {
+            ...(args.continue
+              ? []
+              : [
+                  track("session.list.background", sessionListPromise).then((sessions) =>
+                    setStore("session", reconcile(sessions)),
+                  ),
+                ]),
+            track("experimental.console.get.background", consoleStatePromise).then((consoleState) =>
+              setStore("console_state", reconcile(consoleState)),
+            ),
+            track("command.list", sdk.client.command.list({ workspace })).then((x) =>
+              setStore("command", reconcile(x.data ?? [])),
+            ),
+            track("lsp.status", sdk.client.lsp.status({ workspace })).then((x) => setStore("lsp", reconcile(x.data!))),
+            track("mcp.status", sdk.client.mcp.status({ workspace })).then((x) => setStore("mcp", reconcile(x.data!))),
+            track("experimental.resource.list", sdk.client.experimental.resource.list({ workspace })).then((x) =>
+              setStore("mcp_resource", reconcile(x.data ?? {})),
+            ),
+            track("formatter.status", sdk.client.formatter.status({ workspace })).then((x) =>
+              setStore("formatter", reconcile(x.data!)),
+            ),
+            track("session.status", sdk.client.session.status({ workspace })).then((x) => {
               setStore("session_status", reconcile(x.data!))
             }),
-            sdk.client.provider.auth({ workspace }).then((x) => setStore("provider_auth", reconcile(x.data ?? {}))),
-            sdk.client.vcs.get({ workspace }).then((x) => setStore("vcs", reconcile(x.data))),
-            project.workspace.sync(),
-          ]).then(() => {
-            setStore("status", "complete")
-          })
+            track("provider.auth", sdk.client.provider.auth({ workspace })).then((x) =>
+              setStore("provider_auth", reconcile(x.data ?? {})),
+            ),
+            track("vcs.get", sdk.client.vcs.get({ workspace })).then((x) => setStore("vcs", reconcile(x.data))),
+            track("project.workspace.sync", project.workspace.sync()),
+          ])
+            .then(() => {
+              log.info("bootstrap complete", { workspace })
+              setStore("status", "complete")
+            })
+            .catch((error) => {
+              log.error("bootstrap background failed", {
+                workspace,
+                error: errorData(error),
+              })
+              throw error
+            })
         })
-        .catch(async (e) => {
-          Log.Default.error("tui bootstrap failed", {
-            error: e instanceof Error ? e.message : String(e),
-            name: e instanceof Error ? e.name : undefined,
-            stack: e instanceof Error ? e.stack : undefined,
+        .catch((error) => {
+          log.error("bootstrap failed", {
+            workspace,
+            error: errorData(error),
           })
-          await exit(e)
+          throw error
         })
     }
 
@@ -451,7 +517,15 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
         () => project.workspace.current(),
         () => {
           fullSyncedSessions.clear()
-          void bootstrap()
+          log.info("bootstrap triggered by workspace change", {
+            workspace: project.workspace.current(),
+          })
+          void bootstrap().catch((error) => {
+            log.error("bootstrap effect failed", {
+              workspace: project.workspace.current(),
+              error: errorData(error),
+            })
+          })
         },
       ),
     )
@@ -474,6 +548,13 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
           if (match.found) return store.session[match.index]
           return undefined
         },
+        async refresh() {
+          const start = Date.now() - 30 * 24 * 60 * 60 * 1000
+          const sessions = await sdk.client.session
+            .list({ start })
+            .then((x) => (x.data ?? []).toSorted((a, b) => a.id.localeCompare(b.id)))
+          setStore("session", reconcile(sessions))
+        },
         status(sessionID: string) {
           const session = result.session.get(sessionID)
           if (!session) return "idle"

+ 247 - 23
packages/opencode/src/control-plane/workspace.ts

@@ -1,11 +1,13 @@
 import z from "zod"
 import { setTimeout as sleep } from "node:timers/promises"
 import { fn } from "@/util/fn"
-import { Database, eq } from "@/storage/db"
+import { Database, asc, eq } from "@/storage/db"
 import { Project } from "@/project/project"
 import { BusEvent } from "@/bus/bus-event"
 import { GlobalBus } from "@/bus/global"
 import { SyncEvent } from "@/sync"
+import { EventTable } from "@/sync/event.sql"
+import { EventID } from "@/sync/schema"
 import { Log } from "@/util/log"
 import { Filesystem } from "@/util/filesystem"
 import { ProjectID } from "@/project/schema"
@@ -15,6 +17,10 @@ import { getAdaptor } from "./adaptors"
 import { WorkspaceInfo } from "./types"
 import { WorkspaceID } from "./schema"
 import { parseSSE } from "./sse"
+import { Session } from "@/session"
+import { SessionTable } from "@/session/session.sql"
+import { SessionID } from "@/session/schema"
+import { errorData } from "@/util/error"
 
 export namespace Workspace {
   export const Info = WorkspaceInfo.meta({
@@ -29,6 +35,13 @@ export namespace Workspace {
   })
   export type ConnectionStatus = z.infer<typeof ConnectionStatus>
 
+  const Restore = z.object({
+    workspaceID: WorkspaceID.zod,
+    sessionID: SessionID.zod,
+    total: z.number().int().min(0),
+    step: z.number().int().min(0),
+  })
+
   export const Event = {
     Ready: BusEvent.define(
       "workspace.ready",
@@ -42,6 +55,7 @@ export namespace Workspace {
         message: z.string(),
       }),
     ),
+    Restore: BusEvent.define("workspace.restore", Restore),
     Status: BusEvent.define("workspace.status", ConnectionStatus),
   }
 
@@ -97,16 +111,186 @@ export namespace Workspace {
 
     await adaptor.create(config)
 
+    console.log("starting sync")
     startSync(info)
 
     return info
   })
 
+  const SessionRestoreInput = z.object({
+    workspaceID: WorkspaceID.zod,
+    sessionID: SessionID.zod,
+  })
+
+  export const sessionRestore = fn(SessionRestoreInput, async (input) => {
+    log.info("session restore requested", {
+      workspaceID: input.workspaceID,
+      sessionID: input.sessionID,
+    })
+    try {
+      const space = await get(input.workspaceID)
+      if (!space) throw new Error(`Workspace not found: ${input.workspaceID}`)
+
+      const adaptor = await getAdaptor(space.projectID, space.type)
+      const target = await adaptor.target(space)
+
+      const rows = Database.use((db) =>
+        db
+          .select({
+            id: EventTable.id,
+            aggregateID: EventTable.aggregate_id,
+            seq: EventTable.seq,
+            type: EventTable.type,
+            data: EventTable.data,
+          })
+          .from(EventTable)
+          .where(eq(EventTable.aggregate_id, input.sessionID))
+          .orderBy(asc(EventTable.seq))
+          .all(),
+      )
+      if (rows.length === 0) throw new Error(`No events found for session: ${input.sessionID}`)
+
+      const next = rows.at(-1)!.seq + 1
+      const all = [
+        ...rows,
+        {
+          id: EventID.ascending(),
+          aggregateID: input.sessionID,
+          seq: next,
+          type: SyncEvent.versionedType(Session.Event.Updated.type, Session.Event.Updated.version),
+          data: {
+            sessionID: input.sessionID,
+            info: {
+              workspaceID: input.workspaceID,
+              time: {
+                updated: Date.now(),
+              },
+            },
+          },
+        },
+      ]
+
+      const size = 10
+      const sets = Array.from({ length: Math.ceil(all.length / size) }, (_, i) => all.slice(i * size, (i + 1) * size))
+      const total = sets.length
+      log.info("session restore prepared", {
+        workspaceID: input.workspaceID,
+        sessionID: input.sessionID,
+        workspaceType: space.type,
+        directory: space.directory,
+        target: target.type === "remote" ? String(route(target.url, "/sync/replay")) : target.directory,
+        events: all.length,
+        batches: total,
+        first: all[0]?.seq,
+        last: all.at(-1)?.seq,
+      })
+      GlobalBus.emit("event", {
+        directory: "global",
+        workspace: input.workspaceID,
+        payload: {
+          type: Event.Restore.type,
+          properties: {
+            workspaceID: input.workspaceID,
+            sessionID: input.sessionID,
+            total,
+            step: 0,
+          },
+        },
+      })
+      for (const [i, events] of sets.entries()) {
+        log.info("session restore batch starting", {
+          workspaceID: input.workspaceID,
+          sessionID: input.sessionID,
+          step: i + 1,
+          total,
+          events: events.length,
+          first: events[0]?.seq,
+          last: events.at(-1)?.seq,
+          target: target.type === "remote" ? String(route(target.url, "/sync/replay")) : target.directory,
+        })
+        if (target.type === "local") {
+          SyncEvent.replayAll(events)
+          log.info("session restore batch replayed locally", {
+            workspaceID: input.workspaceID,
+            sessionID: input.sessionID,
+            step: i + 1,
+            total,
+            events: events.length,
+          })
+        } else {
+          const url = route(target.url, "/sync/replay")
+          const headers = new Headers(target.headers)
+          headers.set("content-type", "application/json")
+          const res = await fetch(url, {
+            method: "POST",
+            headers,
+            body: JSON.stringify({
+              directory: space.directory ?? "",
+              events,
+            }),
+          })
+          if (!res.ok) {
+            const body = await res.text()
+            log.error("session restore batch failed", {
+              workspaceID: input.workspaceID,
+              sessionID: input.sessionID,
+              step: i + 1,
+              total,
+              status: res.status,
+              body,
+            })
+            throw new Error(
+              `Failed to replay session ${input.sessionID} into workspace ${input.workspaceID}: HTTP ${res.status} ${body}`,
+            )
+          }
+          log.info("session restore batch posted", {
+            workspaceID: input.workspaceID,
+            sessionID: input.sessionID,
+            step: i + 1,
+            total,
+            status: res.status,
+          })
+        }
+        GlobalBus.emit("event", {
+          directory: "global",
+          workspace: input.workspaceID,
+          payload: {
+            type: Event.Restore.type,
+            properties: {
+              workspaceID: input.workspaceID,
+              sessionID: input.sessionID,
+              total,
+              step: i + 1,
+            },
+          },
+        })
+      }
+
+      log.info("session restore complete", {
+        workspaceID: input.workspaceID,
+        sessionID: input.sessionID,
+        batches: total,
+      })
+
+      return {
+        total,
+      }
+    } catch (err) {
+      log.error("session restore failed", {
+        workspaceID: input.workspaceID,
+        sessionID: input.sessionID,
+        error: errorData(err),
+      })
+      throw err
+    }
+  })
+
   export function list(project: Project.Info) {
     const rows = Database.use((db) =>
       db.select().from(WorkspaceTable).where(eq(WorkspaceTable.project_id, project.id)).all(),
     )
     const spaces = rows.map(fromRow).sort((a, b) => a.id.localeCompare(b.id))
+
     for (const space of spaces) startSync(space)
     return spaces
   }
@@ -120,13 +304,25 @@ export namespace Workspace {
   })
 
   export const remove = fn(WorkspaceID.zod, async (id) => {
+    const sessions = Database.use((db) =>
+      db.select({ id: SessionTable.id }).from(SessionTable).where(eq(SessionTable.workspace_id, id)).all(),
+    )
+    for (const session of sessions) {
+      await Session.remove(session.id)
+    }
+
     const row = Database.use((db) => db.select().from(WorkspaceTable).where(eq(WorkspaceTable.id, id)).get())
+
     if (row) {
       stopSync(id)
 
       const info = fromRow(row)
-      const adaptor = await getAdaptor(info.projectID, row.type)
-      adaptor.remove(info)
+      try {
+        const adaptor = await getAdaptor(info.projectID, row.type)
+        await adaptor.remove(info)
+      } catch (err) {
+        log.error("adaptor not available when removing workspace", { type: row.type })
+      }
       Database.use((db) => db.delete(WorkspaceTable).where(eq(WorkspaceTable.id, id)).run())
       return info
     }
@@ -156,47 +352,75 @@ export namespace Workspace {
 
   const log = Log.create({ service: "workspace-sync" })
 
-  async function workspaceEventLoop(space: Info, signal: AbortSignal) {
-    log.info("starting sync: " + space.id)
+  function route(url: string | URL, path: string) {
+    const next = new URL(url)
+    next.pathname = `${next.pathname.replace(/\/$/, "")}${path}`
+    next.search = ""
+    next.hash = ""
+    return next
+  }
 
+  async function globalEventLoop(space: Info, signal: AbortSignal) {
     while (!signal.aborted) {
-      log.info("connecting to sync: " + space.id)
+      console.log("connecting to global sync", { workspace: space.name })
 
-      setStatus(space.id, "connecting")
       const adaptor = await getAdaptor(space.projectID, space.type)
       const target = await adaptor.target(space)
 
       if (target.type === "local") return
 
-      const res = await fetch(target.url + "/sync/event", { method: "GET", signal }).catch((err: unknown) => {
-        setStatus(space.id, "error", String(err))
+      const res = await fetch(route(target.url, "/global/event"), {
+        method: "GET",
+        headers: target.headers,
+        signal,
+      }).catch((err: unknown) => {
+        setStatus(space.id, "error")
+
+        console.log("failed to connect to global sync", {
+          workspace: space.name,
+          error: err,
+        })
         return undefined
       })
-      if (!res || !res.ok || !res.body) {
-        log.info("failed to connect to sync: " + res?.status)
 
-        setStatus(space.id, "error", res ? `HTTP ${res.status}` : "no response")
-        await sleep(1000)
+      if (!res || !res.ok || !res.body) {
+        console.log("failed to connect to global sync", { workspace: space.name })
+        await sleep(50000)
         continue
       }
+
+      console.log("global sync connected", { workspace: space.name })
       setStatus(space.id, "connected")
-      await parseSSE(res.body, signal, (evt) => {
-        const event = evt as SyncEvent.SerializedEvent
 
+      await parseSSE(res.body, signal, (evt: any) => {
         try {
-          if (!event.type.startsWith("server.")) {
-            SyncEvent.replay(event)
+          if (!("payload" in evt)) return
+
+          if (!evt.payload.type.startsWith("server.")) {
+            console.log("received workspace sse event", evt)
+          }
+
+          if (evt.payload.type === "sync") {
+            // This name -> type is temporary
+            SyncEvent.replay({ ...evt.payload, type: evt.payload.name } as SyncEvent.SerializedEvent)
           }
+
+          GlobalBus.emit("event", {
+            directory: evt.directory,
+            project: evt.project,
+            workspace: space.id,
+            payload: evt.payload,
+          })
         } catch (err) {
-          log.warn("failed to replay sync event", {
+          console.log("failed to replay global event", {
             workspaceID: space.id,
             error: err,
           })
         }
       })
-      setStatus(space.id, "disconnected")
-      log.info("disconnected to sync: " + space.id)
-      await sleep(250)
+
+      log.info("disconnected from global sync: " + space.id)
+      await sleep(50000)
     }
   }
 
@@ -213,9 +437,9 @@ export namespace Workspace {
     aborts.set(space.id, abort)
     setStatus(space.id, "disconnected")
 
-    void workspaceEventLoop(space, abort.signal).catch((error) => {
+    void globalEventLoop(space, abort.signal).catch((error) => {
       setStatus(space.id, "error", String(error))
-      log.warn("workspace sync listener failed", {
+      log.warn("workspace listener failed", {
         workspaceID: space.id,
         error,
       })

+ 1 - 0
packages/opencode/src/plugin/loader.ts

@@ -103,6 +103,7 @@ export namespace PluginLoader {
     } catch (error) {
       return { ok: false, error }
     }
+    console.log('mod', mod)
     if (!mod) return { ok: false, error: new Error(`Plugin ${row.spec} module is empty`) }
     return { ok: true, value: { ...row, mod } }
   }

+ 79 - 18
packages/opencode/src/server/instance/middleware.ts

@@ -11,9 +11,26 @@ import { Session } from "@/session"
 import { SessionID } from "@/session/schema"
 import { WorkspaceContext } from "@/control-plane/workspace-context"
 import { AppRuntime } from "@/effect/app-runtime"
+import { Log } from "@/util/log"
 
 type Rule = { method?: string; path: string; exact?: boolean; action: "local" | "forward" }
 
+const hop = new Set([
+  "connection",
+  "keep-alive",
+  "proxy-authenticate",
+  "proxy-authorization",
+  "proxy-connection",
+  "te",
+  "trailer",
+  "transfer-encoding",
+  "upgrade",
+  "host",
+  "content-length",
+])
+
+const IS_WORKSPACE = process.env.OPENCODE_WORKSPACE === "true"
+
 const RULES: Array<Rule> = [
   { path: "/session/status", action: "forward" },
   { method: "GET", path: "/session", action: "local" },
@@ -37,6 +54,40 @@ function getSessionID(url: URL) {
   return SessionID.make(id)
 }
 
+function sh(value: string) {
+  return `'${value.replace(/'/g, `'"'"'`)}'`
+}
+
+async function curl(url: URL, extra: HeadersInit | undefined, req: Request) {
+  const headers = new Headers(req.headers)
+  for (const key of hop) headers.delete(key)
+  headers.delete("accept-encoding")
+  headers.delete("x-opencode-directory")
+  headers.delete("x-opencode-workspace")
+
+  if (extra) {
+    for (const [key, value] of new Headers(extra).entries()) {
+      headers.set(key, value)
+    }
+  }
+
+  const parts = ["curl", "-X", req.method]
+  for (const [key, value] of headers.entries()) {
+    parts.push("-H", sh(`${key}: ${value}`))
+  }
+
+  if (req.method !== "GET" && req.method !== "HEAD") {
+    const body = await req
+      .clone()
+      .text()
+      .catch(() => "")
+    if (body) parts.push("--data-binary", sh(body))
+  }
+
+  parts.push(sh(url.toString()))
+  return parts.join(" ")
+}
+
 async function getSessionWorkspace(url: URL) {
   const id = getSessionID(url)
   if (!id) return null
@@ -46,6 +97,8 @@ async function getSessionWorkspace(url: URL) {
 }
 
 export function WorkspaceRouterMiddleware(upgrade: UpgradeWebSocket): MiddlewareHandler {
+  const log = Log.Default.clone().tag("service", "workspace-router")
+
   return async (c, next) => {
     const raw = c.req.query("directory") || c.req.header("x-opencode-directory") || process.cwd()
     const directory = Filesystem.resolve(
@@ -64,7 +117,7 @@ export function WorkspaceRouterMiddleware(upgrade: UpgradeWebSocket): Middleware
     const workspaceID = sessionWorkspaceID || url.searchParams.get("workspace")
 
     // If no workspace is provided we use the project
-    if (!workspaceID) {
+    if (!workspaceID || url.pathname.startsWith("/console") || IS_WORKSPACE) {
       return Instance.provide({
         directory,
         init: () => AppRuntime.runPromise(InstanceBootstrap),
@@ -77,16 +130,6 @@ export function WorkspaceRouterMiddleware(upgrade: UpgradeWebSocket): Middleware
     const workspace = await Workspace.get(WorkspaceID.make(workspaceID))
 
     if (!workspace) {
-      // Special-case deleting a session in case user's data in a
-      // weird state. Allow them to forcefully delete a synced session
-      // even if the remote workspace is not in their data.
-      //
-      // The lets the `DELETE /session/:id` endpoint through and we've
-      // made sure that it will run without an instance
-      if (url.pathname.match(/\/session\/[^/]+$/) && c.req.method === "DELETE") {
-        return next()
-      }
-
       return new Response(`Workspace not found: ${workspaceID}`, {
         status: 500,
         headers: {
@@ -98,6 +141,13 @@ export function WorkspaceRouterMiddleware(upgrade: UpgradeWebSocket): Middleware
     const adaptor = await getAdaptor(workspace.projectID, workspace.type)
     const target = await adaptor.target(workspace)
 
+    log.info("workspace route resolved", {
+      workspaceID,
+      workspace_type: workspace.type,
+      target_type: target.type,
+      request: url.toString(),
+    })
+
     if (target.type === "local") {
       return WorkspaceContext.provide({
         workspaceID: WorkspaceID.make(workspaceID),
@@ -118,18 +168,29 @@ export function WorkspaceRouterMiddleware(upgrade: UpgradeWebSocket): Middleware
       return next()
     }
 
+    const proxyURL = new URL(target.url)
+    proxyURL.pathname = `${proxyURL.pathname.replace(/\/$/, "")}${url.pathname}`
+    proxyURL.search = url.search
+    proxyURL.hash = url.hash
+    proxyURL.searchParams.delete("workspace")
+
+    log.info("workspace proxy forwarding", {
+      workspaceID,
+      request: url.toString(),
+      target: String(target.url),
+      proxy: proxyURL.toString(),
+    })
+
     if (c.req.header("upgrade")?.toLowerCase() === "websocket") {
-      return ServerProxy.websocket(upgrade, target, c.req.raw, c.env)
+      return ServerProxy.websocket(upgrade, proxyURL, target.headers, c.req.raw, c.env)
     }
 
     const headers = new Headers(c.req.raw.headers)
     headers.delete("x-opencode-workspace")
 
-    return ServerProxy.http(
-      target,
-      new Request(c.req.raw, {
-        headers,
-      }),
-    )
+    const req = new Request(c.req.raw, { headers })
+    console.log("workspace proxy curl", await curl(proxyURL, target.headers, req))
+
+    return ServerProxy.http(proxyURL, target.headers, req)
   }
 }

+ 118 - 0
packages/opencode/src/server/instance/sync.ts

@@ -0,0 +1,118 @@
+import z from "zod"
+import { Hono } from "hono"
+import { describeRoute, validator, resolver } from "hono-openapi"
+import { SyncEvent } from "@/sync"
+import { Database, asc, and, not, or, lte, eq } from "@/storage/db"
+import { EventTable } from "@/sync/event.sql"
+import { lazy } from "@/util/lazy"
+import { Log } from "@/util/log"
+import { errors } from "../error"
+
+const ReplayEvent = z.object({
+  id: z.string(),
+  aggregateID: z.string(),
+  seq: z.number().int().min(0),
+  type: z.string(),
+  data: z.record(z.string(), z.unknown()),
+})
+
+const log = Log.create({ service: "server.sync" })
+
+export const SyncRoutes = lazy(() =>
+  new Hono()
+    .post(
+      "/replay",
+      describeRoute({
+        summary: "Replay sync events",
+        description: "Validate and replay a complete sync event history.",
+        operationId: "global.sync-replay",
+        responses: {
+          200: {
+            description: "Replayed sync events",
+            content: {
+              "application/json": {
+                schema: resolver(
+                  z.object({
+                    sessionID: z.string(),
+                  }),
+                ),
+              },
+            },
+          },
+          ...errors(400),
+        },
+      }),
+      validator(
+        "json",
+        z.object({
+          directory: z.string(),
+          events: z.array(ReplayEvent).min(1),
+        }),
+      ),
+      async (c) => {
+        const body = c.req.valid("json")
+        const events = body.events
+        const source = events[0].aggregateID
+        log.info("sync replay requested", {
+          sessionID: source,
+          events: events.length,
+          first: events[0]?.seq,
+          last: events.at(-1)?.seq,
+          directory: body.directory,
+        })
+        SyncEvent.replayAll(events)
+
+        log.info("sync replay complete", {
+          sessionID: source,
+          events: events.length,
+          first: events[0]?.seq,
+          last: events.at(-1)?.seq,
+        })
+
+        return c.json({
+          sessionID: source,
+        })
+      },
+    )
+    .get(
+      "/history",
+      describeRoute({
+        summary: "List sync events",
+        description:
+          "List sync events for all aggregates. Keys are aggregate IDs the client already knows about, values are the last known sequence ID. Events with seq > value are returned for those aggregates. Aggregates not listed in the input get their full history.",
+        operationId: "global.sync-history.list",
+        responses: {
+          200: {
+            description: "Sync events",
+            content: {
+              "application/json": {
+                schema: resolver(
+                  z.array(
+                    z.object({
+                      id: z.string(),
+                      aggregate_id: z.string(),
+                      seq: z.number(),
+                      type: z.string(),
+                      data: z.record(z.string(), z.unknown()),
+                    }),
+                  ),
+                ),
+              },
+            },
+          },
+          ...errors(400),
+        },
+      }),
+      validator("json", z.record(z.string(), z.number().int().min(0))),
+      async (c) => {
+        const body = c.req.valid("json")
+        const exclude = Object.entries(body)
+        const where =
+          exclude.length > 0
+            ? not(or(...exclude.map(([id, seq]) => and(eq(EventTable.aggregate_id, id), lte(EventTable.seq, seq))))!)
+            : undefined
+        const rows = Database.use((db) => db.select().from(EventTable).where(where).orderBy(asc(EventTable.seq)).all())
+        return c.json(rows)
+      },
+    ),
+)

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

@@ -6,6 +6,16 @@ import { Workspace } from "../../control-plane/workspace"
 import { Instance } from "../../project/instance"
 import { errors } from "../error"
 import { lazy } from "../../util/lazy"
+import { Log } from "@/util/log"
+import { errorData } from "@/util/error"
+
+const SessionRestoreBody = Workspace.sessionRestore.schema.omit({
+  workspaceID: true,
+})
+
+const SessionRestoreResult = z.object({
+  total: z.number().int().min(0),
+})
 
 const WorkspaceAdaptor = z.object({
   type: z.string(),
@@ -13,6 +23,8 @@ const WorkspaceAdaptor = z.object({
   description: z.string(),
 })
 
+const log = Log.create({ service: "server.workspace" })
+
 export const WorkspaceRoutes = lazy(() =>
   new Hono()
     .get(
@@ -140,5 +152,59 @@ export const WorkspaceRoutes = lazy(() =>
         const { id } = c.req.valid("param")
         return c.json(await Workspace.remove(id))
       },
+    )
+    .post(
+      "/:id/session-restore",
+      describeRoute({
+        summary: "Restore session into workspace",
+        description: "Replay a session's sync events into the target workspace in batches.",
+        operationId: "experimental.workspace.sessionRestore",
+        responses: {
+          200: {
+            description: "Session replay started",
+            content: {
+              "application/json": {
+                schema: resolver(SessionRestoreResult),
+              },
+            },
+          },
+          ...errors(400),
+        },
+      }),
+      validator(
+        "param",
+        z.object({
+          id: Workspace.Info.shape.id,
+        }),
+      ),
+      validator("json", SessionRestoreBody),
+      async (c) => {
+        const { id } = c.req.valid("param")
+        const body = c.req.valid("json")
+        log.info("session restore route requested", {
+          workspaceID: id,
+          sessionID: body.sessionID,
+          directory: Instance.directory,
+        })
+        try {
+          const result = await Workspace.sessionRestore({
+            workspaceID: id,
+            ...body,
+          })
+          log.info("session restore route complete", {
+            workspaceID: id,
+            sessionID: body.sessionID,
+            total: result.total,
+          })
+          return c.json(result)
+        } catch (err) {
+          log.error("session restore route failed", {
+            workspaceID: id,
+            sessionID: body.sessionID,
+            error: errorData(err),
+          })
+          throw err
+        }
+      },
     ),
 )

+ 1 - 1
packages/opencode/src/server/middleware.ts

@@ -86,7 +86,7 @@ const zipped = compress()
 export const CompressionMiddleware: MiddlewareHandler = (c, next) => {
   const path = c.req.path
   const method = c.req.method
-  if (path === "/event" || path === "/global/event" || path === "/global/sync-event") return next()
+  if (path === "/event" || path === "/global/event") return next()
   if (method === "POST" && /\/session\/[^/]+\/(message|prompt_async)$/.test(path)) return next()
   return zipped(c, next)
 }

+ 44 - 11
packages/opencode/src/server/proxy.ts

@@ -1,6 +1,6 @@
-import type { Target } from "@/control-plane/types"
 import { Hono } from "hono"
 import type { UpgradeWebSocket } from "hono/ws"
+import { Log } from "@/util/log"
 
 const hop = new Set([
   "connection",
@@ -20,6 +20,7 @@ type Msg = string | ArrayBuffer | Uint8Array
 function headers(req: Request, extra?: HeadersInit) {
   const out = new Headers(req.headers)
   for (const key of hop) out.delete(key)
+  out.delete("accept-encoding")
   out.delete("x-opencode-directory")
   out.delete("x-opencode-workspace")
   if (!extra) return out
@@ -98,31 +99,63 @@ const app = (upgrade: UpgradeWebSocket) =>
   )
 
 export namespace ServerProxy {
-  export function http(target: Extract<Target, { type: "remote" }>, req: Request) {
+  const log = Log.Default.clone().tag("service", "server-proxy")
+
+  export function http(url: string | URL, extra: HeadersInit | undefined, req: Request) {
+    console.log("proxy http request", {
+      method: req.method,
+      request: req.url,
+      url: String(url),
+    })
     return fetch(
-      new Request(target.url, {
+      new Request(url, {
         method: req.method,
-        headers: headers(req, target.headers),
+        headers: headers(req, extra),
         body: req.method === "GET" || req.method === "HEAD" ? undefined : req.body,
         redirect: "manual",
         signal: req.signal,
       }),
-    )
+    ).then((res) => {
+      const next = new Headers(res.headers)
+      next.delete("content-encoding")
+      next.delete("content-length")
+
+      console.log("proxy http response", {
+        method: req.method,
+        request: req.url,
+        url: String(url),
+        status: res.status,
+        statusText: res.statusText,
+      })
+      return new Response(res.body, {
+        status: res.status,
+        statusText: res.statusText,
+        headers: next,
+      })
+    })
   }
 
   export function websocket(
     upgrade: UpgradeWebSocket,
-    target: Extract<Target, { type: "remote" }>,
+    target: string | URL,
+    extra: HeadersInit | undefined,
     req: Request,
     env: unknown,
   ) {
-    const url = new URL(req.url)
-    url.pathname = "/__workspace_ws"
-    url.search = ""
+    const proxy = new URL(req.url)
+    proxy.pathname = "/__workspace_ws"
+    proxy.search = ""
     const next = new Headers(req.headers)
-    next.set("x-opencode-proxy-url", socket(target.url))
+    next.set("x-opencode-proxy-url", socket(target))
+    for (const [key, value] of new Headers(extra).entries()) {
+      next.set(key, value)
+    }
+    log.info("proxy websocket", {
+      request: req.url,
+      target: String(target),
+    })
     return app(upgrade).fetch(
-      new Request(url, {
+      new Request(proxy, {
         method: req.method,
         headers: next,
         signal: req.signal,

+ 19 - 0
packages/opencode/src/sync/index.ts

@@ -199,6 +199,25 @@ export namespace SyncEvent {
     process(def, event, { publish: !!options?.publish })
   }
 
+  export function replayAll(events: SerializedEvent[], options?: { publish: boolean }) {
+    const source = events[0]?.aggregateID
+    if (!source) return
+    if (events.some((item) => item.aggregateID !== source)) {
+      throw new Error("Replay events must belong to the same session")
+    }
+    const start = events[0].seq
+    for (const [i, item] of events.entries()) {
+      const seq = start + i
+      if (item.seq !== seq) {
+        throw new Error(`Replay sequence mismatch at index ${i}: expected ${seq}, got ${item.seq}`)
+      }
+    }
+    for (const item of events) {
+      replay(item, options)
+    }
+    return source
+  }
+
   export function run<Def extends Definition>(def: Def, data: Event<Def>["data"], options?: { publish?: boolean }) {
     const agg = (data as Record<string, string>)[def.aggregate]
     // This should never happen: we've enforced it via typescript in

+ 89 - 0
packages/opencode/test/cli/tui/workspace-restore.test.ts

@@ -0,0 +1,89 @@
+import { describe, expect, mock, test } from "bun:test"
+import { restoreWorkspaceSession } from "../../../src/cli/cmd/tui/component/dialog-workspace-create"
+
+describe("restoreWorkspaceSession", () => {
+  test("refreshes workspace and session data after a successful restore", async () => {
+    const sessionRestore = mock(async () => ({ data: { total: 2 } }))
+    const syncWorkspace = mock(async () => {})
+    const refresh = mock(async () => {})
+    const clear = mock(() => {})
+    const show = mock(() => {})
+
+    await restoreWorkspaceSession({
+      dialog: { clear } as any,
+      sdk: {
+        client: {
+          experimental: {
+            workspace: {
+              sessionRestore,
+            },
+          },
+        },
+      } as any,
+      sync: {
+        session: {
+          refresh,
+        },
+      } as any,
+      project: {
+        workspace: {
+          sync: syncWorkspace,
+        },
+      } as any,
+      toast: { show } as any,
+      workspaceID: "wrk_1",
+      sessionID: "ses_1",
+    })
+
+    expect(sessionRestore).toHaveBeenCalledWith({ id: "wrk_1", sessionID: "ses_1" })
+    expect(syncWorkspace).toHaveBeenCalledTimes(1)
+    expect(refresh).toHaveBeenCalledTimes(1)
+    expect(show).toHaveBeenCalledWith({
+      message: "Session restored into the new workspace",
+      variant: "success",
+    })
+    expect(clear).toHaveBeenCalledTimes(1)
+  })
+
+  test("shows an error and keeps the dialog open when restore fails", async () => {
+    const sessionRestore = mock(async () => undefined)
+    const syncWorkspace = mock(async () => {})
+    const refresh = mock(async () => {})
+    const clear = mock(() => {})
+    const show = mock(() => {})
+
+    await restoreWorkspaceSession({
+      dialog: { clear } as any,
+      sdk: {
+        client: {
+          experimental: {
+            workspace: {
+              sessionRestore,
+            },
+          },
+        },
+      } as any,
+      sync: {
+        session: {
+          refresh,
+        },
+      } as any,
+      project: {
+        workspace: {
+          sync: syncWorkspace,
+        },
+      } as any,
+      toast: { show } as any,
+      workspaceID: "wrk_1",
+      sessionID: "ses_1",
+    })
+
+    expect(syncWorkspace).not.toHaveBeenCalled()
+    expect(refresh).not.toHaveBeenCalled()
+    expect(clear).not.toHaveBeenCalled()
+    expect(show).toHaveBeenCalledWith({
+      message: "Failed to restore session: no response",
+      variant: "error",
+    })
+  })
+})

+ 268 - 0
packages/opencode/test/server/workspace-restore.test.ts

@@ -0,0 +1,268 @@
+import { afterEach, beforeEach, describe, expect, mock, spyOn, test } from "bun:test"
+import fs from "node:fs/promises"
+import path from "node:path"
+import { GlobalBus } from "../../src/bus/global"
+import { registerAdaptor } from "../../src/control-plane/adaptors"
+import type { WorkspaceAdaptor } from "../../src/control-plane/types"
+import { Workspace } from "../../src/control-plane/workspace"
+import { Flag } from "../../src/flag/flag"
+import { ModelID, ProviderID } from "../../src/provider/schema"
+import { Instance } from "../../src/project/instance"
+import { Server } from "../../src/server/server"
+import { Session } from "../../src/session"
+import { MessageID, PartID, type SessionID } from "../../src/session/schema"
+import { Database, asc, eq } from "../../src/storage/db"
+import { SyncEvent } from "../../src/sync"
+import { EventTable } from "../../src/sync/event.sql"
+import { Log } from "../../src/util/log"
+import { resetDatabase } from "../fixture/db"
+import { tmpdir } from "../fixture/fixture"
+
+Log.init({ print: false })
+
+const original = Flag.OPENCODE_EXPERIMENTAL_WORKSPACES
+
+beforeEach(() => {
+  Database.close()
+  // @ts-expect-error test override
+  Flag.OPENCODE_EXPERIMENTAL_WORKSPACES = true
+})
+
+afterEach(async () => {
+  mock.restore()
+  // @ts-expect-error test override
+  Flag.OPENCODE_EXPERIMENTAL_WORKSPACES = original
+  await resetDatabase()
+})
+
+async function user(sessionID: SessionID, text: string) {
+  const msg = await Session.updateMessage({
+    id: MessageID.ascending(),
+    role: "user",
+    sessionID,
+    agent: "build",
+    model: { providerID: ProviderID.make("test"), modelID: ModelID.make("test") },
+    time: { created: Date.now() },
+  })
+  await Session.updatePart({
+    id: PartID.ascending(),
+    sessionID,
+    messageID: msg.id,
+    type: "text",
+    text,
+  })
+}
+
+function remote(dir: string, url: string): WorkspaceAdaptor {
+  return {
+    name: "remote",
+    description: "remote",
+    configure(info) {
+      return {
+        ...info,
+        directory: dir,
+      }
+    },
+    async create() {
+      await fs.mkdir(dir, { recursive: true })
+    },
+    async remove() {},
+    target() {
+      return {
+        type: "remote" as const,
+        url,
+      }
+    },
+  }
+}
+
+function local(dir: string): WorkspaceAdaptor {
+  return {
+    name: "local",
+    description: "local",
+    configure(info) {
+      return {
+        ...info,
+        directory: dir,
+      }
+    },
+    async create() {
+      await fs.mkdir(dir, { recursive: true })
+    },
+    async remove() {},
+    target() {
+      return {
+        type: "local" as const,
+        directory: dir,
+      }
+    },
+  }
+}
+
+describe("workspace restore route", () => {
+  test("replays session events in batches of 10 and emits progress", async () => {
+    await using tmp = await tmpdir({ git: true })
+    const app = Server.Default().app
+    const dir = path.join(tmp.path, ".restore")
+    const seen: any[] = []
+    const posts: Array<{
+      path: string
+      body: { directory: string; events: Array<{ seq: number; aggregateID: string }> }
+    }> = []
+    const on = (evt: any) => seen.push(evt)
+    GlobalBus.on("event", on)
+
+    const raw = globalThis.fetch
+    const fetch = spyOn(globalThis, "fetch").mockImplementation(
+      Object.assign(
+        async (input: URL | RequestInfo, init?: BunFetchRequestInit | RequestInit) => {
+          const url = new URL(typeof input === "string" || input instanceof URL ? input : input.url)
+          posts.push({
+            path: url.pathname,
+            body: JSON.parse(String(init?.body)),
+          })
+          return Response.json({ sessionID: posts.at(-1)!.body.events[0].aggregateID })
+        },
+        {
+          preconnect: raw.preconnect?.bind(raw),
+        },
+      ) as typeof globalThis.fetch,
+    )
+
+    try {
+      const setup = await Instance.provide({
+        directory: tmp.path,
+        fn: async () => {
+          registerAdaptor(Instance.project.id, "worktree", remote(dir, "https://workspace.test/base"))
+          const space = await Workspace.create({
+            type: "worktree",
+            branch: null,
+            extra: null,
+            projectID: Instance.project.id,
+          })
+          const session = await Session.create({})
+          for (let i = 0; i < 6; i++) {
+            await user(session.id, `msg ${i}`)
+          }
+          const rows = Database.use((db) =>
+            db
+              .select({ seq: EventTable.seq })
+              .from(EventTable)
+              .where(eq(EventTable.aggregate_id, session.id))
+              .orderBy(asc(EventTable.seq))
+              .all(),
+          )
+          return { space, session, rows }
+        },
+      })
+
+      expect(setup.rows).toHaveLength(13)
+
+      const res = await app.request(`/experimental/workspace/${setup.space.id}/session-restore`, {
+        method: "POST",
+        headers: {
+          "content-type": "application/json",
+          "x-opencode-directory": tmp.path,
+        },
+        body: JSON.stringify({
+          sessionID: setup.session.id,
+        }),
+      })
+
+      expect(fetch).toHaveBeenCalledTimes(2)
+      expect(res.status).toBe(200)
+      expect(await res.json()).toEqual({ total: 2 })
+      expect(posts).toHaveLength(2)
+      expect(posts[0]?.path).toBe("/base/sync/replay")
+      expect(posts[1]?.path).toBe("/base/sync/replay")
+      expect(posts[0]?.body.directory).toBe(dir)
+      expect(posts[1]?.body.directory).toBe(dir)
+      expect(posts[0]?.body.events).toHaveLength(10)
+      expect(posts[1]?.body.events).toHaveLength(4)
+      expect(posts.flatMap((item) => item.body.events.map((event) => event.seq))).toEqual([
+        ...setup.rows.map((row) => row.seq),
+        setup.rows.at(-1)!.seq + 1,
+      ])
+      expect(posts[1]?.body.events.at(-1)).toMatchObject({
+        aggregateID: setup.session.id,
+        seq: setup.rows.at(-1)!.seq + 1,
+        type: SyncEvent.versionedType(Session.Event.Updated.type, Session.Event.Updated.version),
+        data: {
+          sessionID: setup.session.id,
+          info: {
+            workspaceID: setup.space.id,
+          },
+        },
+      })
+
+      const restore = seen.filter(
+        (evt) => evt.workspace === setup.space.id && evt.payload.type === Workspace.Event.Restore.type,
+      )
+      expect(restore.map((evt) => evt.payload.properties.step)).toEqual([0, 1, 2])
+      expect(restore.map((evt) => evt.payload.properties.total)).toEqual([2, 2, 2])
+      expect(restore.map((evt) => evt.payload.properties.sessionID)).toEqual([
+        setup.session.id,
+        setup.session.id,
+        setup.session.id,
+      ])
+    } finally {
+      GlobalBus.off("event", on)
+    }
+  })
+
+  test("replays locally without posting to a server", async () => {
+    await using tmp = await tmpdir({ git: true })
+    const app = Server.Default().app
+    const dir = path.join(tmp.path, ".restore-local")
+    const seen: any[] = []
+    const on = (evt: any) => seen.push(evt)
+    GlobalBus.on("event", on)
+
+    const fetch = spyOn(globalThis, "fetch")
+    const replayAll = spyOn(SyncEvent, "replayAll")
+
+    try {
+      const setup = await Instance.provide({
+        directory: tmp.path,
+        fn: async () => {
+          registerAdaptor(Instance.project.id, "local-restore", local(dir))
+          const space = await Workspace.create({
+            type: "local-restore",
+            branch: null,
+            extra: null,
+            projectID: Instance.project.id,
+          })
+          const session = await Session.create({})
+          for (let i = 0; i < 6; i++) {
+            await user(session.id, `msg ${i}`)
+          }
+          return { space, session }
+        },
+      })
+
+      const res = await app.request(`/experimental/workspace/${setup.space.id}/session-restore`, {
+        method: "POST",
+        headers: {
+          "content-type": "application/json",
+          "x-opencode-directory": tmp.path,
+        },
+        body: JSON.stringify({
+          sessionID: setup.session.id,
+        }),
+      })
+
+      expect(res.status).toBe(200)
+      expect(await res.json()).toEqual({ total: 2 })
+      expect(fetch).not.toHaveBeenCalled()
+      expect(replayAll).toHaveBeenCalledTimes(2)
+      expect((await Session.get(setup.session.id)).workspaceID).toBe(setup.space.id)
+
+      const restore = seen.filter(
+        (evt) => evt.workspace === setup.space.id && evt.payload.type === Workspace.Event.Restore.type,
+      )
+      expect(restore.map((evt) => evt.payload.properties.step)).toEqual([0, 1, 2])
+    } finally {
+      GlobalBus.off("event", on)
+    }
+  })
+})

+ 70 - 0
packages/opencode/test/sync/index.test.ts

@@ -9,6 +9,7 @@ import { EventTable } from "../../src/sync/event.sql"
 import { Identifier } from "../../src/id/id"
 import { Flag } from "../../src/flag/flag"
 import { initProjectors } from "../../src/server/projectors"
+import { SyncRoutes } from "../../src/server/routes/sync"
 
 const original = Flag.OPENCODE_EXPERIMENTAL_WORKSPACES
 
@@ -187,5 +188,74 @@ describe("SyncEvent", () => {
         ).toThrow(/Unknown event type/)
       }),
     )
+
+    test(
+      "sync route accepts later chunks after the first batch",
+      withInstance(async () => {
+        const { Created } = setup()
+        const app = SyncRoutes()
+        const id = Identifier.descending("message")
+
+        const one = await app.request("/replay", {
+          method: "POST",
+          headers: {
+            "content-type": "application/json",
+          },
+          body: JSON.stringify({
+            directory: "/tmp/test",
+            events: [
+              {
+                id: "evt_1",
+                type: SyncEvent.versionedType(Created.type, Created.version),
+                seq: 0,
+                aggregateID: id,
+                data: { id, name: "first" },
+              },
+              {
+                id: "evt_2",
+                type: SyncEvent.versionedType(Created.type, Created.version),
+                seq: 1,
+                aggregateID: id,
+                data: { id, name: "second" },
+              },
+            ],
+          }),
+        })
+
+        const two = await app.request("/replay", {
+          method: "POST",
+          headers: {
+            "content-type": "application/json",
+          },
+          body: JSON.stringify({
+            directory: "/tmp/test",
+            events: [
+              {
+                id: "evt_3",
+                type: SyncEvent.versionedType(Created.type, Created.version),
+                seq: 2,
+                aggregateID: id,
+                data: { id, name: "third" },
+              },
+              {
+                id: "evt_4",
+                type: SyncEvent.versionedType(Created.type, Created.version),
+                seq: 3,
+                aggregateID: id,
+                data: { id, name: "fourth" },
+              },
+            ],
+          }),
+        })
+
+        expect(one.status).toBe(200)
+        expect(await one.json()).toEqual({ sessionID: id })
+        expect(two.status).toBe(200)
+        expect(await two.json()).toEqual({ sessionID: id })
+
+        const rows = Database.use((db) => db.select().from(EventTable).all())
+        expect(rows.map((row) => row.seq)).toEqual([0, 1, 2, 3])
+      }),
+    )
   })
 })

+ 149 - 2
packages/sdk/js/src/v2/gen/sdk.gen.ts

@@ -35,6 +35,8 @@ import type {
   ExperimentalWorkspaceListResponses,
   ExperimentalWorkspaceRemoveErrors,
   ExperimentalWorkspaceRemoveResponses,
+  ExperimentalWorkspaceSessionRestoreErrors,
+  ExperimentalWorkspaceSessionRestoreResponses,
   ExperimentalWorkspaceStatusResponses,
   FileListResponses,
   FilePartInput,
@@ -51,6 +53,11 @@ import type {
   GlobalDisposeResponses,
   GlobalEventResponses,
   GlobalHealthResponses,
+  GlobalSyncEventSubscribeResponses,
+  GlobalSyncHistoryListErrors,
+  GlobalSyncHistoryListResponses,
+  GlobalSyncReplayErrors,
+  GlobalSyncReplayResponses,
   GlobalUpgradeErrors,
   GlobalUpgradeResponses,
   InstanceDisposeResponses,
@@ -236,6 +243,20 @@ class HeyApiRegistry<T> {
   }
 }
 
+export class SyncEvent extends HeyApiClient {
+  /**
+   * Subscribe to global sync events
+   *
+   * Get global sync events
+   */
+  public subscribe<ThrowOnError extends boolean = false>(options?: Options<never, ThrowOnError>) {
+    return (options?.client ?? this.client).sse.get<GlobalSyncEventSubscribeResponses, unknown, ThrowOnError>({
+      url: "/global/sync-event",
+      ...options,
+    })
+  }
+}
+
 export class Config extends HeyApiClient {
   /**
    * Get global configuration
@@ -274,6 +295,38 @@ export class Config extends HeyApiClient {
   }
 }
 
+export class SyncHistory extends HeyApiClient {
+  /**
+   * List sync events
+   *
+   * List sync events for all aggregates. Keys are aggregate IDs the client already knows about, values are the last known sequence ID. Events with seq > value are returned for those aggregates. Aggregates not listed in the input get their full history.
+   */
+  public list<ThrowOnError extends boolean = false>(
+    parameters?: {
+      body?: {
+        [key: string]: number
+      }
+    },
+    options?: Options<never, ThrowOnError>,
+  ) {
+    const params = buildClientParams([parameters], [{ args: [{ key: "body", map: "body" }] }])
+    return (options?.client ?? this.client).get<
+      GlobalSyncHistoryListResponses,
+      GlobalSyncHistoryListErrors,
+      ThrowOnError
+    >({
+      url: "/sync/history",
+      ...options,
+      ...params,
+      headers: {
+        "Content-Type": "application/json",
+        ...options?.headers,
+        ...params.headers,
+      },
+    })
+  }
+}
+
 export class Global extends HeyApiClient {
   /**
    * Get health
@@ -335,10 +388,63 @@ export class Global extends HeyApiClient {
     })
   }
 
+  /**
+   * Replay sync events
+   *
+   * Validate and replay a complete sync event history.
+   */
+  public syncReplay<ThrowOnError extends boolean = false>(
+    parameters?: {
+      directory?: string
+      events?: Array<{
+        id: string
+        aggregateID: string
+        seq: number
+        type: string
+        data: {
+          [key: string]: unknown
+        }
+      }>
+    },
+    options?: Options<never, ThrowOnError>,
+  ) {
+    const params = buildClientParams(
+      [parameters],
+      [
+        {
+          args: [
+            { in: "body", key: "directory" },
+            { in: "body", key: "events" },
+          ],
+        },
+      ],
+    )
+    return (options?.client ?? this.client).post<GlobalSyncReplayResponses, GlobalSyncReplayErrors, ThrowOnError>({
+      url: "/sync/replay",
+      ...options,
+      ...params,
+      headers: {
+        "Content-Type": "application/json",
+        ...options?.headers,
+        ...params.headers,
+      },
+    })
+  }
+
+  private _syncEvent?: SyncEvent
+  get syncEvent(): SyncEvent {
+    return (this._syncEvent ??= new SyncEvent({ client: this.client }))
+  }
+
   private _config?: Config
   get config(): Config {
     return (this._config ??= new Config({ client: this.client }))
   }
+
+  private _syncHistory?: SyncHistory
+  get syncHistory(): SyncHistory {
+    return (this._syncHistory ??= new SyncHistory({ client: this.client }))
+  }
 }
 
 export class Auth extends HeyApiClient {
@@ -1243,6 +1349,49 @@ export class Workspace extends HeyApiClient {
     })
   }
 
+  /**
+   * Restore session into workspace
+   *
+   * Replay a session's sync events into the target workspace in batches.
+   */
+  public sessionRestore<ThrowOnError extends boolean = false>(
+    parameters: {
+      id: string
+      directory?: string
+      workspace?: string
+      sessionID?: string
+    },
+    options?: Options<never, ThrowOnError>,
+  ) {
+    const params = buildClientParams(
+      [parameters],
+      [
+        {
+          args: [
+            { in: "path", key: "id" },
+            { in: "query", key: "directory" },
+            { in: "query", key: "workspace" },
+            { in: "body", key: "sessionID" },
+          ],
+        },
+      ],
+    )
+    return (options?.client ?? this.client).post<
+      ExperimentalWorkspaceSessionRestoreResponses,
+      ExperimentalWorkspaceSessionRestoreErrors,
+      ThrowOnError
+    >({
+      url: "/experimental/workspace/{id}/session-restore",
+      ...options,
+      ...params,
+      headers: {
+        "Content-Type": "application/json",
+        ...options?.headers,
+        ...params.headers,
+      },
+    })
+  }
+
   private _adaptor?: Adaptor
   get adaptor(): Adaptor {
     return (this._adaptor ??= new Adaptor({ client: this.client }))
@@ -1743,7 +1892,6 @@ export class Session2 extends HeyApiClient {
       directory?: string
       workspace?: string
       title?: string
-      permission?: PermissionRuleset
       time?: {
         archived?: number
       }
@@ -1759,7 +1907,6 @@ export class Session2 extends HeyApiClient {
             { in: "query", key: "directory" },
             { in: "query", key: "workspace" },
             { in: "body", key: "title" },
-            { in: "body", key: "permission" },
             { in: "body", key: "time" },
           ],
         },

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

@@ -33,13 +33,6 @@ export type EventProjectUpdated = {
   properties: Project
 }
 
-export type EventServerInstanceDisposed = {
-  type: "server.instance.disposed"
-  properties: {
-    directory: string
-  }
-}
-
 export type EventInstallationUpdated = {
   type: "installation.updated"
   properties: {
@@ -54,6 +47,13 @@ export type EventInstallationUpdateAvailable = {
   }
 }
 
+export type EventServerInstanceDisposed = {
+  type: "server.instance.disposed"
+  properties: {
+    directory: string
+  }
+}
+
 export type EventServerConnected = {
   type: "server.connected"
   properties: {
@@ -68,21 +68,6 @@ export type EventGlobalDisposed = {
   }
 }
 
-export type EventFileEdited = {
-  type: "file.edited"
-  properties: {
-    file: string
-  }
-}
-
-export type EventFileWatcherUpdated = {
-  type: "file.watcher.updated"
-  properties: {
-    file: string
-    event: "add" | "change" | "unlink"
-  }
-}
-
 export type EventLspClientDiagnostics = {
   type: "lsp.client.diagnostics"
   properties: {
@@ -230,6 +215,140 @@ export type EventSessionError = {
   }
 }
 
+export type EventFileEdited = {
+  type: "file.edited"
+  properties: {
+    file: string
+  }
+}
+
+export type EventFileWatcherUpdated = {
+  type: "file.watcher.updated"
+  properties: {
+    file: string
+    event: "add" | "change" | "unlink"
+  }
+}
+
+export type EventVcsBranchUpdated = {
+  type: "vcs.branch.updated"
+  properties: {
+    branch?: string
+  }
+}
+
+export type EventTuiPromptAppend = {
+  type: "tui.prompt.append"
+  properties: {
+    text: string
+  }
+}
+
+export type EventTuiCommandExecute = {
+  type: "tui.command.execute"
+  properties: {
+    command:
+      | "session.list"
+      | "session.new"
+      | "session.share"
+      | "session.interrupt"
+      | "session.compact"
+      | "session.page.up"
+      | "session.page.down"
+      | "session.line.up"
+      | "session.line.down"
+      | "session.half.page.up"
+      | "session.half.page.down"
+      | "session.first"
+      | "session.last"
+      | "prompt.clear"
+      | "prompt.submit"
+      | "agent.cycle"
+      | string
+  }
+}
+
+export type EventTuiToastShow = {
+  type: "tui.toast.show"
+  properties: {
+    title?: string
+    message: string
+    variant: "info" | "success" | "warning" | "error"
+    /**
+     * Duration in milliseconds
+     */
+    duration?: number
+  }
+}
+
+export type EventTuiSessionSelect = {
+  type: "tui.session.select"
+  properties: {
+    /**
+     * Session ID to navigate to
+     */
+    sessionID: string
+  }
+}
+
+export type EventMcpToolsChanged = {
+  type: "mcp.tools.changed"
+  properties: {
+    server: string
+  }
+}
+
+export type EventMcpBrowserOpenFailed = {
+  type: "mcp.browser.open.failed"
+  properties: {
+    mcpName: string
+    url: string
+  }
+}
+
+export type EventCommandExecuted = {
+  type: "command.executed"
+  properties: {
+    name: string
+    sessionID: string
+    arguments: string
+    messageID: string
+  }
+}
+
+export type EventWorkspaceReady = {
+  type: "workspace.ready"
+  properties: {
+    name: string
+  }
+}
+
+export type EventWorkspaceFailed = {
+  type: "workspace.failed"
+  properties: {
+    message: string
+  }
+}
+
+export type EventWorkspaceRestore = {
+  type: "workspace.restore"
+  properties: {
+    workspaceID: string
+    sessionID: string
+    total: number
+    step: number
+  }
+}
+
+export type EventWorkspaceStatus = {
+  type: "workspace.status"
+  properties: {
+    workspaceID: string
+    status: "connected" | "connecting" | "disconnected" | "error"
+    error?: string
+  }
+}
+
 export type QuestionOption = {
   /**
    * Display text (1-5 words, concise)
@@ -301,29 +420,6 @@ export type EventQuestionRejected = {
   }
 }
 
-export type Todo = {
-  /**
-   * Brief description of the task
-   */
-  content: string
-  /**
-   * Current status of the task: pending, in_progress, completed, cancelled
-   */
-  status: string
-  /**
-   * Priority level of the task: high, medium, low
-   */
-  priority: string
-}
-
-export type EventTodoUpdated = {
-  type: "todo.updated"
-  properties: {
-    sessionID: string
-    todos: Array<Todo>
-  }
-}
-
 export type SessionStatus =
   | {
       type: "idle"
@@ -360,104 +456,26 @@ export type EventSessionCompacted = {
   }
 }
 
-export type EventTuiPromptAppend = {
-  type: "tui.prompt.append"
-  properties: {
-    text: string
-  }
-}
-
-export type EventTuiCommandExecute = {
-  type: "tui.command.execute"
-  properties: {
-    command:
-      | "session.list"
-      | "session.new"
-      | "session.share"
-      | "session.interrupt"
-      | "session.compact"
-      | "session.page.up"
-      | "session.page.down"
-      | "session.line.up"
-      | "session.line.down"
-      | "session.half.page.up"
-      | "session.half.page.down"
-      | "session.first"
-      | "session.last"
-      | "prompt.clear"
-      | "prompt.submit"
-      | "agent.cycle"
-      | string
-  }
-}
-
-export type EventTuiToastShow = {
-  type: "tui.toast.show"
-  properties: {
-    title?: string
-    message: string
-    variant: "info" | "success" | "warning" | "error"
-    /**
-     * Duration in milliseconds
-     */
-    duration?: number
-  }
-}
-
-export type EventTuiSessionSelect = {
-  type: "tui.session.select"
-  properties: {
-    /**
-     * Session ID to navigate to
-     */
-    sessionID: string
-  }
-}
-
-export type EventMcpToolsChanged = {
-  type: "mcp.tools.changed"
-  properties: {
-    server: string
-  }
-}
-
-export type EventMcpBrowserOpenFailed = {
-  type: "mcp.browser.open.failed"
-  properties: {
-    mcpName: string
-    url: string
-  }
+export type Todo = {
+  /**
+   * Brief description of the task
+   */
+  content: string
+  /**
+   * Current status of the task: pending, in_progress, completed, cancelled
+   */
+  status: string
+  /**
+   * Priority level of the task: high, medium, low
+   */
+  priority: string
 }
 
-export type EventCommandExecuted = {
-  type: "command.executed"
+export type EventTodoUpdated = {
+  type: "todo.updated"
   properties: {
-    name: string
     sessionID: string
-    arguments: string
-    messageID: string
-  }
-}
-
-export type EventVcsBranchUpdated = {
-  type: "vcs.branch.updated"
-  properties: {
-    branch?: string
-  }
-}
-
-export type EventWorktreeReady = {
-  type: "worktree.ready"
-  properties: {
-    name: string
-    branch: string
-  }
-}
-
-export type EventWorktreeFailed = {
-  type: "worktree.failed"
-  properties: {
-    message: string
+    todos: Array<Todo>
   }
 }
 
@@ -500,29 +518,21 @@ export type EventPtyDeleted = {
   }
 }
 
-export type EventWorkspaceReady = {
-  type: "workspace.ready"
+export type EventWorktreeReady = {
+  type: "worktree.ready"
   properties: {
     name: string
+    branch: string
   }
 }
 
-export type EventWorkspaceFailed = {
-  type: "workspace.failed"
+export type EventWorktreeFailed = {
+  type: "worktree.failed"
   properties: {
     message: string
   }
 }
 
-export type EventWorkspaceStatus = {
-  type: "workspace.status"
-  properties: {
-    workspaceID: string
-    status: "connected" | "connecting" | "disconnected" | "error"
-    error?: string
-  }
-}
-
 export type OutputFormatText = {
   type: "text"
 }
@@ -971,12 +981,65 @@ export type EventSessionDeleted = {
   }
 }
 
+export type Event =
+  | EventProjectUpdated
+  | EventInstallationUpdated
+  | EventInstallationUpdateAvailable
+  | EventServerInstanceDisposed
+  | EventServerConnected
+  | EventGlobalDisposed
+  | EventLspClientDiagnostics
+  | EventLspUpdated
+  | EventMessagePartDelta
+  | EventPermissionAsked
+  | EventPermissionReplied
+  | EventSessionDiff
+  | EventSessionError
+  | EventFileEdited
+  | EventFileWatcherUpdated
+  | EventVcsBranchUpdated
+  | EventTuiPromptAppend
+  | EventTuiCommandExecute
+  | EventTuiToastShow
+  | EventTuiSessionSelect
+  | EventMcpToolsChanged
+  | EventMcpBrowserOpenFailed
+  | EventCommandExecuted
+  | EventWorkspaceReady
+  | EventWorkspaceFailed
+  | EventWorkspaceRestore
+  | EventWorkspaceStatus
+  | EventQuestionAsked
+  | EventQuestionReplied
+  | EventQuestionRejected
+  | EventSessionStatus
+  | EventSessionIdle
+  | EventSessionCompacted
+  | EventTodoUpdated
+  | EventPtyCreated
+  | EventPtyUpdated
+  | EventPtyExited
+  | EventPtyDeleted
+  | EventWorktreeReady
+  | EventWorktreeFailed
+  | EventMessageUpdated
+  | EventMessageRemoved
+  | EventMessagePartUpdated
+  | EventMessagePartRemoved
+  | EventSessionCreated
+  | EventSessionUpdated
+  | EventSessionDeleted
+
+export type GlobalEvent = {
+  directory: string
+  project?: string
+  workspace?: string
+  payload: Event
+}
+
 export type SyncEventMessageUpdated = {
-  type: "sync"
-  name: "message.updated.1"
-  id: string
-  seq: number
-  aggregateID: "sessionID"
+  type: "message.updated.1"
+  aggregate: "sessionID"
   data: {
     sessionID: string
     info: Message
@@ -984,11 +1047,8 @@ export type SyncEventMessageUpdated = {
 }
 
 export type SyncEventMessageRemoved = {
-  type: "sync"
-  name: "message.removed.1"
-  id: string
-  seq: number
-  aggregateID: "sessionID"
+  type: "message.removed.1"
+  aggregate: "sessionID"
   data: {
     sessionID: string
     messageID: string
@@ -996,11 +1056,8 @@ export type SyncEventMessageRemoved = {
 }
 
 export type SyncEventMessagePartUpdated = {
-  type: "sync"
-  name: "message.part.updated.1"
-  id: string
-  seq: number
-  aggregateID: "sessionID"
+  type: "message.part.updated.1"
+  aggregate: "sessionID"
   data: {
     sessionID: string
     part: Part
@@ -1009,11 +1066,8 @@ export type SyncEventMessagePartUpdated = {
 }
 
 export type SyncEventMessagePartRemoved = {
-  type: "sync"
-  name: "message.part.removed.1"
-  id: string
-  seq: number
-  aggregateID: "sessionID"
+  type: "message.part.removed.1"
+  aggregate: "sessionID"
   data: {
     sessionID: string
     messageID: string
@@ -1022,11 +1076,8 @@ export type SyncEventMessagePartRemoved = {
 }
 
 export type SyncEventSessionCreated = {
-  type: "sync"
-  name: "session.created.1"
-  id: string
-  seq: number
-  aggregateID: "sessionID"
+  type: "session.created.1"
+  aggregate: "sessionID"
   data: {
     sessionID: string
     info: Session
@@ -1034,11 +1085,8 @@ export type SyncEventSessionCreated = {
 }
 
 export type SyncEventSessionUpdated = {
-  type: "sync"
-  name: "session.updated.1"
-  id: string
-  seq: number
-  aggregateID: "sessionID"
+  type: "session.updated.1"
+  aggregate: "sessionID"
   data: {
     sessionID: string
     info: {
@@ -1077,75 +1125,16 @@ export type SyncEventSessionUpdated = {
 }
 
 export type SyncEventSessionDeleted = {
-  type: "sync"
-  name: "session.deleted.1"
-  id: string
-  seq: number
-  aggregateID: "sessionID"
+  type: "session.deleted.1"
+  aggregate: "sessionID"
   data: {
     sessionID: string
     info: Session
   }
 }
 
-export type GlobalEvent = {
-  directory: string
-  project?: string
-  workspace?: string
-  payload:
-    | EventProjectUpdated
-    | EventServerInstanceDisposed
-    | EventInstallationUpdated
-    | EventInstallationUpdateAvailable
-    | EventServerConnected
-    | EventGlobalDisposed
-    | EventFileEdited
-    | EventFileWatcherUpdated
-    | EventLspClientDiagnostics
-    | EventLspUpdated
-    | EventMessagePartDelta
-    | EventPermissionAsked
-    | EventPermissionReplied
-    | EventSessionDiff
-    | EventSessionError
-    | EventQuestionAsked
-    | EventQuestionReplied
-    | EventQuestionRejected
-    | EventTodoUpdated
-    | EventSessionStatus
-    | EventSessionIdle
-    | EventSessionCompacted
-    | EventTuiPromptAppend
-    | EventTuiCommandExecute
-    | EventTuiToastShow
-    | EventTuiSessionSelect
-    | EventMcpToolsChanged
-    | EventMcpBrowserOpenFailed
-    | EventCommandExecuted
-    | EventVcsBranchUpdated
-    | EventWorktreeReady
-    | EventWorktreeFailed
-    | EventPtyCreated
-    | EventPtyUpdated
-    | EventPtyExited
-    | EventPtyDeleted
-    | EventWorkspaceReady
-    | EventWorkspaceFailed
-    | EventWorkspaceStatus
-    | EventMessageUpdated
-    | EventMessageRemoved
-    | EventMessagePartUpdated
-    | EventMessagePartRemoved
-    | EventSessionCreated
-    | EventSessionUpdated
-    | EventSessionDeleted
-    | SyncEventMessageUpdated
-    | SyncEventMessageRemoved
-    | SyncEventMessagePartUpdated
-    | SyncEventMessagePartRemoved
-    | SyncEventSessionCreated
-    | SyncEventSessionUpdated
-    | SyncEventSessionDeleted
+export type SyncEvent = {
+  payload: SyncEvent
 }
 
 /**
@@ -2004,54 +1993,6 @@ export type File = {
   status: "added" | "deleted" | "modified"
 }
 
-export type Event =
-  | EventProjectUpdated
-  | EventServerInstanceDisposed
-  | EventInstallationUpdated
-  | EventInstallationUpdateAvailable
-  | EventServerConnected
-  | EventGlobalDisposed
-  | EventFileEdited
-  | EventFileWatcherUpdated
-  | EventLspClientDiagnostics
-  | EventLspUpdated
-  | EventMessagePartDelta
-  | EventPermissionAsked
-  | EventPermissionReplied
-  | EventSessionDiff
-  | EventSessionError
-  | EventQuestionAsked
-  | EventQuestionReplied
-  | EventQuestionRejected
-  | EventTodoUpdated
-  | EventSessionStatus
-  | EventSessionIdle
-  | EventSessionCompacted
-  | EventTuiPromptAppend
-  | EventTuiCommandExecute
-  | EventTuiToastShow
-  | EventTuiSessionSelect
-  | EventMcpToolsChanged
-  | EventMcpBrowserOpenFailed
-  | EventCommandExecuted
-  | EventVcsBranchUpdated
-  | EventWorktreeReady
-  | EventWorktreeFailed
-  | EventPtyCreated
-  | EventPtyUpdated
-  | EventPtyExited
-  | EventPtyDeleted
-  | EventWorkspaceReady
-  | EventWorkspaceFailed
-  | EventWorkspaceStatus
-  | EventMessageUpdated
-  | EventMessageRemoved
-  | EventMessagePartUpdated
-  | EventMessagePartRemoved
-  | EventSessionCreated
-  | EventSessionUpdated
-  | EventSessionDeleted
-
 export type McpStatusConnected = {
   status: "connected"
 }
@@ -2183,6 +2124,23 @@ export type GlobalEventResponses = {
 
 export type GlobalEventResponse = GlobalEventResponses[keyof GlobalEventResponses]
 
+export type GlobalSyncEventSubscribeData = {
+  body?: never
+  path?: never
+  query?: never
+  url: "/global/sync-event"
+}
+
+export type GlobalSyncEventSubscribeResponses = {
+  /**
+   * Event stream
+   */
+  200: SyncEvent
+}
+
+export type GlobalSyncEventSubscribeResponse =
+  GlobalSyncEventSubscribeResponses[keyof GlobalSyncEventSubscribeResponses]
+
 export type GlobalConfigGetData = {
   body?: never
   path?: never
@@ -2275,6 +2233,79 @@ export type GlobalUpgradeResponses = {
 
 export type GlobalUpgradeResponse = GlobalUpgradeResponses[keyof GlobalUpgradeResponses]
 
+export type GlobalSyncReplayData = {
+  body?: {
+    directory: string
+    events: Array<{
+      id: string
+      aggregateID: string
+      seq: number
+      type: string
+      data: {
+        [key: string]: unknown
+      }
+    }>
+  }
+  path?: never
+  query?: never
+  url: "/sync/replay"
+}
+
+export type GlobalSyncReplayErrors = {
+  /**
+   * Bad request
+   */
+  400: BadRequestError
+}
+
+export type GlobalSyncReplayError = GlobalSyncReplayErrors[keyof GlobalSyncReplayErrors]
+
+export type GlobalSyncReplayResponses = {
+  /**
+   * Replayed sync events
+   */
+  200: {
+    sessionID: string
+  }
+}
+
+export type GlobalSyncReplayResponse = GlobalSyncReplayResponses[keyof GlobalSyncReplayResponses]
+
+export type GlobalSyncHistoryListData = {
+  body?: {
+    [key: string]: number
+  }
+  path?: never
+  query?: never
+  url: "/sync/history"
+}
+
+export type GlobalSyncHistoryListErrors = {
+  /**
+   * Bad request
+   */
+  400: BadRequestError
+}
+
+export type GlobalSyncHistoryListError = GlobalSyncHistoryListErrors[keyof GlobalSyncHistoryListErrors]
+
+export type GlobalSyncHistoryListResponses = {
+  /**
+   * Sync events
+   */
+  200: Array<{
+    id: string
+    aggregate_id: string
+    seq: number
+    type: string
+    data: {
+      [key: string]: unknown
+    }
+  }>
+}
+
+export type GlobalSyncHistoryListResponse = GlobalSyncHistoryListResponses[keyof GlobalSyncHistoryListResponses]
+
 export type AuthRemoveData = {
   body?: never
   path: {
@@ -3000,6 +3031,42 @@ export type ExperimentalWorkspaceRemoveResponses = {
 export type ExperimentalWorkspaceRemoveResponse =
   ExperimentalWorkspaceRemoveResponses[keyof ExperimentalWorkspaceRemoveResponses]
 
+export type ExperimentalWorkspaceSessionRestoreData = {
+  body?: {
+    sessionID: string
+  }
+  path: {
+    id: string
+  }
+  query?: {
+    directory?: string
+    workspace?: string
+  }
+  url: "/experimental/workspace/{id}/session-restore"
+}
+
+export type ExperimentalWorkspaceSessionRestoreErrors = {
+  /**
+   * Bad request
+   */
+  400: BadRequestError
+}
+
+export type ExperimentalWorkspaceSessionRestoreError =
+  ExperimentalWorkspaceSessionRestoreErrors[keyof ExperimentalWorkspaceSessionRestoreErrors]
+
+export type ExperimentalWorkspaceSessionRestoreResponses = {
+  /**
+   * Session replay started
+   */
+  200: {
+    total: number
+  }
+}
+
+export type ExperimentalWorkspaceSessionRestoreResponse =
+  ExperimentalWorkspaceSessionRestoreResponses[keyof ExperimentalWorkspaceSessionRestoreResponses]
+
 export type WorktreeRemoveData = {
   body?: WorktreeRemoveInput
   path?: never
@@ -3343,7 +3410,6 @@ export type SessionGetResponse = SessionGetResponses[keyof SessionGetResponses]
 export type SessionUpdateData = {
   body?: {
     title?: string
-    permission?: PermissionRuleset
     time?: {
       archived?: number
     }

+ 189 - 0
sync-routes.ts

@@ -0,0 +1,189 @@
+import z from "zod"
+import { Hono } from "hono"
+import { describeRoute, validator, resolver } from "hono-openapi"
+import { SyncEvent } from "@/sync"
+import { Database, asc, and, not, or, lte, eq } from "@/storage/db"
+import { EventTable } from "@/sync/event.sql"
+import { Log } from "@/util/log"
+import { lazy } from "@/util/lazy"
+import { Instance } from "@/project/instance"
+import { InstanceBootstrap } from "../../project/bootstrap"
+import { errors } from "../error"
+import { streamQueue } from "../stream-queue"
+
+const log = Log.create({ service: "server" })
+
+const ReplayEvent = z.object({
+  id: z.string(),
+  aggregateID: z.string(),
+  seq: z.number().int().min(0),
+  type: z.string(),
+  data: z.record(z.string(), z.unknown()),
+})
+
+export const SyncRoutes = lazy(() =>
+  new Hono()
+    .get(
+      "/event",
+      describeRoute({
+        summary: "Subscribe to sync events",
+        description: "Get sync events",
+        operationId: "sync.event",
+        responses: {
+          200: {
+            description: "Event stream",
+            content: {
+              "text/event-stream": {
+                schema: resolver(
+                  z
+                    .object({
+                      payload: SyncEvent.payloads(),
+                    })
+                    .meta({
+                      ref: "SyncEvent",
+                    }),
+                ),
+              },
+            },
+          },
+        },
+      }),
+      async (c) => {
+        log.info("sync event connected")
+        c.header("X-Accel-Buffering", "no")
+        c.header("X-Content-Type-Options", "nosniff")
+        return streamQueue(c, {
+          connect: (q) => {
+            log.info("sync event connected")
+
+            q.push(
+              JSON.stringify({
+                type: "server.connected",
+                properties: {},
+              }),
+            )
+          },
+          heartbeat: (q) => {
+            q.push(
+              JSON.stringify({
+                type: "server.heartbeat",
+                properties: {},
+              }),
+            )
+          },
+
+          subscribe: (q) => {
+            const unsub = SyncEvent.subscribeAll(({ def, event }) => {
+              q.push(JSON.stringify({ ...event, type: SyncEvent.versionedType(def.type, def.version) }))
+            })
+
+            return () => {
+              unsub()
+              log.info("sync event disconnected")
+            }
+          },
+        })
+      },
+    )
+    .post(
+      "/replay",
+      describeRoute({
+        summary: "Replay sync events",
+        description: "Validate and replay a complete sync event history.",
+        operationId: "global.sync-replay",
+        responses: {
+          200: {
+            description: "Replayed sync events",
+            content: {
+              "application/json": {
+                schema: resolver(
+                  z.object({
+                    sessionID: z.string(),
+                  }),
+                ),
+              },
+            },
+          },
+          ...errors(400),
+        },
+      }),
+      validator(
+        "json",
+        z.object({
+          directory: z.string(),
+          events: z.array(ReplayEvent).min(1),
+        }),
+      ),
+      async (c) => {
+        const body = c.req.valid("json")
+        const events = body.events
+        const source = events[0].aggregateID
+        if (events.some((item) => item.aggregateID !== source)) {
+          throw new Error("Replay events must belong to the same session")
+        }
+        for (const [i, item] of events.entries()) {
+          if (item.seq !== i) throw new Error(`Replay sequence mismatch at index ${i}: expected ${i}, got ${item.seq}`)
+        }
+
+        return Instance.provide({
+          directory: body.directory,
+          init: InstanceBootstrap,
+          async fn() {
+            for (const item of events) {
+              SyncEvent.replay(item)
+            }
+            return c.json({ sessionID: source })
+          },
+        })
+      },
+    )
+    .get(
+      "/history",
+      describeRoute({
+        summary: "List sync events",
+        description: "List sync events for all aggregates. Keys are aggregate IDs the client already knows about, values are the last known sequence ID. Events with seq > value are returned for those aggregates. Aggregates not listed in the input get their full history.",
+        operationId: "global.sync-history.list",
+        responses: {
+          200: {
+            description: "Sync events",
+            content: {
+              "application/json": {
+                schema: resolver(
+                  z.array(
+                    z.object({
+                      id: z.string(),
+                      aggregate_id: z.string(),
+                      seq: z.number(),
+                      type: z.string(),
+                      data: z.record(z.string(), z.unknown()),
+                    }),
+                  ),
+                ),
+              },
+            },
+          },
+          ...errors(400),
+        },
+      }),
+      validator(
+        "json",
+        z.record(z.string(), z.number().int().min(0)),
+      ),
+      async (c) => {
+        const body = c.req.valid("json")
+        const exclude = Object.entries(body)
+        const where = exclude.length > 0
+          ? not(or(...exclude.map(([id, seq]) => and(eq(EventTable.aggregate_id, id), lte(EventTable.seq, seq))))!)
+          : undefined
+        const rows = Database.use((db) =>
+          db
+            .select()
+            .from(EventTable)
+            .where(where)
+            .orderBy(asc(EventTable.seq))
+            .all(),
+        )
+        return c.json(rows)
+      },
+    ),
+)