| author | |
| committer | |
| log | e6330ead82319d2be53beb5de3aaf4eda0d91a70 |
| tree | 88eee34595f8f62acbd219e810a6a08acf1d60b6 |
| parent | 4f0f24f055fe5f5346f7e73372e8cdc167e052f9 |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: codex <codex@users.noreply.github.com>48 files changed, 1341 insertions(+), 1103 deletions(-)
.github/workflows/ci.yml+8| ... | @@ -46,6 +46,14 @@ jobs: | ... | @@ -46,6 +46,14 @@ jobs: |
| 46 | - name: Install dependencies | 46 | - name: Install dependencies |
| 47 | run: bun install --frozen-lockfile | 47 | run: bun install --frozen-lockfile |
| 48 | 48 | ||
| 49 | - name: Ensure Electron runtime is installed | ||
| 50 | run: | | ||
| 51 | if ! node -e "require('./apps/desktop/node_modules/electron')" >/dev/null 2>&1; then | ||
| 52 | rm -rf apps/desktop/node_modules/electron/dist apps/desktop/node_modules/electron/path.txt | ||
| 53 | bun apps/desktop/node_modules/electron/install.js | ||
| 54 | fi | ||
| 55 | node -e "require('./apps/desktop/node_modules/electron')" | ||
| 56 | |||
| 49 | - name: Format | 57 | - name: Format |
| 50 | run: bun run fmt:check | 58 | run: bun run fmt:check |
| 51 | 59 |
apps/desktop/src/app/DesktopApp.ts+3-2| ... | @@ -1,8 +1,8 @@ | ... | @@ -1,8 +1,8 @@ |
| 1 | import * as Cause from "effect/Cause"; | 1 | import * as Cause from "effect/Cause"; |
| 2 | import * as Crypto from "effect/Crypto"; | ||
| 2 | import * as Data from "effect/Data"; | 3 | import * as Data from "effect/Data"; |
| 3 | import * as Effect from "effect/Effect"; | 4 | import * as Effect from "effect/Effect"; |
| 4 | import * as Option from "effect/Option"; | 5 | import * as Option from "effect/Option"; |
| 5 | import * as Random from "effect/Random"; | ||
| 6 | import * as Ref from "effect/Ref"; | 6 | import * as Ref from "effect/Ref"; |
| 7 | 7 | ||
| 8 | import * as NetService from "@t3tools/shared/Net"; | 8 | import * as NetService from "@t3tools/shared/Net"; |
| ... | @@ -26,7 +26,8 @@ const DEFAULT_DESKTOP_BACKEND_PORT = 3773; | ... | @@ -26,7 +26,8 @@ const DEFAULT_DESKTOP_BACKEND_PORT = 3773; |
| 26 | const MAX_TCP_PORT = 65_535; | 26 | const MAX_TCP_PORT = 65_535; |
| 27 | const DESKTOP_BACKEND_PORT_PROBE_HOSTS = ["127.0.0.1", "0.0.0.0", "::"] as const; | 27 | const DESKTOP_BACKEND_PORT_PROBE_HOSTS = ["127.0.0.1", "0.0.0.0", "::"] as const; |
| 28 | 28 | ||
| 29 | const makeDesktopRunId = Random.nextUUIDv4.pipe( | 29 | const makeDesktopRunId = Crypto.Crypto.pipe( |
| 30 | Effect.flatMap((crypto) => crypto.randomUUIDv4), | ||
| 30 | Effect.map((value) => value.replaceAll("-", "").slice(0, 12)), | 31 | Effect.map((value) => value.replaceAll("-", "").slice(0, 12)), |
| 31 | ); | 32 | ); |
| 32 | 33 |
apps/desktop/src/backend/DesktopBackendConfiguration.ts+19-20| ... | @@ -1,10 +1,12 @@ | ... | @@ -1,10 +1,12 @@ |
| 1 | import { parsePersistedServerObservabilitySettings } from "@t3tools/shared/serverSettings"; | 1 | import { parsePersistedServerObservabilitySettings } from "@t3tools/shared/serverSettings"; |
| 2 | import * as Context from "effect/Context"; | 2 | import * as Context from "effect/Context"; |
| 3 | import * as Crypto from "effect/Crypto"; | ||
| 3 | import * as Effect from "effect/Effect"; | 4 | import * as Effect from "effect/Effect"; |
| 5 | import * as Encoding from "effect/Encoding"; | ||
| 4 | import * as FileSystem from "effect/FileSystem"; | 6 | import * as FileSystem from "effect/FileSystem"; |
| 5 | import * as Layer from "effect/Layer"; | 7 | import * as Layer from "effect/Layer"; |
| 6 | import * as Option from "effect/Option"; | 8 | import * as Option from "effect/Option"; |
| 7 | import * as Random from "effect/Random"; | 9 | import * as PlatformError from "effect/PlatformError"; |
| 8 | import * as Ref from "effect/Ref"; | 10 | import * as Ref from "effect/Ref"; |
| 9 | 11 | ||
| 10 | import * as DesktopBackendManager from "./DesktopBackendManager.ts"; | 12 | import * as DesktopBackendManager from "./DesktopBackendManager.ts"; |
| ... | @@ -13,7 +15,10 @@ import * as DesktopObservability from "../app/DesktopObservability.ts"; | ... | @@ -13,7 +15,10 @@ import * as DesktopObservability from "../app/DesktopObservability.ts"; |
| 13 | import * as DesktopServerExposure from "./DesktopServerExposure.ts"; | 15 | import * as DesktopServerExposure from "./DesktopServerExposure.ts"; |
| 14 | 16 | ||
| 15 | export interface DesktopBackendConfigurationShape { | 17 | export interface DesktopBackendConfigurationShape { |
| 16 | readonly resolve: Effect.Effect<DesktopBackendManager.DesktopBackendStartConfig>; | 18 | readonly resolve: Effect.Effect< |
| 19 | DesktopBackendManager.DesktopBackendStartConfig, | ||
| 20 | PlatformError.PlatformError | ||
| 21 | >; | ||
| 17 | } | 22 | } |
| 18 | 23 | ||
| 19 | export class DesktopBackendConfiguration extends Context.Service< | 24 | export class DesktopBackendConfiguration extends Context.Service< |
| ... | @@ -80,23 +85,6 @@ const readPersistedBackendObservabilitySettings: Effect.Effect< | ... | @@ -80,23 +85,6 @@ const readPersistedBackendObservabilitySettings: Effect.Effect< |
| 80 | }; | 85 | }; |
| 81 | }); | 86 | }); |
| 82 | 87 | ||
| 83 | const getOrCreateBootstrapToken = Effect.fn("desktop.backendConfiguration.bootstrapToken")( | ||
| 84 | function* (tokenRef: Ref.Ref<Option.Option<string>>) { | ||
| 85 | const existing = yield* Ref.get(tokenRef); | ||
| 86 | if (Option.isSome(existing)) { | ||
| 87 | return existing.value; | ||
| 88 | } | ||
| 89 | |||
| 90 | let token = ""; | ||
| 91 | while (token.length < 48) { | ||
| 92 | token += (yield* Random.nextUUIDv4).replace(/-/g, ""); | ||
| 93 | } | ||
| 94 | token = token.slice(0, 48); | ||
| 95 | yield* Ref.set(tokenRef, Option.some(token)); | ||
| 96 | return token; | ||
| 97 | }, | ||
| 98 | ); | ||
| 99 | |||
| 100 | const resolveBackendStartConfig = Effect.fn("desktop.backendConfiguration.resolveStartConfig")( | 88 | const resolveBackendStartConfig = Effect.fn("desktop.backendConfiguration.resolveStartConfig")( |
| 101 | function* (input: { | 89 | function* (input: { |
| 102 | readonly bootstrapToken: string; | 90 | readonly bootstrapToken: string; |
| ... | @@ -148,11 +136,22 @@ export const layer = Layer.effect( | ... | @@ -148,11 +136,22 @@ export const layer = Layer.effect( |
| 148 | const environment = yield* DesktopEnvironment.DesktopEnvironment; | 136 | const environment = yield* DesktopEnvironment.DesktopEnvironment; |
| 149 | const fileSystem = yield* FileSystem.FileSystem; | 137 | const fileSystem = yield* FileSystem.FileSystem; |
| 150 | const serverExposure = yield* DesktopServerExposure.DesktopServerExposure; | 138 | const serverExposure = yield* DesktopServerExposure.DesktopServerExposure; |
| 139 | const crypto = yield* Crypto.Crypto; | ||
| 151 | const tokenRef = yield* Ref.make(Option.none<string>()); | 140 | const tokenRef = yield* Ref.make(Option.none<string>()); |
| 141 | const getOrCreateBootstrapToken = Effect.gen(function* () { | ||
| 142 | const existing = yield* Ref.get(tokenRef); | ||
| 143 | if (Option.isSome(existing)) { | ||
| 144 | return existing.value; | ||
| 145 | } | ||
| 146 | |||
| 147 | const token = Encoding.encodeHex(yield* crypto.randomBytes(24)); | ||
| 148 | yield* Ref.set(tokenRef, Option.some(token)); | ||
| 149 | return token; | ||
| 150 | }); | ||
| 152 | 151 | ||
| 153 | return DesktopBackendConfiguration.of({ | 152 | return DesktopBackendConfiguration.of({ |
| 154 | resolve: Effect.gen(function* () { | 153 | resolve: Effect.gen(function* () { |
| 155 | const bootstrapToken = yield* getOrCreateBootstrapToken(tokenRef); | 154 | const bootstrapToken = yield* getOrCreateBootstrapToken; |
| 156 | const observabilitySettings = yield* readPersistedBackendObservabilitySettings.pipe( | 155 | const observabilitySettings = yield* readPersistedBackendObservabilitySettings.pipe( |
| 157 | Effect.provideService(FileSystem.FileSystem, fileSystem), | 156 | Effect.provideService(FileSystem.FileSystem, fileSystem), |
| 158 | Effect.provideService(DesktopEnvironment.DesktopEnvironment, environment), | 157 | Effect.provideService(DesktopEnvironment.DesktopEnvironment, environment), |
apps/desktop/src/backend/DesktopBackendManager.ts+16-6| ... | @@ -329,9 +329,19 @@ const makeDesktopBackendManager = Effect.fn("makeDesktopBackendManager")(functio | ... | @@ -329,9 +329,19 @@ const makeDesktopBackendManager = Effect.fn("makeDesktopBackendManager")(functio |
| 329 | } | 329 | } |
| 330 | 330 | ||
| 331 | yield* Ref.set(desktopState.backendReady, false); | 331 | yield* Ref.set(desktopState.backendReady, false); |
| 332 | const config = yield* configuration.resolve; | 332 | const config = yield* configuration.resolve.pipe( |
| 333 | Effect.tapError((error) => | ||
| 334 | logBackendManagerError("failed to generate desktop backend configuration", { | ||
| 335 | cause: error.message, | ||
| 336 | }), | ||
| 337 | ), | ||
| 338 | Effect.option, | ||
| 339 | ); | ||
| 340 | if (Option.isNone(config)) { | ||
| 341 | return; | ||
| 342 | } | ||
| 333 | const entryExists = yield* fileSystem | 343 | const entryExists = yield* fileSystem |
| 334 | .exists(config.entryPath) | 344 | .exists(config.value.entryPath) |
| 335 | .pipe(Effect.orElseSucceed(() => false)); | 345 | .pipe(Effect.orElseSucceed(() => false)); |
| 336 | 346 | ||
| 337 | yield* cancelRestart; | 347 | yield* cancelRestart; |
| ... | @@ -339,11 +349,11 @@ const makeDesktopBackendManager = Effect.fn("makeDesktopBackendManager")(functio | ... | @@ -339,11 +349,11 @@ const makeDesktopBackendManager = Effect.fn("makeDesktopBackendManager")(functio |
| 339 | ...latest, | 349 | ...latest, |
| 340 | desiredRunning: true, | 350 | desiredRunning: true, |
| 341 | ready: false, | 351 | ready: false, |
| 342 | config: Option.some(config), | 352 | config: Option.some(config.value), |
| 343 | })); | 353 | })); |
| 344 | 354 | ||
| 345 | if (!entryExists) { | 355 | if (!entryExists) { |
| 346 | yield* scheduleRestart(`missing server entry at ${config.entryPath}`); | 356 | yield* scheduleRestart(`missing server entry at ${config.value.entryPath}`); |
| 347 | return; | 357 | return; |
| 348 | } | 358 | } |
| 349 | 359 | ||
| ... | @@ -425,7 +435,7 @@ const makeDesktopBackendManager = Effect.fn("makeDesktopBackendManager")(functio | ... | @@ -425,7 +435,7 @@ const makeDesktopBackendManager = Effect.fn("makeDesktopBackendManager")(functio |
| 425 | }); | 435 | }); |
| 426 | 436 | ||
| 427 | const program = runBackendProcess({ | 437 | const program = runBackendProcess({ |
| 428 | ...config, | 438 | ...config.value, |
| 429 | onStarted: Effect.fn("desktop.backendManager.onStarted")(function* (pid) { | 439 | onStarted: Effect.fn("desktop.backendManager.onStarted")(function* (pid) { |
| 430 | yield* updateActiveRun(runId, (run) => ({ | 440 | yield* updateActiveRun(runId, (run) => ({ |
| 431 | ...run, | 441 | ...run, |
| ... | @@ -433,7 +443,7 @@ const makeDesktopBackendManager = Effect.fn("makeDesktopBackendManager")(functio | ... | @@ -433,7 +443,7 @@ const makeDesktopBackendManager = Effect.fn("makeDesktopBackendManager")(functio |
| 433 | })); | 443 | })); |
| 434 | yield* backendOutputLog.writeSessionBoundary({ | 444 | yield* backendOutputLog.writeSessionBoundary({ |
| 435 | phase: "START", | 445 | phase: "START", |
| 436 | details: `pid=${pid} port=${config.bootstrap.port} cwd=${config.cwd}`, | 446 | details: `pid=${pid} port=${config.value.bootstrap.port} cwd=${config.value.cwd}`, |
| 437 | }); | 447 | }); |
| 438 | }), | 448 | }), |
| 439 | onReady: Effect.fn("desktop.backendManager.onReady")(function* () { | 449 | onReady: Effect.fn("desktop.backendManager.onReady")(function* () { |
apps/desktop/src/settings/DesktopAppSettings.ts+16-10| ... | @@ -6,6 +6,7 @@ import { | ... | @@ -6,6 +6,7 @@ import { |
| 6 | } from "@t3tools/contracts"; | 6 | } from "@t3tools/contracts"; |
| 7 | import { fromLenientJson } from "@t3tools/shared/schemaJson"; | 7 | import { fromLenientJson } from "@t3tools/shared/schemaJson"; |
| 8 | import * as Context from "effect/Context"; | 8 | import * as Context from "effect/Context"; |
| 9 | import * as Crypto from "effect/Crypto"; | ||
| 9 | import * as Data from "effect/Data"; | 10 | import * as Data from "effect/Data"; |
| 10 | import * as Effect from "effect/Effect"; | 11 | import * as Effect from "effect/Effect"; |
| 11 | import * as FileSystem from "effect/FileSystem"; | 12 | import * as FileSystem from "effect/FileSystem"; |
| ... | @@ -13,7 +14,6 @@ import * as Layer from "effect/Layer"; | ... | @@ -13,7 +14,6 @@ import * as Layer from "effect/Layer"; |
| 13 | import * as Option from "effect/Option"; | 14 | import * as Option from "effect/Option"; |
| 14 | import * as Path from "effect/Path"; | 15 | import * as Path from "effect/Path"; |
| 15 | import * as PlatformError from "effect/PlatformError"; | 16 | import * as PlatformError from "effect/PlatformError"; |
| 16 | import * as Random from "effect/Random"; | ||
| 17 | import * as Schema from "effect/Schema"; | 17 | import * as Schema from "effect/Schema"; |
| 18 | import * as SynchronizedRef from "effect/SynchronizedRef"; | 18 | import * as SynchronizedRef from "effect/SynchronizedRef"; |
| 19 | 19 | ||
| ... | @@ -222,10 +222,10 @@ const writeSettings = Effect.fn("desktop.settings.writeSettings")(function* (inp | ... | @@ -222,10 +222,10 @@ const writeSettings = Effect.fn("desktop.settings.writeSettings")(function* (inp |
| 222 | readonly settingsPath: string; | 222 | readonly settingsPath: string; |
| 223 | readonly settings: DesktopSettings; | 223 | readonly settings: DesktopSettings; |
| 224 | readonly defaultSettings: DesktopSettings; | 224 | readonly defaultSettings: DesktopSettings; |
| 225 | readonly suffix: string; | ||
| 225 | }): Effect.fn.Return<void, PlatformError.PlatformError | Schema.SchemaError> { | 226 | }): Effect.fn.Return<void, PlatformError.PlatformError | Schema.SchemaError> { |
| 226 | const directory = input.path.dirname(input.settingsPath); | 227 | const directory = input.path.dirname(input.settingsPath); |
| 227 | const suffix = (yield* Random.nextUUIDv4).replace(/-/g, ""); | 228 | const tempPath = `${input.settingsPath}.${process.pid}.${input.suffix}.tmp`; |
| 228 | const tempPath = `${input.settingsPath}.${process.pid}.${suffix}.tmp`; | ||
| 229 | const encoded = yield* encodeDesktopSettingsJson( | 229 | const encoded = yield* encodeDesktopSettingsJson( |
| 230 | toDesktopSettingsDocument(input.settings, input.defaultSettings), | 230 | toDesktopSettingsDocument(input.settings, input.defaultSettings), |
| 231 | ); | 231 | ); |
| ... | @@ -240,6 +240,7 @@ export const layer = Layer.effect( | ... | @@ -240,6 +240,7 @@ export const layer = Layer.effect( |
| 240 | const environment = yield* DesktopEnvironment.DesktopEnvironment; | 240 | const environment = yield* DesktopEnvironment.DesktopEnvironment; |
| 241 | const fileSystem = yield* FileSystem.FileSystem; | 241 | const fileSystem = yield* FileSystem.FileSystem; |
| 242 | const path = yield* Path.Path; | 242 | const path = yield* Path.Path; |
| 243 | const crypto = yield* Crypto.Crypto; | ||
| 243 | const settingsRef = yield* SynchronizedRef.make(environment.defaultDesktopSettings); | 244 | const settingsRef = yield* SynchronizedRef.make(environment.defaultDesktopSettings); |
| 244 | 245 | ||
| 245 | const persist = ( | 246 | const persist = ( |
| ... | @@ -251,13 +252,18 @@ export const layer = Layer.effect( | ... | @@ -251,13 +252,18 @@ export const layer = Layer.effect( |
| 251 | return Effect.succeed([settingsChange(settings, false), settings] as const); | 252 | return Effect.succeed([settingsChange(settings, false), settings] as const); |
| 252 | } | 253 | } |
| 253 | 254 | ||
| 254 | return writeSettings({ | 255 | return crypto.randomUUIDv4.pipe( |
| 255 | fileSystem, | 256 | Effect.map((uuid) => uuid.replace(/-/g, "")), |
| 256 | path, | 257 | Effect.flatMap((suffix) => |
| 257 | settingsPath: environment.desktopSettingsPath, | 258 | writeSettings({ |
| 258 | settings: nextSettings, | 259 | fileSystem, |
| 259 | defaultSettings: environment.defaultDesktopSettings, | 260 | path, |
| 260 | }).pipe( | 261 | settingsPath: environment.desktopSettingsPath, |
| 262 | settings: nextSettings, | ||
| 263 | defaultSettings: environment.defaultDesktopSettings, | ||
| 264 | suffix, | ||
| 265 | }), | ||
| 266 | ), | ||
| 261 | Effect.mapError((cause) => new DesktopSettingsWriteError({ cause })), | 267 | Effect.mapError((cause) => new DesktopSettingsWriteError({ cause })), |
| 262 | Effect.as([settingsChange(nextSettings, true), nextSettings] as const), | 268 | Effect.as([settingsChange(nextSettings, true), nextSettings] as const), |
| 263 | ); | 269 | ); |
apps/desktop/src/settings/DesktopClientSettings.ts+15-9| ... | @@ -1,6 +1,7 @@ | ... | @@ -1,6 +1,7 @@ |
| 1 | import { ClientSettingsSchema, type ClientSettings } from "@t3tools/contracts"; | 1 | import { ClientSettingsSchema, type ClientSettings } from "@t3tools/contracts"; |
| 2 | import { fromLenientJson } from "@t3tools/shared/schemaJson"; | 2 | import { fromLenientJson } from "@t3tools/shared/schemaJson"; |
| 3 | import * as Context from "effect/Context"; | 3 | import * as Context from "effect/Context"; |
| 4 | import * as Crypto from "effect/Crypto"; | ||
| 4 | import * as Data from "effect/Data"; | 5 | import * as Data from "effect/Data"; |
| 5 | import * as Effect from "effect/Effect"; | 6 | import * as Effect from "effect/Effect"; |
| 6 | import * as FileSystem from "effect/FileSystem"; | 7 | import * as FileSystem from "effect/FileSystem"; |
| ... | @@ -8,7 +9,6 @@ import * as Layer from "effect/Layer"; | ... | @@ -8,7 +9,6 @@ import * as Layer from "effect/Layer"; |
| 8 | import * as Option from "effect/Option"; | 9 | import * as Option from "effect/Option"; |
| 9 | import * as Path from "effect/Path"; | 10 | import * as Path from "effect/Path"; |
| 10 | import * as PlatformError from "effect/PlatformError"; | 11 | import * as PlatformError from "effect/PlatformError"; |
| 11 | import * as Random from "effect/Random"; | ||
| 12 | import * as Schema from "effect/Schema"; | 12 | import * as Schema from "effect/Schema"; |
| 13 | import * as Ref from "effect/Ref"; | 13 | import * as Ref from "effect/Ref"; |
| 14 | 14 | ||
| ... | @@ -74,10 +74,10 @@ const writeClientSettings = Effect.fnUntraced(function* (input: { | ... | @@ -74,10 +74,10 @@ const writeClientSettings = Effect.fnUntraced(function* (input: { |
| 74 | readonly path: Path.Path; | 74 | readonly path: Path.Path; |
| 75 | readonly settingsPath: string; | 75 | readonly settingsPath: string; |
| 76 | readonly settings: ClientSettings; | 76 | readonly settings: ClientSettings; |
| 77 | readonly suffix: string; | ||
| 77 | }): Effect.fn.Return<void, PlatformError.PlatformError | Schema.SchemaError> { | 78 | }): Effect.fn.Return<void, PlatformError.PlatformError | Schema.SchemaError> { |
| 78 | const directory = input.path.dirname(input.settingsPath); | 79 | const directory = input.path.dirname(input.settingsPath); |
| 79 | const suffix = (yield* Random.nextUUIDv4).replace(/-/g, ""); | 80 | const tempPath = `${input.settingsPath}.${process.pid}.${input.suffix}.tmp`; |
| 80 | const tempPath = `${input.settingsPath}.${process.pid}.${suffix}.tmp`; | ||
| 81 | const encoded = yield* encodeClientSettingsJson(input.settings); | 81 | const encoded = yield* encodeClientSettingsJson(input.settings); |
| 82 | yield* input.fileSystem.makeDirectory(directory, { recursive: true }); | 82 | yield* input.fileSystem.makeDirectory(directory, { recursive: true }); |
| 83 | yield* input.fileSystem.writeFileString(tempPath, `${encoded}\n`); | 83 | yield* input.fileSystem.writeFileString(tempPath, `${encoded}\n`); |
| ... | @@ -90,18 +90,24 @@ export const layer = Layer.effect( | ... | @@ -90,18 +90,24 @@ export const layer = Layer.effect( |
| 90 | const environment = yield* DesktopEnvironment.DesktopEnvironment; | 90 | const environment = yield* DesktopEnvironment.DesktopEnvironment; |
| 91 | const fileSystem = yield* FileSystem.FileSystem; | 91 | const fileSystem = yield* FileSystem.FileSystem; |
| 92 | const path = yield* Path.Path; | 92 | const path = yield* Path.Path; |
| 93 | const crypto = yield* Crypto.Crypto; | ||
| 93 | 94 | ||
| 94 | return DesktopClientSettings.of({ | 95 | return DesktopClientSettings.of({ |
| 95 | get: readClientSettings(fileSystem, environment.clientSettingsPath).pipe( | 96 | get: readClientSettings(fileSystem, environment.clientSettingsPath).pipe( |
| 96 | Effect.withSpan("desktop.clientSettings.get"), | 97 | Effect.withSpan("desktop.clientSettings.get"), |
| 97 | ), | 98 | ), |
| 98 | set: (settings) => | 99 | set: (settings) => |
| 99 | writeClientSettings({ | 100 | crypto.randomUUIDv4.pipe( |
| 100 | fileSystem, | 101 | Effect.map((uuid) => uuid.replace(/-/g, "")), |
| 101 | path, | 102 | Effect.flatMap((suffix) => |
| 102 | settingsPath: environment.clientSettingsPath, | 103 | writeClientSettings({ |
| 103 | settings, | 104 | fileSystem, |
| 104 | }).pipe( | 105 | path, |
| 106 | settingsPath: environment.clientSettingsPath, | ||
| 107 | settings, | ||
| 108 | suffix, | ||
| 109 | }), | ||
| 110 | ), | ||
| 105 | Effect.mapError((cause) => new DesktopClientSettingsWriteError({ cause })), | 111 | Effect.mapError((cause) => new DesktopClientSettingsWriteError({ cause })), |
| 106 | Effect.withSpan("desktop.clientSettings.set"), | 112 | Effect.withSpan("desktop.clientSettings.set"), |
| 107 | ), | 113 | ), |
apps/desktop/src/settings/DesktopSavedEnvironments.ts+17-9| ... | @@ -1,6 +1,7 @@ | ... | @@ -1,6 +1,7 @@ |
| 1 | import { EnvironmentId, type PersistedSavedEnvironmentRecord } from "@t3tools/contracts"; | 1 | import { EnvironmentId, type PersistedSavedEnvironmentRecord } from "@t3tools/contracts"; |
| 2 | import { fromLenientJson } from "@t3tools/shared/schemaJson"; | 2 | import { fromLenientJson } from "@t3tools/shared/schemaJson"; |
| 3 | import * as Context from "effect/Context"; | 3 | import * as Context from "effect/Context"; |
| 4 | import * as Crypto from "effect/Crypto"; | ||
| 4 | import * as Data from "effect/Data"; | 5 | import * as Data from "effect/Data"; |
| 5 | import * as Effect from "effect/Effect"; | 6 | import * as Effect from "effect/Effect"; |
| 6 | import * as Encoding from "effect/Encoding"; | 7 | import * as Encoding from "effect/Encoding"; |
| ... | @@ -9,7 +10,6 @@ import * as Layer from "effect/Layer"; | ... | @@ -9,7 +10,6 @@ import * as Layer from "effect/Layer"; |
| 9 | import * as Option from "effect/Option"; | 10 | import * as Option from "effect/Option"; |
| 10 | import * as Path from "effect/Path"; | 11 | import * as Path from "effect/Path"; |
| 11 | import * as PlatformError from "effect/PlatformError"; | 12 | import * as PlatformError from "effect/PlatformError"; |
| 12 | import * as Random from "effect/Random"; | ||
| 13 | import * as Schema from "effect/Schema"; | 13 | import * as Schema from "effect/Schema"; |
| 14 | import * as Ref from "effect/Ref"; | 14 | import * as Ref from "effect/Ref"; |
| 15 | 15 | ||
| ... | @@ -200,10 +200,10 @@ const writeRegistryDocument = Effect.fn("desktop.savedEnvironments.writeRegistry | ... | @@ -200,10 +200,10 @@ const writeRegistryDocument = Effect.fn("desktop.savedEnvironments.writeRegistry |
| 200 | readonly path: Path.Path; | 200 | readonly path: Path.Path; |
| 201 | readonly registryPath: string; | 201 | readonly registryPath: string; |
| 202 | readonly document: SavedEnvironmentRegistryDocument; | 202 | readonly document: SavedEnvironmentRegistryDocument; |
| 203 | readonly suffix: string; | ||
| 203 | }): Effect.fn.Return<void, PlatformError.PlatformError | Schema.SchemaError> { | 204 | }): Effect.fn.Return<void, PlatformError.PlatformError | Schema.SchemaError> { |
| 204 | const directory = input.path.dirname(input.registryPath); | 205 | const directory = input.path.dirname(input.registryPath); |
| 205 | const suffix = (yield* Random.nextUUIDv4).replace(/-/g, ""); | 206 | const tempPath = `${input.registryPath}.${process.pid}.${input.suffix}.tmp`; |
| 206 | const tempPath = `${input.registryPath}.${process.pid}.${suffix}.tmp`; | ||
| 207 | const encoded = yield* encodeSavedEnvironmentRegistryDocumentJson(input.document); | 207 | const encoded = yield* encodeSavedEnvironmentRegistryDocumentJson(input.document); |
| 208 | yield* input.fileSystem.makeDirectory(directory, { recursive: true }); | 208 | yield* input.fileSystem.makeDirectory(directory, { recursive: true }); |
| 209 | yield* input.fileSystem.writeFileString(tempPath, `${encoded}\n`); | 209 | yield* input.fileSystem.writeFileString(tempPath, `${encoded}\n`); |
| ... | @@ -247,14 +247,22 @@ export const layer = Layer.effect( | ... | @@ -247,14 +247,22 @@ export const layer = Layer.effect( |
| 247 | const fileSystem = yield* FileSystem.FileSystem; | 247 | const fileSystem = yield* FileSystem.FileSystem; |
| 248 | const path = yield* Path.Path; | 248 | const path = yield* Path.Path; |
| 249 | const safeStorage = yield* ElectronSafeStorage.ElectronSafeStorage; | 249 | const safeStorage = yield* ElectronSafeStorage.ElectronSafeStorage; |
| 250 | const crypto = yield* Crypto.Crypto; | ||
| 250 | 251 | ||
| 251 | const writeDocument = (document: SavedEnvironmentRegistryDocument) => | 252 | const writeDocument = (document: SavedEnvironmentRegistryDocument) => |
| 252 | writeRegistryDocument({ | 253 | crypto.randomUUIDv4.pipe( |
| 253 | fileSystem, | 254 | Effect.map((uuid) => uuid.replace(/-/g, "")), |
| 254 | path, | 255 | Effect.flatMap((suffix) => |
| 255 | registryPath: environment.savedEnvironmentRegistryPath, | 256 | writeRegistryDocument({ |
| 256 | document, | 257 | fileSystem, |
| 257 | }).pipe(Effect.mapError((cause) => new DesktopSavedEnvironmentsWriteError({ cause }))); | 258 | path, |
| 259 | registryPath: environment.savedEnvironmentRegistryPath, | ||
| 260 | document, | ||
| 261 | suffix, | ||
| 262 | }), | ||
| 263 | ), | ||
| 264 | Effect.mapError((cause) => new DesktopSavedEnvironmentsWriteError({ cause })), | ||
| 265 | ); | ||
| 258 | 266 | ||
| 259 | return DesktopSavedEnvironments.of({ | 267 | return DesktopSavedEnvironments.of({ |
| 260 | getRegistry: readRegistryDocument(fileSystem, environment.savedEnvironmentRegistryPath).pipe( | 268 | getRegistry: readRegistryDocument(fileSystem, environment.savedEnvironmentRegistryPath).pipe( |
apps/desktop/src/ssh/DesktopSshPasswordPrompts.test.ts+2| ... | @@ -1,4 +1,5 @@ | ... | @@ -1,4 +1,5 @@ |
| 1 | import { assert, describe, it } from "@effect/vitest"; | 1 | import { assert, describe, it } from "@effect/vitest"; |
| 2 | import * as NodeServices from "@effect/platform-node/NodeServices"; | ||
| 2 | import * as Duration from "effect/Duration"; | 3 | import * as Duration from "effect/Duration"; |
| 3 | import * as Effect from "effect/Effect"; | 4 | import * as Effect from "effect/Effect"; |
| 4 | import * as Fiber from "effect/Fiber"; | 5 | import * as Fiber from "effect/Fiber"; |
| ... | @@ -86,6 +87,7 @@ function makeElectronWindowLayer(window: ReturnType<typeof makeTestWindow>["wind | ... | @@ -86,6 +87,7 @@ function makeElectronWindowLayer(window: ReturnType<typeof makeTestWindow>["wind |
| 86 | function makeLayer(window: ReturnType<typeof makeTestWindow>["window"]) { | 87 | function makeLayer(window: ReturnType<typeof makeTestWindow>["window"]) { |
| 87 | return DesktopSshPasswordPrompts.layer({ passwordPromptTimeoutMs: 1_000 }).pipe( | 88 | return DesktopSshPasswordPrompts.layer({ passwordPromptTimeoutMs: 1_000 }).pipe( |
| 88 | Layer.provide(makeElectronWindowLayer(window)), | 89 | Layer.provide(makeElectronWindowLayer(window)), |
| 90 | Layer.provide(NodeServices.layer), | ||
| 89 | Layer.provideMerge(TestClock.layer()), | 91 | Layer.provideMerge(TestClock.layer()), |
| 90 | ); | 92 | ); |
| 91 | } | 93 | } |
apps/desktop/src/ssh/DesktopSshPasswordPrompts.ts+7-2| ... | @@ -2,6 +2,7 @@ import type { DesktopSshPasswordPromptRequest } from "@t3tools/contracts"; | ... | @@ -2,6 +2,7 @@ import type { DesktopSshPasswordPromptRequest } from "@t3tools/contracts"; |
| 2 | import { DesktopSshPasswordPromptResolutionInputSchema } from "@t3tools/contracts"; | 2 | import { DesktopSshPasswordPromptResolutionInputSchema } from "@t3tools/contracts"; |
| 3 | import type { SshPasswordRequest } from "@t3tools/ssh/auth"; | 3 | import type { SshPasswordRequest } from "@t3tools/ssh/auth"; |
| 4 | import * as Context from "effect/Context"; | 4 | import * as Context from "effect/Context"; |
| 5 | import * as Crypto from "effect/Crypto"; | ||
| 5 | import * as Data from "effect/Data"; | 6 | import * as Data from "effect/Data"; |
| 6 | import * as DateTime from "effect/DateTime"; | 7 | import * as DateTime from "effect/DateTime"; |
| 7 | import * as Deferred from "effect/Deferred"; | 8 | import * as Deferred from "effect/Deferred"; |
| ... | @@ -9,7 +10,6 @@ import * as Duration from "effect/Duration"; | ... | @@ -9,7 +10,6 @@ import * as Duration from "effect/Duration"; |
| 9 | import * as Effect from "effect/Effect"; | 10 | import * as Effect from "effect/Effect"; |
| 10 | import * as Layer from "effect/Layer"; | 11 | import * as Layer from "effect/Layer"; |
| 11 | import * as Option from "effect/Option"; | 12 | import * as Option from "effect/Option"; |
| 12 | import * as Random from "effect/Random"; | ||
| 13 | import * as Ref from "effect/Ref"; | 13 | import * as Ref from "effect/Ref"; |
| 14 | 14 | ||
| 15 | import * as IpcChannels from "../ipc/channels.ts"; | 15 | import * as IpcChannels from "../ipc/channels.ts"; |
| ... | @@ -163,6 +163,7 @@ const failPending = ( | ... | @@ -163,6 +163,7 @@ const failPending = ( |
| 163 | 163 | ||
| 164 | const make = Effect.fn("desktop.sshPasswordPrompts.make")(function* (options: LayerOptions = {}) { | 164 | const make = Effect.fn("desktop.sshPasswordPrompts.make")(function* (options: LayerOptions = {}) { |
| 165 | const electronWindow = yield* ElectronWindow.ElectronWindow; | 165 | const electronWindow = yield* ElectronWindow.ElectronWindow; |
| 166 | const crypto = yield* Crypto.Crypto; | ||
| 166 | const pendingRef = yield* Ref.make(new Map<string, PendingSshPasswordPrompt>()); | 167 | const pendingRef = yield* Ref.make(new Map<string, PendingSshPasswordPrompt>()); |
| 167 | const passwordPromptTimeoutMs = | 168 | const passwordPromptTimeoutMs = |
| 168 | options.passwordPromptTimeoutMs ?? DEFAULT_SSH_PASSWORD_PROMPT_TIMEOUT_MS; | 169 | options.passwordPromptTimeoutMs ?? DEFAULT_SSH_PASSWORD_PROMPT_TIMEOUT_MS; |
| ... | @@ -230,7 +231,11 @@ const make = Effect.fn("desktop.sshPasswordPrompts.make")(function* (options: La | ... | @@ -230,7 +231,11 @@ const make = Effect.fn("desktop.sshPasswordPrompts.make")(function* (options: La |
| 230 | }); | 231 | }); |
| 231 | } | 232 | } |
| 232 | 233 | ||
| 233 | const requestId = yield* Random.nextUUIDv4; | 234 | const requestId = yield* crypto.randomUUIDv4.pipe( |
| 235 | Effect.mapError( | ||
| 236 | () => new DesktopSshPromptUnavailableError({ reason: "Secure randomness is unavailable." }), | ||
| 237 | ), | ||
| 238 | ); | ||
| 234 | const now = yield* DateTime.now; | 239 | const now = yield* DateTime.now; |
| 235 | const expiresAt = DateTime.formatIso( | 240 | const expiresAt = DateTime.formatIso( |
| 236 | DateTime.add(now, { milliseconds: passwordPromptTimeoutMs }), | 241 | DateTime.add(now, { milliseconds: passwordPromptTimeoutMs }), |
apps/server/integration/TestProviderAdapter.integration.ts+16-3| ... | @@ -11,8 +11,8 @@ import { | ... | @@ -11,8 +11,8 @@ import { |
| 11 | ProviderDriverKind, | 11 | ProviderDriverKind, |
| 12 | } from "@t3tools/contracts"; | 12 | } from "@t3tools/contracts"; |
| 13 | import * as Effect from "effect/Effect"; | 13 | import * as Effect from "effect/Effect"; |
| 14 | import * as Crypto from "effect/Crypto"; | ||
| 14 | import * as Queue from "effect/Queue"; | 15 | import * as Queue from "effect/Queue"; |
| 15 | import * as Random from "effect/Random"; | ||
| 16 | import * as Stream from "effect/Stream"; | 16 | import * as Stream from "effect/Stream"; |
| 17 | 17 | ||
| 18 | import { | 18 | import { |
| ... | @@ -226,6 +226,7 @@ function missingSessionEffect( | ... | @@ -226,6 +226,7 @@ function missingSessionEffect( |
| 226 | export const makeTestProviderAdapterHarness = (options?: MakeTestProviderAdapterHarnessOptions) => | 226 | export const makeTestProviderAdapterHarness = (options?: MakeTestProviderAdapterHarnessOptions) => |
| 227 | Effect.gen(function* () { | 227 | Effect.gen(function* () { |
| 228 | const provider = options?.provider ?? ProviderDriverKind.make("codex"); | 228 | const provider = options?.provider ?? ProviderDriverKind.make("codex"); |
| 229 | const crypto = yield* Crypto.Crypto; | ||
| 229 | const runtimeEvents = yield* Queue.unbounded<ProviderRuntimeEvent>(); | 230 | const runtimeEvents = yield* Queue.unbounded<ProviderRuntimeEvent>(); |
| 230 | let sessionCount = 0; | 231 | let sessionCount = 0; |
| 231 | const sessions = new Map<ThreadId, SessionState>(); | 232 | const sessions = new Map<ThreadId, SessionState>(); |
| ... | @@ -241,6 +242,18 @@ export const makeTestProviderAdapterHarness = (options?: MakeTestProviderAdapter | ... | @@ -241,6 +242,18 @@ export const makeTestProviderAdapterHarness = (options?: MakeTestProviderAdapter |
| 241 | >(); | 242 | >(); |
| 242 | 243 | ||
| 243 | const emit = (event: ProviderRuntimeEvent) => Queue.offer(runtimeEvents, event); | 244 | const emit = (event: ProviderRuntimeEvent) => Queue.offer(runtimeEvents, event); |
| 245 | const randomUUIDv4 = (threadId: ThreadId) => | ||
| 246 | crypto.randomUUIDv4.pipe( | ||
| 247 | Effect.mapError( | ||
| 248 | (cause) => | ||
| 249 | new ProviderAdapterValidationError({ | ||
| 250 | provider, | ||
| 251 | operation: "crypto/randomUUIDv4", | ||
| 252 | issue: `Failed to generate test runtime identifier for thread '${threadId}'.`, | ||
| 253 | cause, | ||
| 254 | }), | ||
| 255 | ), | ||
| 256 | ); | ||
| 244 | 257 | ||
| 245 | const startSession: ProviderAdapterShape<ProviderAdapterError>["startSession"] = (input) => | 258 | const startSession: ProviderAdapterShape<ProviderAdapterError>["startSession"] = (input) => |
| 246 | Effect.gen(function* () { | 259 | Effect.gen(function* () { |
| ... | @@ -309,7 +322,7 @@ export const makeTestProviderAdapterHarness = (options?: MakeTestProviderAdapter | ... | @@ -309,7 +322,7 @@ export const makeTestProviderAdapterHarness = (options?: MakeTestProviderAdapter |
| 309 | for (const fixtureEvent of response.events) { | 322 | for (const fixtureEvent of response.events) { |
| 310 | const rawEvent: Record<string, unknown> = { | 323 | const rawEvent: Record<string, unknown> = { |
| 311 | ...(fixtureEvent as Record<string, unknown>), | 324 | ...(fixtureEvent as Record<string, unknown>), |
| 312 | eventId: yield* Random.nextUUIDv4, | 325 | eventId: yield* randomUUIDv4(input.threadId), |
| 313 | provider, | 326 | provider, |
| 314 | sessionId: RuntimeSessionId.make(String(input.threadId)), | 327 | sessionId: RuntimeSessionId.make(String(input.threadId)), |
| 315 | }; | 328 | }; |
| ... | @@ -366,7 +379,7 @@ export const makeTestProviderAdapterHarness = (options?: MakeTestProviderAdapter | ... | @@ -366,7 +379,7 @@ export const makeTestProviderAdapterHarness = (options?: MakeTestProviderAdapter |
| 366 | if (deferredTurnCompletedEvents.length === 0) { | 379 | if (deferredTurnCompletedEvents.length === 0) { |
| 367 | yield* emit({ | 380 | yield* emit({ |
| 368 | type: "turn.completed", | 381 | type: "turn.completed", |
| 369 | eventId: EventId.make(yield* Random.nextUUIDv4), | 382 | eventId: EventId.make(yield* randomUUIDv4(input.threadId)), |
| 370 | provider, | 383 | provider, |
| 371 | createdAt: nowIso(), | 384 | createdAt: nowIso(), |
| 372 | threadId: state.snapshot.threadId, | 385 | threadId: state.snapshot.threadId, |
apps/server/src/atomicWrite.ts+1-3| ... | @@ -1,7 +1,6 @@ | ... | @@ -1,7 +1,6 @@ |
| 1 | import * as Effect from "effect/Effect"; | 1 | import * as Effect from "effect/Effect"; |
| 2 | import * as FileSystem from "effect/FileSystem"; | 2 | import * as FileSystem from "effect/FileSystem"; |
| 3 | import * as Path from "effect/Path"; | 3 | import * as Path from "effect/Path"; |
| 4 | import * as Random from "effect/Random"; | ||
| 5 | 4 | ||
| 6 | export const writeFileStringAtomically = (input: { | 5 | export const writeFileStringAtomically = (input: { |
| 7 | readonly filePath: string; | 6 | readonly filePath: string; |
| ... | @@ -11,7 +10,6 @@ export const writeFileStringAtomically = (input: { | ... | @@ -11,7 +10,6 @@ export const writeFileStringAtomically = (input: { |
| 11 | Effect.gen(function* () { | 10 | Effect.gen(function* () { |
| 12 | const fs = yield* FileSystem.FileSystem; | 11 | const fs = yield* FileSystem.FileSystem; |
| 13 | const path = yield* Path.Path; | 12 | const path = yield* Path.Path; |
| 14 | const tempFileId = yield* Random.nextUUIDv4; | ||
| 15 | const targetDirectory = path.dirname(input.filePath); | 13 | const targetDirectory = path.dirname(input.filePath); |
| 16 | 14 | ||
| 17 | yield* fs.makeDirectory(targetDirectory, { recursive: true }); | 15 | yield* fs.makeDirectory(targetDirectory, { recursive: true }); |
| ... | @@ -19,7 +17,7 @@ export const writeFileStringAtomically = (input: { | ... | @@ -19,7 +17,7 @@ export const writeFileStringAtomically = (input: { |
| 19 | directory: targetDirectory, | 17 | directory: targetDirectory, |
| 20 | prefix: `${path.basename(input.filePath)}.`, | 18 | prefix: `${path.basename(input.filePath)}.`, |
| 21 | }); | 19 | }); |
| 22 | const tempPath = path.join(tempDirectory, `${tempFileId}.tmp`); | 20 | const tempPath = path.join(tempDirectory, "contents.tmp"); |
| 23 | 21 | ||
| 24 | yield* fs.writeFileString(tempPath, input.contents); | 22 | yield* fs.writeFileString(tempPath, input.contents); |
| 25 | yield* fs.rename(tempPath, input.filePath); | 23 | yield* fs.rename(tempPath, input.filePath); |
apps/server/src/cli/project.ts+16-5| ... | @@ -5,6 +5,7 @@ import { | ... | @@ -5,6 +5,7 @@ import { |
| 5 | type ClientOrchestrationCommand, | 5 | type ClientOrchestrationCommand, |
| 6 | } from "@t3tools/contracts"; | 6 | } from "@t3tools/contracts"; |
| 7 | import * as Console from "effect/Console"; | 7 | import * as Console from "effect/Console"; |
| 8 | import * as Crypto from "effect/Crypto"; | ||
| 8 | import * as Data from "effect/Data"; | 9 | import * as Data from "effect/Data"; |
| 9 | import * as DateTime from "effect/DateTime"; | 10 | import * as DateTime from "effect/DateTime"; |
| 10 | import * as Duration from "effect/Duration"; | 11 | import * as Duration from "effect/Duration"; |
| ... | @@ -58,6 +59,16 @@ class ProjectCommandError extends Data.TaggedError("ProjectCommandError")<{ | ... | @@ -58,6 +59,16 @@ class ProjectCommandError extends Data.TaggedError("ProjectCommandError")<{ |
| 58 | readonly message: string; | 59 | readonly message: string; |
| 59 | }> {} | 60 | }> {} |
| 60 | 61 | ||
| 62 | const projectCommandUuid = Crypto.Crypto.pipe( | ||
| 63 | Effect.flatMap((crypto) => crypto.randomUUIDv4), | ||
| 64 | Effect.mapError( | ||
| 65 | () => | ||
| 66 | new ProjectCommandError({ | ||
| 67 | message: "Failed to generate a project command identifier.", | ||
| 68 | }), | ||
| 69 | ), | ||
| 70 | ); | ||
| 71 | |||
| 61 | const ProjectCliRuntimeLive = Layer.mergeAll( | 72 | const ProjectCliRuntimeLive = Layer.mergeAll( |
| 62 | WorkspacePathsLive, | 73 | WorkspacePathsLive, |
| 63 | OrchestrationLayerLive.pipe( | 74 | OrchestrationLayerLive.pipe( |
| ... | @@ -260,7 +271,7 @@ const runProjectMutation = Effect.fn("runProjectMutation")(function* ( | ... | @@ -260,7 +271,7 @@ const runProjectMutation = Effect.fn("runProjectMutation")(function* ( |
| 260 | }) => Effect.Effect< | 271 | }) => Effect.Effect< |
| 261 | string, | 272 | string, |
| 262 | Error, | 273 | Error, |
| 263 | FileSystem.FileSystem | HttpClient.HttpClient | Path.Path | WorkspacePaths | 274 | Crypto.Crypto | FileSystem.FileSystem | HttpClient.HttpClient | Path.Path | WorkspacePaths |
| 264 | >, | 275 | >, |
| 265 | ) { | 276 | ) { |
| 266 | const logLevel = yield* GlobalFlag.LogLevel; | 277 | const logLevel = yield* GlobalFlag.LogLevel; |
| ... | @@ -343,10 +354,10 @@ const projectAddCommand = Command.make("add", { | ... | @@ -343,10 +354,10 @@ const projectAddCommand = Command.make("add", { |
| 343 | } | 354 | } |
| 344 | 355 | ||
| 345 | const title = yield* resolveProjectTitle(workspaceRoot, Option.getOrUndefined(flags.title)); | 356 | const title = yield* resolveProjectTitle(workspaceRoot, Option.getOrUndefined(flags.title)); |
| 346 | const projectId = ProjectId.make(crypto.randomUUID()); | 357 | const projectId = ProjectId.make(yield* projectCommandUuid); |
| 347 | yield* dispatch({ | 358 | yield* dispatch({ |
| 348 | type: "project.create", | 359 | type: "project.create", |
| 349 | commandId: CommandId.make(crypto.randomUUID()), | 360 | commandId: CommandId.make(yield* projectCommandUuid), |
| 350 | projectId, | 361 | projectId, |
| 351 | title, | 362 | title, |
| 352 | workspaceRoot, | 363 | workspaceRoot, |
| ... | @@ -384,7 +395,7 @@ const projectRemoveCommand = Command.make("remove", { | ... | @@ -384,7 +395,7 @@ const projectRemoveCommand = Command.make("remove", { |
| 384 | }); | 395 | }); |
| 385 | yield* dispatch({ | 396 | yield* dispatch({ |
| 386 | type: "project.delete", | 397 | type: "project.delete", |
| 387 | commandId: CommandId.make(crypto.randomUUID()), | 398 | commandId: CommandId.make(yield* projectCommandUuid), |
| 388 | projectId: project.id, | 399 | projectId: project.id, |
| 389 | }); | 400 | }); |
| 390 | return `Removed project ${project.id} (${project.title}).`; | 401 | return `Removed project ${project.id} (${project.title}).`; |
| ... | @@ -424,7 +435,7 @@ const projectRenameCommand = Command.make("rename", { | ... | @@ -424,7 +435,7 @@ const projectRenameCommand = Command.make("rename", { |
| 424 | 435 | ||
| 425 | yield* dispatch({ | 436 | yield* dispatch({ |
| 426 | type: "project.meta.update", | 437 | type: "project.meta.update", |
| 427 | commandId: CommandId.make(crypto.randomUUID()), | 438 | commandId: CommandId.make(yield* projectCommandUuid), |
| 428 | projectId: project.id, | 439 | projectId: project.id, |
| 429 | title: nextTitle, | 440 | title: nextTitle, |
| 430 | }); | 441 | }); |
apps/server/src/environment/Layers/ServerEnvironment.ts+3-2| ... | @@ -1,9 +1,9 @@ | ... | @@ -1,9 +1,9 @@ |
| 1 | import { EnvironmentId, type ExecutionEnvironmentDescriptor } from "@t3tools/contracts"; | 1 | import { EnvironmentId, type ExecutionEnvironmentDescriptor } from "@t3tools/contracts"; |
| 2 | import * as Crypto from "effect/Crypto"; | ||
| 2 | import * as Effect from "effect/Effect"; | 3 | import * as Effect from "effect/Effect"; |
| 3 | import * as FileSystem from "effect/FileSystem"; | 4 | import * as FileSystem from "effect/FileSystem"; |
| 4 | import * as Layer from "effect/Layer"; | 5 | import * as Layer from "effect/Layer"; |
| 5 | import * as Path from "effect/Path"; | 6 | import * as Path from "effect/Path"; |
| 6 | import * as Random from "effect/Random"; | ||
| 7 | 7 | ||
| 8 | import { ServerConfig } from "../../config.ts"; | 8 | import { ServerConfig } from "../../config.ts"; |
| 9 | import { layer as ProcessRunnerLive } from "../../processRunner.ts"; | 9 | import { layer as ProcessRunnerLive } from "../../processRunner.ts"; |
| ... | @@ -39,6 +39,7 @@ export const makeServerEnvironment = Effect.fn("makeServerEnvironment")(function | ... | @@ -39,6 +39,7 @@ export const makeServerEnvironment = Effect.fn("makeServerEnvironment")(function |
| 39 | const fileSystem = yield* FileSystem.FileSystem; | 39 | const fileSystem = yield* FileSystem.FileSystem; |
| 40 | const path = yield* Path.Path; | 40 | const path = yield* Path.Path; |
| 41 | const serverConfig = yield* ServerConfig; | 41 | const serverConfig = yield* ServerConfig; |
| 42 | const crypto = yield* Crypto.Crypto; | ||
| 42 | 43 | ||
| 43 | const readPersistedEnvironmentId = Effect.gen(function* () { | 44 | const readPersistedEnvironmentId = Effect.gen(function* () { |
| 44 | const exists = yield* fileSystem | 45 | const exists = yield* fileSystem |
| ... | @@ -64,7 +65,7 @@ export const makeServerEnvironment = Effect.fn("makeServerEnvironment")(function | ... | @@ -64,7 +65,7 @@ export const makeServerEnvironment = Effect.fn("makeServerEnvironment")(function |
| 64 | return persisted; | 65 | return persisted; |
| 65 | } | 66 | } |
| 66 | 67 | ||
| 67 | const generated = yield* Random.nextUUIDv4; | 68 | const generated = yield* crypto.randomUUIDv4; |
| 68 | yield* persistEnvironmentId(generated); | 69 | yield* persistEnvironmentId(generated); |
| 69 | return generated; | 70 | return generated; |
| 70 | }); | 71 | }); |
apps/server/src/git/GitManager.ts+28-22| ... | @@ -1,8 +1,7 @@ | ... | @@ -1,8 +1,7 @@ |
| 1 | import { randomUUID } from "node:crypto"; | ||
| 2 | |||
| 3 | import * as Arr from "effect/Array"; | 1 | import * as Arr from "effect/Array"; |
| 4 | import * as Cache from "effect/Cache"; | 2 | import * as Cache from "effect/Cache"; |
| 5 | import * as Context from "effect/Context"; | 3 | import * as Context from "effect/Context"; |
| 4 | import * as Crypto from "effect/Crypto"; | ||
| 6 | import * as DateTime from "effect/DateTime"; | 5 | import * as DateTime from "effect/DateTime"; |
| 7 | import * as Duration from "effect/Duration"; | 6 | import * as Duration from "effect/Duration"; |
| 8 | import * as Effect from "effect/Effect"; | 7 | import * as Effect from "effect/Effect"; |
| ... | @@ -532,32 +531,39 @@ export const makeGitManager = Effect.fn("makeGitManager")(function* () { | ... | @@ -532,32 +531,39 @@ export const makeGitManager = Effect.fn("makeGitManager")(function* () { |
| 532 | const sourceControlProviders = yield* SourceControlProviderRegistry; | 531 | const sourceControlProviders = yield* SourceControlProviderRegistry; |
| 533 | const textGeneration = yield* TextGeneration; | 532 | const textGeneration = yield* TextGeneration; |
| 534 | const projectSetupScriptRunner = yield* ProjectSetupScriptRunner; | 533 | const projectSetupScriptRunner = yield* ProjectSetupScriptRunner; |
| 534 | const crypto = yield* Crypto.Crypto; | ||
| 535 | 535 | ||
| 536 | const sourceControlProvider = (cwd: string) => sourceControlProviders.resolve({ cwd }); | 536 | const sourceControlProvider = (cwd: string) => sourceControlProviders.resolve({ cwd }); |
| 537 | const serverSettingsService = yield* ServerSettingsService; | 537 | const serverSettingsService = yield* ServerSettingsService; |
| 538 | const randomUUIDv4 = crypto.randomUUIDv4.pipe( | ||
| 539 | Effect.mapError((cause) => | ||
| 540 | gitManagerError("randomUUIDv4", "Failed to generate Git operation identifier.", cause), | ||
| 541 | ), | ||
| 542 | ); | ||
| 538 | 543 | ||
| 539 | const createProgressEmitter = ( | 544 | const createProgressEmitter = ( |
| 540 | input: { cwd: string; action: GitStackedAction }, | 545 | input: { cwd: string; action: GitStackedAction }, |
| 541 | options?: GitRunStackedActionOptions, | 546 | options?: GitRunStackedActionOptions, |
| 542 | ) => { | 547 | ) => |
| 543 | const actionId = options?.actionId ?? randomUUID(); | 548 | (options?.actionId === undefined ? randomUUIDv4 : Effect.succeed(options.actionId)).pipe( |
| 544 | const reporter = options?.progressReporter; | 549 | Effect.map((actionId) => { |
| 545 | 550 | const reporter = options?.progressReporter; | |
| 546 | const emit = (event: GitActionProgressPayload) => | 551 | const emit = (event: GitActionProgressPayload) => |
| 547 | reporter | 552 | reporter |
| 548 | ? reporter.publish({ | 553 | ? reporter.publish({ |
| 549 | actionId, | 554 | actionId, |
| 550 | cwd: input.cwd, | 555 | cwd: input.cwd, |
| 551 | action: input.action, | 556 | action: input.action, |
| 552 | ...event, | 557 | ...event, |
| 553 | } as GitActionProgressEvent) | 558 | } as GitActionProgressEvent) |
| 554 | : Effect.void; | 559 | : Effect.void; |
| 555 | 560 | ||
| 556 | return { | 561 | return { |
| 557 | actionId, | 562 | actionId, |
| 558 | emit, | 563 | emit, |
| 559 | }; | 564 | }; |
| 560 | }; | 565 | }), |
| 566 | ); | ||
| 561 | 567 | ||
| 562 | const configurePullRequestHeadUpstreamBase = Effect.fn("configurePullRequestHeadUpstream")( | 568 | const configurePullRequestHeadUpstreamBase = Effect.fn("configurePullRequestHeadUpstream")( |
| 563 | function* ( | 569 | function* ( |
| ... | @@ -1301,7 +1307,7 @@ export const makeGitManager = Effect.fn("makeGitManager")(function* () { | ... | @@ -1301,7 +1307,7 @@ export const makeGitManager = Effect.fn("makeGitManager")(function* () { |
| 1301 | modelSelection, | 1307 | modelSelection, |
| 1302 | }); | 1308 | }); |
| 1303 | 1309 | ||
| 1304 | const bodyFile = path.join(tempDir, `t3code-pr-body-${process.pid}-${randomUUID()}.md`); | 1310 | const bodyFile = path.join(tempDir, `t3code-pr-body-${process.pid}-${yield* randomUUIDv4}.md`); |
| 1305 | yield* fileSystem | 1311 | yield* fileSystem |
| 1306 | .writeFileString(bodyFile, generated.body) | 1312 | .writeFileString(bodyFile, generated.body) |
| 1307 | .pipe( | 1313 | .pipe( |
| ... | @@ -1591,7 +1597,7 @@ export const makeGitManager = Effect.fn("makeGitManager")(function* () { | ... | @@ -1591,7 +1597,7 @@ export const makeGitManager = Effect.fn("makeGitManager")(function* () { |
| 1591 | 1597 | ||
| 1592 | const runStackedAction: GitManagerShape["runStackedAction"] = Effect.fn("runStackedAction")( | 1598 | const runStackedAction: GitManagerShape["runStackedAction"] = Effect.fn("runStackedAction")( |
| 1593 | function* (input, options) { | 1599 | function* (input, options) { |
| 1594 | const progress = createProgressEmitter(input, options); | 1600 | const progress = yield* createProgressEmitter(input, options); |
| 1595 | const currentPhase = yield* Ref.make<Option.Option<GitActionProgressPhase>>(Option.none()); | 1601 | const currentPhase = yield* Ref.make<Option.Option<GitActionProgressPhase>>(Option.none()); |
| 1596 | 1602 | ||
| 1597 | const runAction = Effect.fn("runStackedAction.runAction")(function* (): Effect.fn.Return< | 1603 | const runAction = Effect.fn("runStackedAction.runAction")(function* (): Effect.fn.Return< |
apps/server/src/orchestration/Layers/OrchestrationEngine.test.ts+1| ... | @@ -211,6 +211,7 @@ describe("OrchestrationEngine", () => { | ... | @@ -211,6 +211,7 @@ describe("OrchestrationEngine", () => { |
| 211 | Layer.provide(Layer.succeed(OrchestrationEventStore, eventStore)), | 211 | Layer.provide(Layer.succeed(OrchestrationEventStore, eventStore)), |
| 212 | Layer.provide(OrchestrationCommandReceiptRepositoryLive), | 212 | Layer.provide(OrchestrationCommandReceiptRepositoryLive), |
| 213 | Layer.provide(SqlitePersistenceMemory), | 213 | Layer.provide(SqlitePersistenceMemory), |
| 214 | Layer.provideMerge(NodeServices.layer), | ||
| 214 | ); | 215 | ); |
| 215 | 216 | ||
| 216 | const runtime = ManagedRuntime.make(layer); | 217 | const runtime = ManagedRuntime.make(layer); |
apps/server/src/orchestration/Layers/OrchestrationEngine.ts+14-1| ... | @@ -7,6 +7,7 @@ import type { | ... | @@ -7,6 +7,7 @@ import type { |
| 7 | import { OrchestrationCommand } from "@t3tools/contracts"; | 7 | import { OrchestrationCommand } from "@t3tools/contracts"; |
| 8 | import * as Cause from "effect/Cause"; | 8 | import * as Cause from "effect/Cause"; |
| 9 | import * as Clock from "effect/Clock"; | 9 | import * as Clock from "effect/Clock"; |
| 10 | import * as Crypto from "effect/Crypto"; | ||
| 10 | import * as DateTime from "effect/DateTime"; | 11 | import * as DateTime from "effect/DateTime"; |
| 11 | import * as Deferred from "effect/Deferred"; | 12 | import * as Deferred from "effect/Deferred"; |
| 12 | import * as Duration from "effect/Duration"; | 13 | import * as Duration from "effect/Duration"; |
| ... | @@ -81,6 +82,7 @@ const makeOrchestrationEngine = Effect.gen(function* () { | ... | @@ -81,6 +82,7 @@ const makeOrchestrationEngine = Effect.gen(function* () { |
| 81 | const commandReceiptRepository = yield* OrchestrationCommandReceiptRepository; | 82 | const commandReceiptRepository = yield* OrchestrationCommandReceiptRepository; |
| 82 | const projectionPipeline = yield* OrchestrationProjectionPipeline; | 83 | const projectionPipeline = yield* OrchestrationProjectionPipeline; |
| 83 | const projectionSnapshotQuery = yield* ProjectionSnapshotQuery; | 84 | const projectionSnapshotQuery = yield* ProjectionSnapshotQuery; |
| 85 | const crypto = yield* Crypto.Crypto; | ||
| 84 | 86 | ||
| 85 | const nowIso = Effect.map(DateTime.now, DateTime.formatIso); | 87 | const nowIso = Effect.map(DateTime.now, DateTime.formatIso); |
| 86 | let commandReadModel = createEmptyReadModel(yield* nowIso); | 88 | let commandReadModel = createEmptyReadModel(yield* nowIso); |
| ... | @@ -151,7 +153,18 @@ const makeOrchestrationEngine = Effect.gen(function* () { | ... | @@ -151,7 +153,18 @@ const makeOrchestrationEngine = Effect.gen(function* () { |
| 151 | const eventBase = yield* decideOrchestrationCommand({ | 153 | const eventBase = yield* decideOrchestrationCommand({ |
| 152 | command: envelope.command, | 154 | command: envelope.command, |
| 153 | readModel: commandReadModel, | 155 | readModel: commandReadModel, |
| 154 | }); | 156 | }).pipe( |
| 157 | Effect.provideService(Crypto.Crypto, crypto), | ||
| 158 | Effect.mapError((cause) => | ||
| 159 | isOrchestrationCommandInvariantError(cause) | ||
| 160 | ? cause | ||
| 161 | : new OrchestrationCommandInvariantError({ | ||
| 162 | commandType: envelope.command.type, | ||
| 163 | detail: "Failed to generate an event identifier.", | ||
| 164 | cause, | ||
| 165 | }), | ||
| 166 | ), | ||
| 167 | ); | ||
| 155 | const eventBases = Array.isArray(eventBase) ? eventBase : [eventBase]; | 168 | const eventBases = Array.isArray(eventBase) ? eventBase : [eventBase]; |
| 156 | const committedCommand = yield* sql | 169 | const committedCommand = yield* sql |
| 157 | .withTransaction( | 170 | .withTransaction( |
apps/server/src/orchestration/Layers/ProviderCommandReactor.ts+42-29| ... | @@ -15,6 +15,7 @@ import { | ... | @@ -15,6 +15,7 @@ import { |
| 15 | import { isTemporaryWorktreeBranch, WORKTREE_BRANCH_PREFIX } from "@t3tools/shared/git"; | 15 | import { isTemporaryWorktreeBranch, WORKTREE_BRANCH_PREFIX } from "@t3tools/shared/git"; |
| 16 | import * as Cache from "effect/Cache"; | 16 | import * as Cache from "effect/Cache"; |
| 17 | import * as Cause from "effect/Cause"; | 17 | import * as Cause from "effect/Cause"; |
| 18 | import * as Crypto from "effect/Crypto"; | ||
| 18 | import * as Duration from "effect/Duration"; | 19 | import * as Duration from "effect/Duration"; |
| 19 | import * as Effect from "effect/Effect"; | 20 | import * as Effect from "effect/Effect"; |
| 20 | import * as Equal from "effect/Equal"; | 21 | import * as Equal from "effect/Equal"; |
| ... | @@ -81,9 +82,6 @@ function mapProviderSessionStatusToOrchestrationStatus( | ... | @@ -81,9 +82,6 @@ function mapProviderSessionStatusToOrchestrationStatus( |
| 81 | const turnStartKeyForEvent = (event: ProviderIntentEvent): string => | 82 | const turnStartKeyForEvent = (event: ProviderIntentEvent): string => |
| 82 | event.commandId !== null ? `command:${event.commandId}` : `event:${event.eventId}`; | 83 | event.commandId !== null ? `command:${event.commandId}` : `event:${event.eventId}`; |
| 83 | 84 | ||
| 84 | const serverCommandId = (tag: string): CommandId => | ||
| 85 | CommandId.make(`server:${tag}:${crypto.randomUUID()}`); | ||
| 86 | |||
| 87 | const HANDLED_TURN_START_KEY_MAX = 10_000; | 85 | const HANDLED_TURN_START_KEY_MAX = 10_000; |
| 88 | const HANDLED_TURN_START_KEY_TTL = Duration.minutes(30); | 86 | const HANDLED_TURN_START_KEY_TTL = Duration.minutes(30); |
| 89 | const DEFAULT_RUNTIME_MODE: RuntimeMode = "full-access"; | 87 | const DEFAULT_RUNTIME_MODE: RuntimeMode = "full-access"; |
| ... | @@ -178,6 +176,7 @@ function buildGeneratedWorktreeBranchName(raw: string): string { | ... | @@ -178,6 +176,7 @@ function buildGeneratedWorktreeBranchName(raw: string): string { |
| 178 | } | 176 | } |
| 179 | 177 | ||
| 180 | const make = Effect.gen(function* () { | 178 | const make = Effect.gen(function* () { |
| 179 | const crypto = yield* Crypto.Crypto; | ||
| 181 | const orchestrationEngine = yield* OrchestrationEngineService; | 180 | const orchestrationEngine = yield* OrchestrationEngineService; |
| 182 | const projectionSnapshotQuery = yield* ProjectionSnapshotQuery; | 181 | const projectionSnapshotQuery = yield* ProjectionSnapshotQuery; |
| 183 | const providerService = yield* ProviderService; | 182 | const providerService = yield* ProviderService; |
| ... | @@ -185,6 +184,9 @@ const make = Effect.gen(function* () { | ... | @@ -185,6 +184,9 @@ const make = Effect.gen(function* () { |
| 185 | const vcsStatusBroadcaster = yield* VcsStatusBroadcaster; | 184 | const vcsStatusBroadcaster = yield* VcsStatusBroadcaster; |
| 186 | const textGeneration = yield* TextGeneration; | 185 | const textGeneration = yield* TextGeneration; |
| 187 | const serverSettingsService = yield* ServerSettingsService; | 186 | const serverSettingsService = yield* ServerSettingsService; |
| 187 | const serverCommandId = (tag: string) => | ||
| 188 | crypto.randomUUIDv4.pipe(Effect.map((uuid) => CommandId.make(`server:${tag}:${uuid}`))); | ||
| 189 | const serverEventId = () => crypto.randomUUIDv4.pipe(Effect.map(EventId.make)); | ||
| 188 | const handledTurnStartKeys = yield* Cache.make<string, true>({ | 190 | const handledTurnStartKeys = yield* Cache.make<string, true>({ |
| 189 | capacity: HANDLED_TURN_START_KEY_MAX, | 191 | capacity: HANDLED_TURN_START_KEY_MAX, |
| 190 | timeToLive: HANDLED_TURN_START_KEY_TTL, | 192 | timeToLive: HANDLED_TURN_START_KEY_TTL, |
| ... | @@ -214,24 +216,31 @@ const make = Effect.gen(function* () { | ... | @@ -214,24 +216,31 @@ const make = Effect.gen(function* () { |
| 214 | readonly createdAt: string; | 216 | readonly createdAt: string; |
| 215 | readonly requestId?: string; | 217 | readonly requestId?: string; |
| 216 | }) => | 218 | }) => |
| 217 | orchestrationEngine.dispatch({ | 219 | Effect.all({ |
| 218 | type: "thread.activity.append", | ||
| 219 | commandId: serverCommandId("provider-failure-activity"), | 220 | commandId: serverCommandId("provider-failure-activity"), |
| 220 | threadId: input.threadId, | 221 | eventId: serverEventId(), |
| 221 | activity: { | 222 | }).pipe( |
| 222 | id: EventId.make(crypto.randomUUID()), | 223 | Effect.flatMap(({ commandId, eventId }) => |
| 223 | tone: "error", | 224 | orchestrationEngine.dispatch({ |
| 224 | kind: input.kind, | 225 | type: "thread.activity.append", |
| 225 | summary: input.summary, | 226 | commandId, |
| 226 | payload: { | 227 | threadId: input.threadId, |
| 227 | detail: input.detail, | 228 | activity: { |
| 228 | ...(input.requestId ? { requestId: input.requestId } : {}), | 229 | id: eventId, |
| 229 | }, | 230 | tone: "error", |
| 230 | turnId: input.turnId, | 231 | kind: input.kind, |
| 231 | createdAt: input.createdAt, | 232 | summary: input.summary, |
| 232 | }, | 233 | payload: { |
| 233 | createdAt: input.createdAt, | 234 | detail: input.detail, |
| 234 | }); | 235 | ...(input.requestId ? { requestId: input.requestId } : {}), |
| 236 | }, | ||
| 237 | turnId: input.turnId, | ||
| 238 | createdAt: input.createdAt, | ||
| 239 | }, | ||
| 240 | createdAt: input.createdAt, | ||
| 241 | }), | ||
| 242 | ), | ||
| 243 | ); | ||
| 235 | 244 | ||
| 236 | const formatFailureDetail = (cause: Cause.Cause<unknown>): string => { | 245 | const formatFailureDetail = (cause: Cause.Cause<unknown>): string => { |
| 237 | const failReason = cause.reasons.find(Cause.isFailReason); | 246 | const failReason = cause.reasons.find(Cause.isFailReason); |
| ... | @@ -249,13 +258,17 @@ const make = Effect.gen(function* () { | ... | @@ -249,13 +258,17 @@ const make = Effect.gen(function* () { |
| 249 | readonly session: OrchestrationSession; | 258 | readonly session: OrchestrationSession; |
| 250 | readonly createdAt: string; | 259 | readonly createdAt: string; |
| 251 | }) => | 260 | }) => |
| 252 | orchestrationEngine.dispatch({ | 261 | serverCommandId("provider-session-set").pipe( |
| 253 | type: "thread.session.set", | 262 | Effect.flatMap((commandId) => |
| 254 | commandId: serverCommandId("provider-session-set"), | 263 | orchestrationEngine.dispatch({ |
| 255 | threadId: input.threadId, | 264 | type: "thread.session.set", |
| 256 | session: input.session, | 265 | commandId, |
| 257 | createdAt: input.createdAt, | 266 | threadId: input.threadId, |
| 258 | }); | 267 | session: input.session, |
| 268 | createdAt: input.createdAt, | ||
| 269 | }), | ||
| 270 | ), | ||
| 271 | ); | ||
| 259 | 272 | ||
| 260 | const setThreadSessionErrorOnTurnStartFailure = Effect.fnUntraced(function* (input: { | 273 | const setThreadSessionErrorOnTurnStartFailure = Effect.fnUntraced(function* (input: { |
| 261 | readonly threadId: ThreadId; | 274 | readonly threadId: ThreadId; |
| ... | @@ -610,7 +623,7 @@ const make = Effect.gen(function* () { | ... | @@ -610,7 +623,7 @@ const make = Effect.gen(function* () { |
| 610 | const renamed = yield* gitWorkflow.renameBranch({ cwd, oldBranch, newBranch: targetBranch }); | 623 | const renamed = yield* gitWorkflow.renameBranch({ cwd, oldBranch, newBranch: targetBranch }); |
| 611 | yield* orchestrationEngine.dispatch({ | 624 | yield* orchestrationEngine.dispatch({ |
| 612 | type: "thread.meta.update", | 625 | type: "thread.meta.update", |
| 613 | commandId: serverCommandId("worktree-branch-rename"), | 626 | commandId: yield* serverCommandId("worktree-branch-rename"), |
| 614 | threadId: input.threadId, | 627 | threadId: input.threadId, |
| 615 | branch: renamed.branch, | 628 | branch: renamed.branch, |
| 616 | worktreePath: cwd, | 629 | worktreePath: cwd, |
| ... | @@ -657,7 +670,7 @@ const make = Effect.gen(function* () { | ... | @@ -657,7 +670,7 @@ const make = Effect.gen(function* () { |
| 657 | 670 | ||
| 658 | yield* orchestrationEngine.dispatch({ | 671 | yield* orchestrationEngine.dispatch({ |
| 659 | type: "thread.meta.update", | 672 | type: "thread.meta.update", |
| 660 | commandId: serverCommandId("thread-title-rename"), | 673 | commandId: yield* serverCommandId("thread-title-rename"), |
| 661 | threadId: input.threadId, | 674 | threadId: input.threadId, |
| 662 | title: generated.title, | 675 | title: generated.title, |
| 663 | }); | 676 | }); |
apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts+29-20| ... | @@ -19,6 +19,7 @@ import { | ... | @@ -19,6 +19,7 @@ import { |
| 19 | } from "@t3tools/contracts"; | 19 | } from "@t3tools/contracts"; |
| 20 | import * as Cache from "effect/Cache"; | 20 | import * as Cache from "effect/Cache"; |
| 21 | import * as Cause from "effect/Cause"; | 21 | import * as Cause from "effect/Cause"; |
| 22 | import * as Crypto from "effect/Crypto"; | ||
| 22 | import * as Duration from "effect/Duration"; | 23 | import * as Duration from "effect/Duration"; |
| 23 | import * as Effect from "effect/Effect"; | 24 | import * as Effect from "effect/Effect"; |
| 24 | import * as Layer from "effect/Layer"; | 25 | import * as Layer from "effect/Layer"; |
| ... | @@ -39,8 +40,6 @@ import { | ... | @@ -39,8 +40,6 @@ import { |
| 39 | import { ServerSettingsService } from "../../serverSettings.ts"; | 40 | import { ServerSettingsService } from "../../serverSettings.ts"; |
| 40 | 41 | ||
| 41 | const providerTurnKey = (threadId: ThreadId, turnId: TurnId) => `${threadId}:${turnId}`; | 42 | const providerTurnKey = (threadId: ThreadId, turnId: TurnId) => `${threadId}:${turnId}`; |
| 42 | const providerCommandId = (event: ProviderRuntimeEvent, tag: string): CommandId => | ||
| 43 | CommandId.make(`provider:${event.eventId}:${tag}:${crypto.randomUUID()}`); | ||
| 44 | 43 | ||
| 45 | interface AssistantSegmentState { | 44 | interface AssistantSegmentState { |
| 46 | baseKey: string; | 45 | baseKey: string; |
| ... | @@ -607,11 +606,16 @@ function runtimeEventToActivities( | ... | @@ -607,11 +606,16 @@ function runtimeEventToActivities( |
| 607 | } | 606 | } |
| 608 | 607 | ||
| 609 | const make = Effect.gen(function* () { | 608 | const make = Effect.gen(function* () { |
| 609 | const crypto = yield* Crypto.Crypto; | ||
| 610 | const orchestrationEngine = yield* OrchestrationEngineService; | 610 | const orchestrationEngine = yield* OrchestrationEngineService; |
| 611 | const projectionSnapshotQuery = yield* ProjectionSnapshotQuery; | 611 | const projectionSnapshotQuery = yield* ProjectionSnapshotQuery; |
| 612 | const providerService = yield* ProviderService; | 612 | const providerService = yield* ProviderService; |
| 613 | const projectionTurnRepository = yield* ProjectionTurnRepository; | 613 | const projectionTurnRepository = yield* ProjectionTurnRepository; |
| 614 | const serverSettingsService = yield* ServerSettingsService; | 614 | const serverSettingsService = yield* ServerSettingsService; |
| 615 | const providerCommandId = (event: ProviderRuntimeEvent, tag: string) => | ||
| 616 | crypto.randomUUIDv4.pipe( | ||
| 617 | Effect.map((uuid) => CommandId.make(`provider:${event.eventId}:${tag}:${uuid}`)), | ||
| 618 | ); | ||
| 615 | 619 | ||
| 616 | const turnMessageIdsByTurnKey = yield* Cache.make<string, Set<MessageId>>({ | 620 | const turnMessageIdsByTurnKey = yield* Cache.make<string, Set<MessageId>>({ |
| 617 | capacity: TURN_MESSAGE_IDS_BY_TURN_CACHE_CAPACITY, | 621 | capacity: TURN_MESSAGE_IDS_BY_TURN_CACHE_CAPACITY, |
| ... | @@ -848,7 +852,7 @@ const make = Effect.gen(function* () { | ... | @@ -848,7 +852,7 @@ const make = Effect.gen(function* () { |
| 848 | 852 | ||
| 849 | yield* orchestrationEngine.dispatch({ | 853 | yield* orchestrationEngine.dispatch({ |
| 850 | type: "thread.message.assistant.delta", | 854 | type: "thread.message.assistant.delta", |
| 851 | commandId: providerCommandId(input.event, input.commandTag), | 855 | commandId: yield* providerCommandId(input.event, input.commandTag), |
| 852 | threadId: input.threadId, | 856 | threadId: input.threadId, |
| 853 | messageId: input.messageId, | 857 | messageId: input.messageId, |
| 854 | delta: bufferedText, | 858 | delta: bufferedText, |
| ... | @@ -915,7 +919,7 @@ const make = Effect.gen(function* () { | ... | @@ -915,7 +919,7 @@ const make = Effect.gen(function* () { |
| 915 | if (hasRenderableText) { | 919 | if (hasRenderableText) { |
| 916 | yield* orchestrationEngine.dispatch({ | 920 | yield* orchestrationEngine.dispatch({ |
| 917 | type: "thread.message.assistant.delta", | 921 | type: "thread.message.assistant.delta", |
| 918 | commandId: providerCommandId(input.event, input.finalDeltaCommandTag), | 922 | commandId: yield* providerCommandId(input.event, input.finalDeltaCommandTag), |
| 919 | threadId: input.threadId, | 923 | threadId: input.threadId, |
| 920 | messageId: input.messageId, | 924 | messageId: input.messageId, |
| 921 | delta: text, | 925 | delta: text, |
| ... | @@ -927,7 +931,7 @@ const make = Effect.gen(function* () { | ... | @@ -927,7 +931,7 @@ const make = Effect.gen(function* () { |
| 927 | if (input.hasProjectedMessage || hasRenderableText) { | 931 | if (input.hasProjectedMessage || hasRenderableText) { |
| 928 | yield* orchestrationEngine.dispatch({ | 932 | yield* orchestrationEngine.dispatch({ |
| 929 | type: "thread.message.assistant.complete", | 933 | type: "thread.message.assistant.complete", |
| 930 | commandId: providerCommandId(input.event, input.commandTag), | 934 | commandId: yield* providerCommandId(input.event, input.commandTag), |
| 931 | threadId: input.threadId, | 935 | threadId: input.threadId, |
| 932 | messageId: input.messageId, | 936 | messageId: input.messageId, |
| 933 | ...(input.turnId ? { turnId: input.turnId } : {}), | 937 | ...(input.turnId ? { turnId: input.turnId } : {}), |
| ... | @@ -1003,7 +1007,7 @@ const make = Effect.gen(function* () { | ... | @@ -1003,7 +1007,7 @@ const make = Effect.gen(function* () { |
| 1003 | const existingPlan = findProposedPlanById(input.threadProposedPlans, input.planId); | 1007 | const existingPlan = findProposedPlanById(input.threadProposedPlans, input.planId); |
| 1004 | yield* orchestrationEngine.dispatch({ | 1008 | yield* orchestrationEngine.dispatch({ |
| 1005 | type: "thread.proposed-plan.upsert", | 1009 | type: "thread.proposed-plan.upsert", |
| 1006 | commandId: providerCommandId(input.event, "proposed-plan-upsert"), | 1010 | commandId: yield* providerCommandId(input.event, "proposed-plan-upsert"), |
| 1007 | threadId: input.threadId, | 1011 | threadId: input.threadId, |
| 1008 | proposedPlan: { | 1012 | proposedPlan: { |
| 1009 | id: input.planId, | 1013 | id: input.planId, |
| ... | @@ -1159,10 +1163,11 @@ const make = Effect.gen(function* () { | ... | @@ -1159,10 +1163,11 @@ const make = Effect.gen(function* () { |
| 1159 | return; | 1163 | return; |
| 1160 | } | 1164 | } |
| 1161 | 1165 | ||
| 1166 | const commandUuid = yield* crypto.randomUUIDv4; | ||
| 1162 | yield* orchestrationEngine.dispatch({ | 1167 | yield* orchestrationEngine.dispatch({ |
| 1163 | type: "thread.proposed-plan.upsert", | 1168 | type: "thread.proposed-plan.upsert", |
| 1164 | commandId: CommandId.make( | 1169 | commandId: CommandId.make( |
| 1165 | `provider:source-proposed-plan-implemented:${implementationThreadId}:${crypto.randomUUID()}`, | 1170 | `provider:source-proposed-plan-implemented:${implementationThreadId}:${commandUuid}`, |
| 1166 | ), | 1171 | ), |
| 1167 | threadId: sourceThread.id, | 1172 | threadId: sourceThread.id, |
| 1168 | proposedPlan: { | 1173 | proposedPlan: { |
| ... | @@ -1296,7 +1301,7 @@ const make = Effect.gen(function* () { | ... | @@ -1296,7 +1301,7 @@ const make = Effect.gen(function* () { |
| 1296 | 1301 | ||
| 1297 | yield* orchestrationEngine.dispatch({ | 1302 | yield* orchestrationEngine.dispatch({ |
| 1298 | type: "thread.session.set", | 1303 | type: "thread.session.set", |
| 1299 | commandId: providerCommandId(event, "thread-session-set"), | 1304 | commandId: yield* providerCommandId(event, "thread-session-set"), |
| 1300 | threadId: thread.id, | 1305 | threadId: thread.id, |
| 1301 | session: { | 1306 | session: { |
| 1302 | threadId: thread.id, | 1307 | threadId: thread.id, |
| ... | @@ -1342,7 +1347,7 @@ const make = Effect.gen(function* () { | ... | @@ -1342,7 +1347,7 @@ const make = Effect.gen(function* () { |
| 1342 | if (spillChunk.length > 0) { | 1347 | if (spillChunk.length > 0) { |
| 1343 | yield* orchestrationEngine.dispatch({ | 1348 | yield* orchestrationEngine.dispatch({ |
| 1344 | type: "thread.message.assistant.delta", | 1349 | type: "thread.message.assistant.delta", |
| 1345 | commandId: providerCommandId(event, "assistant-delta-buffer-spill"), | 1350 | commandId: yield* providerCommandId(event, "assistant-delta-buffer-spill"), |
| 1346 | threadId: thread.id, | 1351 | threadId: thread.id, |
| 1347 | messageId: assistantMessageId, | 1352 | messageId: assistantMessageId, |
| 1348 | delta: spillChunk, | 1353 | delta: spillChunk, |
| ... | @@ -1353,7 +1358,7 @@ const make = Effect.gen(function* () { | ... | @@ -1353,7 +1358,7 @@ const make = Effect.gen(function* () { |
| 1353 | } else { | 1358 | } else { |
| 1354 | yield* orchestrationEngine.dispatch({ | 1359 | yield* orchestrationEngine.dispatch({ |
| 1355 | type: "thread.message.assistant.delta", | 1360 | type: "thread.message.assistant.delta", |
| 1356 | commandId: providerCommandId(event, "assistant-delta"), | 1361 | commandId: yield* providerCommandId(event, "assistant-delta"), |
| 1357 | threadId: thread.id, | 1362 | threadId: thread.id, |
| 1358 | messageId: assistantMessageId, | 1363 | messageId: assistantMessageId, |
| 1359 | delta: assistantDelta, | 1364 | delta: assistantDelta, |
| ... | @@ -1546,7 +1551,7 @@ const make = Effect.gen(function* () { | ... | @@ -1546,7 +1551,7 @@ const make = Effect.gen(function* () { |
| 1546 | if (shouldApplyRuntimeError) { | 1551 | if (shouldApplyRuntimeError) { |
| 1547 | yield* orchestrationEngine.dispatch({ | 1552 | yield* orchestrationEngine.dispatch({ |
| 1548 | type: "thread.session.set", | 1553 | type: "thread.session.set", |
| 1549 | commandId: providerCommandId(event, "runtime-error-session-set"), | 1554 | commandId: yield* providerCommandId(event, "runtime-error-session-set"), |
| 1550 | threadId: thread.id, | 1555 | threadId: thread.id, |
| 1551 | session: { | 1556 | session: { |
| 1552 | threadId: thread.id, | 1557 | threadId: thread.id, |
| ... | @@ -1568,7 +1573,7 @@ const make = Effect.gen(function* () { | ... | @@ -1568,7 +1573,7 @@ const make = Effect.gen(function* () { |
| 1568 | if (event.type === "thread.metadata.updated" && event.payload.name) { | 1573 | if (event.type === "thread.metadata.updated" && event.payload.name) { |
| 1569 | yield* orchestrationEngine.dispatch({ | 1574 | yield* orchestrationEngine.dispatch({ |
| 1570 | type: "thread.meta.update", | 1575 | type: "thread.meta.update", |
| 1571 | commandId: providerCommandId(event, "thread-meta-update"), | 1576 | commandId: yield* providerCommandId(event, "thread-meta-update"), |
| 1572 | threadId: thread.id, | 1577 | threadId: thread.id, |
| 1573 | title: event.payload.name, | 1578 | title: event.payload.name, |
| 1574 | }); | 1579 | }); |
| ... | @@ -1596,7 +1601,7 @@ const make = Effect.gen(function* () { | ... | @@ -1596,7 +1601,7 @@ const make = Effect.gen(function* () { |
| 1596 | ); | 1601 | ); |
| 1597 | yield* orchestrationEngine.dispatch({ | 1602 | yield* orchestrationEngine.dispatch({ |
| 1598 | type: "thread.turn.diff.complete", | 1603 | type: "thread.turn.diff.complete", |
| 1599 | commandId: providerCommandId(event, "thread-turn-diff-complete"), | 1604 | commandId: yield* providerCommandId(event, "thread-turn-diff-complete"), |
| 1600 | threadId: thread.id, | 1605 | threadId: thread.id, |
| 1601 | turnId, | 1606 | turnId, |
| 1602 | completedAt: now, | 1607 | completedAt: now, |
| ... | @@ -1613,13 +1618,17 @@ const make = Effect.gen(function* () { | ... | @@ -1613,13 +1618,17 @@ const make = Effect.gen(function* () { |
| 1613 | 1618 | ||
| 1614 | const activities = runtimeEventToActivities(event); | 1619 | const activities = runtimeEventToActivities(event); |
| 1615 | yield* Effect.forEach(activities, (activity) => | 1620 | yield* Effect.forEach(activities, (activity) => |
| 1616 | orchestrationEngine.dispatch({ | 1621 | providerCommandId(event, "thread-activity-append").pipe( |
| 1617 | type: "thread.activity.append", | 1622 | Effect.flatMap((commandId) => |
| 1618 | commandId: providerCommandId(event, "thread-activity-append"), | 1623 | orchestrationEngine.dispatch({ |
| 1619 | threadId: thread.id, | 1624 | type: "thread.activity.append", |
| 1620 | activity, | 1625 | commandId, |
| 1621 | createdAt: activity.createdAt, | 1626 | threadId: thread.id, |
| 1622 | }), | 1627 | activity, |
| 1628 | createdAt: activity.createdAt, | ||
| 1629 | }), | ||
| 1630 | ), | ||
| 1631 | ), | ||
| 1623 | ).pipe(Effect.asVoid); | 1632 | ).pipe(Effect.asVoid); |
| 1624 | }); | 1633 | }); |
| 1625 | 1634 |
apps/server/src/orchestration/decider.delete.test.ts+135-145| ... | @@ -6,11 +6,11 @@ import { | ... | @@ -6,11 +6,11 @@ import { |
| 6 | ThreadId, | 6 | ThreadId, |
| 7 | type OrchestrationCommand, | 7 | type OrchestrationCommand, |
| 8 | type OrchestrationEvent, | 8 | type OrchestrationEvent, |
| 9 | type OrchestrationReadModel, | ||
| 10 | ProviderInstanceId, | 9 | ProviderInstanceId, |
| 11 | } from "@t3tools/contracts"; | 10 | } from "@t3tools/contracts"; |
| 12 | import * as Effect from "effect/Effect"; | 11 | import * as Effect from "effect/Effect"; |
| 13 | import { describe, expect, it } from "vitest"; | 12 | import * as NodeServices from "@effect/platform-node/NodeServices"; |
| 13 | import { expect, it } from "@effect/vitest"; | ||
| 14 | 14 | ||
| 15 | import { decideOrchestrationCommand } from "./decider.ts"; | 15 | import { decideOrchestrationCommand } from "./decider.ts"; |
| 16 | import { createEmptyReadModel, projectEvent } from "./projector.ts"; | 16 | import { createEmptyReadModel, projectEvent } from "./projector.ts"; |
| ... | @@ -20,93 +20,87 @@ const asEventId = (value: string): EventId => EventId.make(value); | ... | @@ -20,93 +20,87 @@ const asEventId = (value: string): EventId => EventId.make(value); |
| 20 | const asProjectId = (value: string): ProjectId => ProjectId.make(value); | 20 | const asProjectId = (value: string): ProjectId => ProjectId.make(value); |
| 21 | const asThreadId = (value: string): ThreadId => ThreadId.make(value); | 21 | const asThreadId = (value: string): ThreadId => ThreadId.make(value); |
| 22 | 22 | ||
| 23 | async function seedReadModel(): Promise<OrchestrationReadModel> { | 23 | const seedReadModel = Effect.gen(function* () { |
| 24 | const now = "2026-01-01T00:00:00.000Z"; | 24 | const now = "2026-01-01T00:00:00.000Z"; |
| 25 | const initial = createEmptyReadModel(now); | 25 | const initial = createEmptyReadModel(now); |
| 26 | const withProject = await Effect.runPromise( | 26 | const withProject = yield* projectEvent(initial, { |
| 27 | projectEvent(initial, { | 27 | sequence: 1, |
| 28 | sequence: 1, | 28 | eventId: asEventId("evt-project-create"), |
| 29 | eventId: asEventId("evt-project-create"), | 29 | aggregateKind: "project", |
| 30 | aggregateKind: "project", | 30 | aggregateId: asProjectId("project-delete"), |
| 31 | aggregateId: asProjectId("project-delete"), | 31 | type: "project.created", |
| 32 | type: "project.created", | 32 | occurredAt: now, |
| 33 | occurredAt: now, | 33 | commandId: asCommandId("cmd-project-create"), |
| 34 | commandId: asCommandId("cmd-project-create"), | 34 | causationEventId: null, |
| 35 | causationEventId: null, | 35 | correlationId: asCommandId("cmd-project-create"), |
| 36 | correlationId: asCommandId("cmd-project-create"), | 36 | metadata: {}, |
| 37 | metadata: {}, | 37 | payload: { |
| 38 | payload: { | 38 | projectId: asProjectId("project-delete"), |
| 39 | projectId: asProjectId("project-delete"), | 39 | title: "Project Delete", |
| 40 | title: "Project Delete", | 40 | workspaceRoot: "/tmp/project-delete", |
| 41 | workspaceRoot: "/tmp/project-delete", | 41 | defaultModelSelection: null, |
| 42 | defaultModelSelection: null, | 42 | scripts: [], |
| 43 | scripts: [], | 43 | createdAt: now, |
| 44 | createdAt: now, | 44 | updatedAt: now, |
| 45 | updatedAt: now, | 45 | }, |
| 46 | }, | 46 | }); |
| 47 | }), | ||
| 48 | ); | ||
| 49 | 47 | ||
| 50 | const withFirstThread = await Effect.runPromise( | 48 | const withFirstThread = yield* projectEvent(withProject, { |
| 51 | projectEvent(withProject, { | 49 | sequence: 2, |
| 52 | sequence: 2, | 50 | eventId: asEventId("evt-thread-create-1"), |
| 53 | eventId: asEventId("evt-thread-create-1"), | 51 | aggregateKind: "thread", |
| 54 | aggregateKind: "thread", | 52 | aggregateId: asThreadId("thread-delete-1"), |
| 55 | aggregateId: asThreadId("thread-delete-1"), | 53 | type: "thread.created", |
| 56 | type: "thread.created", | 54 | occurredAt: now, |
| 57 | occurredAt: now, | 55 | commandId: asCommandId("cmd-thread-create-1"), |
| 58 | commandId: asCommandId("cmd-thread-create-1"), | 56 | causationEventId: null, |
| 59 | causationEventId: null, | 57 | correlationId: asCommandId("cmd-thread-create-1"), |
| 60 | correlationId: asCommandId("cmd-thread-create-1"), | 58 | metadata: {}, |
| 61 | metadata: {}, | 59 | payload: { |
| 62 | payload: { | 60 | threadId: asThreadId("thread-delete-1"), |
| 63 | threadId: asThreadId("thread-delete-1"), | 61 | projectId: asProjectId("project-delete"), |
| 64 | projectId: asProjectId("project-delete"), | 62 | title: "Thread Delete 1", |
| 65 | title: "Thread Delete 1", | 63 | modelSelection: { |
| 66 | modelSelection: { | 64 | instanceId: ProviderInstanceId.make("codex"), |
| 67 | instanceId: ProviderInstanceId.make("codex"), | 65 | model: "gpt-5-codex", |
| 68 | model: "gpt-5-codex", | ||
| 69 | }, | ||
| 70 | interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, | ||
| 71 | runtimeMode: "approval-required", | ||
| 72 | branch: null, | ||
| 73 | worktreePath: null, | ||
| 74 | createdAt: now, | ||
| 75 | updatedAt: now, | ||
| 76 | }, | 66 | }, |
| 77 | }), | 67 | interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, |
| 78 | ); | 68 | runtimeMode: "approval-required", |
| 69 | branch: null, | ||
| 70 | worktreePath: null, | ||
| 71 | createdAt: now, | ||
| 72 | updatedAt: now, | ||
| 73 | }, | ||
| 74 | }); | ||
| 79 | 75 | ||
| 80 | return Effect.runPromise( | 76 | return yield* projectEvent(withFirstThread, { |
| 81 | projectEvent(withFirstThread, { | 77 | sequence: 3, |
| 82 | sequence: 3, | 78 | eventId: asEventId("evt-thread-create-2"), |
| 83 | eventId: asEventId("evt-thread-create-2"), | 79 | aggregateKind: "thread", |
| 84 | aggregateKind: "thread", | 80 | aggregateId: asThreadId("thread-delete-2"), |
| 85 | aggregateId: asThreadId("thread-delete-2"), | 81 | type: "thread.created", |
| 86 | type: "thread.created", | 82 | occurredAt: now, |
| 87 | occurredAt: now, | 83 | commandId: asCommandId("cmd-thread-create-2"), |
| 88 | commandId: asCommandId("cmd-thread-create-2"), | 84 | causationEventId: null, |
| 89 | causationEventId: null, | 85 | correlationId: asCommandId("cmd-thread-create-2"), |
| 90 | correlationId: asCommandId("cmd-thread-create-2"), | 86 | metadata: {}, |
| 91 | metadata: {}, | 87 | payload: { |
| 92 | payload: { | 88 | threadId: asThreadId("thread-delete-2"), |
| 93 | threadId: asThreadId("thread-delete-2"), | 89 | projectId: asProjectId("project-delete"), |
| 94 | projectId: asProjectId("project-delete"), | 90 | title: "Thread Delete 2", |
| 95 | title: "Thread Delete 2", | 91 | modelSelection: { |
| 96 | modelSelection: { | 92 | instanceId: ProviderInstanceId.make("codex"), |
| 97 | instanceId: ProviderInstanceId.make("codex"), | 93 | model: "gpt-5-codex", |
| 98 | model: "gpt-5-codex", | ||
| 99 | }, | ||
| 100 | interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, | ||
| 101 | runtimeMode: "approval-required", | ||
| 102 | branch: null, | ||
| 103 | worktreePath: null, | ||
| 104 | createdAt: now, | ||
| 105 | updatedAt: now, | ||
| 106 | }, | 94 | }, |
| 107 | }), | 95 | interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, |
| 108 | ); | 96 | runtimeMode: "approval-required", |
| 109 | } | 97 | branch: null, |
| 98 | worktreePath: null, | ||
| 99 | createdAt: now, | ||
| 100 | updatedAt: now, | ||
| 101 | }, | ||
| 102 | }); | ||
| 103 | }); | ||
| 110 | 104 | ||
| 111 | type PlannedEvent = Omit<OrchestrationEvent, "sequence">; | 105 | type PlannedEvent = Omit<OrchestrationEvent, "sequence">; |
| 112 | 106 | ||
| ... | @@ -142,12 +136,11 @@ function normalizeDeleteEvent(event: PlannedEvent | ReadonlyArray<PlannedEvent>) | ... | @@ -142,12 +136,11 @@ function normalizeDeleteEvent(event: PlannedEvent | ReadonlyArray<PlannedEvent>) |
| 142 | }); | 136 | }); |
| 143 | } | 137 | } |
| 144 | 138 | ||
| 145 | describe("decider deletion flows", () => { | 139 | it.layer(NodeServices.layer)("decider deletion flows", (it) => { |
| 146 | it("rejects deleting a non-empty project without force", async () => { | 140 | it.effect("rejects deleting a non-empty project without force", () => |
| 147 | const readModel = await seedReadModel(); | 141 | Effect.gen(function* () { |
| 148 | 142 | const readModel = yield* seedReadModel; | |
| 149 | await expect( | 143 | const error = yield* Effect.flip( |
| 150 | Effect.runPromise( | ||
| 151 | decideOrchestrationCommand({ | 144 | decideOrchestrationCommand({ |
| 152 | command: { | 145 | command: { |
| 153 | type: "project.delete", | 146 | type: "project.delete", |
| ... | @@ -156,72 +149,69 @@ describe("decider deletion flows", () => { | ... | @@ -156,72 +149,69 @@ describe("decider deletion flows", () => { |
| 156 | }, | 149 | }, |
| 157 | readModel, | 150 | readModel, |
| 158 | }), | 151 | }), |
| 159 | ), | 152 | ); |
| 160 | ).rejects.toThrow("cannot be deleted without force=true"); | 153 | expect(error.message).toContain("cannot be deleted without force=true"); |
| 161 | }); | 154 | }), |
| 155 | ); | ||
| 162 | 156 | ||
| 163 | it("reuses thread.delete semantics when force-deleting a non-empty project", async () => { | 157 | it.effect("reuses thread.delete semantics when force-deleting a non-empty project", () => |
| 164 | const readModel = await seedReadModel(); | 158 | Effect.gen(function* () { |
| 165 | const projectDeleteCommand: Extract<OrchestrationCommand, { type: "project.delete" }> = { | 159 | const readModel = yield* seedReadModel; |
| 166 | type: "project.delete", | 160 | const projectDeleteCommand: Extract<OrchestrationCommand, { type: "project.delete" }> = { |
| 167 | commandId: asCommandId("cmd-project-delete-force"), | 161 | type: "project.delete", |
| 168 | projectId: asProjectId("project-delete"), | 162 | commandId: asCommandId("cmd-project-delete-force"), |
| 169 | force: true, | 163 | projectId: asProjectId("project-delete"), |
| 170 | }; | 164 | force: true, |
| 165 | }; | ||
| 171 | 166 | ||
| 172 | const forcedResult = await Effect.runPromise( | 167 | const forcedResult = yield* decideOrchestrationCommand({ |
| 173 | decideOrchestrationCommand({ | ||
| 174 | command: projectDeleteCommand, | 168 | command: projectDeleteCommand, |
| 175 | readModel, | 169 | readModel, |
| 176 | }), | 170 | }); |
| 177 | ); | 171 | const forcedEvents = Array.isArray(forcedResult) ? forcedResult : [forcedResult]; |
| 178 | const forcedEvents = Array.isArray(forcedResult) ? forcedResult : [forcedResult]; | ||
| 179 | 172 | ||
| 180 | expect(forcedEvents.map((event) => event.type)).toEqual([ | 173 | expect(forcedEvents.map((event) => event.type)).toEqual([ |
| 181 | "thread.deleted", | 174 | "thread.deleted", |
| 182 | "thread.deleted", | 175 | "thread.deleted", |
| 183 | "project.deleted", | 176 | "project.deleted", |
| 184 | ]); | 177 | ]); |
| 185 | 178 | ||
| 186 | let sequentialReadModel = readModel; | 179 | let sequentialReadModel = readModel; |
| 187 | let nextSequence = readModel.snapshotSequence; | 180 | let nextSequence = readModel.snapshotSequence; |
| 188 | const sequentialEvents: PlannedEvent[] = []; | 181 | const sequentialEvents: PlannedEvent[] = []; |
| 189 | for (const nextCommand of [ | 182 | for (const nextCommand of [ |
| 190 | { | 183 | { |
| 191 | type: "thread.delete", | 184 | type: "thread.delete", |
| 192 | commandId: projectDeleteCommand.commandId, | 185 | commandId: projectDeleteCommand.commandId, |
| 193 | threadId: asThreadId("thread-delete-1"), | 186 | threadId: asThreadId("thread-delete-1"), |
| 194 | }, | 187 | }, |
| 195 | { | 188 | { |
| 196 | type: "thread.delete", | 189 | type: "thread.delete", |
| 197 | commandId: projectDeleteCommand.commandId, | 190 | commandId: projectDeleteCommand.commandId, |
| 198 | threadId: asThreadId("thread-delete-2"), | 191 | threadId: asThreadId("thread-delete-2"), |
| 199 | }, | 192 | }, |
| 200 | { | 193 | { |
| 201 | type: "project.delete", | 194 | type: "project.delete", |
| 202 | commandId: projectDeleteCommand.commandId, | 195 | commandId: projectDeleteCommand.commandId, |
| 203 | projectId: asProjectId("project-delete"), | 196 | projectId: asProjectId("project-delete"), |
| 204 | }, | 197 | }, |
| 205 | ] satisfies ReadonlyArray<OrchestrationCommand>) { | 198 | ] satisfies ReadonlyArray<OrchestrationCommand>) { |
| 206 | const decided = await Effect.runPromise( | 199 | const decided = yield* decideOrchestrationCommand({ |
| 207 | decideOrchestrationCommand({ | ||
| 208 | command: nextCommand, | 200 | command: nextCommand, |
| 209 | readModel: sequentialReadModel, | 201 | readModel: sequentialReadModel, |
| 210 | }), | 202 | }); |
| 211 | ); | 203 | const nextEvents = Array.isArray(decided) ? decided : [decided]; |
| 212 | const nextEvents = Array.isArray(decided) ? decided : [decided]; | 204 | sequentialEvents.push(...nextEvents); |
| 213 | sequentialEvents.push(...nextEvents); | 205 | for (const nextEvent of nextEvents) { |
| 214 | for (const nextEvent of nextEvents) { | 206 | nextSequence += 1; |
| 215 | nextSequence += 1; | 207 | sequentialReadModel = yield* projectEvent(sequentialReadModel, { |
| 216 | sequentialReadModel = await Effect.runPromise( | ||
| 217 | projectEvent(sequentialReadModel, { | ||
| 218 | ...nextEvent, | 208 | ...nextEvent, |
| 219 | sequence: nextSequence, | 209 | sequence: nextSequence, |
| 220 | }), | 210 | }); |
| 221 | ); | 211 | } |
| 222 | } | 212 | } |
| 223 | } | ||
| 224 | 213 | ||
| 225 | expect(normalizeDeleteEvent(forcedResult)).toEqual(normalizeDeleteEvent(sequentialEvents)); | 214 | expect(normalizeDeleteEvent(forcedResult)).toEqual(normalizeDeleteEvent(sequentialEvents)); |
| 226 | }); | 215 | }), |
| 216 | ); | ||
| 227 | }); | 217 | }); |
apps/server/src/orchestration/decider.projectScripts.test.ts+113-127| ... | @@ -8,8 +8,9 @@ import { | ... | @@ -8,8 +8,9 @@ import { |
| 8 | ProviderInstanceId, | 8 | ProviderInstanceId, |
| 9 | } from "@t3tools/contracts"; | 9 | } from "@t3tools/contracts"; |
| 10 | import { createModelSelection } from "@t3tools/shared/model"; | 10 | import { createModelSelection } from "@t3tools/shared/model"; |
| 11 | import { describe, expect, it } from "vitest"; | 11 | import { expect, it } from "@effect/vitest"; |
| 12 | import * as Effect from "effect/Effect"; | 12 | import * as Effect from "effect/Effect"; |
| 13 | import * as NodeServices from "@effect/platform-node/NodeServices"; | ||
| 13 | 14 | ||
| 14 | import { decideOrchestrationCommand } from "./decider.ts"; | 15 | import { decideOrchestrationCommand } from "./decider.ts"; |
| 15 | import { createEmptyReadModel, projectEvent } from "./projector.ts"; | 16 | import { createEmptyReadModel, projectEvent } from "./projector.ts"; |
| ... | @@ -17,14 +18,13 @@ import { createEmptyReadModel, projectEvent } from "./projector.ts"; | ... | @@ -17,14 +18,13 @@ import { createEmptyReadModel, projectEvent } from "./projector.ts"; |
| 17 | const asEventId = (value: string): EventId => EventId.make(value); | 18 | const asEventId = (value: string): EventId => EventId.make(value); |
| 18 | const asProjectId = (value: string): ProjectId => ProjectId.make(value); | 19 | const asProjectId = (value: string): ProjectId => ProjectId.make(value); |
| 19 | const asMessageId = (value: string): MessageId => MessageId.make(value); | 20 | const asMessageId = (value: string): MessageId => MessageId.make(value); |
| 21 | it.layer(NodeServices.layer)("decider project scripts", (it) => { | ||
| 22 | it.effect("emits empty scripts on project.create", () => | ||
| 23 | Effect.gen(function* () { | ||
| 24 | const now = "2026-01-01T00:00:00.000Z"; | ||
| 25 | const readModel = createEmptyReadModel(now); | ||
| 20 | 26 | ||
| 21 | describe("decider project scripts", () => { | 27 | const result = yield* decideOrchestrationCommand({ |
| 22 | it("emits empty scripts on project.create", async () => { | ||
| 23 | const now = "2026-01-01T00:00:00.000Z"; | ||
| 24 | const readModel = createEmptyReadModel(now); | ||
| 25 | |||
| 26 | const result = await Effect.runPromise( | ||
| 27 | decideOrchestrationCommand({ | ||
| 28 | command: { | 28 | command: { |
| 29 | type: "project.create", | 29 | type: "project.create", |
| 30 | commandId: CommandId.make("cmd-project-create-scripts"), | 30 | commandId: CommandId.make("cmd-project-create-scripts"), |
| ... | @@ -34,19 +34,19 @@ describe("decider project scripts", () => { | ... | @@ -34,19 +34,19 @@ describe("decider project scripts", () => { |
| 34 | createdAt: now, | 34 | createdAt: now, |
| 35 | }, | 35 | }, |
| 36 | readModel, | 36 | readModel, |
| 37 | }), | 37 | }); |
| 38 | ); | ||
| 39 | 38 | ||
| 40 | const event = Array.isArray(result) ? result[0] : result; | 39 | const event = Array.isArray(result) ? result[0] : result; |
| 41 | expect(event.type).toBe("project.created"); | 40 | expect(event.type).toBe("project.created"); |
| 42 | expect((event.payload as { scripts: unknown[] }).scripts).toEqual([]); | 41 | expect((event.payload as { scripts: unknown[] }).scripts).toEqual([]); |
| 43 | }); | 42 | }), |
| 43 | ); | ||
| 44 | 44 | ||
| 45 | it("propagates scripts in project.meta.update payload", async () => { | 45 | it.effect("propagates scripts in project.meta.update payload", () => |
| 46 | const now = "2026-01-01T00:00:00.000Z"; | 46 | Effect.gen(function* () { |
| 47 | const initial = createEmptyReadModel(now); | 47 | const now = "2026-01-01T00:00:00.000Z"; |
| 48 | const readModel = await Effect.runPromise( | 48 | const initial = createEmptyReadModel(now); |
| 49 | projectEvent(initial, { | 49 | const readModel = yield* projectEvent(initial, { |
| 50 | sequence: 1, | 50 | sequence: 1, |
| 51 | eventId: asEventId("evt-project-create-scripts"), | 51 | eventId: asEventId("evt-project-create-scripts"), |
| 52 | aggregateKind: "project", | 52 | aggregateKind: "project", |
| ... | @@ -66,21 +66,19 @@ describe("decider project scripts", () => { | ... | @@ -66,21 +66,19 @@ describe("decider project scripts", () => { |
| 66 | createdAt: now, | 66 | createdAt: now, |
| 67 | updatedAt: now, | 67 | updatedAt: now, |
| 68 | }, | 68 | }, |
| 69 | }), | 69 | }); |
| 70 | ); | ||
| 71 | 70 | ||
| 72 | const scripts = [ | 71 | const scripts = [ |
| 73 | { | 72 | { |
| 74 | id: "lint", | 73 | id: "lint", |
| 75 | name: "Lint", | 74 | name: "Lint", |
| 76 | command: "bun run lint", | 75 | command: "bun run lint", |
| 77 | icon: "lint", | 76 | icon: "lint", |
| 78 | runOnWorktreeCreate: false, | 77 | runOnWorktreeCreate: false, |
| 79 | }, | 78 | }, |
| 80 | ] as const; | 79 | ] as const; |
| 81 | 80 | ||
| 82 | const result = await Effect.runPromise( | 81 | const result = yield* decideOrchestrationCommand({ |
| 83 | decideOrchestrationCommand({ | ||
| 84 | command: { | 82 | command: { |
| 85 | type: "project.meta.update", | 83 | type: "project.meta.update", |
| 86 | commandId: CommandId.make("cmd-project-update-scripts"), | 84 | commandId: CommandId.make("cmd-project-update-scripts"), |
| ... | @@ -88,19 +86,19 @@ describe("decider project scripts", () => { | ... | @@ -88,19 +86,19 @@ describe("decider project scripts", () => { |
| 88 | scripts: Array.from(scripts), | 86 | scripts: Array.from(scripts), |
| 89 | }, | 87 | }, |
| 90 | readModel, | 88 | readModel, |
| 91 | }), | 89 | }); |
| 92 | ); | ||
| 93 | 90 | ||
| 94 | const event = Array.isArray(result) ? result[0] : result; | 91 | const event = Array.isArray(result) ? result[0] : result; |
| 95 | expect(event.type).toBe("project.meta-updated"); | 92 | expect(event.type).toBe("project.meta-updated"); |
| 96 | expect((event.payload as { scripts?: unknown[] }).scripts).toEqual(scripts); | 93 | expect((event.payload as { scripts?: unknown[] }).scripts).toEqual(scripts); |
| 97 | }); | 94 | }), |
| 95 | ); | ||
| 98 | 96 | ||
| 99 | it("emits user message and turn-start-requested events for thread.turn.start", async () => { | 97 | it.effect("emits user message and turn-start-requested events for thread.turn.start", () => |
| 100 | const now = "2026-01-01T00:00:00.000Z"; | 98 | Effect.gen(function* () { |
| 101 | const initial = createEmptyReadModel(now); | 99 | const now = "2026-01-01T00:00:00.000Z"; |
| 102 | const withProject = await Effect.runPromise( | 100 | const initial = createEmptyReadModel(now); |
| 103 | projectEvent(initial, { | 101 | const withProject = yield* projectEvent(initial, { |
| 104 | sequence: 1, | 102 | sequence: 1, |
| 105 | eventId: asEventId("evt-project-create"), | 103 | eventId: asEventId("evt-project-create"), |
| 106 | aggregateKind: "project", | 104 | aggregateKind: "project", |
| ... | @@ -120,10 +118,8 @@ describe("decider project scripts", () => { | ... | @@ -120,10 +118,8 @@ describe("decider project scripts", () => { |
| 120 | createdAt: now, | 118 | createdAt: now, |
| 121 | updatedAt: now, | 119 | updatedAt: now, |
| 122 | }, | 120 | }, |
| 123 | }), | 121 | }); |
| 124 | ); | 122 | const readModel = yield* projectEvent(withProject, { |
| 125 | const readModel = await Effect.runPromise( | ||
| 126 | projectEvent(withProject, { | ||
| 127 | sequence: 2, | 123 | sequence: 2, |
| 128 | eventId: asEventId("evt-thread-create"), | 124 | eventId: asEventId("evt-thread-create"), |
| 129 | aggregateKind: "thread", | 125 | aggregateKind: "thread", |
| ... | @@ -149,11 +145,9 @@ describe("decider project scripts", () => { | ... | @@ -149,11 +145,9 @@ describe("decider project scripts", () => { |
| 149 | createdAt: now, | 145 | createdAt: now, |
| 150 | updatedAt: now, | 146 | updatedAt: now, |
| 151 | }, | 147 | }, |
| 152 | }), | 148 | }); |
| 153 | ); | ||
| 154 | 149 | ||
| 155 | const result = await Effect.runPromise( | 150 | const result = yield* decideOrchestrationCommand({ |
| 156 | decideOrchestrationCommand({ | ||
| 157 | command: { | 151 | command: { |
| 158 | type: "thread.turn.start", | 152 | type: "thread.turn.start", |
| 159 | commandId: CommandId.make("cmd-turn-start"), | 153 | commandId: CommandId.make("cmd-turn-start"), |
| ... | @@ -173,35 +167,35 @@ describe("decider project scripts", () => { | ... | @@ -173,35 +167,35 @@ describe("decider project scripts", () => { |
| 173 | createdAt: now, | 167 | createdAt: now, |
| 174 | }, | 168 | }, |
| 175 | readModel, | 169 | readModel, |
| 176 | }), | 170 | }); |
| 177 | ); | ||
| 178 | 171 | ||
| 179 | expect(Array.isArray(result)).toBe(true); | 172 | expect(Array.isArray(result)).toBe(true); |
| 180 | const events = Array.isArray(result) ? result : [result]; | 173 | const events = Array.isArray(result) ? result : [result]; |
| 181 | expect(events).toHaveLength(2); | 174 | expect(events).toHaveLength(2); |
| 182 | expect(events[0]?.type).toBe("thread.message-sent"); | 175 | expect(events[0]?.type).toBe("thread.message-sent"); |
| 183 | const turnStartEvent = events[1]; | 176 | const turnStartEvent = events[1]; |
| 184 | expect(turnStartEvent?.type).toBe("thread.turn-start-requested"); | 177 | expect(turnStartEvent?.type).toBe("thread.turn-start-requested"); |
| 185 | expect(turnStartEvent?.causationEventId).toBe(events[0]?.eventId ?? null); | 178 | expect(turnStartEvent?.causationEventId).toBe(events[0]?.eventId ?? null); |
| 186 | if (turnStartEvent?.type !== "thread.turn-start-requested") { | 179 | if (turnStartEvent?.type !== "thread.turn-start-requested") { |
| 187 | return; | 180 | return; |
| 188 | } | 181 | } |
| 189 | expect(turnStartEvent.payload).toMatchObject({ | 182 | expect(turnStartEvent.payload).toMatchObject({ |
| 190 | threadId: ThreadId.make("thread-1"), | 183 | threadId: ThreadId.make("thread-1"), |
| 191 | messageId: asMessageId("message-user-1"), | 184 | messageId: asMessageId("message-user-1"), |
| 192 | modelSelection: createModelSelection(ProviderInstanceId.make("codex"), "gpt-5.3-codex", [ | 185 | modelSelection: createModelSelection(ProviderInstanceId.make("codex"), "gpt-5.3-codex", [ |
| 193 | { id: "reasoningEffort", value: "high" }, | 186 | { id: "reasoningEffort", value: "high" }, |
| 194 | { id: "fastMode", value: true }, | 187 | { id: "fastMode", value: true }, |
| 195 | ]), | 188 | ]), |
| 196 | runtimeMode: "approval-required", | 189 | runtimeMode: "approval-required", |
| 197 | }); | 190 | }); |
| 198 | }); | 191 | }), |
| 192 | ); | ||
| 199 | 193 | ||
| 200 | it("emits thread.runtime-mode-set from thread.runtime-mode.set", async () => { | 194 | it.effect("emits thread.runtime-mode-set from thread.runtime-mode.set", () => |
| 201 | const now = "2026-01-01T00:00:00.000Z"; | 195 | Effect.gen(function* () { |
| 202 | const initial = createEmptyReadModel(now); | 196 | const now = "2026-01-01T00:00:00.000Z"; |
| 203 | const withProject = await Effect.runPromise( | 197 | const initial = createEmptyReadModel(now); |
| 204 | projectEvent(initial, { | 198 | const withProject = yield* projectEvent(initial, { |
| 205 | sequence: 1, | 199 | sequence: 1, |
| 206 | eventId: asEventId("evt-project-create"), | 200 | eventId: asEventId("evt-project-create"), |
| 207 | aggregateKind: "project", | 201 | aggregateKind: "project", |
| ... | @@ -221,10 +215,8 @@ describe("decider project scripts", () => { | ... | @@ -221,10 +215,8 @@ describe("decider project scripts", () => { |
| 221 | createdAt: now, | 215 | createdAt: now, |
| 222 | updatedAt: now, | 216 | updatedAt: now, |
| 223 | }, | 217 | }, |
| 224 | }), | 218 | }); |
| 225 | ); | 219 | const readModel = yield* projectEvent(withProject, { |
| 226 | const readModel = await Effect.runPromise( | ||
| 227 | projectEvent(withProject, { | ||
| 228 | sequence: 2, | 220 | sequence: 2, |
| 229 | eventId: asEventId("evt-thread-create"), | 221 | eventId: asEventId("evt-thread-create"), |
| 230 | aggregateKind: "thread", | 222 | aggregateKind: "thread", |
| ... | @@ -250,11 +242,9 @@ describe("decider project scripts", () => { | ... | @@ -250,11 +242,9 @@ describe("decider project scripts", () => { |
| 250 | createdAt: now, | 242 | createdAt: now, |
| 251 | updatedAt: now, | 243 | updatedAt: now, |
| 252 | }, | 244 | }, |
| 253 | }), | 245 | }); |
| 254 | ); | ||
| 255 | 246 | ||
| 256 | const result = await Effect.runPromise( | 247 | const result = yield* decideOrchestrationCommand({ |
| 257 | decideOrchestrationCommand({ | ||
| 258 | command: { | 248 | command: { |
| 259 | type: "thread.runtime-mode.set", | 249 | type: "thread.runtime-mode.set", |
| 260 | commandId: CommandId.make("cmd-runtime-mode-set"), | 250 | commandId: CommandId.make("cmd-runtime-mode-set"), |
| ... | @@ -263,27 +253,27 @@ describe("decider project scripts", () => { | ... | @@ -263,27 +253,27 @@ describe("decider project scripts", () => { |
| 263 | createdAt: now, | 253 | createdAt: now, |
| 264 | }, | 254 | }, |
| 265 | readModel, | 255 | readModel, |
| 266 | }), | 256 | }); |
| 267 | ); | ||
| 268 | 257 | ||
| 269 | const singleResult = Array.isArray(result) ? null : result; | 258 | const singleResult = Array.isArray(result) ? null : result; |
| 270 | if (singleResult === null) { | 259 | if (singleResult === null) { |
| 271 | throw new Error("Expected a single runtime-mode-set event."); | 260 | throw new Error("Expected a single runtime-mode-set event."); |
| 272 | } | 261 | } |
| 273 | expect(singleResult).toMatchObject({ | 262 | expect(singleResult).toMatchObject({ |
| 274 | type: "thread.runtime-mode-set", | 263 | type: "thread.runtime-mode-set", |
| 275 | payload: { | 264 | payload: { |
| 276 | threadId: ThreadId.make("thread-1"), | 265 | threadId: ThreadId.make("thread-1"), |
| 277 | runtimeMode: "approval-required", | 266 | runtimeMode: "approval-required", |
| 278 | }, | 267 | }, |
| 279 | }); | 268 | }); |
| 280 | }); | 269 | }), |
| 270 | ); | ||
| 281 | 271 | ||
| 282 | it("emits thread.interaction-mode-set from thread.interaction-mode.set", async () => { | 272 | it.effect("emits thread.interaction-mode-set from thread.interaction-mode.set", () => |
| 283 | const now = "2026-01-01T00:00:00.000Z"; | 273 | Effect.gen(function* () { |
| 284 | const initial = createEmptyReadModel(now); | 274 | const now = "2026-01-01T00:00:00.000Z"; |
| 285 | const withProject = await Effect.runPromise( | 275 | const initial = createEmptyReadModel(now); |
| 286 | projectEvent(initial, { | 276 | const withProject = yield* projectEvent(initial, { |
| 287 | sequence: 1, | 277 | sequence: 1, |
| 288 | eventId: asEventId("evt-project-create"), | 278 | eventId: asEventId("evt-project-create"), |
| 289 | aggregateKind: "project", | 279 | aggregateKind: "project", |
| ... | @@ -303,10 +293,8 @@ describe("decider project scripts", () => { | ... | @@ -303,10 +293,8 @@ describe("decider project scripts", () => { |
| 303 | createdAt: now, | 293 | createdAt: now, |
| 304 | updatedAt: now, | 294 | updatedAt: now, |
| 305 | }, | 295 | }, |
| 306 | }), | 296 | }); |
| 307 | ); | 297 | const readModel = yield* projectEvent(withProject, { |
| 308 | const readModel = await Effect.runPromise( | ||
| 309 | projectEvent(withProject, { | ||
| 310 | sequence: 2, | 298 | sequence: 2, |
| 311 | eventId: asEventId("evt-thread-create"), | 299 | eventId: asEventId("evt-thread-create"), |
| 312 | aggregateKind: "thread", | 300 | aggregateKind: "thread", |
| ... | @@ -332,11 +320,9 @@ describe("decider project scripts", () => { | ... | @@ -332,11 +320,9 @@ describe("decider project scripts", () => { |
| 332 | createdAt: now, | 320 | createdAt: now, |
| 333 | updatedAt: now, | 321 | updatedAt: now, |
| 334 | }, | 322 | }, |
| 335 | }), | 323 | }); |
| 336 | ); | ||
| 337 | 324 | ||
| 338 | const result = await Effect.runPromise( | 325 | const result = yield* decideOrchestrationCommand({ |
| 339 | decideOrchestrationCommand({ | ||
| 340 | command: { | 326 | command: { |
| 341 | type: "thread.interaction-mode.set", | 327 | type: "thread.interaction-mode.set", |
| 342 | commandId: CommandId.make("cmd-interaction-mode-set"), | 328 | commandId: CommandId.make("cmd-interaction-mode-set"), |
| ... | @@ -345,19 +331,19 @@ describe("decider project scripts", () => { | ... | @@ -345,19 +331,19 @@ describe("decider project scripts", () => { |
| 345 | createdAt: now, | 331 | createdAt: now, |
| 346 | }, | 332 | }, |
| 347 | readModel, | 333 | readModel, |
| 348 | }), | 334 | }); |
| 349 | ); | ||
| 350 | 335 | ||
| 351 | const singleResult = Array.isArray(result) ? null : result; | 336 | const singleResult = Array.isArray(result) ? null : result; |
| 352 | if (singleResult === null) { | 337 | if (singleResult === null) { |
| 353 | throw new Error("Expected a single interaction-mode-set event."); | 338 | throw new Error("Expected a single interaction-mode-set event."); |
| 354 | } | 339 | } |
| 355 | expect(singleResult).toMatchObject({ | 340 | expect(singleResult).toMatchObject({ |
| 356 | type: "thread.interaction-mode-set", | 341 | type: "thread.interaction-mode-set", |
| 357 | payload: { | 342 | payload: { |
| 358 | threadId: ThreadId.make("thread-1"), | 343 | threadId: ThreadId.make("thread-1"), |
| 359 | interactionMode: "plan", | 344 | interactionMode: "plan", |
| 360 | }, | 345 | }, |
| 361 | }); | 346 | }); |
| 362 | }); | 347 | }), |
| 348 | ); | ||
| 363 | }); | 349 | }); |
apps/server/src/orchestration/decider.ts+86-65| ... | @@ -1,10 +1,13 @@ | ... | @@ -1,10 +1,13 @@ |
| 1 | import type { | 1 | import { |
| 2 | OrchestrationCommand, | 2 | EventId, |
| 3 | OrchestrationEvent, | 3 | type OrchestrationCommand, |
| 4 | OrchestrationReadModel, | 4 | type OrchestrationEvent, |
| 5 | type OrchestrationReadModel, | ||
| 5 | } from "@t3tools/contracts"; | 6 | } from "@t3tools/contracts"; |
| 6 | import * as DateTime from "effect/DateTime"; | 7 | import * as DateTime from "effect/DateTime"; |
| 8 | import * as Crypto from "effect/Crypto"; | ||
| 7 | import * as Effect from "effect/Effect"; | 9 | import * as Effect from "effect/Effect"; |
| 10 | import type * as PlatformError from "effect/PlatformError"; | ||
| 8 | 11 | ||
| 9 | import { OrchestrationCommandInvariantError } from "./Errors.ts"; | 12 | import { OrchestrationCommandInvariantError } from "./Errors.ts"; |
| 10 | import { | 13 | import { |
| ... | @@ -27,17 +30,27 @@ function withEventBase( | ... | @@ -27,17 +30,27 @@ function withEventBase( |
| 27 | readonly occurredAt: string; | 30 | readonly occurredAt: string; |
| 28 | readonly metadata?: OrchestrationEvent["metadata"]; | 31 | readonly metadata?: OrchestrationEvent["metadata"]; |
| 29 | }, | 32 | }, |
| 30 | ): Omit<OrchestrationEvent, "sequence" | "type" | "payload"> { | 33 | ): Effect.Effect< |
| 31 | return { | 34 | Omit<OrchestrationEvent, "sequence" | "type" | "payload">, |
| 32 | eventId: crypto.randomUUID() as OrchestrationEvent["eventId"], | 35 | PlatformError.PlatformError, |
| 33 | aggregateKind: input.aggregateKind, | 36 | Crypto.Crypto |
| 34 | aggregateId: input.aggregateId, | 37 | > { |
| 35 | occurredAt: input.occurredAt, | 38 | return Crypto.Crypto.pipe( |
| 36 | commandId: input.commandId, | 39 | Effect.flatMap((crypto) => |
| 37 | causationEventId: null, | 40 | crypto.randomUUIDv4.pipe( |
| 38 | correlationId: input.commandId, | 41 | Effect.map((eventId) => ({ |
| 39 | metadata: input.metadata ?? {}, | 42 | eventId: EventId.make(eventId), |
| 40 | }; | 43 | aggregateKind: input.aggregateKind, |
| 44 | aggregateId: input.aggregateId, | ||
| 45 | occurredAt: input.occurredAt, | ||
| 46 | commandId: input.commandId, | ||
| 47 | causationEventId: null, | ||
| 48 | correlationId: input.commandId, | ||
| 49 | metadata: input.metadata ?? {}, | ||
| 50 | })), | ||
| 51 | ), | ||
| 52 | ), | ||
| 53 | ); | ||
| 41 | } | 54 | } |
| 42 | 55 | ||
| 43 | type PlannedOrchestrationEvent = Omit<OrchestrationEvent, "sequence">; | 56 | type PlannedOrchestrationEvent = Omit<OrchestrationEvent, "sequence">; |
| ... | @@ -52,7 +65,11 @@ const decideCommandSequence = Effect.fn("decideCommandSequence")(function* ({ | ... | @@ -52,7 +65,11 @@ const decideCommandSequence = Effect.fn("decideCommandSequence")(function* ({ |
| 52 | }: { | 65 | }: { |
| 53 | readonly commands: ReadonlyArray<OrchestrationCommand>; | 66 | readonly commands: ReadonlyArray<OrchestrationCommand>; |
| 54 | readonly readModel: OrchestrationReadModel; | 67 | readonly readModel: OrchestrationReadModel; |
| 55 | }): Effect.fn.Return<ReadonlyArray<PlannedOrchestrationEvent>, OrchestrationCommandInvariantError> { | 68 | }): Effect.fn.Return< |
| 69 | ReadonlyArray<PlannedOrchestrationEvent>, | ||
| 70 | OrchestrationCommandInvariantError | PlatformError.PlatformError, | ||
| 71 | Crypto.Crypto | ||
| 72 | > { | ||
| 56 | let nextReadModel = readModel; | 73 | let nextReadModel = readModel; |
| 57 | let nextSequence = readModel.snapshotSequence; | 74 | let nextSequence = readModel.snapshotSequence; |
| 58 | const plannedEvents: PlannedOrchestrationEvent[] = []; | 75 | const plannedEvents: PlannedOrchestrationEvent[] = []; |
| ... | @@ -82,7 +99,11 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" | ... | @@ -82,7 +99,11 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" |
| 82 | }: { | 99 | }: { |
| 83 | readonly command: OrchestrationCommand; | 100 | readonly command: OrchestrationCommand; |
| 84 | readonly readModel: OrchestrationReadModel; | 101 | readonly readModel: OrchestrationReadModel; |
| 85 | }): Effect.fn.Return<DecideOrchestrationCommandResult, OrchestrationCommandInvariantError> { | 102 | }): Effect.fn.Return< |
| 103 | DecideOrchestrationCommandResult, | ||
| 104 | OrchestrationCommandInvariantError | PlatformError.PlatformError, | ||
| 105 | Crypto.Crypto | ||
| 106 | > { | ||
| 86 | switch (command.type) { | 107 | switch (command.type) { |
| 87 | case "project.create": { | 108 | case "project.create": { |
| 88 | yield* requireProjectAbsent({ | 109 | yield* requireProjectAbsent({ |
| ... | @@ -92,12 +113,12 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" | ... | @@ -92,12 +113,12 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" |
| 92 | }); | 113 | }); |
| 93 | 114 | ||
| 94 | return { | 115 | return { |
| 95 | ...withEventBase({ | 116 | ...(yield* withEventBase({ |
| 96 | aggregateKind: "project", | 117 | aggregateKind: "project", |
| 97 | aggregateId: command.projectId, | 118 | aggregateId: command.projectId, |
| 98 | occurredAt: command.createdAt, | 119 | occurredAt: command.createdAt, |
| 99 | commandId: command.commandId, | 120 | commandId: command.commandId, |
| 100 | }), | 121 | })), |
| 101 | type: "project.created", | 122 | type: "project.created", |
| 102 | payload: { | 123 | payload: { |
| 103 | projectId: command.projectId, | 124 | projectId: command.projectId, |
| ... | @@ -119,12 +140,12 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" | ... | @@ -119,12 +140,12 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" |
| 119 | }); | 140 | }); |
| 120 | const occurredAt = yield* nowIso; | 141 | const occurredAt = yield* nowIso; |
| 121 | return { | 142 | return { |
| 122 | ...withEventBase({ | 143 | ...(yield* withEventBase({ |
| 123 | aggregateKind: "project", | 144 | aggregateKind: "project", |
| 124 | aggregateId: command.projectId, | 145 | aggregateId: command.projectId, |
| 125 | occurredAt, | 146 | occurredAt, |
| 126 | commandId: command.commandId, | 147 | commandId: command.commandId, |
| 127 | }), | 148 | })), |
| 128 | type: "project.meta-updated", | 149 | type: "project.meta-updated", |
| 129 | payload: { | 150 | payload: { |
| 130 | projectId: command.projectId, | 151 | projectId: command.projectId, |
| ... | @@ -176,12 +197,12 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" | ... | @@ -176,12 +197,12 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" |
| 176 | 197 | ||
| 177 | const occurredAt = yield* nowIso; | 198 | const occurredAt = yield* nowIso; |
| 178 | return { | 199 | return { |
| 179 | ...withEventBase({ | 200 | ...(yield* withEventBase({ |
| 180 | aggregateKind: "project", | 201 | aggregateKind: "project", |
| 181 | aggregateId: command.projectId, | 202 | aggregateId: command.projectId, |
| 182 | occurredAt, | 203 | occurredAt, |
| 183 | commandId: command.commandId, | 204 | commandId: command.commandId, |
| 184 | }), | 205 | })), |
| 185 | type: "project.deleted" as const, | 206 | type: "project.deleted" as const, |
| 186 | payload: { | 207 | payload: { |
| 187 | projectId: command.projectId, | 208 | projectId: command.projectId, |
| ... | @@ -202,12 +223,12 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" | ... | @@ -202,12 +223,12 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" |
| 202 | threadId: command.threadId, | 223 | threadId: command.threadId, |
| 203 | }); | 224 | }); |
| 204 | return { | 225 | return { |
| 205 | ...withEventBase({ | 226 | ...(yield* withEventBase({ |
| 206 | aggregateKind: "thread", | 227 | aggregateKind: "thread", |
| 207 | aggregateId: command.threadId, | 228 | aggregateId: command.threadId, |
| 208 | occurredAt: command.createdAt, | 229 | occurredAt: command.createdAt, |
| 209 | commandId: command.commandId, | 230 | commandId: command.commandId, |
| 210 | }), | 231 | })), |
| 211 | type: "thread.created", | 232 | type: "thread.created", |
| 212 | payload: { | 233 | payload: { |
| 213 | threadId: command.threadId, | 234 | threadId: command.threadId, |
| ... | @@ -232,12 +253,12 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" | ... | @@ -232,12 +253,12 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" |
| 232 | }); | 253 | }); |
| 233 | const occurredAt = yield* nowIso; | 254 | const occurredAt = yield* nowIso; |
| 234 | return { | 255 | return { |
| 235 | ...withEventBase({ | 256 | ...(yield* withEventBase({ |
| 236 | aggregateKind: "thread", | 257 | aggregateKind: "thread", |
| 237 | aggregateId: command.threadId, | 258 | aggregateId: command.threadId, |
| 238 | occurredAt, | 259 | occurredAt, |
| 239 | commandId: command.commandId, | 260 | commandId: command.commandId, |
| 240 | }), | 261 | })), |
| 241 | type: "thread.deleted", | 262 | type: "thread.deleted", |
| 242 | payload: { | 263 | payload: { |
| 243 | threadId: command.threadId, | 264 | threadId: command.threadId, |
| ... | @@ -254,12 +275,12 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" | ... | @@ -254,12 +275,12 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" |
| 254 | }); | 275 | }); |
| 255 | const occurredAt = yield* nowIso; | 276 | const occurredAt = yield* nowIso; |
| 256 | return { | 277 | return { |
| 257 | ...withEventBase({ | 278 | ...(yield* withEventBase({ |
| 258 | aggregateKind: "thread", | 279 | aggregateKind: "thread", |
| 259 | aggregateId: command.threadId, | 280 | aggregateId: command.threadId, |
| 260 | occurredAt, | 281 | occurredAt, |
| 261 | commandId: command.commandId, | 282 | commandId: command.commandId, |
| 262 | }), | 283 | })), |
| 263 | type: "thread.archived", | 284 | type: "thread.archived", |
| 264 | payload: { | 285 | payload: { |
| 265 | threadId: command.threadId, | 286 | threadId: command.threadId, |
| ... | @@ -277,12 +298,12 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" | ... | @@ -277,12 +298,12 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" |
| 277 | }); | 298 | }); |
| 278 | const occurredAt = yield* nowIso; | 299 | const occurredAt = yield* nowIso; |
| 279 | return { | 300 | return { |
| 280 | ...withEventBase({ | 301 | ...(yield* withEventBase({ |
| 281 | aggregateKind: "thread", | 302 | aggregateKind: "thread", |
| 282 | aggregateId: command.threadId, | 303 | aggregateId: command.threadId, |
| 283 | occurredAt, | 304 | occurredAt, |
| 284 | commandId: command.commandId, | 305 | commandId: command.commandId, |
| 285 | }), | 306 | })), |
| 286 | type: "thread.unarchived", | 307 | type: "thread.unarchived", |
| 287 | payload: { | 308 | payload: { |
| 288 | threadId: command.threadId, | 309 | threadId: command.threadId, |
| ... | @@ -299,12 +320,12 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" | ... | @@ -299,12 +320,12 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" |
| 299 | }); | 320 | }); |
| 300 | const occurredAt = yield* nowIso; | 321 | const occurredAt = yield* nowIso; |
| 301 | return { | 322 | return { |
| 302 | ...withEventBase({ | 323 | ...(yield* withEventBase({ |
| 303 | aggregateKind: "thread", | 324 | aggregateKind: "thread", |
| 304 | aggregateId: command.threadId, | 325 | aggregateId: command.threadId, |
| 305 | occurredAt, | 326 | occurredAt, |
| 306 | commandId: command.commandId, | 327 | commandId: command.commandId, |
| 307 | }), | 328 | })), |
| 308 | type: "thread.meta-updated", | 329 | type: "thread.meta-updated", |
| 309 | payload: { | 330 | payload: { |
| 310 | threadId: command.threadId, | 331 | threadId: command.threadId, |
| ... | @@ -327,12 +348,12 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" | ... | @@ -327,12 +348,12 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" |
| 327 | }); | 348 | }); |
| 328 | const occurredAt = yield* nowIso; | 349 | const occurredAt = yield* nowIso; |
| 329 | return { | 350 | return { |
| 330 | ...withEventBase({ | 351 | ...(yield* withEventBase({ |
| 331 | aggregateKind: "thread", | 352 | aggregateKind: "thread", |
| 332 | aggregateId: command.threadId, | 353 | aggregateId: command.threadId, |
| 333 | occurredAt, | 354 | occurredAt, |
| 334 | commandId: command.commandId, | 355 | commandId: command.commandId, |
| 335 | }), | 356 | })), |
| 336 | type: "thread.runtime-mode-set", | 357 | type: "thread.runtime-mode-set", |
| 337 | payload: { | 358 | payload: { |
| 338 | threadId: command.threadId, | 359 | threadId: command.threadId, |
| ... | @@ -350,12 +371,12 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" | ... | @@ -350,12 +371,12 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" |
| 350 | }); | 371 | }); |
| 351 | const occurredAt = yield* nowIso; | 372 | const occurredAt = yield* nowIso; |
| 352 | return { | 373 | return { |
| 353 | ...withEventBase({ | 374 | ...(yield* withEventBase({ |
| 354 | aggregateKind: "thread", | 375 | aggregateKind: "thread", |
| 355 | aggregateId: command.threadId, | 376 | aggregateId: command.threadId, |
| 356 | occurredAt, | 377 | occurredAt, |
| 357 | commandId: command.commandId, | 378 | commandId: command.commandId, |
| 358 | }), | 379 | })), |
| 359 | type: "thread.interaction-mode-set", | 380 | type: "thread.interaction-mode-set", |
| 360 | payload: { | 381 | payload: { |
| 361 | threadId: command.threadId, | 382 | threadId: command.threadId, |
| ... | @@ -396,12 +417,12 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" | ... | @@ -396,12 +417,12 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" |
| 396 | }); | 417 | }); |
| 397 | } | 418 | } |
| 398 | const userMessageEvent: Omit<OrchestrationEvent, "sequence"> = { | 419 | const userMessageEvent: Omit<OrchestrationEvent, "sequence"> = { |
| 399 | ...withEventBase({ | 420 | ...(yield* withEventBase({ |
| 400 | aggregateKind: "thread", | 421 | aggregateKind: "thread", |
| 401 | aggregateId: command.threadId, | 422 | aggregateId: command.threadId, |
| 402 | occurredAt: command.createdAt, | 423 | occurredAt: command.createdAt, |
| 403 | commandId: command.commandId, | 424 | commandId: command.commandId, |
| 404 | }), | 425 | })), |
| 405 | type: "thread.message-sent", | 426 | type: "thread.message-sent", |
| 406 | payload: { | 427 | payload: { |
| 407 | threadId: command.threadId, | 428 | threadId: command.threadId, |
| ... | @@ -416,12 +437,12 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" | ... | @@ -416,12 +437,12 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" |
| 416 | }, | 437 | }, |
| 417 | }; | 438 | }; |
| 418 | const turnStartRequestedEvent: Omit<OrchestrationEvent, "sequence"> = { | 439 | const turnStartRequestedEvent: Omit<OrchestrationEvent, "sequence"> = { |
| 419 | ...withEventBase({ | 440 | ...(yield* withEventBase({ |
| 420 | aggregateKind: "thread", | 441 | aggregateKind: "thread", |
| 421 | aggregateId: command.threadId, | 442 | aggregateId: command.threadId, |
| 422 | occurredAt: command.createdAt, | 443 | occurredAt: command.createdAt, |
| 423 | commandId: command.commandId, | 444 | commandId: command.commandId, |
| 424 | }), | 445 | })), |
| 425 | causationEventId: userMessageEvent.eventId, | 446 | causationEventId: userMessageEvent.eventId, |
| 426 | type: "thread.turn-start-requested", | 447 | type: "thread.turn-start-requested", |
| 427 | payload: { | 448 | payload: { |
| ... | @@ -447,12 +468,12 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" | ... | @@ -447,12 +468,12 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" |
| 447 | threadId: command.threadId, | 468 | threadId: command.threadId, |
| 448 | }); | 469 | }); |
| 449 | return { | 470 | return { |
| 450 | ...withEventBase({ | 471 | ...(yield* withEventBase({ |
| 451 | aggregateKind: "thread", | 472 | aggregateKind: "thread", |
| 452 | aggregateId: command.threadId, | 473 | aggregateId: command.threadId, |
| 453 | occurredAt: command.createdAt, | 474 | occurredAt: command.createdAt, |
| 454 | commandId: command.commandId, | 475 | commandId: command.commandId, |
| 455 | }), | 476 | })), |
| 456 | type: "thread.turn-interrupt-requested", | 477 | type: "thread.turn-interrupt-requested", |
| 457 | payload: { | 478 | payload: { |
| 458 | threadId: command.threadId, | 479 | threadId: command.threadId, |
| ... | @@ -469,7 +490,7 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" | ... | @@ -469,7 +490,7 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" |
| 469 | threadId: command.threadId, | 490 | threadId: command.threadId, |
| 470 | }); | 491 | }); |
| 471 | return { | 492 | return { |
| 472 | ...withEventBase({ | 493 | ...(yield* withEventBase({ |
| 473 | aggregateKind: "thread", | 494 | aggregateKind: "thread", |
| 474 | aggregateId: command.threadId, | 495 | aggregateId: command.threadId, |
| 475 | occurredAt: command.createdAt, | 496 | occurredAt: command.createdAt, |
| ... | @@ -477,7 +498,7 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" | ... | @@ -477,7 +498,7 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" |
| 477 | metadata: { | 498 | metadata: { |
| 478 | requestId: command.requestId, | 499 | requestId: command.requestId, |
| 479 | }, | 500 | }, |
| 480 | }), | 501 | })), |
| 481 | type: "thread.approval-response-requested", | 502 | type: "thread.approval-response-requested", |
| 482 | payload: { | 503 | payload: { |
| 483 | threadId: command.threadId, | 504 | threadId: command.threadId, |
| ... | @@ -495,7 +516,7 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" | ... | @@ -495,7 +516,7 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" |
| 495 | threadId: command.threadId, | 516 | threadId: command.threadId, |
| 496 | }); | 517 | }); |
| 497 | return { | 518 | return { |
| 498 | ...withEventBase({ | 519 | ...(yield* withEventBase({ |
| 499 | aggregateKind: "thread", | 520 | aggregateKind: "thread", |
| 500 | aggregateId: command.threadId, | 521 | aggregateId: command.threadId, |
| 501 | occurredAt: command.createdAt, | 522 | occurredAt: command.createdAt, |
| ... | @@ -503,7 +524,7 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" | ... | @@ -503,7 +524,7 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" |
| 503 | metadata: { | 524 | metadata: { |
| 504 | requestId: command.requestId, | 525 | requestId: command.requestId, |
| 505 | }, | 526 | }, |
| 506 | }), | 527 | })), |
| 507 | type: "thread.user-input-response-requested", | 528 | type: "thread.user-input-response-requested", |
| 508 | payload: { | 529 | payload: { |
| 509 | threadId: command.threadId, | 530 | threadId: command.threadId, |
| ... | @@ -521,12 +542,12 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" | ... | @@ -521,12 +542,12 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" |
| 521 | threadId: command.threadId, | 542 | threadId: command.threadId, |
| 522 | }); | 543 | }); |
| 523 | return { | 544 | return { |
| 524 | ...withEventBase({ | 545 | ...(yield* withEventBase({ |
| 525 | aggregateKind: "thread", | 546 | aggregateKind: "thread", |
| 526 | aggregateId: command.threadId, | 547 | aggregateId: command.threadId, |
| 527 | occurredAt: command.createdAt, | 548 | occurredAt: command.createdAt, |
| 528 | commandId: command.commandId, | 549 | commandId: command.commandId, |
| 529 | }), | 550 | })), |
| 530 | type: "thread.checkpoint-revert-requested", | 551 | type: "thread.checkpoint-revert-requested", |
| 531 | payload: { | 552 | payload: { |
| 532 | threadId: command.threadId, | 553 | threadId: command.threadId, |
| ... | @@ -543,12 +564,12 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" | ... | @@ -543,12 +564,12 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" |
| 543 | threadId: command.threadId, | 564 | threadId: command.threadId, |
| 544 | }); | 565 | }); |
| 545 | return { | 566 | return { |
| 546 | ...withEventBase({ | 567 | ...(yield* withEventBase({ |
| 547 | aggregateKind: "thread", | 568 | aggregateKind: "thread", |
| 548 | aggregateId: command.threadId, | 569 | aggregateId: command.threadId, |
| 549 | occurredAt: command.createdAt, | 570 | occurredAt: command.createdAt, |
| 550 | commandId: command.commandId, | 571 | commandId: command.commandId, |
| 551 | }), | 572 | })), |
| 552 | type: "thread.session-stop-requested", | 573 | type: "thread.session-stop-requested", |
| 553 | payload: { | 574 | payload: { |
| 554 | threadId: command.threadId, | 575 | threadId: command.threadId, |
| ... | @@ -564,13 +585,13 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" | ... | @@ -564,13 +585,13 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" |
| 564 | threadId: command.threadId, | 585 | threadId: command.threadId, |
| 565 | }); | 586 | }); |
| 566 | return { | 587 | return { |
| 567 | ...withEventBase({ | 588 | ...(yield* withEventBase({ |
| 568 | aggregateKind: "thread", | 589 | aggregateKind: "thread", |
| 569 | aggregateId: command.threadId, | 590 | aggregateId: command.threadId, |
| 570 | occurredAt: command.createdAt, | 591 | occurredAt: command.createdAt, |
| 571 | commandId: command.commandId, | 592 | commandId: command.commandId, |
| 572 | metadata: {}, | 593 | metadata: {}, |
| 573 | }), | 594 | })), |
| 574 | type: "thread.session-set", | 595 | type: "thread.session-set", |
| 575 | payload: { | 596 | payload: { |
| 576 | threadId: command.threadId, | 597 | threadId: command.threadId, |
| ... | @@ -586,12 +607,12 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" | ... | @@ -586,12 +607,12 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" |
| 586 | threadId: command.threadId, | 607 | threadId: command.threadId, |
| 587 | }); | 608 | }); |
| 588 | return { | 609 | return { |
| 589 | ...withEventBase({ | 610 | ...(yield* withEventBase({ |
| 590 | aggregateKind: "thread", | 611 | aggregateKind: "thread", |
| 591 | aggregateId: command.threadId, | 612 | aggregateId: command.threadId, |
| 592 | occurredAt: command.createdAt, | 613 | occurredAt: command.createdAt, |
| 593 | commandId: command.commandId, | 614 | commandId: command.commandId, |
| 594 | }), | 615 | })), |
| 595 | type: "thread.message-sent", | 616 | type: "thread.message-sent", |
| 596 | payload: { | 617 | payload: { |
| 597 | threadId: command.threadId, | 618 | threadId: command.threadId, |
| ... | @@ -613,12 +634,12 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" | ... | @@ -613,12 +634,12 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" |
| 613 | threadId: command.threadId, | 634 | threadId: command.threadId, |
| 614 | }); | 635 | }); |
| 615 | return { | 636 | return { |
| 616 | ...withEventBase({ | 637 | ...(yield* withEventBase({ |
| 617 | aggregateKind: "thread", | 638 | aggregateKind: "thread", |
| 618 | aggregateId: command.threadId, | 639 | aggregateId: command.threadId, |
| 619 | occurredAt: command.createdAt, | 640 | occurredAt: command.createdAt, |
| 620 | commandId: command.commandId, | 641 | commandId: command.commandId, |
| 621 | }), | 642 | })), |
| 622 | type: "thread.message-sent", | 643 | type: "thread.message-sent", |
| 623 | payload: { | 644 | payload: { |
| 624 | threadId: command.threadId, | 645 | threadId: command.threadId, |
| ... | @@ -640,12 +661,12 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" | ... | @@ -640,12 +661,12 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" |
| 640 | threadId: command.threadId, | 661 | threadId: command.threadId, |
| 641 | }); | 662 | }); |
| 642 | return { | 663 | return { |
| 643 | ...withEventBase({ | 664 | ...(yield* withEventBase({ |
| 644 | aggregateKind: "thread", | 665 | aggregateKind: "thread", |
| 645 | aggregateId: command.threadId, | 666 | aggregateId: command.threadId, |
| 646 | occurredAt: command.createdAt, | 667 | occurredAt: command.createdAt, |
| 647 | commandId: command.commandId, | 668 | commandId: command.commandId, |
| 648 | }), | 669 | })), |
| 649 | type: "thread.proposed-plan-upserted", | 670 | type: "thread.proposed-plan-upserted", |
| 650 | payload: { | 671 | payload: { |
| 651 | threadId: command.threadId, | 672 | threadId: command.threadId, |
| ... | @@ -661,12 +682,12 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" | ... | @@ -661,12 +682,12 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" |
| 661 | threadId: command.threadId, | 682 | threadId: command.threadId, |
| 662 | }); | 683 | }); |
| 663 | return { | 684 | return { |
| 664 | ...withEventBase({ | 685 | ...(yield* withEventBase({ |
| 665 | aggregateKind: "thread", | 686 | aggregateKind: "thread", |
| 666 | aggregateId: command.threadId, | 687 | aggregateId: command.threadId, |
| 667 | occurredAt: command.createdAt, | 688 | occurredAt: command.createdAt, |
| 668 | commandId: command.commandId, | 689 | commandId: command.commandId, |
| 669 | }), | 690 | })), |
| 670 | type: "thread.turn-diff-completed", | 691 | type: "thread.turn-diff-completed", |
| 671 | payload: { | 692 | payload: { |
| 672 | threadId: command.threadId, | 693 | threadId: command.threadId, |
| ... | @@ -688,12 +709,12 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" | ... | @@ -688,12 +709,12 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" |
| 688 | threadId: command.threadId, | 709 | threadId: command.threadId, |
| 689 | }); | 710 | }); |
| 690 | return { | 711 | return { |
| 691 | ...withEventBase({ | 712 | ...(yield* withEventBase({ |
| 692 | aggregateKind: "thread", | 713 | aggregateKind: "thread", |
| 693 | aggregateId: command.threadId, | 714 | aggregateId: command.threadId, |
| 694 | occurredAt: command.createdAt, | 715 | occurredAt: command.createdAt, |
| 695 | commandId: command.commandId, | 716 | commandId: command.commandId, |
| 696 | }), | 717 | })), |
| 697 | type: "thread.reverted", | 718 | type: "thread.reverted", |
| 698 | payload: { | 719 | payload: { |
| 699 | threadId: command.threadId, | 720 | threadId: command.threadId, |
| ... | @@ -717,13 +738,13 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" | ... | @@ -717,13 +738,13 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" |
| 717 | .requestId as OrchestrationEvent["metadata"]["requestId"]) | 738 | .requestId as OrchestrationEvent["metadata"]["requestId"]) |
| 718 | : undefined; | 739 | : undefined; |
| 719 | return { | 740 | return { |
| 720 | ...withEventBase({ | 741 | ...(yield* withEventBase({ |
| 721 | aggregateKind: "thread", | 742 | aggregateKind: "thread", |
| 722 | aggregateId: command.threadId, | 743 | aggregateId: command.threadId, |
| 723 | occurredAt: command.createdAt, | 744 | occurredAt: command.createdAt, |
| 724 | commandId: command.commandId, | 745 | commandId: command.commandId, |
| 725 | ...(requestId !== undefined ? { metadata: { requestId } } : {}), | 746 | ...(requestId !== undefined ? { metadata: { requestId } } : {}), |
| 726 | }), | 747 | })), |
| 727 | type: "thread.activity-appended", | 748 | type: "thread.activity-appended", |
| 728 | payload: { | 749 | payload: { |
| 729 | threadId: command.threadId, | 750 | threadId: command.threadId, |
apps/server/src/persistence/NodeSqliteClient.ts+5-7| ... | @@ -252,14 +252,12 @@ export const layerConfig = ( | ... | @@ -252,14 +252,12 @@ export const layerConfig = ( |
| 252 | config: Config.Wrap<SqliteClientConfig>, | 252 | config: Config.Wrap<SqliteClientConfig>, |
| 253 | ): Layer.Layer<Client.SqlClient, Config.ConfigError> => | 253 | ): Layer.Layer<Client.SqlClient, Config.ConfigError> => |
| 254 | Layer.effectContext( | 254 | Layer.effectContext( |
| 255 | Config.unwrap(config) | 255 | Config.unwrap(config).pipe( |
| 256 | .asEffect() | 256 | Effect.flatMap(make), |
| 257 | .pipe( | 257 | Effect.map((client) => |
| 258 | Effect.flatMap(make), | 258 | Context.make(SqliteClient, client).pipe(Context.add(Client.SqlClient, client)), |
| 259 | Effect.map((client) => | ||
| 260 | Context.make(SqliteClient, client).pipe(Context.add(Client.SqlClient, client)), | ||
| 261 | ), | ||
| 262 | ), | 259 | ), |
| 260 | ), | ||
| 263 | ).pipe(Layer.provide(Reactivity.layer)); | 261 | ).pipe(Layer.provide(Reactivity.layer)); |
| 264 | 262 | ||
| 265 | export const layer = (config: SqliteClientConfig): Layer.Layer<Client.SqlClient> => | 263 | export const layer = (config: SqliteClientConfig): Layer.Layer<Client.SqlClient> => |
apps/server/src/process/externalLauncher.test.ts+4-2| ... | @@ -1,12 +1,12 @@ | ... | @@ -1,12 +1,12 @@ |
| 1 | import * as NodeServices from "@effect/platform-node/NodeServices"; | 1 | import * as NodeServices from "@effect/platform-node/NodeServices"; |
| 2 | import { assert, it } from "@effect/vitest"; | 2 | import { assert, it } from "@effect/vitest"; |
| 3 | import { assertSuccess } from "@effect/vitest/utils"; | 3 | import { assertSuccess } from "@effect/vitest/utils"; |
| 4 | import * as Crypto from "effect/Crypto"; | ||
| 4 | import * as Effect from "effect/Effect"; | 5 | import * as Effect from "effect/Effect"; |
| 5 | import * as Encoding from "effect/Encoding"; | 6 | import * as Encoding from "effect/Encoding"; |
| 6 | import * as FileSystem from "effect/FileSystem"; | 7 | import * as FileSystem from "effect/FileSystem"; |
| 7 | import * as Layer from "effect/Layer"; | 8 | import * as Layer from "effect/Layer"; |
| 8 | import * as Path from "effect/Path"; | 9 | import * as Path from "effect/Path"; |
| 9 | import * as Random from "effect/Random"; | ||
| 10 | import * as Sink from "effect/Sink"; | 10 | import * as Sink from "effect/Sink"; |
| 11 | import * as Stream from "effect/Stream"; | 11 | import * as Stream from "effect/Stream"; |
| 12 | import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"; | 12 | import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"; |
| ... | @@ -651,7 +651,9 @@ it.layer(NodeServices.layer)("launchEditorProcess", (it) => { | ... | @@ -651,7 +651,9 @@ it.layer(NodeServices.layer)("launchEditorProcess", (it) => { |
| 651 | Effect.gen(function* () { | 651 | Effect.gen(function* () { |
| 652 | const spawnerLayer = Layer.mock(ChildProcessSpawner.ChildProcessSpawner, {}); | 652 | const spawnerLayer = Layer.mock(ChildProcessSpawner.ChildProcessSpawner, {}); |
| 653 | const result = yield* launchEditorProcess({ | 653 | const result = yield* launchEditorProcess({ |
| 654 | command: `t3code-no-such-command-${yield* Random.nextUUIDv4}`, | 654 | command: `t3code-no-such-command-${yield* Crypto.Crypto.pipe( |
| 655 | Effect.flatMap((crypto) => crypto.randomUUIDv4), | ||
| 656 | )}`, | ||
| 655 | args: [], | 657 | args: [], |
| 656 | }).pipe(Effect.provide(spawnerLayer), Effect.result); | 658 | }).pipe(Effect.provide(spawnerLayer), Effect.result); |
| 657 | assert.equal(result._tag, "Failure"); | 659 | assert.equal(result._tag, "Failure"); |
apps/server/src/provider/Drivers/ClaudeDriver.ts+2| ... | @@ -15,6 +15,7 @@ | ... | @@ -15,6 +15,7 @@ |
| 15 | import { ClaudeSettings, ProviderDriverKind, type ServerProvider } from "@t3tools/contracts"; | 15 | import { ClaudeSettings, ProviderDriverKind, type ServerProvider } from "@t3tools/contracts"; |
| 16 | import * as Cache from "effect/Cache"; | 16 | import * as Cache from "effect/Cache"; |
| 17 | import * as Duration from "effect/Duration"; | 17 | import * as Duration from "effect/Duration"; |
| 18 | import * as Crypto from "effect/Crypto"; | ||
| 18 | import * as Effect from "effect/Effect"; | 19 | import * as Effect from "effect/Effect"; |
| 19 | import * as FileSystem from "effect/FileSystem"; | 20 | import * as FileSystem from "effect/FileSystem"; |
| 20 | import * as Path from "effect/Path"; | 21 | import * as Path from "effect/Path"; |
| ... | @@ -77,6 +78,7 @@ const UPDATE = makePackageManagedProviderMaintenanceResolver({ | ... | @@ -77,6 +78,7 @@ const UPDATE = makePackageManagedProviderMaintenanceResolver({ |
| 77 | 78 | ||
| 78 | export type ClaudeDriverEnv = | 79 | export type ClaudeDriverEnv = |
| 79 | | ChildProcessSpawner.ChildProcessSpawner | 80 | | ChildProcessSpawner.ChildProcessSpawner |
| 81 | | Crypto.Crypto | ||
| 80 | | FileSystem.FileSystem | 82 | | FileSystem.FileSystem |
| 81 | | HttpClient.HttpClient | 83 | | HttpClient.HttpClient |
| 82 | | Path.Path | 84 | | Path.Path |
apps/server/src/provider/Drivers/CodexDriver.ts+2| ... | @@ -23,6 +23,7 @@ | ... | @@ -23,6 +23,7 @@ |
| 23 | */ | 23 | */ |
| 24 | import { CodexSettings, ProviderDriverKind, type ServerProvider } from "@t3tools/contracts"; | 24 | import { CodexSettings, ProviderDriverKind, type ServerProvider } from "@t3tools/contracts"; |
| 25 | import * as Duration from "effect/Duration"; | 25 | import * as Duration from "effect/Duration"; |
| 26 | import * as Crypto from "effect/Crypto"; | ||
| 26 | import * as Effect from "effect/Effect"; | 27 | import * as Effect from "effect/Effect"; |
| 27 | import * as FileSystem from "effect/FileSystem"; | 28 | import * as FileSystem from "effect/FileSystem"; |
| 28 | import * as Path from "effect/Path"; | 29 | import * as Path from "effect/Path"; |
| ... | @@ -69,6 +70,7 @@ const UPDATE = makePackageManagedProviderMaintenanceResolver({ | ... | @@ -69,6 +70,7 @@ const UPDATE = makePackageManagedProviderMaintenanceResolver({ |
| 69 | */ | 70 | */ |
| 70 | export type CodexDriverEnv = | 71 | export type CodexDriverEnv = |
| 71 | | ChildProcessSpawner.ChildProcessSpawner | 72 | | ChildProcessSpawner.ChildProcessSpawner |
| 73 | | Crypto.Crypto | ||
| 72 | | FileSystem.FileSystem | 74 | | FileSystem.FileSystem |
| 73 | | HttpClient.HttpClient | 75 | | HttpClient.HttpClient |
| 74 | | Path.Path | 76 | | Path.Path |
apps/server/src/provider/Drivers/CursorDriver.ts+2| ... | @@ -14,6 +14,7 @@ | ... | @@ -14,6 +14,7 @@ |
| 14 | */ | 14 | */ |
| 15 | import { CursorSettings, ProviderDriverKind, type ServerProvider } from "@t3tools/contracts"; | 15 | import { CursorSettings, ProviderDriverKind, type ServerProvider } from "@t3tools/contracts"; |
| 16 | import * as Duration from "effect/Duration"; | 16 | import * as Duration from "effect/Duration"; |
| 17 | import * as Crypto from "effect/Crypto"; | ||
| 17 | import * as Effect from "effect/Effect"; | 18 | import * as Effect from "effect/Effect"; |
| 18 | import * as FileSystem from "effect/FileSystem"; | 19 | import * as FileSystem from "effect/FileSystem"; |
| 19 | import * as Path from "effect/Path"; | 20 | import * as Path from "effect/Path"; |
| ... | @@ -61,6 +62,7 @@ const UPDATE = makeStaticProviderMaintenanceResolver( | ... | @@ -61,6 +62,7 @@ const UPDATE = makeStaticProviderMaintenanceResolver( |
| 61 | 62 | ||
| 62 | export type CursorDriverEnv = | 63 | export type CursorDriverEnv = |
| 63 | | ChildProcessSpawner.ChildProcessSpawner | 64 | | ChildProcessSpawner.ChildProcessSpawner |
| 65 | | Crypto.Crypto | ||
| 64 | | FileSystem.FileSystem | 66 | | FileSystem.FileSystem |
| 65 | | HttpClient.HttpClient | 67 | | HttpClient.HttpClient |
| 66 | | Path.Path | 68 | | Path.Path |
apps/server/src/provider/Drivers/OpenCodeDriver.ts+2| ... | @@ -14,6 +14,7 @@ | ... | @@ -14,6 +14,7 @@ |
| 14 | */ | 14 | */ |
| 15 | import { OpenCodeSettings, ProviderDriverKind, type ServerProvider } from "@t3tools/contracts"; | 15 | import { OpenCodeSettings, ProviderDriverKind, type ServerProvider } from "@t3tools/contracts"; |
| 16 | import * as Duration from "effect/Duration"; | 16 | import * as Duration from "effect/Duration"; |
| 17 | import * as Crypto from "effect/Crypto"; | ||
| 17 | import * as Effect from "effect/Effect"; | 18 | import * as Effect from "effect/Effect"; |
| 18 | import * as FileSystem from "effect/FileSystem"; | 19 | import * as FileSystem from "effect/FileSystem"; |
| 19 | import * as Path from "effect/Path"; | 20 | import * as Path from "effect/Path"; |
| ... | @@ -73,6 +74,7 @@ const UPDATE = makePackageManagedProviderMaintenanceResolver({ | ... | @@ -73,6 +74,7 @@ const UPDATE = makePackageManagedProviderMaintenanceResolver({ |
| 73 | 74 | ||
| 74 | export type OpenCodeDriverEnv = | 75 | export type OpenCodeDriverEnv = |
| 75 | | ChildProcessSpawner.ChildProcessSpawner | 76 | | ChildProcessSpawner.ChildProcessSpawner |
| 77 | | Crypto.Crypto | ||
| 76 | | FileSystem.FileSystem | 78 | | FileSystem.FileSystem |
| 77 | | HttpClient.HttpClient | 79 | | HttpClient.HttpClient |
| 78 | | OpenCodeRuntime | 80 | | OpenCodeRuntime |
apps/server/src/provider/Layers/ClaudeAdapter.ts+43-13| ... | @@ -52,6 +52,7 @@ import { | ... | @@ -52,6 +52,7 @@ import { |
| 52 | resolvePromptInjectedEffort, | 52 | resolvePromptInjectedEffort, |
| 53 | } from "@t3tools/shared/model"; | 53 | } from "@t3tools/shared/model"; |
| 54 | import * as Cause from "effect/Cause"; | 54 | import * as Cause from "effect/Cause"; |
| 55 | import * as Crypto from "effect/Crypto"; | ||
| 55 | import * as DateTime from "effect/DateTime"; | 56 | import * as DateTime from "effect/DateTime"; |
| 56 | import * as Deferred from "effect/Deferred"; | 57 | import * as Deferred from "effect/Deferred"; |
| 57 | import * as Effect from "effect/Effect"; | 58 | import * as Effect from "effect/Effect"; |
| ... | @@ -60,7 +61,6 @@ import * as FileSystem from "effect/FileSystem"; | ... | @@ -60,7 +61,6 @@ import * as FileSystem from "effect/FileSystem"; |
| 60 | import * as Fiber from "effect/Fiber"; | 61 | import * as Fiber from "effect/Fiber"; |
| 61 | import * as Path from "effect/Path"; | 62 | import * as Path from "effect/Path"; |
| 62 | import * as Queue from "effect/Queue"; | 63 | import * as Queue from "effect/Queue"; |
| 63 | import * as Random from "effect/Random"; | ||
| 64 | import * as Ref from "effect/Ref"; | 64 | import * as Ref from "effect/Ref"; |
| 65 | import * as Schema from "effect/Schema"; | 65 | import * as Schema from "effect/Schema"; |
| 66 | import * as Stream from "effect/Stream"; | 66 | import * as Stream from "effect/Stream"; |
| ... | @@ -998,6 +998,7 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* ( | ... | @@ -998,6 +998,7 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* ( |
| 998 | const fileSystem = yield* FileSystem.FileSystem; | 998 | const fileSystem = yield* FileSystem.FileSystem; |
| 999 | const path = yield* Path.Path; | 999 | const path = yield* Path.Path; |
| 1000 | const serverConfig = yield* ServerConfig; | 1000 | const serverConfig = yield* ServerConfig; |
| 1001 | const crypto = yield* Crypto.Crypto; | ||
| 1001 | const claudeEnvironment = yield* makeClaudeEnvironment(claudeSettings, options?.environment).pipe( | 1002 | const claudeEnvironment = yield* makeClaudeEnvironment(claudeSettings, options?.environment).pipe( |
| 1002 | Effect.provideService(Path.Path, path), | 1003 | Effect.provideService(Path.Path, path), |
| 1003 | ); | 1004 | ); |
| ... | @@ -1024,7 +1025,18 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* ( | ... | @@ -1024,7 +1025,18 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* ( |
| 1024 | const runtimeEventQueue = yield* Queue.unbounded<ProviderRuntimeEvent>(); | 1025 | const runtimeEventQueue = yield* Queue.unbounded<ProviderRuntimeEvent>(); |
| 1025 | 1026 | ||
| 1026 | const nowIso = Effect.map(DateTime.now, DateTime.formatIso); | 1027 | const nowIso = Effect.map(DateTime.now, DateTime.formatIso); |
| 1027 | const nextEventId = Effect.map(Random.nextUUIDv4, (id) => EventId.make(id)); | 1028 | const randomUUIDv4 = crypto.randomUUIDv4.pipe( |
| 1029 | Effect.mapError( | ||
| 1030 | (cause) => | ||
| 1031 | new ProviderAdapterRequestError({ | ||
| 1032 | provider: PROVIDER, | ||
| 1033 | method: "crypto/randomUUIDv4", | ||
| 1034 | detail: "Failed to generate Claude runtime identifier.", | ||
| 1035 | cause, | ||
| 1036 | }), | ||
| 1037 | ), | ||
| 1038 | ); | ||
| 1039 | const nextEventId = Effect.map(randomUUIDv4, (id) => EventId.make(id)); | ||
| 1028 | const makeEventStamp = () => Effect.all({ eventId: nextEventId, createdAt: nowIso }); | 1040 | const makeEventStamp = () => Effect.all({ eventId: nextEventId, createdAt: nowIso }); |
| 1029 | 1041 | ||
| 1030 | const offerRuntimeEvent = (event: ProviderRuntimeEvent): Effect.Effect<void> => | 1042 | const offerRuntimeEvent = (event: ProviderRuntimeEvent): Effect.Effect<void> => |
| ... | @@ -1048,7 +1060,7 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* ( | ... | @@ -1048,7 +1060,7 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* ( |
| 1048 | id: | 1060 | id: |
| 1049 | "uuid" in message && typeof message.uuid === "string" | 1061 | "uuid" in message && typeof message.uuid === "string" |
| 1050 | ? message.uuid | 1062 | ? message.uuid |
| 1051 | : yield* Random.nextUUIDv4, | 1063 | : yield* randomUUIDv4, |
| 1052 | kind: "notification", | 1064 | kind: "notification", |
| 1053 | provider: PROVIDER, | 1065 | provider: PROVIDER, |
| 1054 | createdAt: observedAt, | 1066 | createdAt: observedAt, |
| ... | @@ -1132,7 +1144,7 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* ( | ... | @@ -1132,7 +1144,7 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* ( |
| 1132 | } | 1144 | } |
| 1133 | 1145 | ||
| 1134 | const block: AssistantTextBlockState = { | 1146 | const block: AssistantTextBlockState = { |
| 1135 | itemId: yield* Random.nextUUIDv4, | 1147 | itemId: yield* randomUUIDv4, |
| 1136 | blockIndex, | 1148 | blockIndex, |
| 1137 | emittedTextDelta: false, | 1149 | emittedTextDelta: false, |
| 1138 | fallbackText: options?.fallbackText ?? "", | 1150 | fallbackText: options?.fallbackText ?? "", |
| ... | @@ -1965,7 +1977,7 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* ( | ... | @@ -1965,7 +1977,7 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* ( |
| 1965 | // Auto-start a synthetic turn for assistant messages that arrive without | 1977 | // Auto-start a synthetic turn for assistant messages that arrive without |
| 1966 | // an active turn (e.g., background agent/subagent responses between user prompts). | 1978 | // an active turn (e.g., background agent/subagent responses between user prompts). |
| 1967 | if (!context.turnState) { | 1979 | if (!context.turnState) { |
| 1968 | const turnId = TurnId.make(yield* Random.nextUUIDv4); | 1980 | const turnId = TurnId.make(yield* randomUUIDv4); |
| 1969 | const startedAt = yield* nowIso; | 1981 | const startedAt = yield* nowIso; |
| 1970 | context.turnState = { | 1982 | context.turnState = { |
| 1971 | turnId, | 1983 | turnId, |
| ... | @@ -2378,7 +2390,17 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* ( | ... | @@ -2378,7 +2390,17 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* ( |
| 2378 | toProcessError(cause, "Claude runtime stream failed.", context.session.threadId), | 2390 | toProcessError(cause, "Claude runtime stream failed.", context.session.threadId), |
| 2379 | ).pipe( | 2391 | ).pipe( |
| 2380 | Stream.takeWhile(() => !context.stopped), | 2392 | Stream.takeWhile(() => !context.stopped), |
| 2381 | Stream.runForEach((message) => handleSdkMessage(context, message)), | 2393 | Stream.runForEach((message) => |
| 2394 | handleSdkMessage(context, message).pipe( | ||
| 2395 | Effect.mapError((cause) => | ||
| 2396 | toProcessError( | ||
| 2397 | cause, | ||
| 2398 | "Failed to process Claude runtime event.", | ||
| 2399 | context.session.threadId, | ||
| 2400 | ), | ||
| 2401 | ), | ||
| 2402 | ), | ||
| 2403 | ), | ||
| 2382 | ); | 2404 | ); |
| 2383 | 2405 | ||
| 2384 | const handleStreamExit = Effect.fn("handleStreamExit")(function* ( | 2406 | const handleStreamExit = Effect.fn("handleStreamExit")(function* ( |
| ... | @@ -2552,8 +2574,7 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* ( | ... | @@ -2552,8 +2574,7 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* ( |
| 2552 | const resumeState = readClaudeResumeState(input.resumeCursor); | 2574 | const resumeState = readClaudeResumeState(input.resumeCursor); |
| 2553 | const threadId = input.threadId; | 2575 | const threadId = input.threadId; |
| 2554 | const existingResumeSessionId = resumeState?.resume; | 2576 | const existingResumeSessionId = resumeState?.resume; |
| 2555 | const newSessionId = | 2577 | const newSessionId = existingResumeSessionId === undefined ? yield* randomUUIDv4 : undefined; |
| 2556 | existingResumeSessionId === undefined ? yield* Random.nextUUIDv4 : undefined; | ||
| 2557 | const sessionId = existingResumeSessionId ?? newSessionId; | 2578 | const sessionId = existingResumeSessionId ?? newSessionId; |
| 2558 | 2579 | ||
| 2559 | const runtimeContext = yield* Effect.context<never>(); | 2580 | const runtimeContext = yield* Effect.context<never>(); |
| ... | @@ -2588,7 +2609,7 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* ( | ... | @@ -2588,7 +2609,7 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* ( |
| 2588 | readonly toolUseID?: string; | 2609 | readonly toolUseID?: string; |
| 2589 | }, | 2610 | }, |
| 2590 | ) { | 2611 | ) { |
| 2591 | const requestId = ApprovalRequestId.make(yield* Random.nextUUIDv4); | 2612 | const requestId = ApprovalRequestId.make(yield* randomUUIDv4); |
| 2592 | 2613 | ||
| 2593 | // Parse questions from the SDK's AskUserQuestion input. | 2614 | // Parse questions from the SDK's AskUserQuestion input. |
| 2594 | // `id` MUST equal the full question text — Claude SDK >= 2.1.121 looks | 2615 | // `id` MUST equal the full question text — Claude SDK >= 2.1.121 looks |
| ... | @@ -2758,7 +2779,7 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* ( | ... | @@ -2758,7 +2779,7 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* ( |
| 2758 | } satisfies PermissionResult; | 2779 | } satisfies PermissionResult; |
| 2759 | } | 2780 | } |
| 2760 | 2781 | ||
| 2761 | const requestId = ApprovalRequestId.make(yield* Random.nextUUIDv4); | 2782 | const requestId = ApprovalRequestId.make(yield* randomUUIDv4); |
| 2762 | const requestType = classifyRequestType(toolName); | 2783 | const requestType = classifyRequestType(toolName); |
| 2763 | const detail = summarizeToolRequest(toolName, toolInput); | 2784 | const detail = summarizeToolRequest(toolName, toolInput); |
| 2764 | const decisionDeferred = yield* Deferred.make<ProviderApprovalDecision>(); | 2785 | const decisionDeferred = yield* Deferred.make<ProviderApprovalDecision>(); |
| ... | @@ -3059,7 +3080,11 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* ( | ... | @@ -3059,7 +3080,11 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* ( |
| 3059 | if (context.streamFiber === streamFiber) { | 3080 | if (context.streamFiber === streamFiber) { |
| 3060 | context.streamFiber = undefined; | 3081 | context.streamFiber = undefined; |
| 3061 | } | 3082 | } |
| 3062 | return handleStreamExit(context, exit); | 3083 | return handleStreamExit(context, exit).pipe( |
| 3084 | Effect.catch((cause) => | ||
| 3085 | Effect.logError("Failed to close Claude runtime stream.", { cause }), | ||
| 3086 | ), | ||
| 3087 | ); | ||
| 3063 | }), | 3088 | }), |
| 3064 | ), | 3089 | ), |
| 3065 | ); | 3090 | ); |
| ... | @@ -3120,7 +3145,7 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* ( | ... | @@ -3120,7 +3145,7 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* ( |
| 3120 | }); | 3145 | }); |
| 3121 | } | 3146 | } |
| 3122 | 3147 | ||
| 3123 | const turnId = TurnId.make(yield* Random.nextUUIDv4); | 3148 | const turnId = TurnId.make(yield* randomUUIDv4); |
| 3124 | const turnState: ClaudeTurnState = { | 3149 | const turnState: ClaudeTurnState = { |
| 3125 | turnId, | 3150 | turnId, |
| 3126 | startedAt: yield* nowIso, | 3151 | startedAt: yield* nowIso, |
| ... | @@ -3269,7 +3294,12 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* ( | ... | @@ -3269,7 +3294,12 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* ( |
| 3269 | emitExitEvent: false, | 3294 | emitExitEvent: false, |
| 3270 | }), | 3295 | }), |
| 3271 | { discard: true }, | 3296 | { discard: true }, |
| 3272 | ).pipe(Effect.tap(() => Queue.shutdown(runtimeEventQueue))), | 3297 | ).pipe( |
| 3298 | Effect.catch((cause) => | ||
| 3299 | Effect.logError("Failed to emit Claude session shutdown event.", { cause }), | ||
| 3300 | ), | ||
| 3301 | Effect.tap(() => Queue.shutdown(runtimeEventQueue)), | ||
| 3302 | ), | ||
| 3273 | ); | 3303 | ); |
| 3274 | 3304 | ||
| 3275 | return { | 3305 | return { |
apps/server/src/provider/Layers/CodexAdapter.ts+3| ... | @@ -25,6 +25,7 @@ import { | ... | @@ -25,6 +25,7 @@ import { |
| 25 | ProviderSendTurnInput, | 25 | ProviderSendTurnInput, |
| 26 | } from "@t3tools/contracts"; | 26 | } from "@t3tools/contracts"; |
| 27 | import * as Effect from "effect/Effect"; | 27 | import * as Effect from "effect/Effect"; |
| 28 | import * as Crypto from "effect/Crypto"; | ||
| 28 | import * as Exit from "effect/Exit"; | 29 | import * as Exit from "effect/Exit"; |
| 29 | import * as Fiber from "effect/Fiber"; | 30 | import * as Fiber from "effect/Fiber"; |
| 30 | import * as FileSystem from "effect/FileSystem"; | 31 | import * as FileSystem from "effect/FileSystem"; |
| ... | @@ -1349,6 +1350,7 @@ export const makeCodexAdapter = Effect.fn("makeCodexAdapter")(function* ( | ... | @@ -1349,6 +1350,7 @@ export const makeCodexAdapter = Effect.fn("makeCodexAdapter")(function* ( |
| 1349 | const boundInstanceId = options?.instanceId ?? ProviderInstanceId.make("codex"); | 1350 | const boundInstanceId = options?.instanceId ?? ProviderInstanceId.make("codex"); |
| 1350 | const fileSystem = yield* FileSystem.FileSystem; | 1351 | const fileSystem = yield* FileSystem.FileSystem; |
| 1351 | const childProcessSpawner = yield* ChildProcessSpawner.ChildProcessSpawner; | 1352 | const childProcessSpawner = yield* ChildProcessSpawner.ChildProcessSpawner; |
| 1353 | const crypto = yield* Crypto.Crypto; | ||
| 1352 | const serverConfig = yield* Effect.service(ServerConfig); | 1354 | const serverConfig = yield* Effect.service(ServerConfig); |
| 1353 | const nativeEventLogger = | 1355 | const nativeEventLogger = |
| 1354 | options?.nativeEventLogger ?? | 1356 | options?.nativeEventLogger ?? |
| ... | @@ -1406,6 +1408,7 @@ export const makeCodexAdapter = Effect.fn("makeCodexAdapter")(function* ( | ... | @@ -1406,6 +1408,7 @@ export const makeCodexAdapter = Effect.fn("makeCodexAdapter")(function* ( |
| 1406 | const runtime = yield* createRuntime(runtimeInput).pipe( | 1408 | const runtime = yield* createRuntime(runtimeInput).pipe( |
| 1407 | Effect.provideService(Scope.Scope, sessionScope), | 1409 | Effect.provideService(Scope.Scope, sessionScope), |
| 1408 | Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, childProcessSpawner), | 1410 | Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, childProcessSpawner), |
| 1411 | Effect.provideService(Crypto.Crypto, crypto), | ||
| 1409 | Effect.mapError( | 1412 | Effect.mapError( |
| 1410 | (cause) => | 1413 | (cause) => |
| 1411 | new ProviderAdapterProcessError({ | 1414 | new ProviderAdapterProcessError({ |
apps/server/src/provider/Layers/CodexSessionRuntime.ts+21-7| ... | @@ -17,6 +17,7 @@ import { | ... | @@ -17,6 +17,7 @@ import { |
| 17 | TurnId, | 17 | TurnId, |
| 18 | } from "@t3tools/contracts"; | 18 | } from "@t3tools/contracts"; |
| 19 | import { normalizeModelSlug } from "@t3tools/shared/model"; | 19 | import { normalizeModelSlug } from "@t3tools/shared/model"; |
| 20 | import * as Crypto from "effect/Crypto"; | ||
| 20 | import * as DateTime from "effect/DateTime"; | 21 | import * as DateTime from "effect/DateTime"; |
| 21 | import * as Deferred from "effect/Deferred"; | 22 | import * as Deferred from "effect/Deferred"; |
| 22 | import * as Effect from "effect/Effect"; | 23 | import * as Effect from "effect/Effect"; |
| ... | @@ -25,7 +26,6 @@ import * as Layer from "effect/Layer"; | ... | @@ -25,7 +26,6 @@ import * as Layer from "effect/Layer"; |
| 25 | import * as Queue from "effect/Queue"; | 26 | import * as Queue from "effect/Queue"; |
| 26 | import * as Ref from "effect/Ref"; | 27 | import * as Ref from "effect/Ref"; |
| 27 | import * as Scope from "effect/Scope"; | 28 | import * as Scope from "effect/Scope"; |
| 28 | import * as Random from "effect/Random"; | ||
| 29 | import * as Schema from "effect/Schema"; | 29 | import * as Schema from "effect/Schema"; |
| 30 | import * as Stream from "effect/Stream"; | 30 | import * as Stream from "effect/Stream"; |
| 31 | import * as SchemaIssue from "effect/SchemaIssue"; | 31 | import * as SchemaIssue from "effect/SchemaIssue"; |
| ... | @@ -697,11 +697,12 @@ export const makeCodexSessionRuntime = ( | ... | @@ -697,11 +697,12 @@ export const makeCodexSessionRuntime = ( |
| 697 | ): Effect.Effect< | 697 | ): Effect.Effect< |
| 698 | CodexSessionRuntimeShape, | 698 | CodexSessionRuntimeShape, |
| 699 | CodexErrors.CodexAppServerError, | 699 | CodexErrors.CodexAppServerError, |
| 700 | ChildProcessSpawner.ChildProcessSpawner | Scope.Scope | 700 | ChildProcessSpawner.ChildProcessSpawner | Crypto.Crypto | Scope.Scope |
| 701 | > => | 701 | > => |
| 702 | Effect.gen(function* () { | 702 | Effect.gen(function* () { |
| 703 | const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; | 703 | const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; |
| 704 | const runtimeScope = yield* Scope.Scope; | 704 | const runtimeScope = yield* Scope.Scope; |
| 705 | const crypto = yield* Crypto.Crypto; | ||
| 705 | const events = yield* Queue.unbounded<ProviderEvent>(); | 706 | const events = yield* Queue.unbounded<ProviderEvent>(); |
| 706 | const pendingApprovalsRef = yield* Ref.make(new Map<ApprovalRequestId, PendingApproval>()); | 707 | const pendingApprovalsRef = yield* Ref.make(new Map<ApprovalRequestId, PendingApproval>()); |
| 707 | const approvalCorrelationsRef = yield* Ref.make(new Map<string, ApprovalCorrelation>()); | 708 | const approvalCorrelationsRef = yield* Ref.make(new Map<string, ApprovalCorrelation>()); |
| ... | @@ -746,6 +747,15 @@ export const makeCodexSessionRuntime = ( | ... | @@ -746,6 +747,15 @@ export const makeCodexSessionRuntime = ( |
| 746 | ); | 747 | ); |
| 747 | const serverNotifications = yield* Queue.unbounded<CodexServerNotification>(); | 748 | const serverNotifications = yield* Queue.unbounded<CodexServerNotification>(); |
| 748 | const nowIso = Effect.map(DateTime.now, DateTime.formatIso); | 749 | const nowIso = Effect.map(DateTime.now, DateTime.formatIso); |
| 750 | const randomUUIDv4 = crypto.randomUUIDv4.pipe( | ||
| 751 | Effect.mapError( | ||
| 752 | (cause) => | ||
| 753 | new CodexErrors.CodexAppServerTransportError({ | ||
| 754 | detail: "Failed to generate Codex runtime identifier.", | ||
| 755 | cause, | ||
| 756 | }), | ||
| 757 | ), | ||
| 758 | ); | ||
| 749 | 759 | ||
| 750 | const sessionCreatedAt = yield* nowIso; | 760 | const sessionCreatedAt = yield* nowIso; |
| 751 | const initialSession = { | 761 | const initialSession = { |
| ... | @@ -765,7 +775,7 @@ export const makeCodexSessionRuntime = ( | ... | @@ -765,7 +775,7 @@ export const makeCodexSessionRuntime = ( |
| 765 | 775 | ||
| 766 | const emitEvent = (event: Omit<ProviderEvent, "id" | "provider" | "createdAt">) => | 776 | const emitEvent = (event: Omit<ProviderEvent, "id" | "provider" | "createdAt">) => |
| 767 | Effect.gen(function* () { | 777 | Effect.gen(function* () { |
| 768 | const id = yield* Random.nextUUIDv4; | 778 | const id = yield* randomUUIDv4; |
| 769 | return yield* offerEvent({ | 779 | return yield* offerEvent({ |
| 770 | id: EventId.make(id), | 780 | id: EventId.make(id), |
| 771 | provider: PROVIDER, | 781 | provider: PROVIDER, |
| ... | @@ -933,7 +943,7 @@ export const makeCodexSessionRuntime = ( | ... | @@ -933,7 +943,7 @@ export const makeCodexSessionRuntime = ( |
| 933 | 943 | ||
| 934 | yield* client.handleServerRequest("item/commandExecution/requestApproval", (payload) => | 944 | yield* client.handleServerRequest("item/commandExecution/requestApproval", (payload) => |
| 935 | Effect.gen(function* () { | 945 | Effect.gen(function* () { |
| 936 | const requestId = ApprovalRequestId.make(yield* Random.nextUUIDv4); | 946 | const requestId = ApprovalRequestId.make(yield* randomUUIDv4); |
| 937 | const turnId = TurnId.make(payload.turnId); | 947 | const turnId = TurnId.make(payload.turnId); |
| 938 | const itemId = ProviderItemId.make(payload.itemId); | 948 | const itemId = ProviderItemId.make(payload.itemId); |
| 939 | const decision = yield* Deferred.make<ProviderApprovalDecision>(); | 949 | const decision = yield* Deferred.make<ProviderApprovalDecision>(); |
| ... | @@ -989,7 +999,7 @@ export const makeCodexSessionRuntime = ( | ... | @@ -989,7 +999,7 @@ export const makeCodexSessionRuntime = ( |
| 989 | 999 | ||
| 990 | yield* client.handleServerRequest("item/fileChange/requestApproval", (payload) => | 1000 | yield* client.handleServerRequest("item/fileChange/requestApproval", (payload) => |
| 991 | Effect.gen(function* () { | 1001 | Effect.gen(function* () { |
| 992 | const requestId = ApprovalRequestId.make(yield* Random.nextUUIDv4); | 1002 | const requestId = ApprovalRequestId.make(yield* randomUUIDv4); |
| 993 | const turnId = TurnId.make(payload.turnId); | 1003 | const turnId = TurnId.make(payload.turnId); |
| 994 | const itemId = ProviderItemId.make(payload.itemId); | 1004 | const itemId = ProviderItemId.make(payload.itemId); |
| 995 | const decision = yield* Deferred.make<ProviderApprovalDecision>(); | 1005 | const decision = yield* Deferred.make<ProviderApprovalDecision>(); |
| ... | @@ -1045,7 +1055,7 @@ export const makeCodexSessionRuntime = ( | ... | @@ -1045,7 +1055,7 @@ export const makeCodexSessionRuntime = ( |
| 1045 | 1055 | ||
| 1046 | yield* client.handleServerRequest("item/tool/requestUserInput", (payload) => | 1056 | yield* client.handleServerRequest("item/tool/requestUserInput", (payload) => |
| 1047 | Effect.gen(function* () { | 1057 | Effect.gen(function* () { |
| 1048 | const requestId = ApprovalRequestId.make(yield* Random.nextUUIDv4); | 1058 | const requestId = ApprovalRequestId.make(yield* randomUUIDv4); |
| 1049 | const turnId = TurnId.make(payload.turnId); | 1059 | const turnId = TurnId.make(payload.turnId); |
| 1050 | const itemId = ProviderItemId.make(payload.itemId); | 1060 | const itemId = ProviderItemId.make(payload.itemId); |
| 1051 | const answers = yield* Deferred.make<ProviderUserInputAnswers>(); | 1061 | const answers = yield* Deferred.make<ProviderUserInputAnswers>(); |
| ... | @@ -1229,7 +1239,11 @@ export const makeCodexSessionRuntime = ( | ... | @@ -1229,7 +1239,11 @@ export const makeCodexSessionRuntime = ( |
| 1229 | status: "closed", | 1239 | status: "closed", |
| 1230 | activeTurnId: undefined, | 1240 | activeTurnId: undefined, |
| 1231 | }); | 1241 | }); |
| 1232 | yield* emitSessionEvent("session/closed", "Session stopped"); | 1242 | yield* emitSessionEvent("session/closed", "Session stopped").pipe( |
| 1243 | Effect.catch((cause) => | ||
| 1244 | Effect.logError("Failed to emit Codex session closed event.", { cause }), | ||
| 1245 | ), | ||
| 1246 | ); | ||
| 1233 | yield* Scope.close(runtimeScope, Exit.void); | 1247 | yield* Scope.close(runtimeScope, Exit.void); |
| 1234 | yield* Queue.shutdown(serverNotifications); | 1248 | yield* Queue.shutdown(serverNotifications); |
| 1235 | yield* Queue.shutdown(events); | 1249 | yield* Queue.shutdown(events); |
apps/server/src/provider/Layers/CursorAdapter.ts+182-142| ... | @@ -22,6 +22,7 @@ import { | ... | @@ -22,6 +22,7 @@ import { |
| 22 | TurnId, | 22 | TurnId, |
| 23 | } from "@t3tools/contracts"; | 23 | } from "@t3tools/contracts"; |
| 24 | import * as DateTime from "effect/DateTime"; | 24 | import * as DateTime from "effect/DateTime"; |
| 25 | import * as Crypto from "effect/Crypto"; | ||
| 25 | import * as Deferred from "effect/Deferred"; | 26 | import * as Deferred from "effect/Deferred"; |
| 26 | import * as Effect from "effect/Effect"; | 27 | import * as Effect from "effect/Effect"; |
| 27 | import * as Exit from "effect/Exit"; | 28 | import * as Exit from "effect/Exit"; |
| ... | @@ -30,13 +31,13 @@ import * as FileSystem from "effect/FileSystem"; | ... | @@ -30,13 +31,13 @@ import * as FileSystem from "effect/FileSystem"; |
| 30 | import * as Option from "effect/Option"; | 31 | import * as Option from "effect/Option"; |
| 31 | import * as Path from "effect/Path"; | 32 | import * as Path from "effect/Path"; |
| 32 | import * as PubSub from "effect/PubSub"; | 33 | import * as PubSub from "effect/PubSub"; |
| 33 | import * as Random from "effect/Random"; | ||
| 34 | import * as Schema from "effect/Schema"; | 34 | import * as Schema from "effect/Schema"; |
| 35 | import * as Scope from "effect/Scope"; | 35 | import * as Scope from "effect/Scope"; |
| 36 | import * as Semaphore from "effect/Semaphore"; | 36 | import * as Semaphore from "effect/Semaphore"; |
| 37 | import * as Stream from "effect/Stream"; | 37 | import * as Stream from "effect/Stream"; |
| 38 | import * as SynchronizedRef from "effect/SynchronizedRef"; | 38 | import * as SynchronizedRef from "effect/SynchronizedRef"; |
| 39 | import { ChildProcessSpawner } from "effect/unstable/process"; | 39 | import { ChildProcessSpawner } from "effect/unstable/process"; |
| 40 | import * as EffectAcpErrors from "effect-acp/errors"; | ||
| 40 | import type * as EffectAcpSchema from "effect-acp/schema"; | 41 | import type * as EffectAcpSchema from "effect-acp/schema"; |
| 41 | 42 | ||
| 42 | import { resolveAttachmentPath } from "../../attachmentStore.ts"; | 43 | import { resolveAttachmentPath } from "../../attachmentStore.ts"; |
| ... | @@ -62,7 +63,7 @@ import { | ... | @@ -62,7 +63,7 @@ import { |
| 62 | type AcpSessionModeState, | 63 | type AcpSessionModeState, |
| 63 | parsePermissionRequest, | 64 | parsePermissionRequest, |
| 64 | } from "../acp/AcpRuntimeModel.ts"; | 65 | } from "../acp/AcpRuntimeModel.ts"; |
| 65 | import { makeAcpNativeLoggers } from "../acp/AcpNativeLogging.ts"; | 66 | import { makeAcpNativeLoggerFactory } from "../acp/AcpNativeLogging.ts"; |
| 66 | import { applyCursorAcpModelSelection, makeCursorAcpRuntime } from "../acp/CursorAcpSupport.ts"; | 67 | import { applyCursorAcpModelSelection, makeCursorAcpRuntime } from "../acp/CursorAcpSupport.ts"; |
| 67 | import { | 68 | import { |
| 68 | CursorAskQuestionRequest, | 69 | CursorAskQuestionRequest, |
| ... | @@ -314,6 +315,7 @@ export function makeCursorAdapter( | ... | @@ -314,6 +315,7 @@ export function makeCursorAdapter( |
| 314 | const path = yield* Path.Path; | 315 | const path = yield* Path.Path; |
| 315 | const childProcessSpawner = yield* ChildProcessSpawner.ChildProcessSpawner; | 316 | const childProcessSpawner = yield* ChildProcessSpawner.ChildProcessSpawner; |
| 316 | const serverConfig = yield* Effect.service(ServerConfig); | 317 | const serverConfig = yield* Effect.service(ServerConfig); |
| 318 | const crypto = yield* Crypto.Crypto; | ||
| 317 | const nativeEventLogger = | 319 | const nativeEventLogger = |
| 318 | options?.nativeEventLogger ?? | 320 | options?.nativeEventLogger ?? |
| 319 | (options?.nativeEventLogPath !== undefined | 321 | (options?.nativeEventLogPath !== undefined |
| ... | @@ -323,14 +325,36 @@ export function makeCursorAdapter( | ... | @@ -323,14 +325,36 @@ export function makeCursorAdapter( |
| 323 | : undefined); | 325 | : undefined); |
| 324 | const managedNativeEventLogger = | 326 | const managedNativeEventLogger = |
| 325 | options?.nativeEventLogger === undefined ? nativeEventLogger : undefined; | 327 | options?.nativeEventLogger === undefined ? nativeEventLogger : undefined; |
| 328 | const makeAcpNativeLoggers = yield* makeAcpNativeLoggerFactory(); | ||
| 326 | 329 | ||
| 327 | const sessions = new Map<ThreadId, CursorSessionContext>(); | 330 | const sessions = new Map<ThreadId, CursorSessionContext>(); |
| 328 | const threadLocksRef = yield* SynchronizedRef.make(new Map<string, Semaphore.Semaphore>()); | 331 | const threadLocksRef = yield* SynchronizedRef.make(new Map<string, Semaphore.Semaphore>()); |
| 329 | const runtimeEventPubSub = yield* PubSub.unbounded<ProviderRuntimeEvent>(); | 332 | const runtimeEventPubSub = yield* PubSub.unbounded<ProviderRuntimeEvent>(); |
| 330 | 333 | ||
| 331 | const nowIso = Effect.map(DateTime.now, DateTime.formatIso); | 334 | const nowIso = Effect.map(DateTime.now, DateTime.formatIso); |
| 332 | const nextEventId = Effect.map(Random.nextUUIDv4, (id) => EventId.make(id)); | 335 | const randomUUIDv4 = crypto.randomUUIDv4.pipe( |
| 336 | Effect.mapError( | ||
| 337 | (cause) => | ||
| 338 | new ProviderAdapterRequestError({ | ||
| 339 | provider: PROVIDER, | ||
| 340 | method: "crypto/randomUUIDv4", | ||
| 341 | detail: "Failed to generate Cursor runtime identifier.", | ||
| 342 | cause, | ||
| 343 | }), | ||
| 344 | ), | ||
| 345 | ); | ||
| 346 | const nextEventId = Effect.map(randomUUIDv4, (id) => EventId.make(id)); | ||
| 333 | const makeEventStamp = () => Effect.all({ eventId: nextEventId, createdAt: nowIso }); | 347 | const makeEventStamp = () => Effect.all({ eventId: nextEventId, createdAt: nowIso }); |
| 348 | const mapExtensionFailure = <A, E, R>(effect: Effect.Effect<A, E, R>) => | ||
| 349 | effect.pipe( | ||
| 350 | Effect.mapError( | ||
| 351 | (cause) => | ||
| 352 | new EffectAcpErrors.AcpTransportError({ | ||
| 353 | detail: "Failed to process Cursor ACP extension event.", | ||
| 354 | cause, | ||
| 355 | }), | ||
| 356 | ), | ||
| 357 | ); | ||
| 334 | 358 | ||
| 335 | const offerRuntimeEvent = (event: ProviderRuntimeEvent) => | 359 | const offerRuntimeEvent = (event: ProviderRuntimeEvent) => |
| 336 | PubSub.publish(runtimeEventPubSub, event).pipe(Effect.asVoid); | 360 | PubSub.publish(runtimeEventPubSub, event).pipe(Effect.asVoid); |
| ... | @@ -369,7 +393,7 @@ export function makeCursorAdapter( | ... | @@ -369,7 +393,7 @@ export function makeCursorAdapter( |
| 369 | { | 393 | { |
| 370 | observedAt, | 394 | observedAt, |
| 371 | event: { | 395 | event: { |
| 372 | id: yield* Random.nextUUIDv4, | 396 | id: yield* randomUUIDv4, |
| 373 | kind: "notification", | 397 | kind: "notification", |
| 374 | provider: PROVIDER, | 398 | provider: PROVIDER, |
| 375 | createdAt: observedAt, | 399 | createdAt: observedAt, |
| ... | @@ -524,159 +548,167 @@ export function makeCursorAdapter( | ... | @@ -524,159 +548,167 @@ export function makeCursorAdapter( |
| 524 | ); | 548 | ); |
| 525 | const started = yield* Effect.gen(function* () { | 549 | const started = yield* Effect.gen(function* () { |
| 526 | yield* acp.handleExtRequest("cursor/ask_question", CursorAskQuestionRequest, (params) => | 550 | yield* acp.handleExtRequest("cursor/ask_question", CursorAskQuestionRequest, (params) => |
| 527 | Effect.gen(function* () { | 551 | mapExtensionFailure( |
| 528 | yield* logNative( | ||
| 529 | input.threadId, | ||
| 530 | "cursor/ask_question", | ||
| 531 | params, | ||
| 532 | "acp.cursor.extension", | ||
| 533 | ); | ||
| 534 | const requestId = ApprovalRequestId.make(crypto.randomUUID()); | ||
| 535 | const runtimeRequestId = RuntimeRequestId.make(requestId); | ||
| 536 | const answers = yield* Deferred.make<ProviderUserInputAnswers>(); | ||
| 537 | pendingUserInputs.set(requestId, { answers }); | ||
| 538 | yield* offerRuntimeEvent({ | ||
| 539 | type: "user-input.requested", | ||
| 540 | ...(yield* makeEventStamp()), | ||
| 541 | provider: PROVIDER, | ||
| 542 | threadId: input.threadId, | ||
| 543 | turnId: ctx?.activeTurnId, | ||
| 544 | requestId: runtimeRequestId, | ||
| 545 | payload: { questions: extractAskQuestions(params) }, | ||
| 546 | raw: { | ||
| 547 | source: "acp.cursor.extension", | ||
| 548 | method: "cursor/ask_question", | ||
| 549 | payload: params, | ||
| 550 | }, | ||
| 551 | }); | ||
| 552 | const resolved = yield* Deferred.await(answers); | ||
| 553 | pendingUserInputs.delete(requestId); | ||
| 554 | yield* offerRuntimeEvent({ | ||
| 555 | type: "user-input.resolved", | ||
| 556 | ...(yield* makeEventStamp()), | ||
| 557 | provider: PROVIDER, | ||
| 558 | threadId: input.threadId, | ||
| 559 | turnId: ctx?.activeTurnId, | ||
| 560 | requestId: runtimeRequestId, | ||
| 561 | payload: { answers: resolved }, | ||
| 562 | }); | ||
| 563 | return { answers: resolved }; | ||
| 564 | }), | ||
| 565 | ); | ||
| 566 | yield* acp.handleExtRequest("cursor/create_plan", CursorCreatePlanRequest, (params) => | ||
| 567 | Effect.gen(function* () { | ||
| 568 | yield* logNative( | ||
| 569 | input.threadId, | ||
| 570 | "cursor/create_plan", | ||
| 571 | params, | ||
| 572 | "acp.cursor.extension", | ||
| 573 | ); | ||
| 574 | yield* offerRuntimeEvent({ | ||
| 575 | type: "turn.proposed.completed", | ||
| 576 | ...(yield* makeEventStamp()), | ||
| 577 | provider: PROVIDER, | ||
| 578 | threadId: input.threadId, | ||
| 579 | turnId: ctx?.activeTurnId, | ||
| 580 | payload: { planMarkdown: extractPlanMarkdown(params) }, | ||
| 581 | raw: { | ||
| 582 | source: "acp.cursor.extension", | ||
| 583 | method: "cursor/create_plan", | ||
| 584 | payload: params, | ||
| 585 | }, | ||
| 586 | }); | ||
| 587 | return { accepted: true } as const; | ||
| 588 | }), | ||
| 589 | ); | ||
| 590 | yield* acp.handleExtNotification( | ||
| 591 | "cursor/update_todos", | ||
| 592 | CursorUpdateTodosRequest, | ||
| 593 | (params) => | ||
| 594 | Effect.gen(function* () { | 552 | Effect.gen(function* () { |
| 595 | yield* logNative( | 553 | yield* logNative( |
| 596 | input.threadId, | 554 | input.threadId, |
| 597 | "cursor/update_todos", | 555 | "cursor/ask_question", |
| 598 | params, | 556 | params, |
| 599 | "acp.cursor.extension", | 557 | "acp.cursor.extension", |
| 600 | ); | 558 | ); |
| 601 | if (ctx) { | 559 | const requestId = ApprovalRequestId.make(yield* randomUUIDv4); |
| 602 | yield* emitPlanUpdate( | 560 | const runtimeRequestId = RuntimeRequestId.make(requestId); |
| 603 | ctx, | 561 | const answers = yield* Deferred.make<ProviderUserInputAnswers>(); |
| 604 | extractTodosAsPlan(params), | 562 | pendingUserInputs.set(requestId, { answers }); |
| 605 | params, | 563 | yield* offerRuntimeEvent({ |
| 606 | "acp.cursor.extension", | 564 | type: "user-input.requested", |
| 607 | "cursor/update_todos", | 565 | ...(yield* makeEventStamp()), |
| 608 | ); | ||
| 609 | } | ||
| 610 | }), | ||
| 611 | ); | ||
| 612 | yield* acp.handleRequestPermission((params) => | ||
| 613 | Effect.gen(function* () { | ||
| 614 | yield* logNative( | ||
| 615 | input.threadId, | ||
| 616 | "session/request_permission", | ||
| 617 | params, | ||
| 618 | "acp.jsonrpc", | ||
| 619 | ); | ||
| 620 | if (input.runtimeMode === "full-access") { | ||
| 621 | const autoApprovedOptionId = selectAutoApprovedPermissionOption(params); | ||
| 622 | if (autoApprovedOptionId !== undefined) { | ||
| 623 | return { | ||
| 624 | outcome: { | ||
| 625 | outcome: "selected" as const, | ||
| 626 | optionId: autoApprovedOptionId, | ||
| 627 | }, | ||
| 628 | }; | ||
| 629 | } | ||
| 630 | } | ||
| 631 | const permissionRequest = parsePermissionRequest(params); | ||
| 632 | const requestId = ApprovalRequestId.make(crypto.randomUUID()); | ||
| 633 | const runtimeRequestId = RuntimeRequestId.make(requestId); | ||
| 634 | const decision = yield* Deferred.make<ProviderApprovalDecision>(); | ||
| 635 | pendingApprovals.set(requestId, { | ||
| 636 | decision, | ||
| 637 | kind: permissionRequest.kind, | ||
| 638 | }); | ||
| 639 | yield* offerRuntimeEvent( | ||
| 640 | makeAcpRequestOpenedEvent({ | ||
| 641 | stamp: yield* makeEventStamp(), | ||
| 642 | provider: PROVIDER, | 566 | provider: PROVIDER, |
| 643 | threadId: input.threadId, | 567 | threadId: input.threadId, |
| 644 | turnId: ctx?.activeTurnId, | 568 | turnId: ctx?.activeTurnId, |
| 645 | requestId: runtimeRequestId, | 569 | requestId: runtimeRequestId, |
| 646 | permissionRequest, | 570 | payload: { questions: extractAskQuestions(params) }, |
| 647 | detail: | 571 | raw: { |
| 648 | permissionRequest.detail ?? | 572 | source: "acp.cursor.extension", |
| 649 | encodeJsonStringForDiagnostics(params)?.slice(0, 2000) ?? | 573 | method: "cursor/ask_question", |
| 650 | "[unserializable params]", | 574 | payload: params, |
| 651 | args: params, | 575 | }, |
| 652 | source: "acp.jsonrpc", | 576 | }); |
| 653 | method: "session/request_permission", | 577 | const resolved = yield* Deferred.await(answers); |
| 654 | rawPayload: params, | 578 | pendingUserInputs.delete(requestId); |
| 655 | }), | 579 | yield* offerRuntimeEvent({ |
| 656 | ); | 580 | type: "user-input.resolved", |
| 657 | const resolved = yield* Deferred.await(decision); | 581 | ...(yield* makeEventStamp()), |
| 658 | pendingApprovals.delete(requestId); | ||
| 659 | yield* offerRuntimeEvent( | ||
| 660 | makeAcpRequestResolvedEvent({ | ||
| 661 | stamp: yield* makeEventStamp(), | ||
| 662 | provider: PROVIDER, | 582 | provider: PROVIDER, |
| 663 | threadId: input.threadId, | 583 | threadId: input.threadId, |
| 664 | turnId: ctx?.activeTurnId, | 584 | turnId: ctx?.activeTurnId, |
| 665 | requestId: runtimeRequestId, | 585 | requestId: runtimeRequestId, |
| 666 | permissionRequest, | 586 | payload: { answers: resolved }, |
| 667 | decision: resolved, | 587 | }); |
| 588 | return { answers: resolved }; | ||
| 589 | }), | ||
| 590 | ), | ||
| 591 | ); | ||
| 592 | yield* acp.handleExtRequest("cursor/create_plan", CursorCreatePlanRequest, (params) => | ||
| 593 | mapExtensionFailure( | ||
| 594 | Effect.gen(function* () { | ||
| 595 | yield* logNative( | ||
| 596 | input.threadId, | ||
| 597 | "cursor/create_plan", | ||
| 598 | params, | ||
| 599 | "acp.cursor.extension", | ||
| 600 | ); | ||
| 601 | yield* offerRuntimeEvent({ | ||
| 602 | type: "turn.proposed.completed", | ||
| 603 | ...(yield* makeEventStamp()), | ||
| 604 | provider: PROVIDER, | ||
| 605 | threadId: input.threadId, | ||
| 606 | turnId: ctx?.activeTurnId, | ||
| 607 | payload: { planMarkdown: extractPlanMarkdown(params) }, | ||
| 608 | raw: { | ||
| 609 | source: "acp.cursor.extension", | ||
| 610 | method: "cursor/create_plan", | ||
| 611 | payload: params, | ||
| 612 | }, | ||
| 613 | }); | ||
| 614 | return { accepted: true } as const; | ||
| 615 | }), | ||
| 616 | ), | ||
| 617 | ); | ||
| 618 | yield* acp.handleExtNotification( | ||
| 619 | "cursor/update_todos", | ||
| 620 | CursorUpdateTodosRequest, | ||
| 621 | (params) => | ||
| 622 | mapExtensionFailure( | ||
| 623 | Effect.gen(function* () { | ||
| 624 | yield* logNative( | ||
| 625 | input.threadId, | ||
| 626 | "cursor/update_todos", | ||
| 627 | params, | ||
| 628 | "acp.cursor.extension", | ||
| 629 | ); | ||
| 630 | if (ctx) { | ||
| 631 | yield* emitPlanUpdate( | ||
| 632 | ctx, | ||
| 633 | extractTodosAsPlan(params), | ||
| 634 | params, | ||
| 635 | "acp.cursor.extension", | ||
| 636 | "cursor/update_todos", | ||
| 637 | ); | ||
| 638 | } | ||
| 668 | }), | 639 | }), |
| 669 | ); | 640 | ), |
| 670 | return { | 641 | ); |
| 671 | outcome: | 642 | yield* acp.handleRequestPermission((params) => |
| 672 | resolved === "cancel" | 643 | mapExtensionFailure( |
| 673 | ? ({ outcome: "cancelled" } as const) | 644 | Effect.gen(function* () { |
| 674 | : { | 645 | yield* logNative( |
| 646 | input.threadId, | ||
| 647 | "session/request_permission", | ||
| 648 | params, | ||
| 649 | "acp.jsonrpc", | ||
| 650 | ); | ||
| 651 | if (input.runtimeMode === "full-access") { | ||
| 652 | const autoApprovedOptionId = selectAutoApprovedPermissionOption(params); | ||
| 653 | if (autoApprovedOptionId !== undefined) { | ||
| 654 | return { | ||
| 655 | outcome: { | ||
| 675 | outcome: "selected" as const, | 656 | outcome: "selected" as const, |
| 676 | optionId: acpPermissionOutcome(resolved), | 657 | optionId: autoApprovedOptionId, |
| 677 | }, | 658 | }, |
| 678 | }; | 659 | }; |
| 679 | }), | 660 | } |
| 661 | } | ||
| 662 | const permissionRequest = parsePermissionRequest(params); | ||
| 663 | const requestId = ApprovalRequestId.make(yield* randomUUIDv4); | ||
| 664 | const runtimeRequestId = RuntimeRequestId.make(requestId); | ||
| 665 | const decision = yield* Deferred.make<ProviderApprovalDecision>(); | ||
| 666 | pendingApprovals.set(requestId, { | ||
| 667 | decision, | ||
| 668 | kind: permissionRequest.kind, | ||
| 669 | }); | ||
| 670 | yield* offerRuntimeEvent( | ||
| 671 | makeAcpRequestOpenedEvent({ | ||
| 672 | stamp: yield* makeEventStamp(), | ||
| 673 | provider: PROVIDER, | ||
| 674 | threadId: input.threadId, | ||
| 675 | turnId: ctx?.activeTurnId, | ||
| 676 | requestId: runtimeRequestId, | ||
| 677 | permissionRequest, | ||
| 678 | detail: | ||
| 679 | permissionRequest.detail ?? | ||
| 680 | encodeJsonStringForDiagnostics(params)?.slice(0, 2000) ?? | ||
| 681 | "[unserializable params]", | ||
| 682 | args: params, | ||
| 683 | source: "acp.jsonrpc", | ||
| 684 | method: "session/request_permission", | ||
| 685 | rawPayload: params, | ||
| 686 | }), | ||
| 687 | ); | ||
| 688 | const resolved = yield* Deferred.await(decision); | ||
| 689 | pendingApprovals.delete(requestId); | ||
| 690 | yield* offerRuntimeEvent( | ||
| 691 | makeAcpRequestResolvedEvent({ | ||
| 692 | stamp: yield* makeEventStamp(), | ||
| 693 | provider: PROVIDER, | ||
| 694 | threadId: input.threadId, | ||
| 695 | turnId: ctx?.activeTurnId, | ||
| 696 | requestId: runtimeRequestId, | ||
| 697 | permissionRequest, | ||
| 698 | decision: resolved, | ||
| 699 | }), | ||
| 700 | ); | ||
| 701 | return { | ||
| 702 | outcome: | ||
| 703 | resolved === "cancel" | ||
| 704 | ? ({ outcome: "cancelled" } as const) | ||
| 705 | : { | ||
| 706 | outcome: "selected" as const, | ||
| 707 | optionId: acpPermissionOutcome(resolved), | ||
| 708 | }, | ||
| 709 | }; | ||
| 710 | }), | ||
| 711 | ), | ||
| 680 | ); | 712 | ); |
| 681 | return yield* acp.start(); | 713 | return yield* acp.start(); |
| 682 | }).pipe( | 714 | }).pipe( |
| ... | @@ -810,7 +842,12 @@ export function makeCursorAdapter( | ... | @@ -810,7 +842,12 @@ export function makeCursorAdapter( |
| 810 | } | 842 | } |
| 811 | }), | 843 | }), |
| 812 | ), | 844 | ), |
| 813 | ).pipe(Effect.forkChild); | 845 | ).pipe( |
| 846 | Effect.catch((cause) => | ||
| 847 | Effect.logError("Failed to process Cursor runtime notification.", { cause }), | ||
| 848 | ), | ||
| 849 | Effect.forkChild, | ||
| 850 | ); | ||
| 814 | 851 | ||
| 815 | ctx.notificationFiber = nf; | 852 | ctx.notificationFiber = nf; |
| 816 | sessions.set(input.threadId, ctx); | 853 | sessions.set(input.threadId, ctx); |
| ... | @@ -845,7 +882,7 @@ export function makeCursorAdapter( | ... | @@ -845,7 +882,7 @@ export function makeCursorAdapter( |
| 845 | const sendTurn: CursorAdapterShape["sendTurn"] = (input) => | 882 | const sendTurn: CursorAdapterShape["sendTurn"] = (input) => |
| 846 | Effect.gen(function* () { | 883 | Effect.gen(function* () { |
| 847 | const ctx = yield* requireSession(input.threadId); | 884 | const ctx = yield* requireSession(input.threadId); |
| 848 | const turnId = TurnId.make(crypto.randomUUID()); | 885 | const turnId = TurnId.make(yield* randomUUIDv4); |
| 849 | const turnModelSelection = | 886 | const turnModelSelection = |
| 850 | input.modelSelection?.instanceId === boundInstanceId ? input.modelSelection : undefined; | 887 | input.modelSelection?.instanceId === boundInstanceId ? input.modelSelection : undefined; |
| 851 | const model = turnModelSelection?.model ?? ctx.session.model; | 888 | const model = turnModelSelection?.model ?? ctx.session.model; |
| ... | @@ -1056,6 +1093,9 @@ export function makeCursorAdapter( | ... | @@ -1056,6 +1093,9 @@ export function makeCursorAdapter( |
| 1056 | 1093 | ||
| 1057 | yield* Effect.addFinalizer(() => | 1094 | yield* Effect.addFinalizer(() => |
| 1058 | Effect.forEach(sessions.values(), stopSessionInternal, { discard: true }).pipe( | 1095 | Effect.forEach(sessions.values(), stopSessionInternal, { discard: true }).pipe( |
| 1096 | Effect.catch((cause) => | ||
| 1097 | Effect.logError("Failed to emit Cursor session shutdown event.", { cause }), | ||
| 1098 | ), | ||
| 1059 | Effect.tap(() => PubSub.shutdown(runtimeEventPubSub)), | 1099 | Effect.tap(() => PubSub.shutdown(runtimeEventPubSub)), |
| 1060 | Effect.tap(() => managedNativeEventLogger?.close() ?? Effect.void), | 1100 | Effect.tap(() => managedNativeEventLogger?.close() ?? Effect.void), |
| 1061 | ), | 1101 | ), |
apps/server/src/provider/Layers/OpenCodeAdapter.ts+39-30| ... | @@ -13,11 +13,11 @@ import { | ... | @@ -13,11 +13,11 @@ import { |
| 13 | type UserInputQuestion, | 13 | type UserInputQuestion, |
| 14 | } from "@t3tools/contracts"; | 14 | } from "@t3tools/contracts"; |
| 15 | import * as Cause from "effect/Cause"; | 15 | import * as Cause from "effect/Cause"; |
| 16 | import * as Crypto from "effect/Crypto"; | ||
| 16 | import * as DateTime from "effect/DateTime"; | 17 | import * as DateTime from "effect/DateTime"; |
| 17 | import * as Effect from "effect/Effect"; | 18 | import * as Effect from "effect/Effect"; |
| 18 | import * as Exit from "effect/Exit"; | 19 | import * as Exit from "effect/Exit"; |
| 19 | import * as Queue from "effect/Queue"; | 20 | import * as Queue from "effect/Queue"; |
| 20 | import * as Random from "effect/Random"; | ||
| 21 | import * as Ref from "effect/Ref"; | 21 | import * as Ref from "effect/Ref"; |
| 22 | import * as Scope from "effect/Scope"; | 22 | import * as Scope from "effect/Scope"; |
| 23 | import * as Stream from "effect/Stream"; | 23 | import * as Stream from "effect/Stream"; |
| ... | @@ -134,40 +134,14 @@ const toProcessError = (threadId: ThreadId, cause: unknown): ProviderAdapterProc | ... | @@ -134,40 +134,14 @@ const toProcessError = (threadId: ThreadId, cause: unknown): ProviderAdapterProc |
| 134 | cause, | 134 | cause, |
| 135 | }); | 135 | }); |
| 136 | 136 | ||
| 137 | const buildEventBase = (input: { | 137 | type EventBaseInput = { |
| 138 | readonly threadId: ThreadId; | 138 | readonly threadId: ThreadId; |
| 139 | readonly turnId?: TurnId | undefined; | 139 | readonly turnId?: TurnId | undefined; |
| 140 | readonly itemId?: string | undefined; | 140 | readonly itemId?: string | undefined; |
| 141 | readonly requestId?: string | undefined; | 141 | readonly requestId?: string | undefined; |
| 142 | readonly createdAt?: string | undefined; | 142 | readonly createdAt?: string | undefined; |
| 143 | readonly raw?: unknown; | 143 | readonly raw?: unknown; |
| 144 | }): Effect.Effect< | 144 | }; |
| 145 | Pick< | ||
| 146 | ProviderRuntimeEvent, | ||
| 147 | "eventId" | "provider" | "threadId" | "createdAt" | "turnId" | "itemId" | "requestId" | "raw" | ||
| 148 | > | ||
| 149 | > => | ||
| 150 | Effect.gen(function* () { | ||
| 151 | const uuid = yield* Random.nextUUIDv4; | ||
| 152 | const createdAt = input.createdAt ?? (yield* nowIso); | ||
| 153 | return { | ||
| 154 | eventId: EventId.make(uuid), | ||
| 155 | provider: PROVIDER, | ||
| 156 | threadId: input.threadId, | ||
| 157 | createdAt, | ||
| 158 | ...(input.turnId ? { turnId: input.turnId } : {}), | ||
| 159 | ...(input.itemId ? { itemId: RuntimeItemId.make(input.itemId) } : {}), | ||
| 160 | ...(input.requestId ? { requestId: RuntimeRequestId.make(input.requestId) } : {}), | ||
| 161 | ...(input.raw !== undefined | ||
| 162 | ? { | ||
| 163 | raw: { | ||
| 164 | source: "opencode.sdk.event", | ||
| 165 | payload: input.raw, | ||
| 166 | }, | ||
| 167 | } | ||
| 168 | : {}), | ||
| 169 | }; | ||
| 170 | }); | ||
| 171 | 145 | ||
| 172 | function toToolLifecycleItemType(toolName: string): ToolLifecycleItemType { | 146 | function toToolLifecycleItemType(toolName: string): ToolLifecycleItemType { |
| 173 | const normalized = toolName.toLowerCase(); | 147 | const normalized = toolName.toLowerCase(); |
| ... | @@ -457,6 +431,7 @@ export function makeOpenCodeAdapter( | ... | @@ -457,6 +431,7 @@ export function makeOpenCodeAdapter( |
| 457 | const boundInstanceId = options?.instanceId ?? ProviderInstanceId.make("opencode"); | 431 | const boundInstanceId = options?.instanceId ?? ProviderInstanceId.make("opencode"); |
| 458 | const serverConfig = yield* ServerConfig; | 432 | const serverConfig = yield* ServerConfig; |
| 459 | const openCodeRuntime = yield* OpenCodeRuntime; | 433 | const openCodeRuntime = yield* OpenCodeRuntime; |
| 434 | const crypto = yield* Crypto.Crypto; | ||
| 460 | const nativeEventLogger = | 435 | const nativeEventLogger = |
| 461 | options?.nativeEventLogger ?? | 436 | options?.nativeEventLogger ?? |
| 462 | (options?.nativeEventLogPath !== undefined | 437 | (options?.nativeEventLogPath !== undefined |
| ... | @@ -470,6 +445,40 @@ export function makeOpenCodeAdapter( | ... | @@ -470,6 +445,40 @@ export function makeOpenCodeAdapter( |
| 470 | options?.nativeEventLogger === undefined ? nativeEventLogger : undefined; | 445 | options?.nativeEventLogger === undefined ? nativeEventLogger : undefined; |
| 471 | const runtimeEvents = yield* Queue.unbounded<ProviderRuntimeEvent>(); | 446 | const runtimeEvents = yield* Queue.unbounded<ProviderRuntimeEvent>(); |
| 472 | const sessions = new Map<ThreadId, OpenCodeSessionContext>(); | 447 | const sessions = new Map<ThreadId, OpenCodeSessionContext>(); |
| 448 | const randomUUIDv4 = crypto.randomUUIDv4.pipe( | ||
| 449 | Effect.mapError( | ||
| 450 | (cause) => | ||
| 451 | new ProviderAdapterRequestError({ | ||
| 452 | provider: PROVIDER, | ||
| 453 | method: "crypto/randomUUIDv4", | ||
| 454 | detail: "Failed to generate OpenCode runtime identifier.", | ||
| 455 | cause, | ||
| 456 | }), | ||
| 457 | ), | ||
| 458 | ); | ||
| 459 | const buildEventBase = (input: EventBaseInput) => | ||
| 460 | Effect.all({ | ||
| 461 | eventId: randomUUIDv4.pipe(Effect.map(EventId.make)), | ||
| 462 | createdAt: input.createdAt === undefined ? nowIso : Effect.succeed(input.createdAt), | ||
| 463 | }).pipe( | ||
| 464 | Effect.map(({ eventId, createdAt }) => ({ | ||
| 465 | eventId, | ||
| 466 | provider: PROVIDER, | ||
| 467 | threadId: input.threadId, | ||
| 468 | createdAt, | ||
| 469 | ...(input.turnId ? { turnId: input.turnId } : {}), | ||
| 470 | ...(input.itemId ? { itemId: RuntimeItemId.make(input.itemId) } : {}), | ||
| 471 | ...(input.requestId ? { requestId: RuntimeRequestId.make(input.requestId) } : {}), | ||
| 472 | ...(input.raw !== undefined | ||
| 473 | ? { | ||
| 474 | raw: { | ||
| 475 | source: "opencode.sdk.event" as const, | ||
| 476 | payload: input.raw, | ||
| 477 | }, | ||
| 478 | } | ||
| 479 | : {}), | ||
| 480 | })), | ||
| 481 | ); | ||
| 473 | 482 | ||
| 474 | // Layer-level finalizer: when the adapter layer shuts down, stop every | 483 | // Layer-level finalizer: when the adapter layer shuts down, stop every |
| 475 | // session. Each session's `Scope.close` tears down its spawned OpenCode | 484 | // session. Each session's `Scope.close` tears down its spawned OpenCode |
| ... | @@ -1142,7 +1151,7 @@ export function makeOpenCodeAdapter( | ... | @@ -1142,7 +1151,7 @@ export function makeOpenCodeAdapter( |
| 1142 | 1151 | ||
| 1143 | const sendTurn: OpenCodeAdapterShape["sendTurn"] = Effect.fn("sendTurn")(function* (input) { | 1152 | const sendTurn: OpenCodeAdapterShape["sendTurn"] = Effect.fn("sendTurn")(function* (input) { |
| 1144 | const context = ensureSessionContext(sessions, input.threadId); | 1153 | const context = ensureSessionContext(sessions, input.threadId); |
| 1145 | const turnId = TurnId.make(`opencode-turn-${yield* Random.nextUUIDv4}`); | 1154 | const turnId = TurnId.make(`opencode-turn-${yield* randomUUIDv4}`); |
| 1146 | const modelSelection = | 1155 | const modelSelection = |
| 1147 | input.modelSelection ?? | 1156 | input.modelSelection ?? |
| 1148 | (context.session.model | 1157 | (context.session.model |
apps/server/src/provider/acp/AcpNativeLogging.ts+60-59| ... | @@ -1,40 +1,13 @@ | ... | @@ -1,40 +1,13 @@ |
| 1 | import type { ProviderDriverKind, ThreadId } from "@t3tools/contracts"; | 1 | import type { ProviderDriverKind, ThreadId } from "@t3tools/contracts"; |
| 2 | import * as Cause from "effect/Cause"; | 2 | import * as Cause from "effect/Cause"; |
| 3 | import * as Crypto from "effect/Crypto"; | ||
| 3 | import * as DateTime from "effect/DateTime"; | 4 | import * as DateTime from "effect/DateTime"; |
| 4 | import * as Effect from "effect/Effect"; | 5 | import * as Effect from "effect/Effect"; |
| 5 | import * as Random from "effect/Random"; | ||
| 6 | import type * as EffectAcpProtocol from "effect-acp/protocol"; | 6 | import type * as EffectAcpProtocol from "effect-acp/protocol"; |
| 7 | 7 | ||
| 8 | import type { EventNdjsonLogger } from "../Layers/EventNdjsonLogger.ts"; | 8 | import type { EventNdjsonLogger } from "../Layers/EventNdjsonLogger.ts"; |
| 9 | import type { AcpSessionRequestLogEvent, AcpSessionRuntimeOptions } from "./AcpSessionRuntime.ts"; | 9 | import type { AcpSessionRequestLogEvent, AcpSessionRuntimeOptions } from "./AcpSessionRuntime.ts"; |
| 10 | 10 | ||
| 11 | function writeNativeAcpLog(input: { | ||
| 12 | readonly nativeEventLogger: EventNdjsonLogger | undefined; | ||
| 13 | readonly provider: ProviderDriverKind; | ||
| 14 | readonly threadId: ThreadId; | ||
| 15 | readonly kind: "request" | "protocol"; | ||
| 16 | readonly payload: unknown; | ||
| 17 | }): Effect.Effect<void, never> { | ||
| 18 | return Effect.gen(function* () { | ||
| 19 | if (!input.nativeEventLogger) return; | ||
| 20 | const observedAt = DateTime.formatIso(yield* DateTime.now); | ||
| 21 | yield* input.nativeEventLogger.write( | ||
| 22 | { | ||
| 23 | observedAt, | ||
| 24 | event: { | ||
| 25 | id: yield* Random.nextUUIDv4, | ||
| 26 | kind: input.kind, | ||
| 27 | provider: input.provider, | ||
| 28 | createdAt: observedAt, | ||
| 29 | threadId: input.threadId, | ||
| 30 | payload: input.payload, | ||
| 31 | }, | ||
| 32 | }, | ||
| 33 | input.threadId, | ||
| 34 | ); | ||
| 35 | }); | ||
| 36 | } | ||
| 37 | |||
| 38 | function formatRequestLogPayload(event: AcpSessionRequestLogEvent) { | 11 | function formatRequestLogPayload(event: AcpSessionRequestLogEvent) { |
| 39 | return { | 12 | return { |
| 40 | method: event.method, | 13 | method: event.method, |
| ... | @@ -45,35 +18,63 @@ function formatRequestLogPayload(event: AcpSessionRequestLogEvent) { | ... | @@ -45,35 +18,63 @@ function formatRequestLogPayload(event: AcpSessionRequestLogEvent) { |
| 45 | }; | 18 | }; |
| 46 | } | 19 | } |
| 47 | 20 | ||
| 48 | export function makeAcpNativeLoggers(input: { | 21 | export const makeAcpNativeLoggerFactory = Effect.fn("makeAcpNativeLoggerFactory")(function* () { |
| 49 | readonly nativeEventLogger: EventNdjsonLogger | undefined; | 22 | const crypto = yield* Crypto.Crypto; |
| 50 | readonly provider: ProviderDriverKind; | 23 | return (input: { |
| 51 | readonly threadId: ThreadId; | 24 | readonly nativeEventLogger: EventNdjsonLogger | undefined; |
| 52 | }): Pick<AcpSessionRuntimeOptions, "requestLogger" | "protocolLogging"> { | 25 | readonly provider: ProviderDriverKind; |
| 53 | return { | 26 | readonly threadId: ThreadId; |
| 54 | requestLogger: (event) => | 27 | }): Pick<AcpSessionRuntimeOptions, "requestLogger" | "protocolLogging"> => { |
| 55 | writeNativeAcpLog({ | 28 | const writeNativeAcpLog = (logInput: { |
| 56 | nativeEventLogger: input.nativeEventLogger, | 29 | readonly kind: "request" | "protocol"; |
| 57 | provider: input.provider, | 30 | readonly payload: unknown; |
| 58 | threadId: input.threadId, | 31 | }) => |
| 59 | kind: "request", | 32 | Effect.gen(function* () { |
| 60 | payload: formatRequestLogPayload(event), | 33 | if (!input.nativeEventLogger) return; |
| 61 | }), | 34 | const observedAt = DateTime.formatIso(yield* DateTime.now); |
| 62 | ...(input.nativeEventLogger | 35 | yield* input.nativeEventLogger.write( |
| 63 | ? { | 36 | { |
| 64 | protocolLogging: { | 37 | observedAt, |
| 65 | logIncoming: true, | 38 | event: { |
| 66 | logOutgoing: true, | 39 | id: yield* crypto.randomUUIDv4, |
| 67 | logger: (event: EffectAcpProtocol.AcpProtocolLogEvent) => | 40 | kind: logInput.kind, |
| 68 | writeNativeAcpLog({ | 41 | provider: input.provider, |
| 69 | nativeEventLogger: input.nativeEventLogger, | 42 | createdAt: observedAt, |
| 70 | provider: input.provider, | 43 | threadId: input.threadId, |
| 71 | threadId: input.threadId, | 44 | payload: logInput.payload, |
| 72 | kind: "protocol", | 45 | }, |
| 73 | payload: event, | 46 | }, |
| 74 | }), | 47 | input.threadId, |
| 75 | } satisfies NonNullable<AcpSessionRuntimeOptions["protocolLogging"]>, | 48 | ); |
| 76 | } | 49 | }).pipe( |
| 77 | : {}), | 50 | Effect.catch((cause) => |
| 51 | Effect.logWarning("Failed to write native ACP event log.", { | ||
| 52 | cause, | ||
| 53 | provider: input.provider, | ||
| 54 | threadId: input.threadId, | ||
| 55 | }), | ||
| 56 | ), | ||
| 57 | ); | ||
| 58 | |||
| 59 | return { | ||
| 60 | requestLogger: (event: AcpSessionRequestLogEvent) => | ||
| 61 | writeNativeAcpLog({ | ||
| 62 | kind: "request", | ||
| 63 | payload: formatRequestLogPayload(event), | ||
| 64 | }), | ||
| 65 | ...(input.nativeEventLogger | ||
| 66 | ? { | ||
| 67 | protocolLogging: { | ||
| 68 | logIncoming: true, | ||
| 69 | logOutgoing: true, | ||
| 70 | logger: (event: EffectAcpProtocol.AcpProtocolLogEvent) => | ||
| 71 | writeNativeAcpLog({ | ||
| 72 | kind: "protocol", | ||
| 73 | payload: event, | ||
| 74 | }), | ||
| 75 | } satisfies NonNullable<AcpSessionRuntimeOptions["protocolLogging"]>, | ||
| 76 | } | ||
| 77 | : {}), | ||
| 78 | }; | ||
| 78 | }; | 79 | }; |
| 79 | } | 80 | }); |
apps/server/src/provider/providerMaintenance.test.ts+10-9| ... | @@ -1,12 +1,12 @@ | ... | @@ -1,12 +1,12 @@ |
| 1 | // @effect-diagnostics nodeBuiltinImport:off | 1 | // @effect-diagnostics nodeBuiltinImport:off |
| 2 | import { afterEach, describe, expect, it } from "@effect/vitest"; | 2 | import { afterEach, expect, it } from "@effect/vitest"; |
| 3 | import { chmodSync, mkdirSync, symlinkSync, writeFileSync } from "node:fs"; | 3 | import { chmodSync, mkdirSync, symlinkSync, writeFileSync } from "node:fs"; |
| 4 | import * as NodeServices from "@effect/platform-node/NodeServices"; | 4 | import * as NodeServices from "@effect/platform-node/NodeServices"; |
| 5 | import os from "node:os"; | 5 | import os from "node:os"; |
| 6 | import path from "node:path"; | 6 | import path from "node:path"; |
| 7 | import { ProviderDriverKind } from "@t3tools/contracts"; | 7 | import { ProviderDriverKind } from "@t3tools/contracts"; |
| 8 | import * as Crypto from "effect/Crypto"; | ||
| 8 | import * as Effect from "effect/Effect"; | 9 | import * as Effect from "effect/Effect"; |
| 9 | import * as Random from "effect/Random"; | ||
| 10 | import { | 10 | import { |
| 11 | clearLatestProviderVersionCacheForTests, | 11 | clearLatestProviderVersionCacheForTests, |
| 12 | createProviderVersionAdvisory, | 12 | createProviderVersionAdvisory, |
| ... | @@ -18,10 +18,11 @@ import { | ... | @@ -18,10 +18,11 @@ import { |
| 18 | } from "./providerMaintenance.ts"; | 18 | } from "./providerMaintenance.ts"; |
| 19 | 19 | ||
| 20 | const driver = (value: string) => ProviderDriverKind.make(value); | 20 | const driver = (value: string) => ProviderDriverKind.make(value); |
| 21 | const makeTempDir = Effect.fn("makeTempDir")(function* (name: string) { | 21 | const makeTempDir = (name: string) => |
| 22 | const id = yield* Random.nextUUIDv4; | 22 | Crypto.Crypto.pipe( |
| 23 | return path.join(os.tmpdir(), `${name}-${id}`); | 23 | Effect.flatMap((crypto) => crypto.randomUUIDv4), |
| 24 | }); | 24 | Effect.map((id) => path.join(os.tmpdir(), `${name}-${id}`)), |
| 25 | ); | ||
| 25 | const isNativeTestCommandPath = | 26 | const isNativeTestCommandPath = |
| 26 | (expectedPathSegment: string) => | 27 | (expectedPathSegment: string) => |
| 27 | (commandPath: string): boolean => | 28 | (commandPath: string): boolean => |
| ... | @@ -68,7 +69,7 @@ afterEach(() => { | ... | @@ -68,7 +69,7 @@ afterEach(() => { |
| 68 | clearLatestProviderVersionCacheForTests(); | 69 | clearLatestProviderVersionCacheForTests(); |
| 69 | }); | 70 | }); |
| 70 | 71 | ||
| 71 | describe("providerMaintenance", () => { | 72 | it.layer(NodeServices.layer)("providerMaintenance", (it) => { |
| 72 | it("marks providers with unknown current versions as unknown", () => { | 73 | it("marks providers with unknown current versions as unknown", () => { |
| 73 | expect( | 74 | expect( |
| 74 | createProviderVersionAdvisory({ | 75 | createProviderVersionAdvisory({ |
| ... | @@ -404,7 +405,7 @@ describe("providerMaintenance", () => { | ... | @@ -404,7 +405,7 @@ describe("providerMaintenance", () => { |
| 404 | env: { | 405 | env: { |
| 405 | PATH: "", | 406 | PATH: "", |
| 406 | }, | 407 | }, |
| 407 | }).pipe(Effect.provide(NodeServices.layer)); | 408 | }); |
| 408 | 409 | ||
| 409 | expect(capabilities).toEqual({ | 410 | expect(capabilities).toEqual({ |
| 410 | provider: driver("packageTool"), | 411 | provider: driver("packageTool"), |
| ... | @@ -452,7 +453,7 @@ describe("providerMaintenance", () => { | ... | @@ -452,7 +453,7 @@ describe("providerMaintenance", () => { |
| 452 | env: { | 453 | env: { |
| 453 | PATH: "", | 454 | PATH: "", |
| 454 | }, | 455 | }, |
| 455 | }).pipe(Effect.provide(NodeServices.layer)); | 456 | }); |
| 456 | 457 | ||
| 457 | expect(capabilities).toEqual({ | 458 | expect(capabilities).toEqual({ |
| 458 | provider: driver("packageTool"), | 459 | provider: driver("packageTool"), |
apps/server/src/telemetry/Identify.ts+17-15| ... | @@ -1,9 +1,9 @@ | ... | @@ -1,9 +1,9 @@ |
| 1 | import * as Crypto from "effect/Crypto"; | ||
| 1 | import * as Effect from "effect/Effect"; | 2 | import * as Effect from "effect/Effect"; |
| 3 | import * as Encoding from "effect/Encoding"; | ||
| 2 | import * as FileSystem from "effect/FileSystem"; | 4 | import * as FileSystem from "effect/FileSystem"; |
| 3 | import * as Path from "effect/Path"; | 5 | import * as Path from "effect/Path"; |
| 4 | import * as Random from "effect/Random"; | ||
| 5 | import * as Schema from "effect/Schema"; | 6 | import * as Schema from "effect/Schema"; |
| 6 | import * as Crypto from "node:crypto"; | ||
| 7 | import { homedir } from "node:os"; | 7 | import { homedir } from "node:os"; |
| 8 | import { ServerConfig } from "../config.ts"; | 8 | import { ServerConfig } from "../config.ts"; |
| 9 | 9 | ||
| ... | @@ -23,14 +23,17 @@ class IdentifyUserError extends Schema.TaggedErrorClass<IdentifyUserError>()("Id | ... | @@ -23,14 +23,17 @@ class IdentifyUserError extends Schema.TaggedErrorClass<IdentifyUserError>()("Id |
| 23 | }) {} | 23 | }) {} |
| 24 | 24 | ||
| 25 | const hash = (value: string) => | 25 | const hash = (value: string) => |
| 26 | Effect.try({ | 26 | Crypto.Crypto.pipe( |
| 27 | try: () => Crypto.createHash("sha256").update(value).digest("hex"), | 27 | Effect.flatMap((crypto) => crypto.digest("SHA-256", new TextEncoder().encode(value))), |
| 28 | catch: (error) => | 28 | Effect.map(Encoding.encodeHex), |
| 29 | new IdentifyUserError({ | 29 | Effect.mapError( |
| 30 | message: "Failed to hash identifier", | 30 | (cause) => |
| 31 | cause: error, | 31 | new IdentifyUserError({ |
| 32 | }), | 32 | message: "Failed to hash identifier", |
| 33 | }); | 33 | cause, |
| 34 | }), | ||
| 35 | ), | ||
| 36 | ); | ||
| 34 | 37 | ||
| 35 | const getCodexAccountId = Effect.gen(function* () { | 38 | const getCodexAccountId = Effect.gen(function* () { |
| 36 | const fileSystem = yield* FileSystem.FileSystem; | 39 | const fileSystem = yield* FileSystem.FileSystem; |
| ... | @@ -64,11 +67,10 @@ const upsertAnonymousId = Effect.gen(function* () { | ... | @@ -64,11 +67,10 @@ const upsertAnonymousId = Effect.gen(function* () { |
| 64 | 67 | ||
| 65 | const anonymousId = yield* fileSystem.readFileString(anonymousIdPath).pipe( | 68 | const anonymousId = yield* fileSystem.readFileString(anonymousIdPath).pipe( |
| 66 | Effect.catch(() => | 69 | Effect.catch(() => |
| 67 | Effect.gen(function* () { | 70 | Crypto.Crypto.pipe( |
| 68 | const randomId = yield* Random.nextUUIDv4; | 71 | Effect.flatMap((crypto) => crypto.randomUUIDv4), |
| 69 | yield* fileSystem.writeFileString(anonymousIdPath, randomId); | 72 | Effect.tap((randomId) => fileSystem.writeFileString(anonymousIdPath, randomId)), |
| 70 | return randomId; | 73 | ), |
| 71 | }), | ||
| 72 | ), | 74 | ), |
| 73 | ); | 75 | ); |
| 74 | 76 |
apps/server/src/telemetry/Layers/AnalyticsService.ts+1-1| ... | @@ -40,7 +40,7 @@ const TelemetryEnvConfig = Config.all({ | ... | @@ -40,7 +40,7 @@ const TelemetryEnvConfig = Config.all({ |
| 40 | }); | 40 | }); |
| 41 | 41 | ||
| 42 | const makeAnalyticsService = Effect.gen(function* () { | 42 | const makeAnalyticsService = Effect.gen(function* () { |
| 43 | const telemetryConfig = yield* TelemetryEnvConfig.asEffect(); | 43 | const telemetryConfig = yield* TelemetryEnvConfig; |
| 44 | const httpClient = yield* HttpClient.HttpClient; | 44 | const httpClient = yield* HttpClient.HttpClient; |
| 45 | const serverConfig = yield* ServerConfig; | 45 | const serverConfig = yield* ServerConfig; |
| 46 | const identifier = yield* getTelemetryIdentifier; | 46 | const identifier = yield* getTelemetryIdentifier; |
apps/server/src/textGeneration/CodexTextGeneration.ts+1-3| ... | @@ -2,7 +2,6 @@ import * as Effect from "effect/Effect"; | ... | @@ -2,7 +2,6 @@ import * as Effect from "effect/Effect"; |
| 2 | import * as FileSystem from "effect/FileSystem"; | 2 | import * as FileSystem from "effect/FileSystem"; |
| 3 | import * as Option from "effect/Option"; | 3 | import * as Option from "effect/Option"; |
| 4 | import * as Path from "effect/Path"; | 4 | import * as Path from "effect/Path"; |
| 5 | import * as Random from "effect/Random"; | ||
| 6 | import * as Schema from "effect/Schema"; | 5 | import * as Schema from "effect/Schema"; |
| 7 | import * as Scope from "effect/Scope"; | 6 | import * as Scope from "effect/Scope"; |
| 8 | import * as Stream from "effect/Stream"; | 7 | import * as Stream from "effect/Stream"; |
| ... | @@ -79,10 +78,9 @@ export const makeCodexTextGeneration = Effect.fn("makeCodexTextGeneration")(func | ... | @@ -79,10 +78,9 @@ export const makeCodexTextGeneration = Effect.fn("makeCodexTextGeneration")(func |
| 79 | content: string, | 78 | content: string, |
| 80 | ): Effect.Effect<string, TextGenerationError, Scope.Scope> => { | 79 | ): Effect.Effect<string, TextGenerationError, Scope.Scope> => { |
| 81 | return Effect.gen(function* () { | 80 | return Effect.gen(function* () { |
| 82 | const tempFileId = yield* Random.nextUUIDv4; | ||
| 83 | return yield* fileSystem | 81 | return yield* fileSystem |
| 84 | .makeTempFileScoped({ | 82 | .makeTempFileScoped({ |
| 85 | prefix: `t3code-${prefix}-${process.pid}-${tempFileId}.tmp`, | 83 | prefix: `t3code-${prefix}-${process.pid}-`, |
| 86 | }) | 84 | }) |
| 87 | .pipe(Effect.tap((filePath) => fileSystem.writeFileString(filePath, content))); | 85 | .pipe(Effect.tap((filePath) => fileSystem.writeFileString(filePath, content))); |
| 88 | }).pipe( | 86 | }).pipe( |
apps/web/src/components/ChatView.tsx+2-2| ... | @@ -105,7 +105,7 @@ import { resolveShortcutCommand, shortcutLabelForCommand } from "../keybindings" | ... | @@ -105,7 +105,7 @@ import { resolveShortcutCommand, shortcutLabelForCommand } from "../keybindings" |
| 105 | import PlanSidebar from "./PlanSidebar"; | 105 | import PlanSidebar from "./PlanSidebar"; |
| 106 | import ThreadTerminalDrawer from "./ThreadTerminalDrawer"; | 106 | import ThreadTerminalDrawer from "./ThreadTerminalDrawer"; |
| 107 | import { ChevronDownIcon, TriangleAlertIcon, WifiOffIcon } from "lucide-react"; | 107 | import { ChevronDownIcon, TriangleAlertIcon, WifiOffIcon } from "lucide-react"; |
| 108 | import { cn, randomUUID } from "~/lib/utils"; | 108 | import { cn, randomHex, randomUUID } from "~/lib/utils"; |
| 109 | import { stackedThreadToast, toastManager } from "./ui/toast"; | 109 | import { stackedThreadToast, toastManager } from "./ui/toast"; |
| 110 | import { decodeProjectScriptKeybindingRule } from "~/lib/projectScriptKeybindings"; | 110 | import { decodeProjectScriptKeybindingRule } from "~/lib/projectScriptKeybindings"; |
| 111 | import { type NewProjectScriptInput } from "./ProjectScriptsControl"; | 111 | import { type NewProjectScriptInput } from "./ProjectScriptsControl"; |
| ... | @@ -2846,7 +2846,7 @@ export default function ChatView(props: ChatViewProps) { | ... | @@ -2846,7 +2846,7 @@ export default function ChatView(props: ChatViewProps) { |
| 2846 | prepareWorktree: { | 2846 | prepareWorktree: { |
| 2847 | projectCwd: activeProject.cwd, | 2847 | projectCwd: activeProject.cwd, |
| 2848 | baseBranch: baseBranchForWorktree, | 2848 | baseBranch: baseBranchForWorktree, |
| 2849 | branch: buildTemporaryWorktreeBranchName(), | 2849 | branch: buildTemporaryWorktreeBranchName(randomHex), |
| 2850 | }, | 2850 | }, |
| 2851 | runSetupScript: true, | 2851 | runSetupScript: true, |
| 2852 | } | 2852 | } |
apps/web/src/lib/utils.ts+10-6| ... | @@ -1,8 +1,7 @@ | ... | @@ -1,8 +1,7 @@ |
| 1 | import { CommandId, MessageId, ProjectId, ThreadId } from "@t3tools/contracts"; | 1 | import { CommandId, MessageId, ProjectId, ThreadId } from "@t3tools/contracts"; |
| 2 | import { type CxOptions, cx } from "class-variance-authority"; | 2 | import { type CxOptions, cx } from "class-variance-authority"; |
| 3 | import * as Encoding from "effect/Encoding"; | ||
| 3 | import { twMerge } from "tailwind-merge"; | 4 | import { twMerge } from "tailwind-merge"; |
| 4 | import * as Random from "effect/Random"; | ||
| 5 | import * as Effect from "effect/Effect"; | ||
| 6 | import { DraftId } from "../composerDraftStore"; | 5 | import { DraftId } from "../composerDraftStore"; |
| 7 | 6 | ||
| 8 | export function cn(...inputs: CxOptions) { | 7 | export function cn(...inputs: CxOptions) { |
| ... | @@ -21,11 +20,16 @@ export function isLinuxPlatform(platform: string): boolean { | ... | @@ -21,11 +20,16 @@ export function isLinuxPlatform(platform: string): boolean { |
| 21 | return /linux/i.test(platform); | 20 | return /linux/i.test(platform); |
| 22 | } | 21 | } |
| 23 | 22 | ||
| 23 | export function randomHex(byteLength: number): string { | ||
| 24 | return Encoding.encodeHex(globalThis.crypto.getRandomValues(new Uint8Array(byteLength))); | ||
| 25 | } | ||
| 26 | |||
| 24 | export function randomUUID(): string { | 27 | export function randomUUID(): string { |
| 25 | if (typeof crypto.randomUUID === "function") { | 28 | const bytes = globalThis.crypto.getRandomValues(new Uint8Array(16)); |
| 26 | return crypto.randomUUID(); | 29 | bytes[6] = (bytes[6]! & 0x0f) | 0x40; |
| 27 | } | 30 | bytes[8] = (bytes[8]! & 0x3f) | 0x80; |
| 28 | return Effect.runSync(Random.nextUUIDv4); | 31 | const hex = Encoding.encodeHex(bytes); |
| 32 | return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`; | ||
| 29 | } | 33 | } |
| 30 | 34 | ||
| 31 | export const newCommandId = (): CommandId => CommandId.make(randomUUID()); | 35 | export const newCommandId = (): CommandId => CommandId.make(randomUUID()); |
bun.lock+33-33| ... | @@ -17,7 +17,7 @@ | ... | @@ -17,7 +17,7 @@ |
| 17 | }, | 17 | }, |
| 18 | "apps/desktop": { | 18 | "apps/desktop": { |
| 19 | "name": "@t3tools/desktop", | 19 | "name": "@t3tools/desktop", |
| 20 | "version": "0.0.23", | 20 | "version": "0.0.24", |
| 21 | "dependencies": { | 21 | "dependencies": { |
| 22 | "@effect/platform-node": "catalog:", | 22 | "@effect/platform-node": "catalog:", |
| 23 | "@t3tools/contracts": "workspace:*", | 23 | "@t3tools/contracts": "workspace:*", |
| ... | @@ -50,7 +50,7 @@ | ... | @@ -50,7 +50,7 @@ |
| 50 | }, | 50 | }, |
| 51 | "apps/server": { | 51 | "apps/server": { |
| 52 | "name": "t3", | 52 | "name": "t3", |
| 53 | "version": "0.0.23", | 53 | "version": "0.0.24", |
| 54 | "bin": { | 54 | "bin": { |
| 55 | "t3": "./dist/bin.mjs", | 55 | "t3": "./dist/bin.mjs", |
| 56 | }, | 56 | }, |
| ... | @@ -83,7 +83,7 @@ | ... | @@ -83,7 +83,7 @@ |
| 83 | }, | 83 | }, |
| 84 | "apps/web": { | 84 | "apps/web": { |
| 85 | "name": "@t3tools/web", | 85 | "name": "@t3tools/web", |
| 86 | "version": "0.0.23", | 86 | "version": "0.0.24", |
| 87 | "dependencies": { | 87 | "dependencies": { |
| 88 | "@base-ui/react": "^1.4.1", | 88 | "@base-ui/react": "^1.4.1", |
| 89 | "@dnd-kit/core": "^6.3.1", | 89 | "@dnd-kit/core": "^6.3.1", |
| ... | @@ -165,7 +165,7 @@ | ... | @@ -165,7 +165,7 @@ |
| 165 | }, | 165 | }, |
| 166 | "packages/contracts": { | 166 | "packages/contracts": { |
| 167 | "name": "@t3tools/contracts", | 167 | "name": "@t3tools/contracts", |
| 168 | "version": "0.0.23", | 168 | "version": "0.0.24", |
| 169 | "dependencies": { | 169 | "dependencies": { |
| 170 | "effect": "catalog:", | 170 | "effect": "catalog:", |
| 171 | }, | 171 | }, |
| ... | @@ -272,7 +272,7 @@ | ... | @@ -272,7 +272,7 @@ |
| 272 | "node-pty", | 272 | "node-pty", |
| 273 | ], | 273 | ], |
| 274 | "patchedDependencies": { | 274 | "patchedDependencies": { |
| 275 | "effect@4.0.0-beta.59": "patches/effect@4.0.0-beta.59.patch", | 275 | "effect@4.0.0-beta.73": "patches/effect@4.0.0-beta.73.patch", |
| 276 | }, | 276 | }, |
| 277 | "overrides": { | 277 | "overrides": { |
| 278 | "@effect/atom-react": "catalog:", | 278 | "@effect/atom-react": "catalog:", |
| ... | @@ -285,18 +285,18 @@ | ... | @@ -285,18 +285,18 @@ |
| 285 | "vite": "^8.0.0", | 285 | "vite": "^8.0.0", |
| 286 | }, | 286 | }, |
| 287 | "catalog": { | 287 | "catalog": { |
| 288 | "@effect/atom-react": "4.0.0-beta.59", | 288 | "@effect/atom-react": "4.0.0-beta.73", |
| 289 | "@effect/language-service": "0.84.2", | 289 | "@effect/language-service": "0.84.2", |
| 290 | "@effect/openapi-generator": "4.0.0-beta.59", | 290 | "@effect/openapi-generator": "4.0.0-beta.73", |
| 291 | "@effect/platform-bun": "4.0.0-beta.59", | 291 | "@effect/platform-bun": "4.0.0-beta.73", |
| 292 | "@effect/platform-node": "4.0.0-beta.59", | 292 | "@effect/platform-node": "4.0.0-beta.73", |
| 293 | "@effect/platform-node-shared": "4.0.0-beta.59", | 293 | "@effect/platform-node-shared": "4.0.0-beta.73", |
| 294 | "@effect/sql-sqlite-bun": "4.0.0-beta.59", | 294 | "@effect/sql-sqlite-bun": "4.0.0-beta.73", |
| 295 | "@effect/vitest": "4.0.0-beta.59", | 295 | "@effect/vitest": "4.0.0-beta.73", |
| 296 | "@pierre/diffs": "1.1.20", | 296 | "@pierre/diffs": "1.1.20", |
| 297 | "@types/bun": "^1.3.11", | 297 | "@types/bun": "^1.3.11", |
| 298 | "@types/node": "^24.10.13", | 298 | "@types/node": "^24.10.13", |
| 299 | "effect": "4.0.0-beta.59", | 299 | "effect": "4.0.0-beta.73", |
| 300 | "tsdown": "^0.20.3", | 300 | "tsdown": "^0.20.3", |
| 301 | "typescript": "^5.7.3", | 301 | "typescript": "^5.7.3", |
| 302 | "vitest": "^4.0.0", | 302 | "vitest": "^4.0.0", |
| ... | @@ -384,21 +384,21 @@ | ... | @@ -384,21 +384,21 @@ |
| 384 | 384 | ||
| 385 | "@dnd-kit/utilities": ["@dnd-kit/utilities@3.2.2", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg=="], | 385 | "@dnd-kit/utilities": ["@dnd-kit/utilities@3.2.2", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg=="], |
| 386 | 386 | ||
| 387 | "@effect/atom-react": ["@effect/atom-react@4.0.0-beta.59", "", { "peerDependencies": { "effect": "^4.0.0-beta.59", "react": "^19.2.4", "scheduler": "*" } }, "sha512-VkznQz5c+Z/BLxX+hQNPzPOyUnLQjnbppFSNP7tbPru7HKR4ihzCDC6Xjbx87156MOrZ+JOa6shTMbmvGT5W0w=="], | 387 | "@effect/atom-react": ["@effect/atom-react@4.0.0-beta.73", "", { "peerDependencies": { "effect": "^4.0.0-beta.73", "react": "^19.2.4", "scheduler": "*" } }, "sha512-RjNqEMV3z3iEFRBtJ1RVuxBNFMYxhDo2iRwck3kBYjrBuXWrKIRyqnunWJdL2KX97Gqp0orxaujG7qnIA5aIEA=="], |
| 388 | 388 | ||
| 389 | "@effect/language-service": ["@effect/language-service@0.84.2", "", { "bin": { "effect-language-service": "cli.js" } }, "sha512-l04qNxpiA8rY5yXWckRPJ7Mk5MNerXuNymSFf+IdflfI5i8jgL1bpBNLuP6ijg7wgjdHc/KmTnCj2kT0SCntuA=="], | 389 | "@effect/language-service": ["@effect/language-service@0.84.2", "", { "bin": { "effect-language-service": "cli.js" } }, "sha512-l04qNxpiA8rY5yXWckRPJ7Mk5MNerXuNymSFf+IdflfI5i8jgL1bpBNLuP6ijg7wgjdHc/KmTnCj2kT0SCntuA=="], |
| 390 | 390 | ||
| 391 | "@effect/openapi-generator": ["@effect/openapi-generator@4.0.0-beta.59", "", { "peerDependencies": { "@effect/platform-node": "^4.0.0-beta.59", "effect": "^4.0.0-beta.59" }, "bin": { "openapigen": "dist/bin.js" } }, "sha512-y1KF6mEb3pA5AdQsNGeWxV42Xi1uBt9Wzo5xMsp06j/WkOM2vWFmvSXw6xvwINAUi1HVjdOLo3xnTXtSv9YdRg=="], | 391 | "@effect/openapi-generator": ["@effect/openapi-generator@4.0.0-beta.73", "", { "peerDependencies": { "@effect/platform-node": "^4.0.0-beta.73", "effect": "^4.0.0-beta.73" }, "bin": { "openapigen": "dist/bin.js" } }, "sha512-tN1mKreMo6kypkuPy8Jt3eGoI0DberzD2/5XxEh17lTdyLg7/SUZv3LssRxtzO5+uIYzLNIHfT2zESz7gjCnwA=="], |
| 392 | 392 | ||
| 393 | "@effect/platform-bun": ["@effect/platform-bun@4.0.0-beta.59", "", { "dependencies": { "@effect/platform-node-shared": "^4.0.0-beta.59" }, "peerDependencies": { "effect": "^4.0.0-beta.59" } }, "sha512-1jXCIx34X80ojiK9ACO9Q7RST9CXudRmlAbsP4kPqjUo6aqOuGSWXq9ueBO4LbaIZiioRxtTciom35U9ldfTkQ=="], | 393 | "@effect/platform-bun": ["@effect/platform-bun@4.0.0-beta.73", "", { "dependencies": { "@effect/platform-node-shared": "^4.0.0-beta.73" }, "peerDependencies": { "effect": "^4.0.0-beta.73" } }, "sha512-AQcE89eEVp0xA3SafaswiHPfo6V2YPg7T6PQbQqYK4/S2Sia5cadCm/6y7pfRmuOd4jkWI6VMbNtEDkM0NOBpw=="], |
| 394 | 394 | ||
| 395 | "@effect/platform-node": ["@effect/platform-node@4.0.0-beta.59", "", { "dependencies": { "@effect/platform-node-shared": "^4.0.0-beta.59", "mime": "^4.1.0", "undici": "^8.0.2" }, "peerDependencies": { "effect": "^4.0.0-beta.59", "ioredis": "^5.7.0" } }, "sha512-jHRW0l953FjYNhQHexr48jFiBu5iGEZH5nmKD6Ha+lPtm1MrKG2V4njfWA3Fv0nUmd3VN87eBJ557wU0twN1Hg=="], | 395 | "@effect/platform-node": ["@effect/platform-node@4.0.0-beta.73", "", { "dependencies": { "@effect/platform-node-shared": "^4.0.0-beta.73", "mime": "^4.1.0", "undici": "^8.2.0" }, "peerDependencies": { "effect": "^4.0.0-beta.73", "ioredis": "^5.7.0" } }, "sha512-YYkUnmcs6EiRk8K/xNCU3l3jl1YbrhCHCJp+y32DWlKhhZot24/hvw0zYw0aymAtuI9dzAdPryVZkksH4+7ovw=="], |
| 396 | 396 | ||
| 397 | "@effect/platform-node-shared": ["@effect/platform-node-shared@4.0.0-beta.59", "", { "dependencies": { "@types/ws": "^8.18.1", "ws": "^8.20.0" }, "peerDependencies": { "effect": "^4.0.0-beta.59" } }, "sha512-fGwFJuG0Te9U/ZeqeDZ2HcSKZBhX5wLjX2/Rxb5+yaOkvvFAN9MvIh05R0QQK5DCcERvnbhHSl1CjSIAN4aEwQ=="], | 397 | "@effect/platform-node-shared": ["@effect/platform-node-shared@4.0.0-beta.73", "", { "dependencies": { "@types/ws": "^8.18.1", "ws": "^8.20.0" }, "peerDependencies": { "effect": "^4.0.0-beta.73" } }, "sha512-/7L8eftDS40VWgLx2GVtBtb8x97Vx4OF5IlRJfh14L5xDFdb9MZXxOuHfHAi+71d90pkbFpOFVtQCSOBWKRAWg=="], |
| 398 | 398 | ||
| 399 | "@effect/sql-sqlite-bun": ["@effect/sql-sqlite-bun@4.0.0-beta.59", "", { "peerDependencies": { "effect": "^4.0.0-beta.59" } }, "sha512-qZWt2HeLgSY8ygEfwbldK9lJ5xg/WP4lgCp1VLlyE8BImSB5g1h+UbhitXr+pOIf+ry4Xmc4ycp7YwTBOFH5/A=="], | 399 | "@effect/sql-sqlite-bun": ["@effect/sql-sqlite-bun@4.0.0-beta.73", "", { "peerDependencies": { "effect": "^4.0.0-beta.73" } }, "sha512-dkTDDeBLjaKo7h1IE049Oj2BDKQOT+Kc57KbBVaSk36dRdsEmVspRMP7OGbZj3etJU3lL1GJi1BIfnb4N3uMRw=="], |
| 400 | 400 | ||
| 401 | "@effect/vitest": ["@effect/vitest@4.0.0-beta.59", "", { "peerDependencies": { "effect": "^4.0.0-beta.59", "vitest": "^3.0.0 || ^4.0.0" } }, "sha512-jhpJbpDs1ED58SmFzcfmqLXxle84fiexaMEhV8SBl8WC2GM3FT5DW0WJYFjbZIH5/735brp3iGdjY5/uAxS7Bg=="], | 401 | "@effect/vitest": ["@effect/vitest@4.0.0-beta.73", "", { "peerDependencies": { "effect": "^4.0.0-beta.73", "vitest": "^3.0.0 || ^4.0.0" } }, "sha512-HIZa0aekh0ak1YfzCv8GTN75Vbvxp8g+W+z9KczvtUT+LvitX3i9nhLWZLKRrUP1LaJFLPJS2UG9ZGXjxgbjBg=="], |
| 402 | 402 | ||
| 403 | "@electron/get": ["@electron/get@2.0.3", "", { "dependencies": { "debug": "^4.1.1", "env-paths": "^2.2.0", "fs-extra": "^8.1.0", "got": "^11.8.5", "progress": "^2.0.3", "semver": "^6.2.0", "sumchecker": "^3.0.1" }, "optionalDependencies": { "global-agent": "^3.0.0" } }, "sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ=="], | 403 | "@electron/get": ["@electron/get@2.0.3", "", { "dependencies": { "debug": "^4.1.1", "env-paths": "^2.2.0", "fs-extra": "^8.1.0", "got": "^11.8.5", "progress": "^2.0.3", "semver": "^6.2.0", "sumchecker": "^3.0.1" }, "optionalDependencies": { "global-agent": "^3.0.0" } }, "sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ=="], |
| 404 | 404 | ||
| ... | @@ -608,17 +608,17 @@ | ... | @@ -608,17 +608,17 @@ |
| 608 | 608 | ||
| 609 | "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.29.0", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ=="], | 609 | "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.29.0", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ=="], |
| 610 | 610 | ||
| 611 | "@msgpackr-extract/msgpackr-extract-darwin-arm64": ["@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw=="], | 611 | "@msgpackr-extract/msgpackr-extract-darwin-arm64": ["@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-LCkGo6JDfaBhgST7UpPWgNgLINpcpabaHfyz5OBx75nUYxBsaEPxjnyNjWpeb/xBup/682QnBfRBy2/LvPutZQ=="], |
| 612 | 612 | ||
| 613 | "@msgpackr-extract/msgpackr-extract-darwin-x64": ["@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw=="], | 613 | "@msgpackr-extract/msgpackr-extract-darwin-x64": ["@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-zExlW9zUJKZH/tOtVMttwjKa4Xm/3KcNjnE3dPN92uCktwavMxpgCA3MoJK/DOnTWsQgo224OaST27/mPNAf+w=="], |
| 614 | 614 | ||
| 615 | "@msgpackr-extract/msgpackr-extract-linux-arm": ["@msgpackr-extract/msgpackr-extract-linux-arm@3.0.3", "", { "os": "linux", "cpu": "arm" }, "sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw=="], | 615 | "@msgpackr-extract/msgpackr-extract-linux-arm": ["@msgpackr-extract/msgpackr-extract-linux-arm@3.0.4", "", { "os": "linux", "cpu": "arm" }, "sha512-Tg3yX65f5GbtXLkrYEHE5oibZG9epyYWas7FogTTEJeDEF9JlXJzKgXaNhT3UXlTOeA+AfZpYZYZ0uPj7Cfquw=="], |
| 616 | 616 | ||
| 617 | "@msgpackr-extract/msgpackr-extract-linux-arm64": ["@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg=="], | 617 | "@msgpackr-extract/msgpackr-extract-linux-arm64": ["@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-dgX0P/9wGPJeHFBG+ZmhgE6bmtMt7NP5CRBGyyktpopdk/mW4POnrpQsSLtKI1dwpc+pPLuXHDh6vvskyQE/sw=="], |
| 618 | 618 | ||
| 619 | "@msgpackr-extract/msgpackr-extract-linux-x64": ["@msgpackr-extract/msgpackr-extract-linux-x64@3.0.3", "", { "os": "linux", "cpu": "x64" }, "sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg=="], | 619 | "@msgpackr-extract/msgpackr-extract-linux-x64": ["@msgpackr-extract/msgpackr-extract-linux-x64@3.0.4", "", { "os": "linux", "cpu": "x64" }, "sha512-8TNXMEjJc3QEy7R/x1INhgiU+XakDAFUzBhaz7+Rbrs8NH5UQeHQxxmzsSBJGyV6I1jW79undiQm8tOI+D+8FQ=="], |
| 620 | 620 | ||
| 621 | "@msgpackr-extract/msgpackr-extract-win32-x64": ["@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3", "", { "os": "win32", "cpu": "x64" }, "sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ=="], | 621 | "@msgpackr-extract/msgpackr-extract-win32-x64": ["@msgpackr-extract/msgpackr-extract-win32-x64@3.0.4", "", { "os": "win32", "cpu": "x64" }, "sha512-CmCXPQrkbwExx3j946/PtHWHbYJiCRBRDl4BlkRQcJB/YOwQxJRTpoo7aTsortjgoJ1x7opzTSxn7C+ASSLVjQ=="], |
| 622 | 622 | ||
| 623 | "@mswjs/interceptors": ["@mswjs/interceptors@0.41.3", "", { "dependencies": { "@open-draft/deferred-promise": "^2.2.0", "@open-draft/logger": "^0.3.0", "@open-draft/until": "^2.0.0", "is-node-process": "^1.2.0", "outvariant": "^1.4.3", "strict-event-emitter": "^0.5.1" } }, "sha512-cXu86tF4VQVfwz8W1SPbhoRyHJkti6mjH/XJIxp40jhO4j2k1m4KYrEykxqWPkFF3vrK4rgQppBh//AwyGSXPA=="], | 623 | "@mswjs/interceptors": ["@mswjs/interceptors@0.41.3", "", { "dependencies": { "@open-draft/deferred-promise": "^2.2.0", "@open-draft/logger": "^0.3.0", "@open-draft/until": "^2.0.0", "is-node-process": "^1.2.0", "outvariant": "^1.4.3", "strict-event-emitter": "^0.5.1" } }, "sha512-cXu86tF4VQVfwz8W1SPbhoRyHJkti6mjH/XJIxp40jhO4j2k1m4KYrEykxqWPkFF3vrK4rgQppBh//AwyGSXPA=="], |
| 624 | 624 | ||
| ... | @@ -1160,7 +1160,7 @@ | ... | @@ -1160,7 +1160,7 @@ |
| 1160 | 1160 | ||
| 1161 | "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], | 1161 | "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], |
| 1162 | 1162 | ||
| 1163 | "effect": ["effect@4.0.0-beta.59", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.6.0", "find-my-way-ts": "^0.1.6", "ini": "^6.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^1.11.9", "multipasta": "^0.2.7", "toml": "^4.1.1", "uuid": "^13.0.0", "yaml": "^2.8.3" } }, "sha512-xyUDLeHSe8d6lWGOvR6Fgn2HL6gYeTZ/S4Jzk9uc4ZUxMPPsNZlNXrvk0C7/utQFzeX7uAWcVnG2BjbA0SRoAA=="], | 1163 | "effect": ["effect@4.0.0-beta.73", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.8.0", "find-my-way-ts": "^0.1.6", "ini": "^7.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^2.0.1", "multipasta": "^0.2.7", "toml": "^4.1.1", "uuid": "^14.0.0", "yaml": "^2.9.0" } }, "sha512-rL7jjpT31JhxNiZ0X4sgu+WSJi7uwKTX2jz2OW7PrPLfz5DPjT9iXxxVrekxwpfGdgo92bvxbCc5kBlXgusQJw=="], |
| 1164 | 1164 | ||
| 1165 | "effect-acp": ["effect-acp@workspace:packages/effect-acp"], | 1165 | "effect-acp": ["effect-acp@workspace:packages/effect-acp"], |
| 1166 | 1166 | ||
| ... | @@ -1230,7 +1230,7 @@ | ... | @@ -1230,7 +1230,7 @@ |
| 1230 | 1230 | ||
| 1231 | "extract-zip": ["extract-zip@2.0.1", "", { "dependencies": { "debug": "^4.1.1", "get-stream": "^5.1.0", "yauzl": "^2.10.0" }, "optionalDependencies": { "@types/yauzl": "^2.9.1" }, "bin": { "extract-zip": "cli.js" } }, "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg=="], | 1231 | "extract-zip": ["extract-zip@2.0.1", "", { "dependencies": { "debug": "^4.1.1", "get-stream": "^5.1.0", "yauzl": "^2.10.0" }, "optionalDependencies": { "@types/yauzl": "^2.9.1" }, "bin": { "extract-zip": "cli.js" } }, "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg=="], |
| 1232 | 1232 | ||
| 1233 | "fast-check": ["fast-check@4.6.0", "", { "dependencies": { "pure-rand": "^8.0.0" } }, "sha512-h7H6Dm0Fy+H4ciQYFxFjXnXkzR2kr9Fb22c0UBpHnm59K2zpr2t13aPTHlltFiNT6zuxp6HMPAVVvgur4BLdpA=="], | 1233 | "fast-check": ["fast-check@4.8.0", "", { "dependencies": { "pure-rand": "^8.0.0" } }, "sha512-GOJ158CUMnN6cSahsv4+ExARvIDuzzinFjkp0E9WtiBa5zcVeLozVkWaE4IzFcc+Y48Wp1EDlUZsXRyAztQcSg=="], |
| 1234 | 1234 | ||
| 1235 | "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], | 1235 | "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], |
| 1236 | 1236 | ||
| ... | @@ -1346,7 +1346,7 @@ | ... | @@ -1346,7 +1346,7 @@ |
| 1346 | 1346 | ||
| 1347 | "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], | 1347 | "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], |
| 1348 | 1348 | ||
| 1349 | "ini": ["ini@6.0.0", "", {}, "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ=="], | 1349 | "ini": ["ini@7.0.0", "", {}, "sha512-ifK0CgjALofS5bkrcTy4RaQ9Vx2Knf/eLeIO+NaswQEpH1UblrtTSCIvN71qQDMq0PeQ/SSPojvEJp9vvvfr+w=="], |
| 1350 | 1350 | ||
| 1351 | "inline-style-parser": ["inline-style-parser@0.2.7", "", {}, "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA=="], | 1351 | "inline-style-parser": ["inline-style-parser@0.2.7", "", {}, "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA=="], |
| 1352 | 1352 | ||
| ... | @@ -1590,9 +1590,9 @@ | ... | @@ -1590,9 +1590,9 @@ |
| 1590 | 1590 | ||
| 1591 | "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], | 1591 | "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], |
| 1592 | 1592 | ||
| 1593 | "msgpackr": ["msgpackr@1.11.9", "", { "optionalDependencies": { "msgpackr-extract": "^3.0.2" } }, "sha512-FkoAAyyA6HM8wL882EcEyFZ9s7hVADSwG9xrVx3dxxNQAtgADTrJoEWivID82Iv1zWDsv/OtbrrcZAzGzOMdNw=="], | 1593 | "msgpackr": ["msgpackr@2.0.2", "", { "optionalDependencies": { "msgpackr-extract": "^3.0.4" } }, "sha512-c5hYOXFbP79Slh6Dzd2wzk+jnV7mX1UxfMYtilnY1NmalXPqG8DGb5cYCMBrW4AsH3zekBBZd4QrKz9NhtvYLQ=="], |
| 1594 | 1594 | ||
| 1595 | "msgpackr-extract": ["msgpackr-extract@3.0.3", "", { "dependencies": { "node-gyp-build-optional-packages": "5.2.2" }, "optionalDependencies": { "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.3", "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.3", "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.3", "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.3", "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.3", "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3" }, "bin": { "download-msgpackr-prebuilds": "bin/download-prebuilds.js" } }, "sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA=="], | 1595 | "msgpackr-extract": ["msgpackr-extract@3.0.4", "", { "dependencies": { "node-gyp-build-optional-packages": "5.2.2" }, "optionalDependencies": { "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.4", "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.4", "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.4", "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.4", "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.4", "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.4" }, "bin": { "download-msgpackr-prebuilds": "bin/download-prebuilds.js" } }, "sha512-4kmO/MdyUIkLIvTPr8VHLil4AtoKIoniWPIEk5+CDy0xnWC84azhSFmuJ7PxZdsYtiP5kEeQsORAVIeMgxT+Hw=="], |
| 1596 | 1596 | ||
| 1597 | "msw": ["msw@2.12.11", "", { "dependencies": { "@inquirer/confirm": "^5.0.0", "@mswjs/interceptors": "^0.41.2", "@open-draft/deferred-promise": "^2.2.0", "@types/statuses": "^2.0.6", "cookie": "^1.0.2", "graphql": "^16.12.0", "headers-polyfill": "^4.0.2", "is-node-process": "^1.2.0", "outvariant": "^1.4.3", "path-to-regexp": "^6.3.0", "picocolors": "^1.1.1", "rettime": "^0.10.1", "statuses": "^2.0.2", "strict-event-emitter": "^0.5.1", "tough-cookie": "^6.0.0", "type-fest": "^5.2.0", "until-async": "^3.0.2", "yargs": "^17.7.2" }, "peerDependencies": { "typescript": ">= 4.8.x" }, "optionalPeers": ["typescript"], "bin": { "msw": "cli/index.js" } }, "sha512-dVg20zi2I2EvnwH/+WupzsOC2mCa7qsIhyMAWtfRikn6RKtwL9+7SaF1IQ5LyZry4tlUtf6KyTVhnlQiZXozTQ=="], | 1597 | "msw": ["msw@2.12.11", "", { "dependencies": { "@inquirer/confirm": "^5.0.0", "@mswjs/interceptors": "^0.41.2", "@open-draft/deferred-promise": "^2.2.0", "@types/statuses": "^2.0.6", "cookie": "^1.0.2", "graphql": "^16.12.0", "headers-polyfill": "^4.0.2", "is-node-process": "^1.2.0", "outvariant": "^1.4.3", "path-to-regexp": "^6.3.0", "picocolors": "^1.1.1", "rettime": "^0.10.1", "statuses": "^2.0.2", "strict-event-emitter": "^0.5.1", "tough-cookie": "^6.0.0", "type-fest": "^5.2.0", "until-async": "^3.0.2", "yargs": "^17.7.2" }, "peerDependencies": { "typescript": ">= 4.8.x" }, "optionalPeers": ["typescript"], "bin": { "msw": "cli/index.js" } }, "sha512-dVg20zi2I2EvnwH/+WupzsOC2mCa7qsIhyMAWtfRikn6RKtwL9+7SaF1IQ5LyZry4tlUtf6KyTVhnlQiZXozTQ=="], |
| 1598 | 1598 | ||
| ... | @@ -2022,7 +2022,7 @@ | ... | @@ -2022,7 +2022,7 @@ |
| 2022 | 2022 | ||
| 2023 | "use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="], | 2023 | "use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="], |
| 2024 | 2024 | ||
| 2025 | "uuid": ["uuid@13.0.0", "", { "bin": { "uuid": "dist-node/bin/uuid" } }, "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w=="], | 2025 | "uuid": ["uuid@14.0.0", "", { "bin": { "uuid": "dist-node/bin/uuid" } }, "sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg=="], |
| 2026 | 2026 | ||
| 2027 | "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], | 2027 | "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], |
| 2028 | 2028 | ||
| ... | @@ -2096,7 +2096,7 @@ | ... | @@ -2096,7 +2096,7 @@ |
| 2096 | 2096 | ||
| 2097 | "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], | 2097 | "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], |
| 2098 | 2098 | ||
| 2099 | "yaml": ["yaml@2.8.3", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg=="], | 2099 | "yaml": ["yaml@2.9.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="], |
| 2100 | 2100 | ||
| 2101 | "yaml-language-server": ["yaml-language-server@1.20.0", "", { "dependencies": { "@vscode/l10n": "^0.0.18", "ajv": "^8.17.1", "ajv-draft-04": "^1.0.0", "prettier": "^3.5.0", "request-light": "^0.5.7", "vscode-json-languageservice": "4.1.8", "vscode-languageserver": "^9.0.0", "vscode-languageserver-textdocument": "^1.0.1", "vscode-languageserver-types": "^3.16.0", "vscode-uri": "^3.0.2", "yaml": "2.7.1" }, "bin": { "yaml-language-server": "bin/yaml-language-server" } }, "sha512-qhjK/bzSRZ6HtTvgeFvjNPJGWdZ0+x5NREV/9XZWFjIGezew2b4r5JPy66IfOhd5OA7KeFwk1JfmEbnTvev0cA=="], | 2101 | "yaml-language-server": ["yaml-language-server@1.20.0", "", { "dependencies": { "@vscode/l10n": "^0.0.18", "ajv": "^8.17.1", "ajv-draft-04": "^1.0.0", "prettier": "^3.5.0", "request-light": "^0.5.7", "vscode-json-languageservice": "4.1.8", "vscode-languageserver": "^9.0.0", "vscode-languageserver-textdocument": "^1.0.1", "vscode-languageserver-types": "^3.16.0", "vscode-uri": "^3.0.2", "yaml": "2.7.1" }, "bin": { "yaml-language-server": "bin/yaml-language-server" } }, "sha512-qhjK/bzSRZ6HtTvgeFvjNPJGWdZ0+x5NREV/9XZWFjIGezew2b4r5JPy66IfOhd5OA7KeFwk1JfmEbnTvev0cA=="], |
| 2102 | 2102 |
package.json+9-9| ... | @@ -9,14 +9,14 @@ | ... | @@ -9,14 +9,14 @@ |
| 9 | "scripts" | 9 | "scripts" |
| 10 | ], | 10 | ], |
| 11 | "catalog": { | 11 | "catalog": { |
| 12 | "effect": "4.0.0-beta.59", | 12 | "effect": "4.0.0-beta.73", |
| 13 | "@effect/atom-react": "4.0.0-beta.59", | 13 | "@effect/atom-react": "4.0.0-beta.73", |
| 14 | "@effect/openapi-generator": "4.0.0-beta.59", | 14 | "@effect/openapi-generator": "4.0.0-beta.73", |
| 15 | "@effect/platform-bun": "4.0.0-beta.59", | 15 | "@effect/platform-bun": "4.0.0-beta.73", |
| 16 | "@effect/platform-node": "4.0.0-beta.59", | 16 | "@effect/platform-node": "4.0.0-beta.73", |
| 17 | "@effect/platform-node-shared": "4.0.0-beta.59", | 17 | "@effect/platform-node-shared": "4.0.0-beta.73", |
| 18 | "@effect/sql-sqlite-bun": "4.0.0-beta.59", | 18 | "@effect/sql-sqlite-bun": "4.0.0-beta.73", |
| 19 | "@effect/vitest": "4.0.0-beta.59", | 19 | "@effect/vitest": "4.0.0-beta.73", |
| 20 | "@effect/language-service": "0.84.2", | 20 | "@effect/language-service": "0.84.2", |
| 21 | "@pierre/diffs": "1.1.20", | 21 | "@pierre/diffs": "1.1.20", |
| 22 | "@types/bun": "^1.3.11", | 22 | "@types/bun": "^1.3.11", |
| ... | @@ -91,7 +91,7 @@ | ... | @@ -91,7 +91,7 @@ |
| 91 | ] | 91 | ] |
| 92 | }, | 92 | }, |
| 93 | "patchedDependencies": { | 93 | "patchedDependencies": { |
| 94 | "effect@4.0.0-beta.59": "patches/effect@4.0.0-beta.59.patch" | 94 | "effect@4.0.0-beta.73": "patches/effect@4.0.0-beta.73.patch" |
| 95 | }, | 95 | }, |
| 96 | "trustedDependencies": [ | 96 | "trustedDependencies": [ |
| 97 | "node-pty", | 97 | "node-pty", |
packages/shared/src/git.test.ts+8-1| ... | @@ -55,7 +55,14 @@ describe("parseGitHubRepositoryNameWithOwnerFromRemoteUrl", () => { | ... | @@ -55,7 +55,14 @@ describe("parseGitHubRepositoryNameWithOwnerFromRemoteUrl", () => { |
| 55 | 55 | ||
| 56 | describe("isTemporaryWorktreeBranch", () => { | 56 | describe("isTemporaryWorktreeBranch", () => { |
| 57 | it("matches the generated temporary worktree refName format", () => { | 57 | it("matches the generated temporary worktree refName format", () => { |
| 58 | expect(isTemporaryWorktreeBranch(buildTemporaryWorktreeBranchName())).toBe(true); | 58 | expect( |
| 59 | isTemporaryWorktreeBranch( | ||
| 60 | buildTemporaryWorktreeBranchName((byteLength) => { | ||
| 61 | expect(byteLength).toBe(4); | ||
| 62 | return "DEADBEEF"; | ||
| 63 | }), | ||
| 64 | ), | ||
| 65 | ).toBe(true); | ||
| 59 | }); | 66 | }); |
| 60 | 67 | ||
| 61 | it("matches generated temporary worktree refs", () => { | 68 | it("matches generated temporary worktree refs", () => { |
packages/shared/src/git.ts+4-4| ... | @@ -6,8 +6,6 @@ import type { | ... | @@ -6,8 +6,6 @@ import type { |
| 6 | VcsStatusResult, | 6 | VcsStatusResult, |
| 7 | VcsStatusStreamEvent, | 7 | VcsStatusStreamEvent, |
| 8 | } from "@t3tools/contracts"; | 8 | } from "@t3tools/contracts"; |
| 9 | import * as Effect from "effect/Effect"; | ||
| 10 | import * as Random from "effect/Random"; | ||
| 11 | import { detectSourceControlProviderFromRemoteUrl } from "./sourceControl.ts"; | 9 | import { detectSourceControlProviderFromRemoteUrl } from "./sourceControl.ts"; |
| 12 | 10 | ||
| 13 | export const WORKTREE_BRANCH_PREFIX = "t3code"; | 11 | export const WORKTREE_BRANCH_PREFIX = "t3code"; |
| ... | @@ -86,8 +84,10 @@ export function deriveLocalBranchNameFromRemoteRef(branchName: string): string { | ... | @@ -86,8 +84,10 @@ export function deriveLocalBranchNameFromRemoteRef(branchName: string): string { |
| 86 | return branchName.slice(firstSeparatorIndex + 1); | 84 | return branchName.slice(firstSeparatorIndex + 1); |
| 87 | } | 85 | } |
| 88 | 86 | ||
| 89 | export function buildTemporaryWorktreeBranchName(): string { | 87 | export function buildTemporaryWorktreeBranchName( |
| 90 | const token = Effect.runSync(Random.nextUUIDv4).replace(/-/g, "").slice(0, 8).toLowerCase(); | 88 | randomHex: (byteLength: number) => string, |
| 89 | ): string { | ||
| 90 | const token = randomHex(4).toLowerCase(); | ||
| 91 | return `${WORKTREE_BRANCH_PREFIX}/${token}`; | 91 | return `${WORKTREE_BRANCH_PREFIX}/${token}`; |
| 92 | } | 92 | } |
| 93 | 93 |
patches/effect@4.0.0-beta.59.patch deleted-277| ... | @@ -1,277 +0,0 @@ | ||
| 1 | diff --git a/dist/unstable/rpc/RpcClient.d.ts b/dist/unstable/rpc/RpcClient.d.ts | ||
| 2 | index 7c5970042df67a25b4145df017960b3e6febed9c..7af3374632d53ad59f24e80835aa6741853b1f2b 100644 | ||
| 3 | --- a/dist/unstable/rpc/RpcClient.d.ts | ||
| 4 | +++ b/dist/unstable/rpc/RpcClient.d.ts | ||
| 5 | @@ -2,6 +2,7 @@ import * as Cause from "../../Cause.ts"; | ||
| 6 | import * as Context from "../../Context.ts"; | ||
| 7 | import type * as Duration from "../../Duration.ts"; | ||
| 8 | import * as Effect from "../../Effect.ts"; | ||
| 9 | +import * as Exit from "../../Exit.ts"; | ||
| 10 | import * as Layer from "../../Layer.ts"; | ||
| 11 | import * as Queue from "../../Queue.ts"; | ||
| 12 | import * as Schedule from "../../Schedule.ts"; | ||
| 13 | @@ -192,9 +193,40 @@ export declare const layerProtocolWorker: (options: { | ||
| 14 | readonly targetUtilization?: number | undefined; | ||
| 15 | readonly timeToLive: Duration.Input; | ||
| 16 | }) => Layer.Layer<Protocol, WorkerError, Worker.WorkerPlatform | Worker.Spawner>; | ||
| 17 | +declare const RequestHooks_base: Context.ServiceClass<RequestHooks, "effect/rpc/RpcClient/RequestHooks", { | ||
| 18 | + readonly onRequestStart?: ((info: { | ||
| 19 | + readonly id: RequestId; | ||
| 20 | + readonly tag: string; | ||
| 21 | + readonly stream: boolean; | ||
| 22 | + }) => Effect.Effect<void>) | undefined; | ||
| 23 | + readonly onRequestChunk?: ((info: { | ||
| 24 | + readonly id: RequestId; | ||
| 25 | + readonly tag: string; | ||
| 26 | + readonly chunkCount: number; | ||
| 27 | + }) => Effect.Effect<void>) | undefined; | ||
| 28 | + readonly onRequestExit?: ((info: { | ||
| 29 | + readonly id: RequestId; | ||
| 30 | + readonly tag: string; | ||
| 31 | + readonly stream: boolean; | ||
| 32 | + readonly exit: Exit.Exit<unknown, unknown>; | ||
| 33 | + }) => Effect.Effect<void>) | undefined; | ||
| 34 | + readonly onRequestInterrupt?: ((info: { | ||
| 35 | + readonly id: RequestId; | ||
| 36 | + readonly tag?: string | undefined; | ||
| 37 | + }) => Effect.Effect<void>) | undefined; | ||
| 38 | +}>; | ||
| 39 | +/** | ||
| 40 | + * @since 4.0.0 | ||
| 41 | + * @category RequestHooks | ||
| 42 | + */ | ||
| 43 | +export declare class RequestHooks extends RequestHooks_base { | ||
| 44 | +} | ||
| 45 | declare const ConnectionHooks_base: Context.ServiceClass<ConnectionHooks, "effect/rpc/RpcClient/ConnectionHooks", { | ||
| 46 | readonly onConnect: Effect.Effect<void>; | ||
| 47 | readonly onDisconnect: Effect.Effect<void>; | ||
| 48 | + readonly onPing?: Effect.Effect<void> | undefined; | ||
| 49 | + readonly onPong?: Effect.Effect<void> | undefined; | ||
| 50 | + readonly onPingTimeout?: Effect.Effect<void> | undefined; | ||
| 51 | }>; | ||
| 52 | /** | ||
| 53 | * @since 4.0.0 | ||
| 54 | diff --git a/dist/unstable/rpc/RpcClient.js b/dist/unstable/rpc/RpcClient.js | ||
| 55 | index 951dfc2d667a9136cfa714dcd3195e6575917fde..599c0345873bedd72bc93afe80eab0c676319421 100644 | ||
| 56 | --- a/dist/unstable/rpc/RpcClient.js | ||
| 57 | +++ b/dist/unstable/rpc/RpcClient.js | ||
| 58 | @@ -42,6 +42,7 @@ export const makeNoSerialization = /*#__PURE__*/Effect.fnUntraced(function* (gro | ||
| 59 | const services = yield* Effect.context(); | ||
| 60 | const scope = Context.get(services, Scope.Scope); | ||
| 61 | const entries = new Map(); | ||
| 62 | + const hooks = yield* Effect.serviceOption(RequestHooks); | ||
| 63 | let isShutdown = false; | ||
| 64 | yield* Scope.addFinalizer(scope, Effect.withFiber(parent => { | ||
| 65 | isShutdown = true; | ||
| 66 | @@ -79,6 +80,11 @@ export const makeNoSerialization = /*#__PURE__*/Effect.fnUntraced(function* (gro | ||
| 67 | return Effect.interrupt; | ||
| 68 | } | ||
| 69 | const id = generateRequestId(); | ||
| 70 | + const onStart = Option.isSome(hooks) && hooks.value.onRequestStart ? hooks.value.onRequestStart({ | ||
| 71 | + id, | ||
| 72 | + tag: rpc._tag, | ||
| 73 | + stream: false | ||
| 74 | + }) : Effect.void; | ||
| 75 | const send = middleware(message => options.onFromClient({ | ||
| 76 | message, | ||
| 77 | context, | ||
| 78 | @@ -96,7 +102,7 @@ export const makeNoSerialization = /*#__PURE__*/Effect.fnUntraced(function* (gro | ||
| 79 | headers: Headers.merge(parentFiber.getRef(CurrentHeaders), headers) | ||
| 80 | }); | ||
| 81 | if (discard) { | ||
| 82 | - return send; | ||
| 83 | + return onStart.pipe(Effect.andThen(send)); | ||
| 84 | } | ||
| 85 | let fiber; | ||
| 86 | return Effect.onInterrupt(Effect.callback(resume => { | ||
| 87 | @@ -114,7 +120,7 @@ export const makeNoSerialization = /*#__PURE__*/Effect.fnUntraced(function* (gro | ||
| 88 | } | ||
| 89 | }; | ||
| 90 | entries.set(id, entry); | ||
| 91 | - fiber = send.pipe(span ? Effect.withParentSpan(span, { | ||
| 92 | + fiber = onStart.pipe(Effect.andThen(send), span ? Effect.withParentSpan(span, { | ||
| 93 | captureStackTrace: false | ||
| 94 | }) : identity, Effect.runForkWith(parentFiber.context)); | ||
| 95 | fiber.addObserver(exit => { | ||
| 96 | @@ -123,8 +129,9 @@ export const makeNoSerialization = /*#__PURE__*/Effect.fnUntraced(function* (gro | ||
| 97 | } | ||
| 98 | }); | ||
| 99 | }), interruptors => { | ||
| 100 | + const entry = entries.get(id); | ||
| 101 | entries.delete(id); | ||
| 102 | - return Effect.andThen(Fiber.interrupt(fiber), sendInterrupt(id, Array.from(interruptors), context)); | ||
| 103 | + return Effect.andThen(Fiber.interrupt(fiber), sendInterrupt(id, Array.from(interruptors), context, entry?.rpc._tag)); | ||
| 104 | }); | ||
| 105 | }); | ||
| 106 | const onStreamRequest = Effect.fnUntraced(function* (rpc, middleware, payload, headers, streamBufferSize, context) { | ||
| 107 | @@ -136,11 +143,17 @@ export const makeNoSerialization = /*#__PURE__*/Effect.fnUntraced(function* (gro | ||
| 108 | }); | ||
| 109 | const fiber = Fiber.getCurrent(); | ||
| 110 | const id = generateRequestId(); | ||
| 111 | + const onStart = Option.isSome(hooks) && hooks.value.onRequestStart ? hooks.value.onRequestStart({ | ||
| 112 | + id, | ||
| 113 | + tag: rpc._tag, | ||
| 114 | + stream: true | ||
| 115 | + }) : Effect.void; | ||
| 116 | const scope = Context.getUnsafe(fiber.context, Scope.Scope); | ||
| 117 | yield* Scope.addFinalizerExit(scope, exit => { | ||
| 118 | - if (!entries.has(id)) return Effect.void; | ||
| 119 | + const entry = entries.get(id); | ||
| 120 | + if (!entry) return Effect.void; | ||
| 121 | entries.delete(id); | ||
| 122 | - return sendInterrupt(id, Exit.isFailure(exit) ? Array.from(Cause.interruptors(exit.cause)) : [], context); | ||
| 123 | + return sendInterrupt(id, Exit.isFailure(exit) ? Array.from(Cause.interruptors(exit.cause)) : [], context, entry.rpc._tag); | ||
| 124 | }); | ||
| 125 | const queue = yield* Queue.bounded(streamBufferSize); | ||
| 126 | entries.set(id, { | ||
| 127 | @@ -148,9 +161,10 @@ export const makeNoSerialization = /*#__PURE__*/Effect.fnUntraced(function* (gro | ||
| 128 | rpc, | ||
| 129 | queue, | ||
| 130 | scope, | ||
| 131 | - context | ||
| 132 | + context, | ||
| 133 | + chunkCount: 0 | ||
| 134 | }); | ||
| 135 | - yield* middleware(message => options.onFromClient({ | ||
| 136 | + yield* onStart.pipe(Effect.andThen(middleware(message => options.onFromClient({ | ||
| 137 | message, | ||
| 138 | context, | ||
| 139 | discard: false | ||
| 140 | @@ -165,7 +179,7 @@ export const makeNoSerialization = /*#__PURE__*/Effect.fnUntraced(function* (gro | ||
| 141 | sampled: span.sampled | ||
| 142 | } : {}), | ||
| 143 | headers: Headers.merge(fiber.getRef(CurrentHeaders), headers) | ||
| 144 | - }).pipe(span ? Effect.withParentSpan(span, { | ||
| 145 | + }))).pipe(span ? Effect.withParentSpan(span, { | ||
| 146 | captureStackTrace: false | ||
| 147 | }) : identity, Effect.catchCause(error => Queue.failCause(queue, error)), Effect.interruptible, Effect.forkIn(scope, { | ||
| 148 | startImmediately: true | ||
| 149 | @@ -195,7 +209,10 @@ export const makeNoSerialization = /*#__PURE__*/Effect.fnUntraced(function* (gro | ||
| 150 | }); | ||
| 151 | }; | ||
| 152 | }; | ||
| 153 | - const sendInterrupt = (requestId, interruptors, context) => Effect.callback(resume => { | ||
| 154 | + const sendInterrupt = (requestId, interruptors, context, tag) => (Option.isSome(hooks) && hooks.value.onRequestInterrupt ? hooks.value.onRequestInterrupt({ | ||
| 155 | + id: requestId, | ||
| 156 | + tag | ||
| 157 | + }) : Effect.void).pipe(Effect.andThen(Effect.callback(resume => { | ||
| 158 | const parentFiber = Fiber.getCurrent(); | ||
| 159 | const fiber = options.onFromClient({ | ||
| 160 | message: { | ||
| 161 | @@ -209,7 +226,7 @@ export const makeNoSerialization = /*#__PURE__*/Effect.fnUntraced(function* (gro | ||
| 162 | fiber.addObserver(() => { | ||
| 163 | resume(Effect.void); | ||
| 164 | }); | ||
| 165 | - }); | ||
| 166 | + }))); | ||
| 167 | const write = message => { | ||
| 168 | switch (message._tag) { | ||
| 169 | case "Chunk": | ||
| 170 | @@ -217,7 +234,13 @@ export const makeNoSerialization = /*#__PURE__*/Effect.fnUntraced(function* (gro | ||
| 171 | const requestId = message.requestId; | ||
| 172 | const entry = entries.get(requestId); | ||
| 173 | if (!entry || entry._tag !== "Queue") return Effect.void; | ||
| 174 | - return Queue.offerAll(entry.queue, message.values).pipe(supportsAck ? Effect.flatMap(() => options.onFromClient({ | ||
| 175 | + const chunkCount = entry.chunkCount += message.values.length; | ||
| 176 | + const onChunk = Option.isSome(hooks) && hooks.value.onRequestChunk ? hooks.value.onRequestChunk({ | ||
| 177 | + id: requestId, | ||
| 178 | + tag: entry.rpc._tag, | ||
| 179 | + chunkCount | ||
| 180 | + }) : Effect.void; | ||
| 181 | + return Queue.offerAll(entry.queue, message.values).pipe(Effect.andThen(onChunk), supportsAck ? Effect.flatMap(() => options.onFromClient({ | ||
| 182 | message: { | ||
| 183 | _tag: "Ack", | ||
| 184 | requestId: message.requestId | ||
| 185 | @@ -232,11 +255,18 @@ export const makeNoSerialization = /*#__PURE__*/Effect.fnUntraced(function* (gro | ||
| 186 | const entry = entries.get(requestId); | ||
| 187 | if (!entry) return Effect.void; | ||
| 188 | entries.delete(requestId); | ||
| 189 | + const onExit = Option.isSome(hooks) && hooks.value.onRequestExit ? hooks.value.onRequestExit({ | ||
| 190 | + id: requestId, | ||
| 191 | + tag: entry.rpc._tag, | ||
| 192 | + stream: entry._tag === "Queue", | ||
| 193 | + exit: message.exit | ||
| 194 | + }) : Effect.void; | ||
| 195 | if (entry._tag === "Effect") { | ||
| 196 | - entry.resume(message.exit); | ||
| 197 | - return Effect.void; | ||
| 198 | + return onExit.pipe(Effect.andThen(Effect.sync(() => { | ||
| 199 | + entry.resume(message.exit); | ||
| 200 | + }))); | ||
| 201 | } | ||
| 202 | - return message.exit._tag === "Success" ? Queue.end(entry.queue) : Queue.failCause(entry.queue, message.exit.cause); | ||
| 203 | + return onExit.pipe(Effect.andThen(message.exit._tag === "Success" ? Queue.end(entry.queue) : Queue.failCause(entry.queue, message.exit.cause))); | ||
| 204 | } | ||
| 205 | case "Defect": | ||
| 206 | { | ||
| 207 | @@ -530,7 +560,7 @@ export const makeProtocolSocket = options => Protocol.make(Effect.fnUntraced(fun | ||
| 208 | const requestClientMap = new Map(); | ||
| 209 | const write = yield* socket.writer; | ||
| 210 | let parser = serialization.makeUnsafe(); | ||
| 211 | - const pinger = yield* makePinger(write(parser.encode(constPing))); | ||
| 212 | + const pinger = yield* makePinger(write(parser.encode(constPing)), Option.isSome(hooks) ? hooks.value : undefined); | ||
| 213 | let currentError; | ||
| 214 | const onOpen = Effect.suspend(() => { | ||
| 215 | currentError = undefined; | ||
| 216 | @@ -550,8 +580,7 @@ export const makeProtocolSocket = options => Protocol.make(Effect.fnUntraced(fun | ||
| 217 | body: () => { | ||
| 218 | const response = responses[i++]; | ||
| 219 | if (response._tag === "Pong") { | ||
| 220 | - pinger.onPong(); | ||
| 221 | - return Effect.void; | ||
| 222 | + return pinger.onPong; | ||
| 223 | } | ||
| 224 | if ("requestId" in response) { | ||
| 225 | const clientId = requestClientMap.get(response.requestId); | ||
| 226 | @@ -579,12 +608,12 @@ export const makeProtocolSocket = options => Protocol.make(Effect.fnUntraced(fun | ||
| 227 | } | ||
| 228 | }, { | ||
| 229 | onOpen | ||
| 230 | - }).pipe(Effect.raceFirst(Effect.flatMap(pinger.timeout, () => Effect.fail(new Socket.SocketError({ | ||
| 231 | + }).pipe(Effect.raceFirst(Effect.flatMap(pinger.timeout, () => (Option.isSome(hooks) && hooks.value.onPingTimeout ? hooks.value.onPingTimeout : Effect.void).pipe(Effect.andThen(Effect.fail(new Socket.SocketError({ | ||
| 232 | reason: new Socket.SocketOpenError({ | ||
| 233 | kind: "Timeout", | ||
| 234 | cause: new Error("ping timeout") | ||
| 235 | }) | ||
| 236 | - }))))); | ||
| 237 | + }))))))); | ||
| 238 | }).pipe(Effect.flatMap(() => Effect.fail(new Socket.SocketError({ | ||
| 239 | reason: new Socket.SocketCloseError({ | ||
| 240 | code: 1000 | ||
| 241 | @@ -626,20 +655,20 @@ export const makeProtocolSocket = options => Protocol.make(Effect.fnUntraced(fun | ||
| 242 | }; | ||
| 243 | })); | ||
| 244 | const defaultRetryPolicy = /*#__PURE__*/Schedule.exponential(500, 1.5).pipe(/*#__PURE__*/Schedule.either(/*#__PURE__*/Schedule.spaced(5000))); | ||
| 245 | -const makePinger = /*#__PURE__*/Effect.fnUntraced(function* (writePing) { | ||
| 246 | +const makePinger = /*#__PURE__*/Effect.fnUntraced(function* (writePing, hooks) { | ||
| 247 | let recievedPong = true; | ||
| 248 | const latch = Latch.makeUnsafe(); | ||
| 249 | const reset = () => { | ||
| 250 | recievedPong = true; | ||
| 251 | latch.closeUnsafe(); | ||
| 252 | }; | ||
| 253 | - const onPong = () => { | ||
| 254 | + const onPong = Effect.sync(() => { | ||
| 255 | recievedPong = true; | ||
| 256 | - }; | ||
| 257 | + }).pipe(Effect.andThen(hooks?.onPong ?? Effect.void)); | ||
| 258 | yield* Effect.suspend(() => { | ||
| 259 | if (!recievedPong) return latch.open; | ||
| 260 | recievedPong = false; | ||
| 261 | - return writePing; | ||
| 262 | + return (hooks?.onPing ?? Effect.void).pipe(Effect.andThen(writePing)); | ||
| 263 | }).pipe(Effect.delay("5 seconds"), Effect.ignore, Effect.forever, Effect.interruptible, Effect.forkScoped); | ||
| 264 | return { | ||
| 265 | timeout: latch.await, | ||
| 266 | @@ -773,6 +802,11 @@ export const makeProtocolWorker = options => Protocol.make(Effect.fnUntraced(fun | ||
| 267 | * @category protocol | ||
| 268 | */ | ||
| 269 | export const layerProtocolWorker = /*#__PURE__*/flow(makeProtocolWorker, /*#__PURE__*/Layer.effect(Protocol)); | ||
| 270 | +/** | ||
| 271 | + * @since 4.0.0 | ||
| 272 | + * @category RequestHooks | ||
| 273 | + */ | ||
| 274 | +export class RequestHooks extends /*#__PURE__*/Context.Service()("effect/rpc/RpcClient/RequestHooks") {} | ||
| 275 | /** | ||
| 276 | * @since 4.0.0 | ||
| 277 | * @category ConnectionHooks | ||
patches/effect@4.0.0-beta.73.patch created+291| ... | @@ -0,0 +1,291 @@ | ||
| 1 | diff --git a/dist/unstable/rpc/RpcClient.d.ts b/dist/unstable/rpc/RpcClient.d.ts | ||
| 2 | index b0f61df..ead663e 100644 | ||
| 3 | --- a/dist/unstable/rpc/RpcClient.d.ts | ||
| 4 | +++ b/dist/unstable/rpc/RpcClient.d.ts | ||
| 5 | @@ -2,6 +2,7 @@ import * as Cause from "../../Cause.ts"; | ||
| 6 | import * as Context from "../../Context.ts"; | ||
| 7 | import type * as Duration from "../../Duration.ts"; | ||
| 8 | import * as Effect from "../../Effect.ts"; | ||
| 9 | +import * as Exit from "../../Exit.ts"; | ||
| 10 | import * as Layer from "../../Layer.ts"; | ||
| 11 | import * as Queue from "../../Queue.ts"; | ||
| 12 | import * as Schedule from "../../Schedule.ts"; | ||
| 13 | @@ -260,9 +261,40 @@ export declare const layerProtocolWorker: (options: { | ||
| 14 | readonly targetUtilization?: number | undefined; | ||
| 15 | readonly timeToLive: Duration.Input; | ||
| 16 | }) => Layer.Layer<Protocol, WorkerError, Worker.WorkerPlatform | Worker.Spawner>; | ||
| 17 | +declare const RequestHooks_base: Context.ServiceClass<RequestHooks, "effect/rpc/RpcClient/RequestHooks", { | ||
| 18 | + readonly onRequestStart?: ((info: { | ||
| 19 | + readonly id: RequestId; | ||
| 20 | + readonly tag: string; | ||
| 21 | + readonly stream: boolean; | ||
| 22 | + }) => Effect.Effect<void>) | undefined; | ||
| 23 | + readonly onRequestChunk?: ((info: { | ||
| 24 | + readonly id: RequestId; | ||
| 25 | + readonly tag: string; | ||
| 26 | + readonly chunkCount: number; | ||
| 27 | + }) => Effect.Effect<void>) | undefined; | ||
| 28 | + readonly onRequestExit?: ((info: { | ||
| 29 | + readonly id: RequestId; | ||
| 30 | + readonly tag: string; | ||
| 31 | + readonly stream: boolean; | ||
| 32 | + readonly exit: Exit.Exit<unknown, unknown>; | ||
| 33 | + }) => Effect.Effect<void>) | undefined; | ||
| 34 | + readonly onRequestInterrupt?: ((info: { | ||
| 35 | + readonly id: RequestId; | ||
| 36 | + readonly tag?: string | undefined; | ||
| 37 | + }) => Effect.Effect<void>) | undefined; | ||
| 38 | +}>; | ||
| 39 | +/** | ||
| 40 | + * @since 4.0.0 | ||
| 41 | + * @category RequestHooks | ||
| 42 | + */ | ||
| 43 | +export declare class RequestHooks extends RequestHooks_base { | ||
| 44 | +} | ||
| 45 | declare const ConnectionHooks_base: Context.ServiceClass<ConnectionHooks, "effect/rpc/RpcClient/ConnectionHooks", { | ||
| 46 | readonly onConnect: Effect.Effect<void>; | ||
| 47 | readonly onDisconnect: Effect.Effect<void>; | ||
| 48 | + readonly onPing?: Effect.Effect<void> | undefined; | ||
| 49 | + readonly onPong?: Effect.Effect<void> | undefined; | ||
| 50 | + readonly onPingTimeout?: Effect.Effect<void> | undefined; | ||
| 51 | }>; | ||
| 52 | /** | ||
| 53 | * Represents optional client protocol hooks that run when a transport connects | ||
| 54 | @@ -279,4 +311,4 @@ declare const ConnectionHooks_base: Context.ServiceClass<ConnectionHooks, "effec | ||
| 55 | export declare class ConnectionHooks extends ConnectionHooks_base { | ||
| 56 | } | ||
| 57 | export {}; | ||
| 58 | -//# sourceMappingURL=RpcClient.d.ts.map | ||
| 59 | \ No newline at end of file | ||
| 60 | +//# sourceMappingURL=RpcClient.d.ts.map | ||
| 61 | diff --git a/dist/unstable/rpc/RpcClient.js b/dist/unstable/rpc/RpcClient.js | ||
| 62 | index 811a8a8..f9cf2eb 100644 | ||
| 63 | --- a/dist/unstable/rpc/RpcClient.js | ||
| 64 | +++ b/dist/unstable/rpc/RpcClient.js | ||
| 65 | @@ -46,6 +46,7 @@ export const makeNoSerialization = /*#__PURE__*/Effect.fnUntraced(function* (gro | ||
| 66 | const services = yield* Effect.context(); | ||
| 67 | const scope = Context.get(services, Scope.Scope); | ||
| 68 | const entries = new Map(); | ||
| 69 | + const hooks = yield* Effect.serviceOption(RequestHooks); | ||
| 70 | let isShutdown = false; | ||
| 71 | yield* Scope.addFinalizer(scope, Effect.withFiber(parent => { | ||
| 72 | isShutdown = true; | ||
| 73 | @@ -83,6 +84,11 @@ export const makeNoSerialization = /*#__PURE__*/Effect.fnUntraced(function* (gro | ||
| 74 | return Effect.interrupt; | ||
| 75 | } | ||
| 76 | const id = generateRequestId(); | ||
| 77 | + const onStart = Option.isSome(hooks) && hooks.value.onRequestStart ? hooks.value.onRequestStart({ | ||
| 78 | + id, | ||
| 79 | + tag: rpc._tag, | ||
| 80 | + stream: false | ||
| 81 | + }) : Effect.void; | ||
| 82 | const send = middleware(message => options.onFromClient({ | ||
| 83 | message, | ||
| 84 | context, | ||
| 85 | @@ -100,7 +106,7 @@ export const makeNoSerialization = /*#__PURE__*/Effect.fnUntraced(function* (gro | ||
| 86 | headers: Headers.merge(parentFiber.getRef(CurrentHeaders), headers) | ||
| 87 | }); | ||
| 88 | if (discard) { | ||
| 89 | - return send; | ||
| 90 | + return onStart.pipe(Effect.andThen(send)); | ||
| 91 | } | ||
| 92 | let fiber; | ||
| 93 | return Effect.onInterrupt(Effect.callback(resume => { | ||
| 94 | @@ -118,7 +124,7 @@ export const makeNoSerialization = /*#__PURE__*/Effect.fnUntraced(function* (gro | ||
| 95 | } | ||
| 96 | }; | ||
| 97 | entries.set(id, entry); | ||
| 98 | - fiber = send.pipe(span ? Effect.withParentSpan(span, { | ||
| 99 | + fiber = onStart.pipe(Effect.andThen(send), span ? Effect.withParentSpan(span, { | ||
| 100 | captureStackTrace: false | ||
| 101 | }) : identity, Effect.runForkWith(parentFiber.context)); | ||
| 102 | fiber.addObserver(exit => { | ||
| 103 | @@ -127,8 +133,9 @@ export const makeNoSerialization = /*#__PURE__*/Effect.fnUntraced(function* (gro | ||
| 104 | } | ||
| 105 | }); | ||
| 106 | }), interruptors => { | ||
| 107 | + const entry = entries.get(id); | ||
| 108 | entries.delete(id); | ||
| 109 | - return Effect.andThen(Fiber.interrupt(fiber), sendInterrupt(id, Array.from(interruptors), context)); | ||
| 110 | + return Effect.andThen(Fiber.interrupt(fiber), sendInterrupt(id, Array.from(interruptors), context, entry?.rpc._tag)); | ||
| 111 | }); | ||
| 112 | }); | ||
| 113 | const onStreamRequest = Effect.fnUntraced(function* (rpc, middleware, payload, headers, streamBufferSize, context) { | ||
| 114 | @@ -140,11 +147,17 @@ export const makeNoSerialization = /*#__PURE__*/Effect.fnUntraced(function* (gro | ||
| 115 | }); | ||
| 116 | const fiber = Fiber.getCurrent(); | ||
| 117 | const id = generateRequestId(); | ||
| 118 | + const onStart = Option.isSome(hooks) && hooks.value.onRequestStart ? hooks.value.onRequestStart({ | ||
| 119 | + id, | ||
| 120 | + tag: rpc._tag, | ||
| 121 | + stream: true | ||
| 122 | + }) : Effect.void; | ||
| 123 | const scope = Context.getUnsafe(fiber.context, Scope.Scope); | ||
| 124 | yield* Scope.addFinalizerExit(scope, exit => { | ||
| 125 | - if (!entries.has(id)) return Effect.void; | ||
| 126 | + const entry = entries.get(id); | ||
| 127 | + if (!entry) return Effect.void; | ||
| 128 | entries.delete(id); | ||
| 129 | - return sendInterrupt(id, Exit.isFailure(exit) ? Array.from(Cause.interruptors(exit.cause)) : [], context); | ||
| 130 | + return sendInterrupt(id, Exit.isFailure(exit) ? Array.from(Cause.interruptors(exit.cause)) : [], context, entry.rpc._tag); | ||
| 131 | }); | ||
| 132 | const queue = yield* Queue.bounded(streamBufferSize); | ||
| 133 | entries.set(id, { | ||
| 134 | @@ -152,9 +165,10 @@ export const makeNoSerialization = /*#__PURE__*/Effect.fnUntraced(function* (gro | ||
| 135 | rpc, | ||
| 136 | queue, | ||
| 137 | scope, | ||
| 138 | - context | ||
| 139 | + context, | ||
| 140 | + chunkCount: 0 | ||
| 141 | }); | ||
| 142 | - yield* middleware(message => options.onFromClient({ | ||
| 143 | + yield* onStart.pipe(Effect.andThen(middleware(message => options.onFromClient({ | ||
| 144 | message, | ||
| 145 | context, | ||
| 146 | discard: false | ||
| 147 | @@ -169,7 +183,7 @@ export const makeNoSerialization = /*#__PURE__*/Effect.fnUntraced(function* (gro | ||
| 148 | sampled: span.sampled | ||
| 149 | } : {}), | ||
| 150 | headers: Headers.merge(fiber.getRef(CurrentHeaders), headers) | ||
| 151 | - }).pipe(span ? Effect.withParentSpan(span, { | ||
| 152 | + }))).pipe(span ? Effect.withParentSpan(span, { | ||
| 153 | captureStackTrace: false | ||
| 154 | }) : identity, Effect.catchCause(error => Queue.failCause(queue, error)), Effect.interruptible, Effect.forkIn(scope, { | ||
| 155 | startImmediately: true | ||
| 156 | @@ -199,7 +213,10 @@ export const makeNoSerialization = /*#__PURE__*/Effect.fnUntraced(function* (gro | ||
| 157 | }); | ||
| 158 | }; | ||
| 159 | }; | ||
| 160 | - const sendInterrupt = (requestId, interruptors, context) => Effect.callback(resume => { | ||
| 161 | + const sendInterrupt = (requestId, interruptors, context, tag) => (Option.isSome(hooks) && hooks.value.onRequestInterrupt ? hooks.value.onRequestInterrupt({ | ||
| 162 | + id: requestId, | ||
| 163 | + tag | ||
| 164 | + }) : Effect.void).pipe(Effect.andThen(Effect.callback(resume => { | ||
| 165 | const parentFiber = Fiber.getCurrent(); | ||
| 166 | const fiber = options.onFromClient({ | ||
| 167 | message: { | ||
| 168 | @@ -213,7 +230,7 @@ export const makeNoSerialization = /*#__PURE__*/Effect.fnUntraced(function* (gro | ||
| 169 | fiber.addObserver(() => { | ||
| 170 | resume(Effect.void); | ||
| 171 | }); | ||
| 172 | - }); | ||
| 173 | + }))); | ||
| 174 | const write = message => { | ||
| 175 | switch (message._tag) { | ||
| 176 | case "Chunk": | ||
| 177 | @@ -221,7 +238,13 @@ export const makeNoSerialization = /*#__PURE__*/Effect.fnUntraced(function* (gro | ||
| 178 | const requestId = message.requestId; | ||
| 179 | const entry = entries.get(requestId); | ||
| 180 | if (!entry || entry._tag !== "Queue") return Effect.void; | ||
| 181 | - return Queue.offerAll(entry.queue, message.values).pipe(supportsAck ? Effect.flatMap(() => options.onFromClient({ | ||
| 182 | + const chunkCount = entry.chunkCount += message.values.length; | ||
| 183 | + const onChunk = Option.isSome(hooks) && hooks.value.onRequestChunk ? hooks.value.onRequestChunk({ | ||
| 184 | + id: requestId, | ||
| 185 | + tag: entry.rpc._tag, | ||
| 186 | + chunkCount | ||
| 187 | + }) : Effect.void; | ||
| 188 | + return Queue.offerAll(entry.queue, message.values).pipe(Effect.andThen(onChunk), supportsAck ? Effect.flatMap(() => options.onFromClient({ | ||
| 189 | message: { | ||
| 190 | _tag: "Ack", | ||
| 191 | requestId: message.requestId | ||
| 192 | @@ -236,11 +259,18 @@ export const makeNoSerialization = /*#__PURE__*/Effect.fnUntraced(function* (gro | ||
| 193 | const entry = entries.get(requestId); | ||
| 194 | if (!entry) return Effect.void; | ||
| 195 | entries.delete(requestId); | ||
| 196 | + const onExit = Option.isSome(hooks) && hooks.value.onRequestExit ? hooks.value.onRequestExit({ | ||
| 197 | + id: requestId, | ||
| 198 | + tag: entry.rpc._tag, | ||
| 199 | + stream: entry._tag === "Queue", | ||
| 200 | + exit: message.exit | ||
| 201 | + }) : Effect.void; | ||
| 202 | if (entry._tag === "Effect") { | ||
| 203 | - entry.resume(message.exit); | ||
| 204 | - return Effect.void; | ||
| 205 | + return onExit.pipe(Effect.andThen(Effect.sync(() => { | ||
| 206 | + entry.resume(message.exit); | ||
| 207 | + }))); | ||
| 208 | } | ||
| 209 | - return message.exit._tag === "Success" ? Queue.end(entry.queue) : Queue.failCause(entry.queue, message.exit.cause); | ||
| 210 | + return onExit.pipe(Effect.andThen(message.exit._tag === "Success" ? Queue.end(entry.queue) : Queue.failCause(entry.queue, message.exit.cause))); | ||
| 211 | } | ||
| 212 | case "Defect": | ||
| 213 | { | ||
| 214 | @@ -568,7 +598,7 @@ export const makeProtocolSocket = options => Protocol.make(Effect.fnUntraced(fun | ||
| 215 | const requestClientMap = new Map(); | ||
| 216 | const write = yield* socket.writer; | ||
| 217 | let parser = serialization.makeUnsafe(); | ||
| 218 | - const pinger = yield* makePinger(write(parser.encode(constPing))); | ||
| 219 | + const pinger = yield* makePinger(write(parser.encode(constPing)), Option.isSome(hooks) ? hooks.value : undefined); | ||
| 220 | let currentError; | ||
| 221 | const onOpen = Effect.suspend(() => { | ||
| 222 | currentError = undefined; | ||
| 223 | @@ -588,8 +618,7 @@ export const makeProtocolSocket = options => Protocol.make(Effect.fnUntraced(fun | ||
| 224 | body: () => { | ||
| 225 | const response = responses[i++]; | ||
| 226 | if (response._tag === "Pong") { | ||
| 227 | - pinger.onPong(); | ||
| 228 | - return Effect.void; | ||
| 229 | + return pinger.onPong; | ||
| 230 | } | ||
| 231 | if ("requestId" in response) { | ||
| 232 | const clientId = requestClientMap.get(response.requestId); | ||
| 233 | @@ -617,12 +646,12 @@ export const makeProtocolSocket = options => Protocol.make(Effect.fnUntraced(fun | ||
| 234 | } | ||
| 235 | }, { | ||
| 236 | onOpen | ||
| 237 | - }).pipe(Effect.raceFirst(Effect.flatMap(pinger.timeout, () => Effect.fail(new Socket.SocketError({ | ||
| 238 | + }).pipe(Effect.raceFirst(Effect.flatMap(pinger.timeout, () => (Option.isSome(hooks) && hooks.value.onPingTimeout ? hooks.value.onPingTimeout : Effect.void).pipe(Effect.andThen(Effect.fail(new Socket.SocketError({ | ||
| 239 | reason: new Socket.SocketOpenError({ | ||
| 240 | kind: "Timeout", | ||
| 241 | cause: new Error("ping timeout") | ||
| 242 | }) | ||
| 243 | - }))))); | ||
| 244 | + }))))))); | ||
| 245 | }).pipe(Effect.flatMap(() => Effect.fail(new Socket.SocketError({ | ||
| 246 | reason: new Socket.SocketCloseError({ | ||
| 247 | code: 1000 | ||
| 248 | @@ -664,20 +693,20 @@ export const makeProtocolSocket = options => Protocol.make(Effect.fnUntraced(fun | ||
| 249 | }; | ||
| 250 | })); | ||
| 251 | const defaultRetryPolicy = /*#__PURE__*/Schedule.exponential(500, 1.5).pipe(/*#__PURE__*/Schedule.either(/*#__PURE__*/Schedule.spaced(5000))); | ||
| 252 | -const makePinger = /*#__PURE__*/Effect.fnUntraced(function* (writePing) { | ||
| 253 | +const makePinger = /*#__PURE__*/Effect.fnUntraced(function* (writePing, hooks) { | ||
| 254 | let recievedPong = true; | ||
| 255 | const latch = Latch.makeUnsafe(); | ||
| 256 | const reset = () => { | ||
| 257 | recievedPong = true; | ||
| 258 | latch.closeUnsafe(); | ||
| 259 | }; | ||
| 260 | - const onPong = () => { | ||
| 261 | + const onPong = Effect.sync(() => { | ||
| 262 | recievedPong = true; | ||
| 263 | - }; | ||
| 264 | + }).pipe(Effect.andThen(hooks?.onPong ?? Effect.void)); | ||
| 265 | yield* Effect.suspend(() => { | ||
| 266 | if (!recievedPong) return latch.open; | ||
| 267 | recievedPong = false; | ||
| 268 | - return writePing; | ||
| 269 | + return (hooks?.onPing ?? Effect.void).pipe(Effect.andThen(writePing)); | ||
| 270 | }).pipe(Effect.delay("5 seconds"), Effect.ignore, Effect.forever, Effect.interruptible, Effect.forkScoped); | ||
| 271 | return { | ||
| 272 | timeout: latch.await, | ||
| 273 | @@ -820,6 +849,11 @@ export const makeProtocolWorker = options => Protocol.make(Effect.fnUntraced(fun | ||
| 274 | * @since 4.0.0 | ||
| 275 | */ | ||
| 276 | export const layerProtocolWorker = /*#__PURE__*/flow(makeProtocolWorker, /*#__PURE__*/Layer.effect(Protocol)); | ||
| 277 | +/** | ||
| 278 | + * @since 4.0.0 | ||
| 279 | + * @category RequestHooks | ||
| 280 | + */ | ||
| 281 | +export class RequestHooks extends /*#__PURE__*/Context.Service()("effect/rpc/RpcClient/RequestHooks") {} | ||
| 282 | /** | ||
| 283 | * Represents optional client protocol hooks that run when a transport connects | ||
| 284 | * and disconnects. | ||
| 285 | @@ -835,4 +869,4 @@ export const layerProtocolWorker = /*#__PURE__*/flow(makeProtocolWorker, /*#__PU | ||
| 286 | export class ConnectionHooks extends /*#__PURE__*/Context.Service()("effect/rpc/RpcClient/ConnectionHooks") {} | ||
| 287 | // internal | ||
| 288 | const decodeDefect = /*#__PURE__*/Schema.decodeSync(Schema.Defect); | ||
| 289 | -//# sourceMappingURL=RpcClient.js.map | ||
| 290 | \ No newline at end of file | ||
| 291 | +//# sourceMappingURL=RpcClient.js.map | ||
scripts/build-desktop-artifact.ts+1-1| ... | @@ -278,7 +278,7 @@ export const resolveBuildOptions = Effect.fn("resolveBuildOptions")(function* ( | ... | @@ -278,7 +278,7 @@ export const resolveBuildOptions = Effect.fn("resolveBuildOptions")(function* ( |
| 278 | ) { | 278 | ) { |
| 279 | const path = yield* Path.Path; | 279 | const path = yield* Path.Path; |
| 280 | const repoRoot = yield* RepoRoot; | 280 | const repoRoot = yield* RepoRoot; |
| 281 | const env = yield* BuildEnvConfig.asEffect(); | 281 | const env = yield* BuildEnvConfig; |
| 282 | 282 | ||
| 283 | const platform = mergeOptions( | 283 | const platform = mergeOptions( |
| 284 | input.platform, | 284 | input.platform, |
scripts/dev-runner.ts+1-1| ... | @@ -385,7 +385,7 @@ interface DevRunnerCliInput { | ... | @@ -385,7 +385,7 @@ interface DevRunnerCliInput { |
| 385 | 385 | ||
| 386 | export function runDevRunnerWithInput(input: DevRunnerCliInput) { | 386 | export function runDevRunnerWithInput(input: DevRunnerCliInput) { |
| 387 | return Effect.gen(function* () { | 387 | return Effect.gen(function* () { |
| 388 | const { portOffset, devInstance } = yield* OffsetConfig.asEffect().pipe( | 388 | const { portOffset, devInstance } = yield* OffsetConfig.pipe( |
| 389 | Effect.mapError( | 389 | Effect.mapError( |
| 390 | (cause) => | 390 | (cause) => |
| 391 | new DevRunnerError({ | 391 | new DevRunnerError({ |
scripts/mock-update-server.ts+1-1| ... | @@ -24,7 +24,7 @@ const resolveMockUpdateServerConfig = Effect.gen(function* () { | ... | @@ -24,7 +24,7 @@ const resolveMockUpdateServerConfig = Effect.gen(function* () { |
| 24 | root: Config.string("T3CODE_DESKTOP_MOCK_UPDATE_SERVER_ROOT").pipe( | 24 | root: Config.string("T3CODE_DESKTOP_MOCK_UPDATE_SERVER_ROOT").pipe( |
| 25 | Config.withDefault("../release-mock"), | 25 | Config.withDefault("../release-mock"), |
| 26 | ), | 26 | ), |
| 27 | }).asEffect(); | 27 | }); |
| 28 | 28 | ||
| 29 | const resolvedRoot = path.resolve(import.meta.dirname, config.root); | 29 | const resolvedRoot = path.resolve(import.meta.dirname, config.root); |
| 30 | 30 |