diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e3329b1dad9..3d56abe8419 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -46,6 +46,14 @@ jobs: - name: Install dependencies run: bun install --frozen-lockfile + - name: Ensure Electron runtime is installed + run: | + if ! node -e "require('./apps/desktop/node_modules/electron')" >/dev/null 2>&1; then + rm -rf apps/desktop/node_modules/electron/dist apps/desktop/node_modules/electron/path.txt + bun apps/desktop/node_modules/electron/install.js + fi + node -e "require('./apps/desktop/node_modules/electron')" + - name: Format run: bun run fmt:check diff --git a/apps/desktop/src/app/DesktopApp.ts b/apps/desktop/src/app/DesktopApp.ts index b9817552969..85b8aed4a42 100644 --- a/apps/desktop/src/app/DesktopApp.ts +++ b/apps/desktop/src/app/DesktopApp.ts @@ -1,8 +1,8 @@ import * as Cause from "effect/Cause"; +import * as Crypto from "effect/Crypto"; import * as Data from "effect/Data"; import * as Effect from "effect/Effect"; import * as Option from "effect/Option"; -import * as Random from "effect/Random"; import * as Ref from "effect/Ref"; import * as NetService from "@t3tools/shared/Net"; @@ -26,7 +26,8 @@ const DEFAULT_DESKTOP_BACKEND_PORT = 3773; const MAX_TCP_PORT = 65_535; const DESKTOP_BACKEND_PORT_PROBE_HOSTS = ["127.0.0.1", "0.0.0.0", "::"] as const; -const makeDesktopRunId = Random.nextUUIDv4.pipe( +const makeDesktopRunId = Crypto.Crypto.pipe( + Effect.flatMap((crypto) => crypto.randomUUIDv4), Effect.map((value) => value.replaceAll("-", "").slice(0, 12)), ); diff --git a/apps/desktop/src/backend/DesktopBackendConfiguration.ts b/apps/desktop/src/backend/DesktopBackendConfiguration.ts index 42e4ada438b..4ce117205a6 100644 --- a/apps/desktop/src/backend/DesktopBackendConfiguration.ts +++ b/apps/desktop/src/backend/DesktopBackendConfiguration.ts @@ -1,10 +1,12 @@ import { parsePersistedServerObservabilitySettings } from "@t3tools/shared/serverSettings"; import * as Context from "effect/Context"; +import * as Crypto from "effect/Crypto"; import * as Effect from "effect/Effect"; +import * as Encoding from "effect/Encoding"; import * as FileSystem from "effect/FileSystem"; import * as Layer from "effect/Layer"; import * as Option from "effect/Option"; -import * as Random from "effect/Random"; +import * as PlatformError from "effect/PlatformError"; import * as Ref from "effect/Ref"; import * as DesktopBackendManager from "./DesktopBackendManager.ts"; @@ -13,7 +15,10 @@ import * as DesktopObservability from "../app/DesktopObservability.ts"; import * as DesktopServerExposure from "./DesktopServerExposure.ts"; export interface DesktopBackendConfigurationShape { - readonly resolve: Effect.Effect; + readonly resolve: Effect.Effect< + DesktopBackendManager.DesktopBackendStartConfig, + PlatformError.PlatformError + >; } export class DesktopBackendConfiguration extends Context.Service< @@ -80,23 +85,6 @@ const readPersistedBackendObservabilitySettings: Effect.Effect< }; }); -const getOrCreateBootstrapToken = Effect.fn("desktop.backendConfiguration.bootstrapToken")( - function* (tokenRef: Ref.Ref>) { - const existing = yield* Ref.get(tokenRef); - if (Option.isSome(existing)) { - return existing.value; - } - - let token = ""; - while (token.length < 48) { - token += (yield* Random.nextUUIDv4).replace(/-/g, ""); - } - token = token.slice(0, 48); - yield* Ref.set(tokenRef, Option.some(token)); - return token; - }, -); - const resolveBackendStartConfig = Effect.fn("desktop.backendConfiguration.resolveStartConfig")( function* (input: { readonly bootstrapToken: string; @@ -148,11 +136,22 @@ export const layer = Layer.effect( const environment = yield* DesktopEnvironment.DesktopEnvironment; const fileSystem = yield* FileSystem.FileSystem; const serverExposure = yield* DesktopServerExposure.DesktopServerExposure; + const crypto = yield* Crypto.Crypto; const tokenRef = yield* Ref.make(Option.none()); + const getOrCreateBootstrapToken = Effect.gen(function* () { + const existing = yield* Ref.get(tokenRef); + if (Option.isSome(existing)) { + return existing.value; + } + + const token = Encoding.encodeHex(yield* crypto.randomBytes(24)); + yield* Ref.set(tokenRef, Option.some(token)); + return token; + }); return DesktopBackendConfiguration.of({ resolve: Effect.gen(function* () { - const bootstrapToken = yield* getOrCreateBootstrapToken(tokenRef); + const bootstrapToken = yield* getOrCreateBootstrapToken; const observabilitySettings = yield* readPersistedBackendObservabilitySettings.pipe( Effect.provideService(FileSystem.FileSystem, fileSystem), Effect.provideService(DesktopEnvironment.DesktopEnvironment, environment), diff --git a/apps/desktop/src/backend/DesktopBackendManager.ts b/apps/desktop/src/backend/DesktopBackendManager.ts index 97931f42dbd..2e3bc39b309 100644 --- a/apps/desktop/src/backend/DesktopBackendManager.ts +++ b/apps/desktop/src/backend/DesktopBackendManager.ts @@ -329,9 +329,19 @@ const makeDesktopBackendManager = Effect.fn("makeDesktopBackendManager")(functio } yield* Ref.set(desktopState.backendReady, false); - const config = yield* configuration.resolve; + const config = yield* configuration.resolve.pipe( + Effect.tapError((error) => + logBackendManagerError("failed to generate desktop backend configuration", { + cause: error.message, + }), + ), + Effect.option, + ); + if (Option.isNone(config)) { + return; + } const entryExists = yield* fileSystem - .exists(config.entryPath) + .exists(config.value.entryPath) .pipe(Effect.orElseSucceed(() => false)); yield* cancelRestart; @@ -339,11 +349,11 @@ const makeDesktopBackendManager = Effect.fn("makeDesktopBackendManager")(functio ...latest, desiredRunning: true, ready: false, - config: Option.some(config), + config: Option.some(config.value), })); if (!entryExists) { - yield* scheduleRestart(`missing server entry at ${config.entryPath}`); + yield* scheduleRestart(`missing server entry at ${config.value.entryPath}`); return; } @@ -425,7 +435,7 @@ const makeDesktopBackendManager = Effect.fn("makeDesktopBackendManager")(functio }); const program = runBackendProcess({ - ...config, + ...config.value, onStarted: Effect.fn("desktop.backendManager.onStarted")(function* (pid) { yield* updateActiveRun(runId, (run) => ({ ...run, @@ -433,7 +443,7 @@ const makeDesktopBackendManager = Effect.fn("makeDesktopBackendManager")(functio })); yield* backendOutputLog.writeSessionBoundary({ phase: "START", - details: `pid=${pid} port=${config.bootstrap.port} cwd=${config.cwd}`, + details: `pid=${pid} port=${config.value.bootstrap.port} cwd=${config.value.cwd}`, }); }), onReady: Effect.fn("desktop.backendManager.onReady")(function* () { diff --git a/apps/desktop/src/settings/DesktopAppSettings.ts b/apps/desktop/src/settings/DesktopAppSettings.ts index 177f05a4b2b..0a5079b1057 100644 --- a/apps/desktop/src/settings/DesktopAppSettings.ts +++ b/apps/desktop/src/settings/DesktopAppSettings.ts @@ -6,6 +6,7 @@ import { } from "@t3tools/contracts"; import { fromLenientJson } from "@t3tools/shared/schemaJson"; import * as Context from "effect/Context"; +import * as Crypto from "effect/Crypto"; import * as Data from "effect/Data"; import * as Effect from "effect/Effect"; import * as FileSystem from "effect/FileSystem"; @@ -13,7 +14,6 @@ import * as Layer from "effect/Layer"; import * as Option from "effect/Option"; import * as Path from "effect/Path"; import * as PlatformError from "effect/PlatformError"; -import * as Random from "effect/Random"; import * as Schema from "effect/Schema"; import * as SynchronizedRef from "effect/SynchronizedRef"; @@ -222,10 +222,10 @@ const writeSettings = Effect.fn("desktop.settings.writeSettings")(function* (inp readonly settingsPath: string; readonly settings: DesktopSettings; readonly defaultSettings: DesktopSettings; + readonly suffix: string; }): Effect.fn.Return { const directory = input.path.dirname(input.settingsPath); - const suffix = (yield* Random.nextUUIDv4).replace(/-/g, ""); - const tempPath = `${input.settingsPath}.${process.pid}.${suffix}.tmp`; + const tempPath = `${input.settingsPath}.${process.pid}.${input.suffix}.tmp`; const encoded = yield* encodeDesktopSettingsJson( toDesktopSettingsDocument(input.settings, input.defaultSettings), ); @@ -240,6 +240,7 @@ export const layer = Layer.effect( const environment = yield* DesktopEnvironment.DesktopEnvironment; const fileSystem = yield* FileSystem.FileSystem; const path = yield* Path.Path; + const crypto = yield* Crypto.Crypto; const settingsRef = yield* SynchronizedRef.make(environment.defaultDesktopSettings); const persist = ( @@ -251,13 +252,18 @@ export const layer = Layer.effect( return Effect.succeed([settingsChange(settings, false), settings] as const); } - return writeSettings({ - fileSystem, - path, - settingsPath: environment.desktopSettingsPath, - settings: nextSettings, - defaultSettings: environment.defaultDesktopSettings, - }).pipe( + return crypto.randomUUIDv4.pipe( + Effect.map((uuid) => uuid.replace(/-/g, "")), + Effect.flatMap((suffix) => + writeSettings({ + fileSystem, + path, + settingsPath: environment.desktopSettingsPath, + settings: nextSettings, + defaultSettings: environment.defaultDesktopSettings, + suffix, + }), + ), Effect.mapError((cause) => new DesktopSettingsWriteError({ cause })), Effect.as([settingsChange(nextSettings, true), nextSettings] as const), ); diff --git a/apps/desktop/src/settings/DesktopClientSettings.ts b/apps/desktop/src/settings/DesktopClientSettings.ts index 2153125e8e7..521ea467afa 100644 --- a/apps/desktop/src/settings/DesktopClientSettings.ts +++ b/apps/desktop/src/settings/DesktopClientSettings.ts @@ -1,6 +1,7 @@ import { ClientSettingsSchema, type ClientSettings } from "@t3tools/contracts"; import { fromLenientJson } from "@t3tools/shared/schemaJson"; import * as Context from "effect/Context"; +import * as Crypto from "effect/Crypto"; import * as Data from "effect/Data"; import * as Effect from "effect/Effect"; import * as FileSystem from "effect/FileSystem"; @@ -8,7 +9,6 @@ import * as Layer from "effect/Layer"; import * as Option from "effect/Option"; import * as Path from "effect/Path"; import * as PlatformError from "effect/PlatformError"; -import * as Random from "effect/Random"; import * as Schema from "effect/Schema"; import * as Ref from "effect/Ref"; @@ -74,10 +74,10 @@ const writeClientSettings = Effect.fnUntraced(function* (input: { readonly path: Path.Path; readonly settingsPath: string; readonly settings: ClientSettings; + readonly suffix: string; }): Effect.fn.Return { const directory = input.path.dirname(input.settingsPath); - const suffix = (yield* Random.nextUUIDv4).replace(/-/g, ""); - const tempPath = `${input.settingsPath}.${process.pid}.${suffix}.tmp`; + const tempPath = `${input.settingsPath}.${process.pid}.${input.suffix}.tmp`; const encoded = yield* encodeClientSettingsJson(input.settings); yield* input.fileSystem.makeDirectory(directory, { recursive: true }); yield* input.fileSystem.writeFileString(tempPath, `${encoded}\n`); @@ -90,18 +90,24 @@ export const layer = Layer.effect( const environment = yield* DesktopEnvironment.DesktopEnvironment; const fileSystem = yield* FileSystem.FileSystem; const path = yield* Path.Path; + const crypto = yield* Crypto.Crypto; return DesktopClientSettings.of({ get: readClientSettings(fileSystem, environment.clientSettingsPath).pipe( Effect.withSpan("desktop.clientSettings.get"), ), set: (settings) => - writeClientSettings({ - fileSystem, - path, - settingsPath: environment.clientSettingsPath, - settings, - }).pipe( + crypto.randomUUIDv4.pipe( + Effect.map((uuid) => uuid.replace(/-/g, "")), + Effect.flatMap((suffix) => + writeClientSettings({ + fileSystem, + path, + settingsPath: environment.clientSettingsPath, + settings, + suffix, + }), + ), Effect.mapError((cause) => new DesktopClientSettingsWriteError({ cause })), Effect.withSpan("desktop.clientSettings.set"), ), diff --git a/apps/desktop/src/settings/DesktopSavedEnvironments.ts b/apps/desktop/src/settings/DesktopSavedEnvironments.ts index ec36aa4f6ef..77524ed7697 100644 --- a/apps/desktop/src/settings/DesktopSavedEnvironments.ts +++ b/apps/desktop/src/settings/DesktopSavedEnvironments.ts @@ -1,6 +1,7 @@ import { EnvironmentId, type PersistedSavedEnvironmentRecord } from "@t3tools/contracts"; import { fromLenientJson } from "@t3tools/shared/schemaJson"; import * as Context from "effect/Context"; +import * as Crypto from "effect/Crypto"; import * as Data from "effect/Data"; import * as Effect from "effect/Effect"; import * as Encoding from "effect/Encoding"; @@ -9,7 +10,6 @@ import * as Layer from "effect/Layer"; import * as Option from "effect/Option"; import * as Path from "effect/Path"; import * as PlatformError from "effect/PlatformError"; -import * as Random from "effect/Random"; import * as Schema from "effect/Schema"; import * as Ref from "effect/Ref"; @@ -200,10 +200,10 @@ const writeRegistryDocument = Effect.fn("desktop.savedEnvironments.writeRegistry readonly path: Path.Path; readonly registryPath: string; readonly document: SavedEnvironmentRegistryDocument; + readonly suffix: string; }): Effect.fn.Return { const directory = input.path.dirname(input.registryPath); - const suffix = (yield* Random.nextUUIDv4).replace(/-/g, ""); - const tempPath = `${input.registryPath}.${process.pid}.${suffix}.tmp`; + const tempPath = `${input.registryPath}.${process.pid}.${input.suffix}.tmp`; const encoded = yield* encodeSavedEnvironmentRegistryDocumentJson(input.document); yield* input.fileSystem.makeDirectory(directory, { recursive: true }); yield* input.fileSystem.writeFileString(tempPath, `${encoded}\n`); @@ -247,14 +247,22 @@ export const layer = Layer.effect( const fileSystem = yield* FileSystem.FileSystem; const path = yield* Path.Path; const safeStorage = yield* ElectronSafeStorage.ElectronSafeStorage; + const crypto = yield* Crypto.Crypto; const writeDocument = (document: SavedEnvironmentRegistryDocument) => - writeRegistryDocument({ - fileSystem, - path, - registryPath: environment.savedEnvironmentRegistryPath, - document, - }).pipe(Effect.mapError((cause) => new DesktopSavedEnvironmentsWriteError({ cause }))); + crypto.randomUUIDv4.pipe( + Effect.map((uuid) => uuid.replace(/-/g, "")), + Effect.flatMap((suffix) => + writeRegistryDocument({ + fileSystem, + path, + registryPath: environment.savedEnvironmentRegistryPath, + document, + suffix, + }), + ), + Effect.mapError((cause) => new DesktopSavedEnvironmentsWriteError({ cause })), + ); return DesktopSavedEnvironments.of({ getRegistry: readRegistryDocument(fileSystem, environment.savedEnvironmentRegistryPath).pipe( diff --git a/apps/desktop/src/ssh/DesktopSshPasswordPrompts.test.ts b/apps/desktop/src/ssh/DesktopSshPasswordPrompts.test.ts index 9e9cfcc737e..080a2fe465d 100644 --- a/apps/desktop/src/ssh/DesktopSshPasswordPrompts.test.ts +++ b/apps/desktop/src/ssh/DesktopSshPasswordPrompts.test.ts @@ -1,4 +1,5 @@ import { assert, describe, it } from "@effect/vitest"; +import * as NodeServices from "@effect/platform-node/NodeServices"; import * as Duration from "effect/Duration"; import * as Effect from "effect/Effect"; import * as Fiber from "effect/Fiber"; @@ -86,6 +87,7 @@ function makeElectronWindowLayer(window: ReturnType["wind function makeLayer(window: ReturnType["window"]) { return DesktopSshPasswordPrompts.layer({ passwordPromptTimeoutMs: 1_000 }).pipe( Layer.provide(makeElectronWindowLayer(window)), + Layer.provide(NodeServices.layer), Layer.provideMerge(TestClock.layer()), ); } diff --git a/apps/desktop/src/ssh/DesktopSshPasswordPrompts.ts b/apps/desktop/src/ssh/DesktopSshPasswordPrompts.ts index a53de9fd8e4..b3e7bd23032 100644 --- a/apps/desktop/src/ssh/DesktopSshPasswordPrompts.ts +++ b/apps/desktop/src/ssh/DesktopSshPasswordPrompts.ts @@ -2,6 +2,7 @@ import type { DesktopSshPasswordPromptRequest } from "@t3tools/contracts"; import { DesktopSshPasswordPromptResolutionInputSchema } from "@t3tools/contracts"; import type { SshPasswordRequest } from "@t3tools/ssh/auth"; import * as Context from "effect/Context"; +import * as Crypto from "effect/Crypto"; import * as Data from "effect/Data"; import * as DateTime from "effect/DateTime"; import * as Deferred from "effect/Deferred"; @@ -9,7 +10,6 @@ import * as Duration from "effect/Duration"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; import * as Option from "effect/Option"; -import * as Random from "effect/Random"; import * as Ref from "effect/Ref"; import * as IpcChannels from "../ipc/channels.ts"; @@ -163,6 +163,7 @@ const failPending = ( const make = Effect.fn("desktop.sshPasswordPrompts.make")(function* (options: LayerOptions = {}) { const electronWindow = yield* ElectronWindow.ElectronWindow; + const crypto = yield* Crypto.Crypto; const pendingRef = yield* Ref.make(new Map()); const passwordPromptTimeoutMs = options.passwordPromptTimeoutMs ?? DEFAULT_SSH_PASSWORD_PROMPT_TIMEOUT_MS; @@ -230,7 +231,11 @@ const make = Effect.fn("desktop.sshPasswordPrompts.make")(function* (options: La }); } - const requestId = yield* Random.nextUUIDv4; + const requestId = yield* crypto.randomUUIDv4.pipe( + Effect.mapError( + () => new DesktopSshPromptUnavailableError({ reason: "Secure randomness is unavailable." }), + ), + ); const now = yield* DateTime.now; const expiresAt = DateTime.formatIso( DateTime.add(now, { milliseconds: passwordPromptTimeoutMs }), diff --git a/apps/server/integration/TestProviderAdapter.integration.ts b/apps/server/integration/TestProviderAdapter.integration.ts index 28873c51f97..0e64699de97 100644 --- a/apps/server/integration/TestProviderAdapter.integration.ts +++ b/apps/server/integration/TestProviderAdapter.integration.ts @@ -11,8 +11,8 @@ import { ProviderDriverKind, } from "@t3tools/contracts"; import * as Effect from "effect/Effect"; +import * as Crypto from "effect/Crypto"; import * as Queue from "effect/Queue"; -import * as Random from "effect/Random"; import * as Stream from "effect/Stream"; import { @@ -226,6 +226,7 @@ function missingSessionEffect( export const makeTestProviderAdapterHarness = (options?: MakeTestProviderAdapterHarnessOptions) => Effect.gen(function* () { const provider = options?.provider ?? ProviderDriverKind.make("codex"); + const crypto = yield* Crypto.Crypto; const runtimeEvents = yield* Queue.unbounded(); let sessionCount = 0; const sessions = new Map(); @@ -241,6 +242,18 @@ export const makeTestProviderAdapterHarness = (options?: MakeTestProviderAdapter >(); const emit = (event: ProviderRuntimeEvent) => Queue.offer(runtimeEvents, event); + const randomUUIDv4 = (threadId: ThreadId) => + crypto.randomUUIDv4.pipe( + Effect.mapError( + (cause) => + new ProviderAdapterValidationError({ + provider, + operation: "crypto/randomUUIDv4", + issue: `Failed to generate test runtime identifier for thread '${threadId}'.`, + cause, + }), + ), + ); const startSession: ProviderAdapterShape["startSession"] = (input) => Effect.gen(function* () { @@ -309,7 +322,7 @@ export const makeTestProviderAdapterHarness = (options?: MakeTestProviderAdapter for (const fixtureEvent of response.events) { const rawEvent: Record = { ...(fixtureEvent as Record), - eventId: yield* Random.nextUUIDv4, + eventId: yield* randomUUIDv4(input.threadId), provider, sessionId: RuntimeSessionId.make(String(input.threadId)), }; @@ -366,7 +379,7 @@ export const makeTestProviderAdapterHarness = (options?: MakeTestProviderAdapter if (deferredTurnCompletedEvents.length === 0) { yield* emit({ type: "turn.completed", - eventId: EventId.make(yield* Random.nextUUIDv4), + eventId: EventId.make(yield* randomUUIDv4(input.threadId)), provider, createdAt: nowIso(), threadId: state.snapshot.threadId, diff --git a/apps/server/src/atomicWrite.ts b/apps/server/src/atomicWrite.ts index 764b6781919..fcd0345fc8d 100644 --- a/apps/server/src/atomicWrite.ts +++ b/apps/server/src/atomicWrite.ts @@ -1,7 +1,6 @@ import * as Effect from "effect/Effect"; import * as FileSystem from "effect/FileSystem"; import * as Path from "effect/Path"; -import * as Random from "effect/Random"; export const writeFileStringAtomically = (input: { readonly filePath: string; @@ -11,7 +10,6 @@ export const writeFileStringAtomically = (input: { Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; - const tempFileId = yield* Random.nextUUIDv4; const targetDirectory = path.dirname(input.filePath); yield* fs.makeDirectory(targetDirectory, { recursive: true }); @@ -19,7 +17,7 @@ export const writeFileStringAtomically = (input: { directory: targetDirectory, prefix: `${path.basename(input.filePath)}.`, }); - const tempPath = path.join(tempDirectory, `${tempFileId}.tmp`); + const tempPath = path.join(tempDirectory, "contents.tmp"); yield* fs.writeFileString(tempPath, input.contents); yield* fs.rename(tempPath, input.filePath); diff --git a/apps/server/src/cli/project.ts b/apps/server/src/cli/project.ts index 39772fcd061..05e96160c5e 100644 --- a/apps/server/src/cli/project.ts +++ b/apps/server/src/cli/project.ts @@ -5,6 +5,7 @@ import { type ClientOrchestrationCommand, } from "@t3tools/contracts"; import * as Console from "effect/Console"; +import * as Crypto from "effect/Crypto"; import * as Data from "effect/Data"; import * as DateTime from "effect/DateTime"; import * as Duration from "effect/Duration"; @@ -58,6 +59,16 @@ class ProjectCommandError extends Data.TaggedError("ProjectCommandError")<{ readonly message: string; }> {} +const projectCommandUuid = Crypto.Crypto.pipe( + Effect.flatMap((crypto) => crypto.randomUUIDv4), + Effect.mapError( + () => + new ProjectCommandError({ + message: "Failed to generate a project command identifier.", + }), + ), +); + const ProjectCliRuntimeLive = Layer.mergeAll( WorkspacePathsLive, OrchestrationLayerLive.pipe( @@ -260,7 +271,7 @@ const runProjectMutation = Effect.fn("runProjectMutation")(function* ( }) => Effect.Effect< string, Error, - FileSystem.FileSystem | HttpClient.HttpClient | Path.Path | WorkspacePaths + Crypto.Crypto | FileSystem.FileSystem | HttpClient.HttpClient | Path.Path | WorkspacePaths >, ) { const logLevel = yield* GlobalFlag.LogLevel; @@ -343,10 +354,10 @@ const projectAddCommand = Command.make("add", { } const title = yield* resolveProjectTitle(workspaceRoot, Option.getOrUndefined(flags.title)); - const projectId = ProjectId.make(crypto.randomUUID()); + const projectId = ProjectId.make(yield* projectCommandUuid); yield* dispatch({ type: "project.create", - commandId: CommandId.make(crypto.randomUUID()), + commandId: CommandId.make(yield* projectCommandUuid), projectId, title, workspaceRoot, @@ -384,7 +395,7 @@ const projectRemoveCommand = Command.make("remove", { }); yield* dispatch({ type: "project.delete", - commandId: CommandId.make(crypto.randomUUID()), + commandId: CommandId.make(yield* projectCommandUuid), projectId: project.id, }); return `Removed project ${project.id} (${project.title}).`; @@ -424,7 +435,7 @@ const projectRenameCommand = Command.make("rename", { yield* dispatch({ type: "project.meta.update", - commandId: CommandId.make(crypto.randomUUID()), + commandId: CommandId.make(yield* projectCommandUuid), projectId: project.id, title: nextTitle, }); diff --git a/apps/server/src/environment/Layers/ServerEnvironment.ts b/apps/server/src/environment/Layers/ServerEnvironment.ts index 6972af9b3df..cc8d803c970 100644 --- a/apps/server/src/environment/Layers/ServerEnvironment.ts +++ b/apps/server/src/environment/Layers/ServerEnvironment.ts @@ -1,9 +1,9 @@ import { EnvironmentId, type ExecutionEnvironmentDescriptor } from "@t3tools/contracts"; +import * as Crypto from "effect/Crypto"; import * as Effect from "effect/Effect"; import * as FileSystem from "effect/FileSystem"; import * as Layer from "effect/Layer"; import * as Path from "effect/Path"; -import * as Random from "effect/Random"; import { ServerConfig } from "../../config.ts"; import { layer as ProcessRunnerLive } from "../../processRunner.ts"; @@ -39,6 +39,7 @@ export const makeServerEnvironment = Effect.fn("makeServerEnvironment")(function const fileSystem = yield* FileSystem.FileSystem; const path = yield* Path.Path; const serverConfig = yield* ServerConfig; + const crypto = yield* Crypto.Crypto; const readPersistedEnvironmentId = Effect.gen(function* () { const exists = yield* fileSystem @@ -64,7 +65,7 @@ export const makeServerEnvironment = Effect.fn("makeServerEnvironment")(function return persisted; } - const generated = yield* Random.nextUUIDv4; + const generated = yield* crypto.randomUUIDv4; yield* persistEnvironmentId(generated); return generated; }); diff --git a/apps/server/src/git/GitManager.ts b/apps/server/src/git/GitManager.ts index 8dfb957b89d..8d1a71f8302 100644 --- a/apps/server/src/git/GitManager.ts +++ b/apps/server/src/git/GitManager.ts @@ -1,8 +1,7 @@ -import { randomUUID } from "node:crypto"; - import * as Arr from "effect/Array"; import * as Cache from "effect/Cache"; import * as Context from "effect/Context"; +import * as Crypto from "effect/Crypto"; import * as DateTime from "effect/DateTime"; import * as Duration from "effect/Duration"; import * as Effect from "effect/Effect"; @@ -532,32 +531,39 @@ export const makeGitManager = Effect.fn("makeGitManager")(function* () { const sourceControlProviders = yield* SourceControlProviderRegistry; const textGeneration = yield* TextGeneration; const projectSetupScriptRunner = yield* ProjectSetupScriptRunner; + const crypto = yield* Crypto.Crypto; const sourceControlProvider = (cwd: string) => sourceControlProviders.resolve({ cwd }); const serverSettingsService = yield* ServerSettingsService; + const randomUUIDv4 = crypto.randomUUIDv4.pipe( + Effect.mapError((cause) => + gitManagerError("randomUUIDv4", "Failed to generate Git operation identifier.", cause), + ), + ); const createProgressEmitter = ( input: { cwd: string; action: GitStackedAction }, options?: GitRunStackedActionOptions, - ) => { - const actionId = options?.actionId ?? randomUUID(); - const reporter = options?.progressReporter; - - const emit = (event: GitActionProgressPayload) => - reporter - ? reporter.publish({ - actionId, - cwd: input.cwd, - action: input.action, - ...event, - } as GitActionProgressEvent) - : Effect.void; + ) => + (options?.actionId === undefined ? randomUUIDv4 : Effect.succeed(options.actionId)).pipe( + Effect.map((actionId) => { + const reporter = options?.progressReporter; + const emit = (event: GitActionProgressPayload) => + reporter + ? reporter.publish({ + actionId, + cwd: input.cwd, + action: input.action, + ...event, + } as GitActionProgressEvent) + : Effect.void; - return { - actionId, - emit, - }; - }; + return { + actionId, + emit, + }; + }), + ); const configurePullRequestHeadUpstreamBase = Effect.fn("configurePullRequestHeadUpstream")( function* ( @@ -1301,7 +1307,7 @@ export const makeGitManager = Effect.fn("makeGitManager")(function* () { modelSelection, }); - const bodyFile = path.join(tempDir, `t3code-pr-body-${process.pid}-${randomUUID()}.md`); + const bodyFile = path.join(tempDir, `t3code-pr-body-${process.pid}-${yield* randomUUIDv4}.md`); yield* fileSystem .writeFileString(bodyFile, generated.body) .pipe( @@ -1591,7 +1597,7 @@ export const makeGitManager = Effect.fn("makeGitManager")(function* () { const runStackedAction: GitManagerShape["runStackedAction"] = Effect.fn("runStackedAction")( function* (input, options) { - const progress = createProgressEmitter(input, options); + const progress = yield* createProgressEmitter(input, options); const currentPhase = yield* Ref.make>(Option.none()); const runAction = Effect.fn("runStackedAction.runAction")(function* (): Effect.fn.Return< diff --git a/apps/server/src/orchestration/Layers/OrchestrationEngine.test.ts b/apps/server/src/orchestration/Layers/OrchestrationEngine.test.ts index 7909d5cd6b1..e11720d72ca 100644 --- a/apps/server/src/orchestration/Layers/OrchestrationEngine.test.ts +++ b/apps/server/src/orchestration/Layers/OrchestrationEngine.test.ts @@ -211,6 +211,7 @@ describe("OrchestrationEngine", () => { Layer.provide(Layer.succeed(OrchestrationEventStore, eventStore)), Layer.provide(OrchestrationCommandReceiptRepositoryLive), Layer.provide(SqlitePersistenceMemory), + Layer.provideMerge(NodeServices.layer), ); const runtime = ManagedRuntime.make(layer); diff --git a/apps/server/src/orchestration/Layers/OrchestrationEngine.ts b/apps/server/src/orchestration/Layers/OrchestrationEngine.ts index cf8407bd212..7277663e948 100644 --- a/apps/server/src/orchestration/Layers/OrchestrationEngine.ts +++ b/apps/server/src/orchestration/Layers/OrchestrationEngine.ts @@ -7,6 +7,7 @@ import type { import { OrchestrationCommand } from "@t3tools/contracts"; import * as Cause from "effect/Cause"; import * as Clock from "effect/Clock"; +import * as Crypto from "effect/Crypto"; import * as DateTime from "effect/DateTime"; import * as Deferred from "effect/Deferred"; import * as Duration from "effect/Duration"; @@ -81,6 +82,7 @@ const makeOrchestrationEngine = Effect.gen(function* () { const commandReceiptRepository = yield* OrchestrationCommandReceiptRepository; const projectionPipeline = yield* OrchestrationProjectionPipeline; const projectionSnapshotQuery = yield* ProjectionSnapshotQuery; + const crypto = yield* Crypto.Crypto; const nowIso = Effect.map(DateTime.now, DateTime.formatIso); let commandReadModel = createEmptyReadModel(yield* nowIso); @@ -151,7 +153,18 @@ const makeOrchestrationEngine = Effect.gen(function* () { const eventBase = yield* decideOrchestrationCommand({ command: envelope.command, readModel: commandReadModel, - }); + }).pipe( + Effect.provideService(Crypto.Crypto, crypto), + Effect.mapError((cause) => + isOrchestrationCommandInvariantError(cause) + ? cause + : new OrchestrationCommandInvariantError({ + commandType: envelope.command.type, + detail: "Failed to generate an event identifier.", + cause, + }), + ), + ); const eventBases = Array.isArray(eventBase) ? eventBase : [eventBase]; const committedCommand = yield* sql .withTransaction( diff --git a/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts b/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts index 8b71a976808..f63b873bc3d 100644 --- a/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts +++ b/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts @@ -15,6 +15,7 @@ import { import { isTemporaryWorktreeBranch, WORKTREE_BRANCH_PREFIX } from "@t3tools/shared/git"; import * as Cache from "effect/Cache"; import * as Cause from "effect/Cause"; +import * as Crypto from "effect/Crypto"; import * as Duration from "effect/Duration"; import * as Effect from "effect/Effect"; import * as Equal from "effect/Equal"; @@ -81,9 +82,6 @@ function mapProviderSessionStatusToOrchestrationStatus( const turnStartKeyForEvent = (event: ProviderIntentEvent): string => event.commandId !== null ? `command:${event.commandId}` : `event:${event.eventId}`; -const serverCommandId = (tag: string): CommandId => - CommandId.make(`server:${tag}:${crypto.randomUUID()}`); - const HANDLED_TURN_START_KEY_MAX = 10_000; const HANDLED_TURN_START_KEY_TTL = Duration.minutes(30); const DEFAULT_RUNTIME_MODE: RuntimeMode = "full-access"; @@ -178,6 +176,7 @@ function buildGeneratedWorktreeBranchName(raw: string): string { } const make = Effect.gen(function* () { + const crypto = yield* Crypto.Crypto; const orchestrationEngine = yield* OrchestrationEngineService; const projectionSnapshotQuery = yield* ProjectionSnapshotQuery; const providerService = yield* ProviderService; @@ -185,6 +184,9 @@ const make = Effect.gen(function* () { const vcsStatusBroadcaster = yield* VcsStatusBroadcaster; const textGeneration = yield* TextGeneration; const serverSettingsService = yield* ServerSettingsService; + const serverCommandId = (tag: string) => + crypto.randomUUIDv4.pipe(Effect.map((uuid) => CommandId.make(`server:${tag}:${uuid}`))); + const serverEventId = () => crypto.randomUUIDv4.pipe(Effect.map(EventId.make)); const handledTurnStartKeys = yield* Cache.make({ capacity: HANDLED_TURN_START_KEY_MAX, timeToLive: HANDLED_TURN_START_KEY_TTL, @@ -214,24 +216,31 @@ const make = Effect.gen(function* () { readonly createdAt: string; readonly requestId?: string; }) => - orchestrationEngine.dispatch({ - type: "thread.activity.append", + Effect.all({ commandId: serverCommandId("provider-failure-activity"), - threadId: input.threadId, - activity: { - id: EventId.make(crypto.randomUUID()), - tone: "error", - kind: input.kind, - summary: input.summary, - payload: { - detail: input.detail, - ...(input.requestId ? { requestId: input.requestId } : {}), - }, - turnId: input.turnId, - createdAt: input.createdAt, - }, - createdAt: input.createdAt, - }); + eventId: serverEventId(), + }).pipe( + Effect.flatMap(({ commandId, eventId }) => + orchestrationEngine.dispatch({ + type: "thread.activity.append", + commandId, + threadId: input.threadId, + activity: { + id: eventId, + tone: "error", + kind: input.kind, + summary: input.summary, + payload: { + detail: input.detail, + ...(input.requestId ? { requestId: input.requestId } : {}), + }, + turnId: input.turnId, + createdAt: input.createdAt, + }, + createdAt: input.createdAt, + }), + ), + ); const formatFailureDetail = (cause: Cause.Cause): string => { const failReason = cause.reasons.find(Cause.isFailReason); @@ -249,13 +258,17 @@ const make = Effect.gen(function* () { readonly session: OrchestrationSession; readonly createdAt: string; }) => - orchestrationEngine.dispatch({ - type: "thread.session.set", - commandId: serverCommandId("provider-session-set"), - threadId: input.threadId, - session: input.session, - createdAt: input.createdAt, - }); + serverCommandId("provider-session-set").pipe( + Effect.flatMap((commandId) => + orchestrationEngine.dispatch({ + type: "thread.session.set", + commandId, + threadId: input.threadId, + session: input.session, + createdAt: input.createdAt, + }), + ), + ); const setThreadSessionErrorOnTurnStartFailure = Effect.fnUntraced(function* (input: { readonly threadId: ThreadId; @@ -610,7 +623,7 @@ const make = Effect.gen(function* () { const renamed = yield* gitWorkflow.renameBranch({ cwd, oldBranch, newBranch: targetBranch }); yield* orchestrationEngine.dispatch({ type: "thread.meta.update", - commandId: serverCommandId("worktree-branch-rename"), + commandId: yield* serverCommandId("worktree-branch-rename"), threadId: input.threadId, branch: renamed.branch, worktreePath: cwd, @@ -657,7 +670,7 @@ const make = Effect.gen(function* () { yield* orchestrationEngine.dispatch({ type: "thread.meta.update", - commandId: serverCommandId("thread-title-rename"), + commandId: yield* serverCommandId("thread-title-rename"), threadId: input.threadId, title: generated.title, }); diff --git a/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts b/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts index 2c07ac91b1e..59787e0b545 100644 --- a/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts +++ b/apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts @@ -19,6 +19,7 @@ import { } from "@t3tools/contracts"; import * as Cache from "effect/Cache"; import * as Cause from "effect/Cause"; +import * as Crypto from "effect/Crypto"; import * as Duration from "effect/Duration"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; @@ -39,8 +40,6 @@ import { import { ServerSettingsService } from "../../serverSettings.ts"; const providerTurnKey = (threadId: ThreadId, turnId: TurnId) => `${threadId}:${turnId}`; -const providerCommandId = (event: ProviderRuntimeEvent, tag: string): CommandId => - CommandId.make(`provider:${event.eventId}:${tag}:${crypto.randomUUID()}`); interface AssistantSegmentState { baseKey: string; @@ -607,11 +606,16 @@ function runtimeEventToActivities( } const make = Effect.gen(function* () { + const crypto = yield* Crypto.Crypto; const orchestrationEngine = yield* OrchestrationEngineService; const projectionSnapshotQuery = yield* ProjectionSnapshotQuery; const providerService = yield* ProviderService; const projectionTurnRepository = yield* ProjectionTurnRepository; const serverSettingsService = yield* ServerSettingsService; + const providerCommandId = (event: ProviderRuntimeEvent, tag: string) => + crypto.randomUUIDv4.pipe( + Effect.map((uuid) => CommandId.make(`provider:${event.eventId}:${tag}:${uuid}`)), + ); const turnMessageIdsByTurnKey = yield* Cache.make>({ capacity: TURN_MESSAGE_IDS_BY_TURN_CACHE_CAPACITY, @@ -848,7 +852,7 @@ const make = Effect.gen(function* () { yield* orchestrationEngine.dispatch({ type: "thread.message.assistant.delta", - commandId: providerCommandId(input.event, input.commandTag), + commandId: yield* providerCommandId(input.event, input.commandTag), threadId: input.threadId, messageId: input.messageId, delta: bufferedText, @@ -915,7 +919,7 @@ const make = Effect.gen(function* () { if (hasRenderableText) { yield* orchestrationEngine.dispatch({ type: "thread.message.assistant.delta", - commandId: providerCommandId(input.event, input.finalDeltaCommandTag), + commandId: yield* providerCommandId(input.event, input.finalDeltaCommandTag), threadId: input.threadId, messageId: input.messageId, delta: text, @@ -927,7 +931,7 @@ const make = Effect.gen(function* () { if (input.hasProjectedMessage || hasRenderableText) { yield* orchestrationEngine.dispatch({ type: "thread.message.assistant.complete", - commandId: providerCommandId(input.event, input.commandTag), + commandId: yield* providerCommandId(input.event, input.commandTag), threadId: input.threadId, messageId: input.messageId, ...(input.turnId ? { turnId: input.turnId } : {}), @@ -1003,7 +1007,7 @@ const make = Effect.gen(function* () { const existingPlan = findProposedPlanById(input.threadProposedPlans, input.planId); yield* orchestrationEngine.dispatch({ type: "thread.proposed-plan.upsert", - commandId: providerCommandId(input.event, "proposed-plan-upsert"), + commandId: yield* providerCommandId(input.event, "proposed-plan-upsert"), threadId: input.threadId, proposedPlan: { id: input.planId, @@ -1159,10 +1163,11 @@ const make = Effect.gen(function* () { return; } + const commandUuid = yield* crypto.randomUUIDv4; yield* orchestrationEngine.dispatch({ type: "thread.proposed-plan.upsert", commandId: CommandId.make( - `provider:source-proposed-plan-implemented:${implementationThreadId}:${crypto.randomUUID()}`, + `provider:source-proposed-plan-implemented:${implementationThreadId}:${commandUuid}`, ), threadId: sourceThread.id, proposedPlan: { @@ -1296,7 +1301,7 @@ const make = Effect.gen(function* () { yield* orchestrationEngine.dispatch({ type: "thread.session.set", - commandId: providerCommandId(event, "thread-session-set"), + commandId: yield* providerCommandId(event, "thread-session-set"), threadId: thread.id, session: { threadId: thread.id, @@ -1342,7 +1347,7 @@ const make = Effect.gen(function* () { if (spillChunk.length > 0) { yield* orchestrationEngine.dispatch({ type: "thread.message.assistant.delta", - commandId: providerCommandId(event, "assistant-delta-buffer-spill"), + commandId: yield* providerCommandId(event, "assistant-delta-buffer-spill"), threadId: thread.id, messageId: assistantMessageId, delta: spillChunk, @@ -1353,7 +1358,7 @@ const make = Effect.gen(function* () { } else { yield* orchestrationEngine.dispatch({ type: "thread.message.assistant.delta", - commandId: providerCommandId(event, "assistant-delta"), + commandId: yield* providerCommandId(event, "assistant-delta"), threadId: thread.id, messageId: assistantMessageId, delta: assistantDelta, @@ -1546,7 +1551,7 @@ const make = Effect.gen(function* () { if (shouldApplyRuntimeError) { yield* orchestrationEngine.dispatch({ type: "thread.session.set", - commandId: providerCommandId(event, "runtime-error-session-set"), + commandId: yield* providerCommandId(event, "runtime-error-session-set"), threadId: thread.id, session: { threadId: thread.id, @@ -1568,7 +1573,7 @@ const make = Effect.gen(function* () { if (event.type === "thread.metadata.updated" && event.payload.name) { yield* orchestrationEngine.dispatch({ type: "thread.meta.update", - commandId: providerCommandId(event, "thread-meta-update"), + commandId: yield* providerCommandId(event, "thread-meta-update"), threadId: thread.id, title: event.payload.name, }); @@ -1596,7 +1601,7 @@ const make = Effect.gen(function* () { ); yield* orchestrationEngine.dispatch({ type: "thread.turn.diff.complete", - commandId: providerCommandId(event, "thread-turn-diff-complete"), + commandId: yield* providerCommandId(event, "thread-turn-diff-complete"), threadId: thread.id, turnId, completedAt: now, @@ -1613,13 +1618,17 @@ const make = Effect.gen(function* () { const activities = runtimeEventToActivities(event); yield* Effect.forEach(activities, (activity) => - orchestrationEngine.dispatch({ - type: "thread.activity.append", - commandId: providerCommandId(event, "thread-activity-append"), - threadId: thread.id, - activity, - createdAt: activity.createdAt, - }), + providerCommandId(event, "thread-activity-append").pipe( + Effect.flatMap((commandId) => + orchestrationEngine.dispatch({ + type: "thread.activity.append", + commandId, + threadId: thread.id, + activity, + createdAt: activity.createdAt, + }), + ), + ), ).pipe(Effect.asVoid); }); diff --git a/apps/server/src/orchestration/decider.delete.test.ts b/apps/server/src/orchestration/decider.delete.test.ts index dcdaaecc1fb..fea36b5717f 100644 --- a/apps/server/src/orchestration/decider.delete.test.ts +++ b/apps/server/src/orchestration/decider.delete.test.ts @@ -6,11 +6,11 @@ import { ThreadId, type OrchestrationCommand, type OrchestrationEvent, - type OrchestrationReadModel, ProviderInstanceId, } from "@t3tools/contracts"; import * as Effect from "effect/Effect"; -import { describe, expect, it } from "vitest"; +import * as NodeServices from "@effect/platform-node/NodeServices"; +import { expect, it } from "@effect/vitest"; import { decideOrchestrationCommand } from "./decider.ts"; import { createEmptyReadModel, projectEvent } from "./projector.ts"; @@ -20,93 +20,87 @@ const asEventId = (value: string): EventId => EventId.make(value); const asProjectId = (value: string): ProjectId => ProjectId.make(value); const asThreadId = (value: string): ThreadId => ThreadId.make(value); -async function seedReadModel(): Promise { +const seedReadModel = Effect.gen(function* () { const now = "2026-01-01T00:00:00.000Z"; const initial = createEmptyReadModel(now); - const withProject = await Effect.runPromise( - projectEvent(initial, { - sequence: 1, - eventId: asEventId("evt-project-create"), - aggregateKind: "project", - aggregateId: asProjectId("project-delete"), - type: "project.created", - occurredAt: now, - commandId: asCommandId("cmd-project-create"), - causationEventId: null, - correlationId: asCommandId("cmd-project-create"), - metadata: {}, - payload: { - projectId: asProjectId("project-delete"), - title: "Project Delete", - workspaceRoot: "/tmp/project-delete", - defaultModelSelection: null, - scripts: [], - createdAt: now, - updatedAt: now, - }, - }), - ); + const withProject = yield* projectEvent(initial, { + sequence: 1, + eventId: asEventId("evt-project-create"), + aggregateKind: "project", + aggregateId: asProjectId("project-delete"), + type: "project.created", + occurredAt: now, + commandId: asCommandId("cmd-project-create"), + causationEventId: null, + correlationId: asCommandId("cmd-project-create"), + metadata: {}, + payload: { + projectId: asProjectId("project-delete"), + title: "Project Delete", + workspaceRoot: "/tmp/project-delete", + defaultModelSelection: null, + scripts: [], + createdAt: now, + updatedAt: now, + }, + }); - const withFirstThread = await Effect.runPromise( - projectEvent(withProject, { - sequence: 2, - eventId: asEventId("evt-thread-create-1"), - aggregateKind: "thread", - aggregateId: asThreadId("thread-delete-1"), - type: "thread.created", - occurredAt: now, - commandId: asCommandId("cmd-thread-create-1"), - causationEventId: null, - correlationId: asCommandId("cmd-thread-create-1"), - metadata: {}, - payload: { - threadId: asThreadId("thread-delete-1"), - projectId: asProjectId("project-delete"), - title: "Thread Delete 1", - modelSelection: { - instanceId: ProviderInstanceId.make("codex"), - model: "gpt-5-codex", - }, - interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, - runtimeMode: "approval-required", - branch: null, - worktreePath: null, - createdAt: now, - updatedAt: now, + const withFirstThread = yield* projectEvent(withProject, { + sequence: 2, + eventId: asEventId("evt-thread-create-1"), + aggregateKind: "thread", + aggregateId: asThreadId("thread-delete-1"), + type: "thread.created", + occurredAt: now, + commandId: asCommandId("cmd-thread-create-1"), + causationEventId: null, + correlationId: asCommandId("cmd-thread-create-1"), + metadata: {}, + payload: { + threadId: asThreadId("thread-delete-1"), + projectId: asProjectId("project-delete"), + title: "Thread Delete 1", + modelSelection: { + instanceId: ProviderInstanceId.make("codex"), + model: "gpt-5-codex", }, - }), - ); + interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, + runtimeMode: "approval-required", + branch: null, + worktreePath: null, + createdAt: now, + updatedAt: now, + }, + }); - return Effect.runPromise( - projectEvent(withFirstThread, { - sequence: 3, - eventId: asEventId("evt-thread-create-2"), - aggregateKind: "thread", - aggregateId: asThreadId("thread-delete-2"), - type: "thread.created", - occurredAt: now, - commandId: asCommandId("cmd-thread-create-2"), - causationEventId: null, - correlationId: asCommandId("cmd-thread-create-2"), - metadata: {}, - payload: { - threadId: asThreadId("thread-delete-2"), - projectId: asProjectId("project-delete"), - title: "Thread Delete 2", - modelSelection: { - instanceId: ProviderInstanceId.make("codex"), - model: "gpt-5-codex", - }, - interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, - runtimeMode: "approval-required", - branch: null, - worktreePath: null, - createdAt: now, - updatedAt: now, + return yield* projectEvent(withFirstThread, { + sequence: 3, + eventId: asEventId("evt-thread-create-2"), + aggregateKind: "thread", + aggregateId: asThreadId("thread-delete-2"), + type: "thread.created", + occurredAt: now, + commandId: asCommandId("cmd-thread-create-2"), + causationEventId: null, + correlationId: asCommandId("cmd-thread-create-2"), + metadata: {}, + payload: { + threadId: asThreadId("thread-delete-2"), + projectId: asProjectId("project-delete"), + title: "Thread Delete 2", + modelSelection: { + instanceId: ProviderInstanceId.make("codex"), + model: "gpt-5-codex", }, - }), - ); -} + interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, + runtimeMode: "approval-required", + branch: null, + worktreePath: null, + createdAt: now, + updatedAt: now, + }, + }); +}); type PlannedEvent = Omit; @@ -142,12 +136,11 @@ function normalizeDeleteEvent(event: PlannedEvent | ReadonlyArray) }); } -describe("decider deletion flows", () => { - it("rejects deleting a non-empty project without force", async () => { - const readModel = await seedReadModel(); - - await expect( - Effect.runPromise( +it.layer(NodeServices.layer)("decider deletion flows", (it) => { + it.effect("rejects deleting a non-empty project without force", () => + Effect.gen(function* () { + const readModel = yield* seedReadModel; + const error = yield* Effect.flip( decideOrchestrationCommand({ command: { type: "project.delete", @@ -156,72 +149,69 @@ describe("decider deletion flows", () => { }, readModel, }), - ), - ).rejects.toThrow("cannot be deleted without force=true"); - }); + ); + expect(error.message).toContain("cannot be deleted without force=true"); + }), + ); - it("reuses thread.delete semantics when force-deleting a non-empty project", async () => { - const readModel = await seedReadModel(); - const projectDeleteCommand: Extract = { - type: "project.delete", - commandId: asCommandId("cmd-project-delete-force"), - projectId: asProjectId("project-delete"), - force: true, - }; + it.effect("reuses thread.delete semantics when force-deleting a non-empty project", () => + Effect.gen(function* () { + const readModel = yield* seedReadModel; + const projectDeleteCommand: Extract = { + type: "project.delete", + commandId: asCommandId("cmd-project-delete-force"), + projectId: asProjectId("project-delete"), + force: true, + }; - const forcedResult = await Effect.runPromise( - decideOrchestrationCommand({ + const forcedResult = yield* decideOrchestrationCommand({ command: projectDeleteCommand, readModel, - }), - ); - const forcedEvents = Array.isArray(forcedResult) ? forcedResult : [forcedResult]; + }); + const forcedEvents = Array.isArray(forcedResult) ? forcedResult : [forcedResult]; - expect(forcedEvents.map((event) => event.type)).toEqual([ - "thread.deleted", - "thread.deleted", - "project.deleted", - ]); + expect(forcedEvents.map((event) => event.type)).toEqual([ + "thread.deleted", + "thread.deleted", + "project.deleted", + ]); - let sequentialReadModel = readModel; - let nextSequence = readModel.snapshotSequence; - const sequentialEvents: PlannedEvent[] = []; - for (const nextCommand of [ - { - type: "thread.delete", - commandId: projectDeleteCommand.commandId, - threadId: asThreadId("thread-delete-1"), - }, - { - type: "thread.delete", - commandId: projectDeleteCommand.commandId, - threadId: asThreadId("thread-delete-2"), - }, - { - type: "project.delete", - commandId: projectDeleteCommand.commandId, - projectId: asProjectId("project-delete"), - }, - ] satisfies ReadonlyArray) { - const decided = await Effect.runPromise( - decideOrchestrationCommand({ + let sequentialReadModel = readModel; + let nextSequence = readModel.snapshotSequence; + const sequentialEvents: PlannedEvent[] = []; + for (const nextCommand of [ + { + type: "thread.delete", + commandId: projectDeleteCommand.commandId, + threadId: asThreadId("thread-delete-1"), + }, + { + type: "thread.delete", + commandId: projectDeleteCommand.commandId, + threadId: asThreadId("thread-delete-2"), + }, + { + type: "project.delete", + commandId: projectDeleteCommand.commandId, + projectId: asProjectId("project-delete"), + }, + ] satisfies ReadonlyArray) { + const decided = yield* decideOrchestrationCommand({ command: nextCommand, readModel: sequentialReadModel, - }), - ); - const nextEvents = Array.isArray(decided) ? decided : [decided]; - sequentialEvents.push(...nextEvents); - for (const nextEvent of nextEvents) { - nextSequence += 1; - sequentialReadModel = await Effect.runPromise( - projectEvent(sequentialReadModel, { + }); + const nextEvents = Array.isArray(decided) ? decided : [decided]; + sequentialEvents.push(...nextEvents); + for (const nextEvent of nextEvents) { + nextSequence += 1; + sequentialReadModel = yield* projectEvent(sequentialReadModel, { ...nextEvent, sequence: nextSequence, - }), - ); + }); + } } - } - expect(normalizeDeleteEvent(forcedResult)).toEqual(normalizeDeleteEvent(sequentialEvents)); - }); + expect(normalizeDeleteEvent(forcedResult)).toEqual(normalizeDeleteEvent(sequentialEvents)); + }), + ); }); diff --git a/apps/server/src/orchestration/decider.projectScripts.test.ts b/apps/server/src/orchestration/decider.projectScripts.test.ts index c5b7086eb12..64ba159c740 100644 --- a/apps/server/src/orchestration/decider.projectScripts.test.ts +++ b/apps/server/src/orchestration/decider.projectScripts.test.ts @@ -8,8 +8,9 @@ import { ProviderInstanceId, } from "@t3tools/contracts"; import { createModelSelection } from "@t3tools/shared/model"; -import { describe, expect, it } from "vitest"; +import { expect, it } from "@effect/vitest"; import * as Effect from "effect/Effect"; +import * as NodeServices from "@effect/platform-node/NodeServices"; import { decideOrchestrationCommand } from "./decider.ts"; import { createEmptyReadModel, projectEvent } from "./projector.ts"; @@ -17,14 +18,13 @@ import { createEmptyReadModel, projectEvent } from "./projector.ts"; const asEventId = (value: string): EventId => EventId.make(value); const asProjectId = (value: string): ProjectId => ProjectId.make(value); const asMessageId = (value: string): MessageId => MessageId.make(value); +it.layer(NodeServices.layer)("decider project scripts", (it) => { + it.effect("emits empty scripts on project.create", () => + Effect.gen(function* () { + const now = "2026-01-01T00:00:00.000Z"; + const readModel = createEmptyReadModel(now); -describe("decider project scripts", () => { - it("emits empty scripts on project.create", async () => { - const now = "2026-01-01T00:00:00.000Z"; - const readModel = createEmptyReadModel(now); - - const result = await Effect.runPromise( - decideOrchestrationCommand({ + const result = yield* decideOrchestrationCommand({ command: { type: "project.create", commandId: CommandId.make("cmd-project-create-scripts"), @@ -34,19 +34,19 @@ describe("decider project scripts", () => { createdAt: now, }, readModel, - }), - ); + }); - const event = Array.isArray(result) ? result[0] : result; - expect(event.type).toBe("project.created"); - expect((event.payload as { scripts: unknown[] }).scripts).toEqual([]); - }); + const event = Array.isArray(result) ? result[0] : result; + expect(event.type).toBe("project.created"); + expect((event.payload as { scripts: unknown[] }).scripts).toEqual([]); + }), + ); - it("propagates scripts in project.meta.update payload", async () => { - const now = "2026-01-01T00:00:00.000Z"; - const initial = createEmptyReadModel(now); - const readModel = await Effect.runPromise( - projectEvent(initial, { + it.effect("propagates scripts in project.meta.update payload", () => + Effect.gen(function* () { + const now = "2026-01-01T00:00:00.000Z"; + const initial = createEmptyReadModel(now); + const readModel = yield* projectEvent(initial, { sequence: 1, eventId: asEventId("evt-project-create-scripts"), aggregateKind: "project", @@ -66,21 +66,19 @@ describe("decider project scripts", () => { createdAt: now, updatedAt: now, }, - }), - ); + }); - const scripts = [ - { - id: "lint", - name: "Lint", - command: "bun run lint", - icon: "lint", - runOnWorktreeCreate: false, - }, - ] as const; + const scripts = [ + { + id: "lint", + name: "Lint", + command: "bun run lint", + icon: "lint", + runOnWorktreeCreate: false, + }, + ] as const; - const result = await Effect.runPromise( - decideOrchestrationCommand({ + const result = yield* decideOrchestrationCommand({ command: { type: "project.meta.update", commandId: CommandId.make("cmd-project-update-scripts"), @@ -88,19 +86,19 @@ describe("decider project scripts", () => { scripts: Array.from(scripts), }, readModel, - }), - ); + }); - const event = Array.isArray(result) ? result[0] : result; - expect(event.type).toBe("project.meta-updated"); - expect((event.payload as { scripts?: unknown[] }).scripts).toEqual(scripts); - }); + const event = Array.isArray(result) ? result[0] : result; + expect(event.type).toBe("project.meta-updated"); + expect((event.payload as { scripts?: unknown[] }).scripts).toEqual(scripts); + }), + ); - it("emits user message and turn-start-requested events for thread.turn.start", async () => { - const now = "2026-01-01T00:00:00.000Z"; - const initial = createEmptyReadModel(now); - const withProject = await Effect.runPromise( - projectEvent(initial, { + it.effect("emits user message and turn-start-requested events for thread.turn.start", () => + Effect.gen(function* () { + const now = "2026-01-01T00:00:00.000Z"; + const initial = createEmptyReadModel(now); + const withProject = yield* projectEvent(initial, { sequence: 1, eventId: asEventId("evt-project-create"), aggregateKind: "project", @@ -120,10 +118,8 @@ describe("decider project scripts", () => { createdAt: now, updatedAt: now, }, - }), - ); - const readModel = await Effect.runPromise( - projectEvent(withProject, { + }); + const readModel = yield* projectEvent(withProject, { sequence: 2, eventId: asEventId("evt-thread-create"), aggregateKind: "thread", @@ -149,11 +145,9 @@ describe("decider project scripts", () => { createdAt: now, updatedAt: now, }, - }), - ); + }); - const result = await Effect.runPromise( - decideOrchestrationCommand({ + const result = yield* decideOrchestrationCommand({ command: { type: "thread.turn.start", commandId: CommandId.make("cmd-turn-start"), @@ -173,35 +167,35 @@ describe("decider project scripts", () => { createdAt: now, }, readModel, - }), - ); + }); - expect(Array.isArray(result)).toBe(true); - const events = Array.isArray(result) ? result : [result]; - expect(events).toHaveLength(2); - expect(events[0]?.type).toBe("thread.message-sent"); - const turnStartEvent = events[1]; - expect(turnStartEvent?.type).toBe("thread.turn-start-requested"); - expect(turnStartEvent?.causationEventId).toBe(events[0]?.eventId ?? null); - if (turnStartEvent?.type !== "thread.turn-start-requested") { - return; - } - expect(turnStartEvent.payload).toMatchObject({ - threadId: ThreadId.make("thread-1"), - messageId: asMessageId("message-user-1"), - modelSelection: createModelSelection(ProviderInstanceId.make("codex"), "gpt-5.3-codex", [ - { id: "reasoningEffort", value: "high" }, - { id: "fastMode", value: true }, - ]), - runtimeMode: "approval-required", - }); - }); + expect(Array.isArray(result)).toBe(true); + const events = Array.isArray(result) ? result : [result]; + expect(events).toHaveLength(2); + expect(events[0]?.type).toBe("thread.message-sent"); + const turnStartEvent = events[1]; + expect(turnStartEvent?.type).toBe("thread.turn-start-requested"); + expect(turnStartEvent?.causationEventId).toBe(events[0]?.eventId ?? null); + if (turnStartEvent?.type !== "thread.turn-start-requested") { + return; + } + expect(turnStartEvent.payload).toMatchObject({ + threadId: ThreadId.make("thread-1"), + messageId: asMessageId("message-user-1"), + modelSelection: createModelSelection(ProviderInstanceId.make("codex"), "gpt-5.3-codex", [ + { id: "reasoningEffort", value: "high" }, + { id: "fastMode", value: true }, + ]), + runtimeMode: "approval-required", + }); + }), + ); - it("emits thread.runtime-mode-set from thread.runtime-mode.set", async () => { - const now = "2026-01-01T00:00:00.000Z"; - const initial = createEmptyReadModel(now); - const withProject = await Effect.runPromise( - projectEvent(initial, { + it.effect("emits thread.runtime-mode-set from thread.runtime-mode.set", () => + Effect.gen(function* () { + const now = "2026-01-01T00:00:00.000Z"; + const initial = createEmptyReadModel(now); + const withProject = yield* projectEvent(initial, { sequence: 1, eventId: asEventId("evt-project-create"), aggregateKind: "project", @@ -221,10 +215,8 @@ describe("decider project scripts", () => { createdAt: now, updatedAt: now, }, - }), - ); - const readModel = await Effect.runPromise( - projectEvent(withProject, { + }); + const readModel = yield* projectEvent(withProject, { sequence: 2, eventId: asEventId("evt-thread-create"), aggregateKind: "thread", @@ -250,11 +242,9 @@ describe("decider project scripts", () => { createdAt: now, updatedAt: now, }, - }), - ); + }); - const result = await Effect.runPromise( - decideOrchestrationCommand({ + const result = yield* decideOrchestrationCommand({ command: { type: "thread.runtime-mode.set", commandId: CommandId.make("cmd-runtime-mode-set"), @@ -263,27 +253,27 @@ describe("decider project scripts", () => { createdAt: now, }, readModel, - }), - ); + }); - const singleResult = Array.isArray(result) ? null : result; - if (singleResult === null) { - throw new Error("Expected a single runtime-mode-set event."); - } - expect(singleResult).toMatchObject({ - type: "thread.runtime-mode-set", - payload: { - threadId: ThreadId.make("thread-1"), - runtimeMode: "approval-required", - }, - }); - }); + const singleResult = Array.isArray(result) ? null : result; + if (singleResult === null) { + throw new Error("Expected a single runtime-mode-set event."); + } + expect(singleResult).toMatchObject({ + type: "thread.runtime-mode-set", + payload: { + threadId: ThreadId.make("thread-1"), + runtimeMode: "approval-required", + }, + }); + }), + ); - it("emits thread.interaction-mode-set from thread.interaction-mode.set", async () => { - const now = "2026-01-01T00:00:00.000Z"; - const initial = createEmptyReadModel(now); - const withProject = await Effect.runPromise( - projectEvent(initial, { + it.effect("emits thread.interaction-mode-set from thread.interaction-mode.set", () => + Effect.gen(function* () { + const now = "2026-01-01T00:00:00.000Z"; + const initial = createEmptyReadModel(now); + const withProject = yield* projectEvent(initial, { sequence: 1, eventId: asEventId("evt-project-create"), aggregateKind: "project", @@ -303,10 +293,8 @@ describe("decider project scripts", () => { createdAt: now, updatedAt: now, }, - }), - ); - const readModel = await Effect.runPromise( - projectEvent(withProject, { + }); + const readModel = yield* projectEvent(withProject, { sequence: 2, eventId: asEventId("evt-thread-create"), aggregateKind: "thread", @@ -332,11 +320,9 @@ describe("decider project scripts", () => { createdAt: now, updatedAt: now, }, - }), - ); + }); - const result = await Effect.runPromise( - decideOrchestrationCommand({ + const result = yield* decideOrchestrationCommand({ command: { type: "thread.interaction-mode.set", commandId: CommandId.make("cmd-interaction-mode-set"), @@ -345,19 +331,19 @@ describe("decider project scripts", () => { createdAt: now, }, readModel, - }), - ); + }); - const singleResult = Array.isArray(result) ? null : result; - if (singleResult === null) { - throw new Error("Expected a single interaction-mode-set event."); - } - expect(singleResult).toMatchObject({ - type: "thread.interaction-mode-set", - payload: { - threadId: ThreadId.make("thread-1"), - interactionMode: "plan", - }, - }); - }); + const singleResult = Array.isArray(result) ? null : result; + if (singleResult === null) { + throw new Error("Expected a single interaction-mode-set event."); + } + expect(singleResult).toMatchObject({ + type: "thread.interaction-mode-set", + payload: { + threadId: ThreadId.make("thread-1"), + interactionMode: "plan", + }, + }); + }), + ); }); diff --git a/apps/server/src/orchestration/decider.ts b/apps/server/src/orchestration/decider.ts index 1004c945dbf..0d4af771ca8 100644 --- a/apps/server/src/orchestration/decider.ts +++ b/apps/server/src/orchestration/decider.ts @@ -1,10 +1,13 @@ -import type { - OrchestrationCommand, - OrchestrationEvent, - OrchestrationReadModel, +import { + EventId, + type OrchestrationCommand, + type OrchestrationEvent, + type OrchestrationReadModel, } from "@t3tools/contracts"; import * as DateTime from "effect/DateTime"; +import * as Crypto from "effect/Crypto"; import * as Effect from "effect/Effect"; +import type * as PlatformError from "effect/PlatformError"; import { OrchestrationCommandInvariantError } from "./Errors.ts"; import { @@ -27,17 +30,27 @@ function withEventBase( readonly occurredAt: string; readonly metadata?: OrchestrationEvent["metadata"]; }, -): Omit { - return { - eventId: crypto.randomUUID() as OrchestrationEvent["eventId"], - aggregateKind: input.aggregateKind, - aggregateId: input.aggregateId, - occurredAt: input.occurredAt, - commandId: input.commandId, - causationEventId: null, - correlationId: input.commandId, - metadata: input.metadata ?? {}, - }; +): Effect.Effect< + Omit, + PlatformError.PlatformError, + Crypto.Crypto +> { + return Crypto.Crypto.pipe( + Effect.flatMap((crypto) => + crypto.randomUUIDv4.pipe( + Effect.map((eventId) => ({ + eventId: EventId.make(eventId), + aggregateKind: input.aggregateKind, + aggregateId: input.aggregateId, + occurredAt: input.occurredAt, + commandId: input.commandId, + causationEventId: null, + correlationId: input.commandId, + metadata: input.metadata ?? {}, + })), + ), + ), + ); } type PlannedOrchestrationEvent = Omit; @@ -52,7 +65,11 @@ const decideCommandSequence = Effect.fn("decideCommandSequence")(function* ({ }: { readonly commands: ReadonlyArray; readonly readModel: OrchestrationReadModel; -}): Effect.fn.Return, OrchestrationCommandInvariantError> { +}): Effect.fn.Return< + ReadonlyArray, + OrchestrationCommandInvariantError | PlatformError.PlatformError, + Crypto.Crypto +> { let nextReadModel = readModel; let nextSequence = readModel.snapshotSequence; const plannedEvents: PlannedOrchestrationEvent[] = []; @@ -82,7 +99,11 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" }: { readonly command: OrchestrationCommand; readonly readModel: OrchestrationReadModel; -}): Effect.fn.Return { +}): Effect.fn.Return< + DecideOrchestrationCommandResult, + OrchestrationCommandInvariantError | PlatformError.PlatformError, + Crypto.Crypto +> { switch (command.type) { case "project.create": { yield* requireProjectAbsent({ @@ -92,12 +113,12 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" }); return { - ...withEventBase({ + ...(yield* withEventBase({ aggregateKind: "project", aggregateId: command.projectId, occurredAt: command.createdAt, commandId: command.commandId, - }), + })), type: "project.created", payload: { projectId: command.projectId, @@ -119,12 +140,12 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" }); const occurredAt = yield* nowIso; return { - ...withEventBase({ + ...(yield* withEventBase({ aggregateKind: "project", aggregateId: command.projectId, occurredAt, commandId: command.commandId, - }), + })), type: "project.meta-updated", payload: { projectId: command.projectId, @@ -176,12 +197,12 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" const occurredAt = yield* nowIso; return { - ...withEventBase({ + ...(yield* withEventBase({ aggregateKind: "project", aggregateId: command.projectId, occurredAt, commandId: command.commandId, - }), + })), type: "project.deleted" as const, payload: { projectId: command.projectId, @@ -202,12 +223,12 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" threadId: command.threadId, }); return { - ...withEventBase({ + ...(yield* withEventBase({ aggregateKind: "thread", aggregateId: command.threadId, occurredAt: command.createdAt, commandId: command.commandId, - }), + })), type: "thread.created", payload: { threadId: command.threadId, @@ -232,12 +253,12 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" }); const occurredAt = yield* nowIso; return { - ...withEventBase({ + ...(yield* withEventBase({ aggregateKind: "thread", aggregateId: command.threadId, occurredAt, commandId: command.commandId, - }), + })), type: "thread.deleted", payload: { threadId: command.threadId, @@ -254,12 +275,12 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" }); const occurredAt = yield* nowIso; return { - ...withEventBase({ + ...(yield* withEventBase({ aggregateKind: "thread", aggregateId: command.threadId, occurredAt, commandId: command.commandId, - }), + })), type: "thread.archived", payload: { threadId: command.threadId, @@ -277,12 +298,12 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" }); const occurredAt = yield* nowIso; return { - ...withEventBase({ + ...(yield* withEventBase({ aggregateKind: "thread", aggregateId: command.threadId, occurredAt, commandId: command.commandId, - }), + })), type: "thread.unarchived", payload: { threadId: command.threadId, @@ -299,12 +320,12 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" }); const occurredAt = yield* nowIso; return { - ...withEventBase({ + ...(yield* withEventBase({ aggregateKind: "thread", aggregateId: command.threadId, occurredAt, commandId: command.commandId, - }), + })), type: "thread.meta-updated", payload: { threadId: command.threadId, @@ -327,12 +348,12 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" }); const occurredAt = yield* nowIso; return { - ...withEventBase({ + ...(yield* withEventBase({ aggregateKind: "thread", aggregateId: command.threadId, occurredAt, commandId: command.commandId, - }), + })), type: "thread.runtime-mode-set", payload: { threadId: command.threadId, @@ -350,12 +371,12 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" }); const occurredAt = yield* nowIso; return { - ...withEventBase({ + ...(yield* withEventBase({ aggregateKind: "thread", aggregateId: command.threadId, occurredAt, commandId: command.commandId, - }), + })), type: "thread.interaction-mode-set", payload: { threadId: command.threadId, @@ -396,12 +417,12 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" }); } const userMessageEvent: Omit = { - ...withEventBase({ + ...(yield* withEventBase({ aggregateKind: "thread", aggregateId: command.threadId, occurredAt: command.createdAt, commandId: command.commandId, - }), + })), type: "thread.message-sent", payload: { threadId: command.threadId, @@ -416,12 +437,12 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" }, }; const turnStartRequestedEvent: Omit = { - ...withEventBase({ + ...(yield* withEventBase({ aggregateKind: "thread", aggregateId: command.threadId, occurredAt: command.createdAt, commandId: command.commandId, - }), + })), causationEventId: userMessageEvent.eventId, type: "thread.turn-start-requested", payload: { @@ -447,12 +468,12 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" threadId: command.threadId, }); return { - ...withEventBase({ + ...(yield* withEventBase({ aggregateKind: "thread", aggregateId: command.threadId, occurredAt: command.createdAt, commandId: command.commandId, - }), + })), type: "thread.turn-interrupt-requested", payload: { threadId: command.threadId, @@ -469,7 +490,7 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" threadId: command.threadId, }); return { - ...withEventBase({ + ...(yield* withEventBase({ aggregateKind: "thread", aggregateId: command.threadId, occurredAt: command.createdAt, @@ -477,7 +498,7 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" metadata: { requestId: command.requestId, }, - }), + })), type: "thread.approval-response-requested", payload: { threadId: command.threadId, @@ -495,7 +516,7 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" threadId: command.threadId, }); return { - ...withEventBase({ + ...(yield* withEventBase({ aggregateKind: "thread", aggregateId: command.threadId, occurredAt: command.createdAt, @@ -503,7 +524,7 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" metadata: { requestId: command.requestId, }, - }), + })), type: "thread.user-input-response-requested", payload: { threadId: command.threadId, @@ -521,12 +542,12 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" threadId: command.threadId, }); return { - ...withEventBase({ + ...(yield* withEventBase({ aggregateKind: "thread", aggregateId: command.threadId, occurredAt: command.createdAt, commandId: command.commandId, - }), + })), type: "thread.checkpoint-revert-requested", payload: { threadId: command.threadId, @@ -543,12 +564,12 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" threadId: command.threadId, }); return { - ...withEventBase({ + ...(yield* withEventBase({ aggregateKind: "thread", aggregateId: command.threadId, occurredAt: command.createdAt, commandId: command.commandId, - }), + })), type: "thread.session-stop-requested", payload: { threadId: command.threadId, @@ -564,13 +585,13 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" threadId: command.threadId, }); return { - ...withEventBase({ + ...(yield* withEventBase({ aggregateKind: "thread", aggregateId: command.threadId, occurredAt: command.createdAt, commandId: command.commandId, metadata: {}, - }), + })), type: "thread.session-set", payload: { threadId: command.threadId, @@ -586,12 +607,12 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" threadId: command.threadId, }); return { - ...withEventBase({ + ...(yield* withEventBase({ aggregateKind: "thread", aggregateId: command.threadId, occurredAt: command.createdAt, commandId: command.commandId, - }), + })), type: "thread.message-sent", payload: { threadId: command.threadId, @@ -613,12 +634,12 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" threadId: command.threadId, }); return { - ...withEventBase({ + ...(yield* withEventBase({ aggregateKind: "thread", aggregateId: command.threadId, occurredAt: command.createdAt, commandId: command.commandId, - }), + })), type: "thread.message-sent", payload: { threadId: command.threadId, @@ -640,12 +661,12 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" threadId: command.threadId, }); return { - ...withEventBase({ + ...(yield* withEventBase({ aggregateKind: "thread", aggregateId: command.threadId, occurredAt: command.createdAt, commandId: command.commandId, - }), + })), type: "thread.proposed-plan-upserted", payload: { threadId: command.threadId, @@ -661,12 +682,12 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" threadId: command.threadId, }); return { - ...withEventBase({ + ...(yield* withEventBase({ aggregateKind: "thread", aggregateId: command.threadId, occurredAt: command.createdAt, commandId: command.commandId, - }), + })), type: "thread.turn-diff-completed", payload: { threadId: command.threadId, @@ -688,12 +709,12 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" threadId: command.threadId, }); return { - ...withEventBase({ + ...(yield* withEventBase({ aggregateKind: "thread", aggregateId: command.threadId, occurredAt: command.createdAt, commandId: command.commandId, - }), + })), type: "thread.reverted", payload: { threadId: command.threadId, @@ -717,13 +738,13 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand" .requestId as OrchestrationEvent["metadata"]["requestId"]) : undefined; return { - ...withEventBase({ + ...(yield* withEventBase({ aggregateKind: "thread", aggregateId: command.threadId, occurredAt: command.createdAt, commandId: command.commandId, ...(requestId !== undefined ? { metadata: { requestId } } : {}), - }), + })), type: "thread.activity-appended", payload: { threadId: command.threadId, diff --git a/apps/server/src/persistence/NodeSqliteClient.ts b/apps/server/src/persistence/NodeSqliteClient.ts index 631df9c8b92..6b91b5bd07b 100644 --- a/apps/server/src/persistence/NodeSqliteClient.ts +++ b/apps/server/src/persistence/NodeSqliteClient.ts @@ -252,14 +252,12 @@ export const layerConfig = ( config: Config.Wrap, ): Layer.Layer => Layer.effectContext( - Config.unwrap(config) - .asEffect() - .pipe( - Effect.flatMap(make), - Effect.map((client) => - Context.make(SqliteClient, client).pipe(Context.add(Client.SqlClient, client)), - ), + Config.unwrap(config).pipe( + Effect.flatMap(make), + Effect.map((client) => + Context.make(SqliteClient, client).pipe(Context.add(Client.SqlClient, client)), ), + ), ).pipe(Layer.provide(Reactivity.layer)); export const layer = (config: SqliteClientConfig): Layer.Layer => diff --git a/apps/server/src/process/externalLauncher.test.ts b/apps/server/src/process/externalLauncher.test.ts index 151600c47d9..75e76b5e8e2 100644 --- a/apps/server/src/process/externalLauncher.test.ts +++ b/apps/server/src/process/externalLauncher.test.ts @@ -1,12 +1,12 @@ import * as NodeServices from "@effect/platform-node/NodeServices"; import { assert, it } from "@effect/vitest"; import { assertSuccess } from "@effect/vitest/utils"; +import * as Crypto from "effect/Crypto"; import * as Effect from "effect/Effect"; import * as Encoding from "effect/Encoding"; import * as FileSystem from "effect/FileSystem"; import * as Layer from "effect/Layer"; import * as Path from "effect/Path"; -import * as Random from "effect/Random"; import * as Sink from "effect/Sink"; import * as Stream from "effect/Stream"; import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"; @@ -651,7 +651,9 @@ it.layer(NodeServices.layer)("launchEditorProcess", (it) => { Effect.gen(function* () { const spawnerLayer = Layer.mock(ChildProcessSpawner.ChildProcessSpawner, {}); const result = yield* launchEditorProcess({ - command: `t3code-no-such-command-${yield* Random.nextUUIDv4}`, + command: `t3code-no-such-command-${yield* Crypto.Crypto.pipe( + Effect.flatMap((crypto) => crypto.randomUUIDv4), + )}`, args: [], }).pipe(Effect.provide(spawnerLayer), Effect.result); assert.equal(result._tag, "Failure"); diff --git a/apps/server/src/provider/Drivers/ClaudeDriver.ts b/apps/server/src/provider/Drivers/ClaudeDriver.ts index e3f15d865c9..b126028f813 100644 --- a/apps/server/src/provider/Drivers/ClaudeDriver.ts +++ b/apps/server/src/provider/Drivers/ClaudeDriver.ts @@ -15,6 +15,7 @@ import { ClaudeSettings, ProviderDriverKind, type ServerProvider } from "@t3tools/contracts"; import * as Cache from "effect/Cache"; import * as Duration from "effect/Duration"; +import * as Crypto from "effect/Crypto"; import * as Effect from "effect/Effect"; import * as FileSystem from "effect/FileSystem"; import * as Path from "effect/Path"; @@ -77,6 +78,7 @@ const UPDATE = makePackageManagedProviderMaintenanceResolver({ export type ClaudeDriverEnv = | ChildProcessSpawner.ChildProcessSpawner + | Crypto.Crypto | FileSystem.FileSystem | HttpClient.HttpClient | Path.Path diff --git a/apps/server/src/provider/Drivers/CodexDriver.ts b/apps/server/src/provider/Drivers/CodexDriver.ts index 48bc19e5612..441edda479f 100644 --- a/apps/server/src/provider/Drivers/CodexDriver.ts +++ b/apps/server/src/provider/Drivers/CodexDriver.ts @@ -23,6 +23,7 @@ */ import { CodexSettings, ProviderDriverKind, type ServerProvider } from "@t3tools/contracts"; import * as Duration from "effect/Duration"; +import * as Crypto from "effect/Crypto"; import * as Effect from "effect/Effect"; import * as FileSystem from "effect/FileSystem"; import * as Path from "effect/Path"; @@ -69,6 +70,7 @@ const UPDATE = makePackageManagedProviderMaintenanceResolver({ */ export type CodexDriverEnv = | ChildProcessSpawner.ChildProcessSpawner + | Crypto.Crypto | FileSystem.FileSystem | HttpClient.HttpClient | Path.Path diff --git a/apps/server/src/provider/Drivers/CursorDriver.ts b/apps/server/src/provider/Drivers/CursorDriver.ts index b399f9aa948..46347091b4c 100644 --- a/apps/server/src/provider/Drivers/CursorDriver.ts +++ b/apps/server/src/provider/Drivers/CursorDriver.ts @@ -14,6 +14,7 @@ */ import { CursorSettings, ProviderDriverKind, type ServerProvider } from "@t3tools/contracts"; import * as Duration from "effect/Duration"; +import * as Crypto from "effect/Crypto"; import * as Effect from "effect/Effect"; import * as FileSystem from "effect/FileSystem"; import * as Path from "effect/Path"; @@ -61,6 +62,7 @@ const UPDATE = makeStaticProviderMaintenanceResolver( export type CursorDriverEnv = | ChildProcessSpawner.ChildProcessSpawner + | Crypto.Crypto | FileSystem.FileSystem | HttpClient.HttpClient | Path.Path diff --git a/apps/server/src/provider/Drivers/OpenCodeDriver.ts b/apps/server/src/provider/Drivers/OpenCodeDriver.ts index 816e8b70f55..e7216f83366 100644 --- a/apps/server/src/provider/Drivers/OpenCodeDriver.ts +++ b/apps/server/src/provider/Drivers/OpenCodeDriver.ts @@ -14,6 +14,7 @@ */ import { OpenCodeSettings, ProviderDriverKind, type ServerProvider } from "@t3tools/contracts"; import * as Duration from "effect/Duration"; +import * as Crypto from "effect/Crypto"; import * as Effect from "effect/Effect"; import * as FileSystem from "effect/FileSystem"; import * as Path from "effect/Path"; @@ -73,6 +74,7 @@ const UPDATE = makePackageManagedProviderMaintenanceResolver({ export type OpenCodeDriverEnv = | ChildProcessSpawner.ChildProcessSpawner + | Crypto.Crypto | FileSystem.FileSystem | HttpClient.HttpClient | OpenCodeRuntime diff --git a/apps/server/src/provider/Layers/ClaudeAdapter.ts b/apps/server/src/provider/Layers/ClaudeAdapter.ts index e9bd8fb7a16..6e7fa57b4f4 100644 --- a/apps/server/src/provider/Layers/ClaudeAdapter.ts +++ b/apps/server/src/provider/Layers/ClaudeAdapter.ts @@ -52,6 +52,7 @@ import { resolvePromptInjectedEffort, } from "@t3tools/shared/model"; import * as Cause from "effect/Cause"; +import * as Crypto from "effect/Crypto"; import * as DateTime from "effect/DateTime"; import * as Deferred from "effect/Deferred"; import * as Effect from "effect/Effect"; @@ -60,7 +61,6 @@ import * as FileSystem from "effect/FileSystem"; import * as Fiber from "effect/Fiber"; import * as Path from "effect/Path"; import * as Queue from "effect/Queue"; -import * as Random from "effect/Random"; import * as Ref from "effect/Ref"; import * as Schema from "effect/Schema"; import * as Stream from "effect/Stream"; @@ -998,6 +998,7 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* ( const fileSystem = yield* FileSystem.FileSystem; const path = yield* Path.Path; const serverConfig = yield* ServerConfig; + const crypto = yield* Crypto.Crypto; const claudeEnvironment = yield* makeClaudeEnvironment(claudeSettings, options?.environment).pipe( Effect.provideService(Path.Path, path), ); @@ -1024,7 +1025,18 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* ( const runtimeEventQueue = yield* Queue.unbounded(); const nowIso = Effect.map(DateTime.now, DateTime.formatIso); - const nextEventId = Effect.map(Random.nextUUIDv4, (id) => EventId.make(id)); + const randomUUIDv4 = crypto.randomUUIDv4.pipe( + Effect.mapError( + (cause) => + new ProviderAdapterRequestError({ + provider: PROVIDER, + method: "crypto/randomUUIDv4", + detail: "Failed to generate Claude runtime identifier.", + cause, + }), + ), + ); + const nextEventId = Effect.map(randomUUIDv4, (id) => EventId.make(id)); const makeEventStamp = () => Effect.all({ eventId: nextEventId, createdAt: nowIso }); const offerRuntimeEvent = (event: ProviderRuntimeEvent): Effect.Effect => @@ -1048,7 +1060,7 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* ( id: "uuid" in message && typeof message.uuid === "string" ? message.uuid - : yield* Random.nextUUIDv4, + : yield* randomUUIDv4, kind: "notification", provider: PROVIDER, createdAt: observedAt, @@ -1132,7 +1144,7 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* ( } const block: AssistantTextBlockState = { - itemId: yield* Random.nextUUIDv4, + itemId: yield* randomUUIDv4, blockIndex, emittedTextDelta: false, fallbackText: options?.fallbackText ?? "", @@ -1965,7 +1977,7 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* ( // Auto-start a synthetic turn for assistant messages that arrive without // an active turn (e.g., background agent/subagent responses between user prompts). if (!context.turnState) { - const turnId = TurnId.make(yield* Random.nextUUIDv4); + const turnId = TurnId.make(yield* randomUUIDv4); const startedAt = yield* nowIso; context.turnState = { turnId, @@ -2378,7 +2390,17 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* ( toProcessError(cause, "Claude runtime stream failed.", context.session.threadId), ).pipe( Stream.takeWhile(() => !context.stopped), - Stream.runForEach((message) => handleSdkMessage(context, message)), + Stream.runForEach((message) => + handleSdkMessage(context, message).pipe( + Effect.mapError((cause) => + toProcessError( + cause, + "Failed to process Claude runtime event.", + context.session.threadId, + ), + ), + ), + ), ); const handleStreamExit = Effect.fn("handleStreamExit")(function* ( @@ -2552,8 +2574,7 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* ( const resumeState = readClaudeResumeState(input.resumeCursor); const threadId = input.threadId; const existingResumeSessionId = resumeState?.resume; - const newSessionId = - existingResumeSessionId === undefined ? yield* Random.nextUUIDv4 : undefined; + const newSessionId = existingResumeSessionId === undefined ? yield* randomUUIDv4 : undefined; const sessionId = existingResumeSessionId ?? newSessionId; const runtimeContext = yield* Effect.context(); @@ -2588,7 +2609,7 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* ( readonly toolUseID?: string; }, ) { - const requestId = ApprovalRequestId.make(yield* Random.nextUUIDv4); + const requestId = ApprovalRequestId.make(yield* randomUUIDv4); // Parse questions from the SDK's AskUserQuestion input. // `id` MUST equal the full question text — Claude SDK >= 2.1.121 looks @@ -2758,7 +2779,7 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* ( } satisfies PermissionResult; } - const requestId = ApprovalRequestId.make(yield* Random.nextUUIDv4); + const requestId = ApprovalRequestId.make(yield* randomUUIDv4); const requestType = classifyRequestType(toolName); const detail = summarizeToolRequest(toolName, toolInput); const decisionDeferred = yield* Deferred.make(); @@ -3059,7 +3080,11 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* ( if (context.streamFiber === streamFiber) { context.streamFiber = undefined; } - return handleStreamExit(context, exit); + return handleStreamExit(context, exit).pipe( + Effect.catch((cause) => + Effect.logError("Failed to close Claude runtime stream.", { cause }), + ), + ); }), ), ); @@ -3120,7 +3145,7 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* ( }); } - const turnId = TurnId.make(yield* Random.nextUUIDv4); + const turnId = TurnId.make(yield* randomUUIDv4); const turnState: ClaudeTurnState = { turnId, startedAt: yield* nowIso, @@ -3269,7 +3294,12 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* ( emitExitEvent: false, }), { discard: true }, - ).pipe(Effect.tap(() => Queue.shutdown(runtimeEventQueue))), + ).pipe( + Effect.catch((cause) => + Effect.logError("Failed to emit Claude session shutdown event.", { cause }), + ), + Effect.tap(() => Queue.shutdown(runtimeEventQueue)), + ), ); return { diff --git a/apps/server/src/provider/Layers/CodexAdapter.ts b/apps/server/src/provider/Layers/CodexAdapter.ts index 28af1cda27b..9893cf6c149 100644 --- a/apps/server/src/provider/Layers/CodexAdapter.ts +++ b/apps/server/src/provider/Layers/CodexAdapter.ts @@ -25,6 +25,7 @@ import { ProviderSendTurnInput, } from "@t3tools/contracts"; import * as Effect from "effect/Effect"; +import * as Crypto from "effect/Crypto"; import * as Exit from "effect/Exit"; import * as Fiber from "effect/Fiber"; import * as FileSystem from "effect/FileSystem"; @@ -1349,6 +1350,7 @@ export const makeCodexAdapter = Effect.fn("makeCodexAdapter")(function* ( const boundInstanceId = options?.instanceId ?? ProviderInstanceId.make("codex"); const fileSystem = yield* FileSystem.FileSystem; const childProcessSpawner = yield* ChildProcessSpawner.ChildProcessSpawner; + const crypto = yield* Crypto.Crypto; const serverConfig = yield* Effect.service(ServerConfig); const nativeEventLogger = options?.nativeEventLogger ?? @@ -1406,6 +1408,7 @@ export const makeCodexAdapter = Effect.fn("makeCodexAdapter")(function* ( const runtime = yield* createRuntime(runtimeInput).pipe( Effect.provideService(Scope.Scope, sessionScope), Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, childProcessSpawner), + Effect.provideService(Crypto.Crypto, crypto), Effect.mapError( (cause) => new ProviderAdapterProcessError({ diff --git a/apps/server/src/provider/Layers/CodexSessionRuntime.ts b/apps/server/src/provider/Layers/CodexSessionRuntime.ts index 7f71ef46b2c..f9b9c6ab4fb 100644 --- a/apps/server/src/provider/Layers/CodexSessionRuntime.ts +++ b/apps/server/src/provider/Layers/CodexSessionRuntime.ts @@ -17,6 +17,7 @@ import { TurnId, } from "@t3tools/contracts"; import { normalizeModelSlug } from "@t3tools/shared/model"; +import * as Crypto from "effect/Crypto"; import * as DateTime from "effect/DateTime"; import * as Deferred from "effect/Deferred"; import * as Effect from "effect/Effect"; @@ -25,7 +26,6 @@ import * as Layer from "effect/Layer"; import * as Queue from "effect/Queue"; import * as Ref from "effect/Ref"; import * as Scope from "effect/Scope"; -import * as Random from "effect/Random"; import * as Schema from "effect/Schema"; import * as Stream from "effect/Stream"; import * as SchemaIssue from "effect/SchemaIssue"; @@ -697,11 +697,12 @@ export const makeCodexSessionRuntime = ( ): Effect.Effect< CodexSessionRuntimeShape, CodexErrors.CodexAppServerError, - ChildProcessSpawner.ChildProcessSpawner | Scope.Scope + ChildProcessSpawner.ChildProcessSpawner | Crypto.Crypto | Scope.Scope > => Effect.gen(function* () { const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; const runtimeScope = yield* Scope.Scope; + const crypto = yield* Crypto.Crypto; const events = yield* Queue.unbounded(); const pendingApprovalsRef = yield* Ref.make(new Map()); const approvalCorrelationsRef = yield* Ref.make(new Map()); @@ -746,6 +747,15 @@ export const makeCodexSessionRuntime = ( ); const serverNotifications = yield* Queue.unbounded(); const nowIso = Effect.map(DateTime.now, DateTime.formatIso); + const randomUUIDv4 = crypto.randomUUIDv4.pipe( + Effect.mapError( + (cause) => + new CodexErrors.CodexAppServerTransportError({ + detail: "Failed to generate Codex runtime identifier.", + cause, + }), + ), + ); const sessionCreatedAt = yield* nowIso; const initialSession = { @@ -765,7 +775,7 @@ export const makeCodexSessionRuntime = ( const emitEvent = (event: Omit) => Effect.gen(function* () { - const id = yield* Random.nextUUIDv4; + const id = yield* randomUUIDv4; return yield* offerEvent({ id: EventId.make(id), provider: PROVIDER, @@ -933,7 +943,7 @@ export const makeCodexSessionRuntime = ( yield* client.handleServerRequest("item/commandExecution/requestApproval", (payload) => Effect.gen(function* () { - const requestId = ApprovalRequestId.make(yield* Random.nextUUIDv4); + const requestId = ApprovalRequestId.make(yield* randomUUIDv4); const turnId = TurnId.make(payload.turnId); const itemId = ProviderItemId.make(payload.itemId); const decision = yield* Deferred.make(); @@ -989,7 +999,7 @@ export const makeCodexSessionRuntime = ( yield* client.handleServerRequest("item/fileChange/requestApproval", (payload) => Effect.gen(function* () { - const requestId = ApprovalRequestId.make(yield* Random.nextUUIDv4); + const requestId = ApprovalRequestId.make(yield* randomUUIDv4); const turnId = TurnId.make(payload.turnId); const itemId = ProviderItemId.make(payload.itemId); const decision = yield* Deferred.make(); @@ -1045,7 +1055,7 @@ export const makeCodexSessionRuntime = ( yield* client.handleServerRequest("item/tool/requestUserInput", (payload) => Effect.gen(function* () { - const requestId = ApprovalRequestId.make(yield* Random.nextUUIDv4); + const requestId = ApprovalRequestId.make(yield* randomUUIDv4); const turnId = TurnId.make(payload.turnId); const itemId = ProviderItemId.make(payload.itemId); const answers = yield* Deferred.make(); @@ -1229,7 +1239,11 @@ export const makeCodexSessionRuntime = ( status: "closed", activeTurnId: undefined, }); - yield* emitSessionEvent("session/closed", "Session stopped"); + yield* emitSessionEvent("session/closed", "Session stopped").pipe( + Effect.catch((cause) => + Effect.logError("Failed to emit Codex session closed event.", { cause }), + ), + ); yield* Scope.close(runtimeScope, Exit.void); yield* Queue.shutdown(serverNotifications); yield* Queue.shutdown(events); diff --git a/apps/server/src/provider/Layers/CursorAdapter.ts b/apps/server/src/provider/Layers/CursorAdapter.ts index efef5f0a83b..0671af0d4e8 100644 --- a/apps/server/src/provider/Layers/CursorAdapter.ts +++ b/apps/server/src/provider/Layers/CursorAdapter.ts @@ -22,6 +22,7 @@ import { TurnId, } from "@t3tools/contracts"; import * as DateTime from "effect/DateTime"; +import * as Crypto from "effect/Crypto"; import * as Deferred from "effect/Deferred"; import * as Effect from "effect/Effect"; import * as Exit from "effect/Exit"; @@ -30,13 +31,13 @@ import * as FileSystem from "effect/FileSystem"; import * as Option from "effect/Option"; import * as Path from "effect/Path"; import * as PubSub from "effect/PubSub"; -import * as Random from "effect/Random"; import * as Schema from "effect/Schema"; import * as Scope from "effect/Scope"; import * as Semaphore from "effect/Semaphore"; import * as Stream from "effect/Stream"; import * as SynchronizedRef from "effect/SynchronizedRef"; import { ChildProcessSpawner } from "effect/unstable/process"; +import * as EffectAcpErrors from "effect-acp/errors"; import type * as EffectAcpSchema from "effect-acp/schema"; import { resolveAttachmentPath } from "../../attachmentStore.ts"; @@ -62,7 +63,7 @@ import { type AcpSessionModeState, parsePermissionRequest, } from "../acp/AcpRuntimeModel.ts"; -import { makeAcpNativeLoggers } from "../acp/AcpNativeLogging.ts"; +import { makeAcpNativeLoggerFactory } from "../acp/AcpNativeLogging.ts"; import { applyCursorAcpModelSelection, makeCursorAcpRuntime } from "../acp/CursorAcpSupport.ts"; import { CursorAskQuestionRequest, @@ -314,6 +315,7 @@ export function makeCursorAdapter( const path = yield* Path.Path; const childProcessSpawner = yield* ChildProcessSpawner.ChildProcessSpawner; const serverConfig = yield* Effect.service(ServerConfig); + const crypto = yield* Crypto.Crypto; const nativeEventLogger = options?.nativeEventLogger ?? (options?.nativeEventLogPath !== undefined @@ -323,14 +325,36 @@ export function makeCursorAdapter( : undefined); const managedNativeEventLogger = options?.nativeEventLogger === undefined ? nativeEventLogger : undefined; + const makeAcpNativeLoggers = yield* makeAcpNativeLoggerFactory(); const sessions = new Map(); const threadLocksRef = yield* SynchronizedRef.make(new Map()); const runtimeEventPubSub = yield* PubSub.unbounded(); const nowIso = Effect.map(DateTime.now, DateTime.formatIso); - const nextEventId = Effect.map(Random.nextUUIDv4, (id) => EventId.make(id)); + const randomUUIDv4 = crypto.randomUUIDv4.pipe( + Effect.mapError( + (cause) => + new ProviderAdapterRequestError({ + provider: PROVIDER, + method: "crypto/randomUUIDv4", + detail: "Failed to generate Cursor runtime identifier.", + cause, + }), + ), + ); + const nextEventId = Effect.map(randomUUIDv4, (id) => EventId.make(id)); const makeEventStamp = () => Effect.all({ eventId: nextEventId, createdAt: nowIso }); + const mapExtensionFailure = (effect: Effect.Effect) => + effect.pipe( + Effect.mapError( + (cause) => + new EffectAcpErrors.AcpTransportError({ + detail: "Failed to process Cursor ACP extension event.", + cause, + }), + ), + ); const offerRuntimeEvent = (event: ProviderRuntimeEvent) => PubSub.publish(runtimeEventPubSub, event).pipe(Effect.asVoid); @@ -369,7 +393,7 @@ export function makeCursorAdapter( { observedAt, event: { - id: yield* Random.nextUUIDv4, + id: yield* randomUUIDv4, kind: "notification", provider: PROVIDER, createdAt: observedAt, @@ -524,159 +548,167 @@ export function makeCursorAdapter( ); const started = yield* Effect.gen(function* () { yield* acp.handleExtRequest("cursor/ask_question", CursorAskQuestionRequest, (params) => - Effect.gen(function* () { - yield* logNative( - input.threadId, - "cursor/ask_question", - params, - "acp.cursor.extension", - ); - const requestId = ApprovalRequestId.make(crypto.randomUUID()); - const runtimeRequestId = RuntimeRequestId.make(requestId); - const answers = yield* Deferred.make(); - pendingUserInputs.set(requestId, { answers }); - yield* offerRuntimeEvent({ - type: "user-input.requested", - ...(yield* makeEventStamp()), - provider: PROVIDER, - threadId: input.threadId, - turnId: ctx?.activeTurnId, - requestId: runtimeRequestId, - payload: { questions: extractAskQuestions(params) }, - raw: { - source: "acp.cursor.extension", - method: "cursor/ask_question", - payload: params, - }, - }); - const resolved = yield* Deferred.await(answers); - pendingUserInputs.delete(requestId); - yield* offerRuntimeEvent({ - type: "user-input.resolved", - ...(yield* makeEventStamp()), - provider: PROVIDER, - threadId: input.threadId, - turnId: ctx?.activeTurnId, - requestId: runtimeRequestId, - payload: { answers: resolved }, - }); - return { answers: resolved }; - }), - ); - yield* acp.handleExtRequest("cursor/create_plan", CursorCreatePlanRequest, (params) => - Effect.gen(function* () { - yield* logNative( - input.threadId, - "cursor/create_plan", - params, - "acp.cursor.extension", - ); - yield* offerRuntimeEvent({ - type: "turn.proposed.completed", - ...(yield* makeEventStamp()), - provider: PROVIDER, - threadId: input.threadId, - turnId: ctx?.activeTurnId, - payload: { planMarkdown: extractPlanMarkdown(params) }, - raw: { - source: "acp.cursor.extension", - method: "cursor/create_plan", - payload: params, - }, - }); - return { accepted: true } as const; - }), - ); - yield* acp.handleExtNotification( - "cursor/update_todos", - CursorUpdateTodosRequest, - (params) => + mapExtensionFailure( Effect.gen(function* () { yield* logNative( input.threadId, - "cursor/update_todos", + "cursor/ask_question", params, "acp.cursor.extension", ); - if (ctx) { - yield* emitPlanUpdate( - ctx, - extractTodosAsPlan(params), - params, - "acp.cursor.extension", - "cursor/update_todos", - ); - } - }), - ); - yield* acp.handleRequestPermission((params) => - Effect.gen(function* () { - yield* logNative( - input.threadId, - "session/request_permission", - params, - "acp.jsonrpc", - ); - if (input.runtimeMode === "full-access") { - const autoApprovedOptionId = selectAutoApprovedPermissionOption(params); - if (autoApprovedOptionId !== undefined) { - return { - outcome: { - outcome: "selected" as const, - optionId: autoApprovedOptionId, - }, - }; - } - } - const permissionRequest = parsePermissionRequest(params); - const requestId = ApprovalRequestId.make(crypto.randomUUID()); - const runtimeRequestId = RuntimeRequestId.make(requestId); - const decision = yield* Deferred.make(); - pendingApprovals.set(requestId, { - decision, - kind: permissionRequest.kind, - }); - yield* offerRuntimeEvent( - makeAcpRequestOpenedEvent({ - stamp: yield* makeEventStamp(), + const requestId = ApprovalRequestId.make(yield* randomUUIDv4); + const runtimeRequestId = RuntimeRequestId.make(requestId); + const answers = yield* Deferred.make(); + pendingUserInputs.set(requestId, { answers }); + yield* offerRuntimeEvent({ + type: "user-input.requested", + ...(yield* makeEventStamp()), provider: PROVIDER, threadId: input.threadId, turnId: ctx?.activeTurnId, requestId: runtimeRequestId, - permissionRequest, - detail: - permissionRequest.detail ?? - encodeJsonStringForDiagnostics(params)?.slice(0, 2000) ?? - "[unserializable params]", - args: params, - source: "acp.jsonrpc", - method: "session/request_permission", - rawPayload: params, - }), - ); - const resolved = yield* Deferred.await(decision); - pendingApprovals.delete(requestId); - yield* offerRuntimeEvent( - makeAcpRequestResolvedEvent({ - stamp: yield* makeEventStamp(), + payload: { questions: extractAskQuestions(params) }, + raw: { + source: "acp.cursor.extension", + method: "cursor/ask_question", + payload: params, + }, + }); + const resolved = yield* Deferred.await(answers); + pendingUserInputs.delete(requestId); + yield* offerRuntimeEvent({ + type: "user-input.resolved", + ...(yield* makeEventStamp()), provider: PROVIDER, threadId: input.threadId, turnId: ctx?.activeTurnId, requestId: runtimeRequestId, - permissionRequest, - decision: resolved, + payload: { answers: resolved }, + }); + return { answers: resolved }; + }), + ), + ); + yield* acp.handleExtRequest("cursor/create_plan", CursorCreatePlanRequest, (params) => + mapExtensionFailure( + Effect.gen(function* () { + yield* logNative( + input.threadId, + "cursor/create_plan", + params, + "acp.cursor.extension", + ); + yield* offerRuntimeEvent({ + type: "turn.proposed.completed", + ...(yield* makeEventStamp()), + provider: PROVIDER, + threadId: input.threadId, + turnId: ctx?.activeTurnId, + payload: { planMarkdown: extractPlanMarkdown(params) }, + raw: { + source: "acp.cursor.extension", + method: "cursor/create_plan", + payload: params, + }, + }); + return { accepted: true } as const; + }), + ), + ); + yield* acp.handleExtNotification( + "cursor/update_todos", + CursorUpdateTodosRequest, + (params) => + mapExtensionFailure( + Effect.gen(function* () { + yield* logNative( + input.threadId, + "cursor/update_todos", + params, + "acp.cursor.extension", + ); + if (ctx) { + yield* emitPlanUpdate( + ctx, + extractTodosAsPlan(params), + params, + "acp.cursor.extension", + "cursor/update_todos", + ); + } }), - ); - return { - outcome: - resolved === "cancel" - ? ({ outcome: "cancelled" } as const) - : { + ), + ); + yield* acp.handleRequestPermission((params) => + mapExtensionFailure( + Effect.gen(function* () { + yield* logNative( + input.threadId, + "session/request_permission", + params, + "acp.jsonrpc", + ); + if (input.runtimeMode === "full-access") { + const autoApprovedOptionId = selectAutoApprovedPermissionOption(params); + if (autoApprovedOptionId !== undefined) { + return { + outcome: { outcome: "selected" as const, - optionId: acpPermissionOutcome(resolved), + optionId: autoApprovedOptionId, }, - }; - }), + }; + } + } + const permissionRequest = parsePermissionRequest(params); + const requestId = ApprovalRequestId.make(yield* randomUUIDv4); + const runtimeRequestId = RuntimeRequestId.make(requestId); + const decision = yield* Deferred.make(); + pendingApprovals.set(requestId, { + decision, + kind: permissionRequest.kind, + }); + yield* offerRuntimeEvent( + makeAcpRequestOpenedEvent({ + stamp: yield* makeEventStamp(), + provider: PROVIDER, + threadId: input.threadId, + turnId: ctx?.activeTurnId, + requestId: runtimeRequestId, + permissionRequest, + detail: + permissionRequest.detail ?? + encodeJsonStringForDiagnostics(params)?.slice(0, 2000) ?? + "[unserializable params]", + args: params, + source: "acp.jsonrpc", + method: "session/request_permission", + rawPayload: params, + }), + ); + const resolved = yield* Deferred.await(decision); + pendingApprovals.delete(requestId); + yield* offerRuntimeEvent( + makeAcpRequestResolvedEvent({ + stamp: yield* makeEventStamp(), + provider: PROVIDER, + threadId: input.threadId, + turnId: ctx?.activeTurnId, + requestId: runtimeRequestId, + permissionRequest, + decision: resolved, + }), + ); + return { + outcome: + resolved === "cancel" + ? ({ outcome: "cancelled" } as const) + : { + outcome: "selected" as const, + optionId: acpPermissionOutcome(resolved), + }, + }; + }), + ), ); return yield* acp.start(); }).pipe( @@ -810,7 +842,12 @@ export function makeCursorAdapter( } }), ), - ).pipe(Effect.forkChild); + ).pipe( + Effect.catch((cause) => + Effect.logError("Failed to process Cursor runtime notification.", { cause }), + ), + Effect.forkChild, + ); ctx.notificationFiber = nf; sessions.set(input.threadId, ctx); @@ -845,7 +882,7 @@ export function makeCursorAdapter( const sendTurn: CursorAdapterShape["sendTurn"] = (input) => Effect.gen(function* () { const ctx = yield* requireSession(input.threadId); - const turnId = TurnId.make(crypto.randomUUID()); + const turnId = TurnId.make(yield* randomUUIDv4); const turnModelSelection = input.modelSelection?.instanceId === boundInstanceId ? input.modelSelection : undefined; const model = turnModelSelection?.model ?? ctx.session.model; @@ -1056,6 +1093,9 @@ export function makeCursorAdapter( yield* Effect.addFinalizer(() => Effect.forEach(sessions.values(), stopSessionInternal, { discard: true }).pipe( + Effect.catch((cause) => + Effect.logError("Failed to emit Cursor session shutdown event.", { cause }), + ), Effect.tap(() => PubSub.shutdown(runtimeEventPubSub)), Effect.tap(() => managedNativeEventLogger?.close() ?? Effect.void), ), diff --git a/apps/server/src/provider/Layers/OpenCodeAdapter.ts b/apps/server/src/provider/Layers/OpenCodeAdapter.ts index 512e9ed6bfe..bfc36aff4c3 100644 --- a/apps/server/src/provider/Layers/OpenCodeAdapter.ts +++ b/apps/server/src/provider/Layers/OpenCodeAdapter.ts @@ -13,11 +13,11 @@ import { type UserInputQuestion, } from "@t3tools/contracts"; import * as Cause from "effect/Cause"; +import * as Crypto from "effect/Crypto"; import * as DateTime from "effect/DateTime"; import * as Effect from "effect/Effect"; import * as Exit from "effect/Exit"; import * as Queue from "effect/Queue"; -import * as Random from "effect/Random"; import * as Ref from "effect/Ref"; import * as Scope from "effect/Scope"; import * as Stream from "effect/Stream"; @@ -134,40 +134,14 @@ const toProcessError = (threadId: ThreadId, cause: unknown): ProviderAdapterProc cause, }); -const buildEventBase = (input: { +type EventBaseInput = { readonly threadId: ThreadId; readonly turnId?: TurnId | undefined; readonly itemId?: string | undefined; readonly requestId?: string | undefined; readonly createdAt?: string | undefined; readonly raw?: unknown; -}): Effect.Effect< - Pick< - ProviderRuntimeEvent, - "eventId" | "provider" | "threadId" | "createdAt" | "turnId" | "itemId" | "requestId" | "raw" - > -> => - Effect.gen(function* () { - const uuid = yield* Random.nextUUIDv4; - const createdAt = input.createdAt ?? (yield* nowIso); - return { - eventId: EventId.make(uuid), - provider: PROVIDER, - threadId: input.threadId, - createdAt, - ...(input.turnId ? { turnId: input.turnId } : {}), - ...(input.itemId ? { itemId: RuntimeItemId.make(input.itemId) } : {}), - ...(input.requestId ? { requestId: RuntimeRequestId.make(input.requestId) } : {}), - ...(input.raw !== undefined - ? { - raw: { - source: "opencode.sdk.event", - payload: input.raw, - }, - } - : {}), - }; - }); +}; function toToolLifecycleItemType(toolName: string): ToolLifecycleItemType { const normalized = toolName.toLowerCase(); @@ -457,6 +431,7 @@ export function makeOpenCodeAdapter( const boundInstanceId = options?.instanceId ?? ProviderInstanceId.make("opencode"); const serverConfig = yield* ServerConfig; const openCodeRuntime = yield* OpenCodeRuntime; + const crypto = yield* Crypto.Crypto; const nativeEventLogger = options?.nativeEventLogger ?? (options?.nativeEventLogPath !== undefined @@ -470,6 +445,40 @@ export function makeOpenCodeAdapter( options?.nativeEventLogger === undefined ? nativeEventLogger : undefined; const runtimeEvents = yield* Queue.unbounded(); const sessions = new Map(); + const randomUUIDv4 = crypto.randomUUIDv4.pipe( + Effect.mapError( + (cause) => + new ProviderAdapterRequestError({ + provider: PROVIDER, + method: "crypto/randomUUIDv4", + detail: "Failed to generate OpenCode runtime identifier.", + cause, + }), + ), + ); + const buildEventBase = (input: EventBaseInput) => + Effect.all({ + eventId: randomUUIDv4.pipe(Effect.map(EventId.make)), + createdAt: input.createdAt === undefined ? nowIso : Effect.succeed(input.createdAt), + }).pipe( + Effect.map(({ eventId, createdAt }) => ({ + eventId, + provider: PROVIDER, + threadId: input.threadId, + createdAt, + ...(input.turnId ? { turnId: input.turnId } : {}), + ...(input.itemId ? { itemId: RuntimeItemId.make(input.itemId) } : {}), + ...(input.requestId ? { requestId: RuntimeRequestId.make(input.requestId) } : {}), + ...(input.raw !== undefined + ? { + raw: { + source: "opencode.sdk.event" as const, + payload: input.raw, + }, + } + : {}), + })), + ); // Layer-level finalizer: when the adapter layer shuts down, stop every // session. Each session's `Scope.close` tears down its spawned OpenCode @@ -1142,7 +1151,7 @@ export function makeOpenCodeAdapter( const sendTurn: OpenCodeAdapterShape["sendTurn"] = Effect.fn("sendTurn")(function* (input) { const context = ensureSessionContext(sessions, input.threadId); - const turnId = TurnId.make(`opencode-turn-${yield* Random.nextUUIDv4}`); + const turnId = TurnId.make(`opencode-turn-${yield* randomUUIDv4}`); const modelSelection = input.modelSelection ?? (context.session.model diff --git a/apps/server/src/provider/acp/AcpNativeLogging.ts b/apps/server/src/provider/acp/AcpNativeLogging.ts index f00ffcb6655..6146980e4fb 100644 --- a/apps/server/src/provider/acp/AcpNativeLogging.ts +++ b/apps/server/src/provider/acp/AcpNativeLogging.ts @@ -1,40 +1,13 @@ import type { ProviderDriverKind, ThreadId } from "@t3tools/contracts"; import * as Cause from "effect/Cause"; +import * as Crypto from "effect/Crypto"; import * as DateTime from "effect/DateTime"; import * as Effect from "effect/Effect"; -import * as Random from "effect/Random"; import type * as EffectAcpProtocol from "effect-acp/protocol"; import type { EventNdjsonLogger } from "../Layers/EventNdjsonLogger.ts"; import type { AcpSessionRequestLogEvent, AcpSessionRuntimeOptions } from "./AcpSessionRuntime.ts"; -function writeNativeAcpLog(input: { - readonly nativeEventLogger: EventNdjsonLogger | undefined; - readonly provider: ProviderDriverKind; - readonly threadId: ThreadId; - readonly kind: "request" | "protocol"; - readonly payload: unknown; -}): Effect.Effect { - return Effect.gen(function* () { - if (!input.nativeEventLogger) return; - const observedAt = DateTime.formatIso(yield* DateTime.now); - yield* input.nativeEventLogger.write( - { - observedAt, - event: { - id: yield* Random.nextUUIDv4, - kind: input.kind, - provider: input.provider, - createdAt: observedAt, - threadId: input.threadId, - payload: input.payload, - }, - }, - input.threadId, - ); - }); -} - function formatRequestLogPayload(event: AcpSessionRequestLogEvent) { return { method: event.method, @@ -45,35 +18,63 @@ function formatRequestLogPayload(event: AcpSessionRequestLogEvent) { }; } -export function makeAcpNativeLoggers(input: { - readonly nativeEventLogger: EventNdjsonLogger | undefined; - readonly provider: ProviderDriverKind; - readonly threadId: ThreadId; -}): Pick { - return { - requestLogger: (event) => - writeNativeAcpLog({ - nativeEventLogger: input.nativeEventLogger, - provider: input.provider, - threadId: input.threadId, - kind: "request", - payload: formatRequestLogPayload(event), - }), - ...(input.nativeEventLogger - ? { - protocolLogging: { - logIncoming: true, - logOutgoing: true, - logger: (event: EffectAcpProtocol.AcpProtocolLogEvent) => - writeNativeAcpLog({ - nativeEventLogger: input.nativeEventLogger, - provider: input.provider, - threadId: input.threadId, - kind: "protocol", - payload: event, - }), - } satisfies NonNullable, - } - : {}), +export const makeAcpNativeLoggerFactory = Effect.fn("makeAcpNativeLoggerFactory")(function* () { + const crypto = yield* Crypto.Crypto; + return (input: { + readonly nativeEventLogger: EventNdjsonLogger | undefined; + readonly provider: ProviderDriverKind; + readonly threadId: ThreadId; + }): Pick => { + const writeNativeAcpLog = (logInput: { + readonly kind: "request" | "protocol"; + readonly payload: unknown; + }) => + Effect.gen(function* () { + if (!input.nativeEventLogger) return; + const observedAt = DateTime.formatIso(yield* DateTime.now); + yield* input.nativeEventLogger.write( + { + observedAt, + event: { + id: yield* crypto.randomUUIDv4, + kind: logInput.kind, + provider: input.provider, + createdAt: observedAt, + threadId: input.threadId, + payload: logInput.payload, + }, + }, + input.threadId, + ); + }).pipe( + Effect.catch((cause) => + Effect.logWarning("Failed to write native ACP event log.", { + cause, + provider: input.provider, + threadId: input.threadId, + }), + ), + ); + + return { + requestLogger: (event: AcpSessionRequestLogEvent) => + writeNativeAcpLog({ + kind: "request", + payload: formatRequestLogPayload(event), + }), + ...(input.nativeEventLogger + ? { + protocolLogging: { + logIncoming: true, + logOutgoing: true, + logger: (event: EffectAcpProtocol.AcpProtocolLogEvent) => + writeNativeAcpLog({ + kind: "protocol", + payload: event, + }), + } satisfies NonNullable, + } + : {}), + }; }; -} +}); diff --git a/apps/server/src/provider/providerMaintenance.test.ts b/apps/server/src/provider/providerMaintenance.test.ts index e032b87a4d0..73428f0a445 100644 --- a/apps/server/src/provider/providerMaintenance.test.ts +++ b/apps/server/src/provider/providerMaintenance.test.ts @@ -1,12 +1,12 @@ // @effect-diagnostics nodeBuiltinImport:off -import { afterEach, describe, expect, it } from "@effect/vitest"; +import { afterEach, expect, it } from "@effect/vitest"; import { chmodSync, mkdirSync, symlinkSync, writeFileSync } from "node:fs"; import * as NodeServices from "@effect/platform-node/NodeServices"; import os from "node:os"; import path from "node:path"; import { ProviderDriverKind } from "@t3tools/contracts"; +import * as Crypto from "effect/Crypto"; import * as Effect from "effect/Effect"; -import * as Random from "effect/Random"; import { clearLatestProviderVersionCacheForTests, createProviderVersionAdvisory, @@ -18,10 +18,11 @@ import { } from "./providerMaintenance.ts"; const driver = (value: string) => ProviderDriverKind.make(value); -const makeTempDir = Effect.fn("makeTempDir")(function* (name: string) { - const id = yield* Random.nextUUIDv4; - return path.join(os.tmpdir(), `${name}-${id}`); -}); +const makeTempDir = (name: string) => + Crypto.Crypto.pipe( + Effect.flatMap((crypto) => crypto.randomUUIDv4), + Effect.map((id) => path.join(os.tmpdir(), `${name}-${id}`)), + ); const isNativeTestCommandPath = (expectedPathSegment: string) => (commandPath: string): boolean => @@ -68,7 +69,7 @@ afterEach(() => { clearLatestProviderVersionCacheForTests(); }); -describe("providerMaintenance", () => { +it.layer(NodeServices.layer)("providerMaintenance", (it) => { it("marks providers with unknown current versions as unknown", () => { expect( createProviderVersionAdvisory({ @@ -404,7 +405,7 @@ describe("providerMaintenance", () => { env: { PATH: "", }, - }).pipe(Effect.provide(NodeServices.layer)); + }); expect(capabilities).toEqual({ provider: driver("packageTool"), @@ -452,7 +453,7 @@ describe("providerMaintenance", () => { env: { PATH: "", }, - }).pipe(Effect.provide(NodeServices.layer)); + }); expect(capabilities).toEqual({ provider: driver("packageTool"), diff --git a/apps/server/src/telemetry/Identify.ts b/apps/server/src/telemetry/Identify.ts index ed8e98a87a4..ade6ac485ba 100644 --- a/apps/server/src/telemetry/Identify.ts +++ b/apps/server/src/telemetry/Identify.ts @@ -1,9 +1,9 @@ +import * as Crypto from "effect/Crypto"; import * as Effect from "effect/Effect"; +import * as Encoding from "effect/Encoding"; import * as FileSystem from "effect/FileSystem"; import * as Path from "effect/Path"; -import * as Random from "effect/Random"; import * as Schema from "effect/Schema"; -import * as Crypto from "node:crypto"; import { homedir } from "node:os"; import { ServerConfig } from "../config.ts"; @@ -23,14 +23,17 @@ class IdentifyUserError extends Schema.TaggedErrorClass()("Id }) {} const hash = (value: string) => - Effect.try({ - try: () => Crypto.createHash("sha256").update(value).digest("hex"), - catch: (error) => - new IdentifyUserError({ - message: "Failed to hash identifier", - cause: error, - }), - }); + Crypto.Crypto.pipe( + Effect.flatMap((crypto) => crypto.digest("SHA-256", new TextEncoder().encode(value))), + Effect.map(Encoding.encodeHex), + Effect.mapError( + (cause) => + new IdentifyUserError({ + message: "Failed to hash identifier", + cause, + }), + ), + ); const getCodexAccountId = Effect.gen(function* () { const fileSystem = yield* FileSystem.FileSystem; @@ -64,11 +67,10 @@ const upsertAnonymousId = Effect.gen(function* () { const anonymousId = yield* fileSystem.readFileString(anonymousIdPath).pipe( Effect.catch(() => - Effect.gen(function* () { - const randomId = yield* Random.nextUUIDv4; - yield* fileSystem.writeFileString(anonymousIdPath, randomId); - return randomId; - }), + Crypto.Crypto.pipe( + Effect.flatMap((crypto) => crypto.randomUUIDv4), + Effect.tap((randomId) => fileSystem.writeFileString(anonymousIdPath, randomId)), + ), ), ); diff --git a/apps/server/src/telemetry/Layers/AnalyticsService.ts b/apps/server/src/telemetry/Layers/AnalyticsService.ts index ec859fb18f5..27bf64c7be0 100644 --- a/apps/server/src/telemetry/Layers/AnalyticsService.ts +++ b/apps/server/src/telemetry/Layers/AnalyticsService.ts @@ -40,7 +40,7 @@ const TelemetryEnvConfig = Config.all({ }); const makeAnalyticsService = Effect.gen(function* () { - const telemetryConfig = yield* TelemetryEnvConfig.asEffect(); + const telemetryConfig = yield* TelemetryEnvConfig; const httpClient = yield* HttpClient.HttpClient; const serverConfig = yield* ServerConfig; const identifier = yield* getTelemetryIdentifier; diff --git a/apps/server/src/textGeneration/CodexTextGeneration.ts b/apps/server/src/textGeneration/CodexTextGeneration.ts index 3d1637a7fc0..db2f47ca565 100644 --- a/apps/server/src/textGeneration/CodexTextGeneration.ts +++ b/apps/server/src/textGeneration/CodexTextGeneration.ts @@ -2,7 +2,6 @@ import * as Effect from "effect/Effect"; import * as FileSystem from "effect/FileSystem"; import * as Option from "effect/Option"; import * as Path from "effect/Path"; -import * as Random from "effect/Random"; import * as Schema from "effect/Schema"; import * as Scope from "effect/Scope"; import * as Stream from "effect/Stream"; @@ -79,10 +78,9 @@ export const makeCodexTextGeneration = Effect.fn("makeCodexTextGeneration")(func content: string, ): Effect.Effect => { return Effect.gen(function* () { - const tempFileId = yield* Random.nextUUIDv4; return yield* fileSystem .makeTempFileScoped({ - prefix: `t3code-${prefix}-${process.pid}-${tempFileId}.tmp`, + prefix: `t3code-${prefix}-${process.pid}-`, }) .pipe(Effect.tap((filePath) => fileSystem.writeFileString(filePath, content))); }).pipe( diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index d66d2487ce3..f18502cb1db 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -105,7 +105,7 @@ import { resolveShortcutCommand, shortcutLabelForCommand } from "../keybindings" import PlanSidebar from "./PlanSidebar"; import ThreadTerminalDrawer from "./ThreadTerminalDrawer"; import { ChevronDownIcon, TriangleAlertIcon, WifiOffIcon } from "lucide-react"; -import { cn, randomUUID } from "~/lib/utils"; +import { cn, randomHex, randomUUID } from "~/lib/utils"; import { stackedThreadToast, toastManager } from "./ui/toast"; import { decodeProjectScriptKeybindingRule } from "~/lib/projectScriptKeybindings"; import { type NewProjectScriptInput } from "./ProjectScriptsControl"; @@ -2846,7 +2846,7 @@ export default function ChatView(props: ChatViewProps) { prepareWorktree: { projectCwd: activeProject.cwd, baseBranch: baseBranchForWorktree, - branch: buildTemporaryWorktreeBranchName(), + branch: buildTemporaryWorktreeBranchName(randomHex), }, runSetupScript: true, } diff --git a/apps/web/src/lib/utils.ts b/apps/web/src/lib/utils.ts index b4ccb36c77c..b2be5eb7176 100644 --- a/apps/web/src/lib/utils.ts +++ b/apps/web/src/lib/utils.ts @@ -1,8 +1,7 @@ import { CommandId, MessageId, ProjectId, ThreadId } from "@t3tools/contracts"; import { type CxOptions, cx } from "class-variance-authority"; +import * as Encoding from "effect/Encoding"; import { twMerge } from "tailwind-merge"; -import * as Random from "effect/Random"; -import * as Effect from "effect/Effect"; import { DraftId } from "../composerDraftStore"; export function cn(...inputs: CxOptions) { @@ -21,11 +20,16 @@ export function isLinuxPlatform(platform: string): boolean { return /linux/i.test(platform); } +export function randomHex(byteLength: number): string { + return Encoding.encodeHex(globalThis.crypto.getRandomValues(new Uint8Array(byteLength))); +} + export function randomUUID(): string { - if (typeof crypto.randomUUID === "function") { - return crypto.randomUUID(); - } - return Effect.runSync(Random.nextUUIDv4); + const bytes = globalThis.crypto.getRandomValues(new Uint8Array(16)); + bytes[6] = (bytes[6]! & 0x0f) | 0x40; + bytes[8] = (bytes[8]! & 0x3f) | 0x80; + const hex = Encoding.encodeHex(bytes); + return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`; } export const newCommandId = (): CommandId => CommandId.make(randomUUID()); diff --git a/bun.lock b/bun.lock index ffc4a5922bd..3e77ba1c513 100644 --- a/bun.lock +++ b/bun.lock @@ -17,7 +17,7 @@ }, "apps/desktop": { "name": "@t3tools/desktop", - "version": "0.0.23", + "version": "0.0.24", "dependencies": { "@effect/platform-node": "catalog:", "@t3tools/contracts": "workspace:*", @@ -50,7 +50,7 @@ }, "apps/server": { "name": "t3", - "version": "0.0.23", + "version": "0.0.24", "bin": { "t3": "./dist/bin.mjs", }, @@ -83,7 +83,7 @@ }, "apps/web": { "name": "@t3tools/web", - "version": "0.0.23", + "version": "0.0.24", "dependencies": { "@base-ui/react": "^1.4.1", "@dnd-kit/core": "^6.3.1", @@ -165,7 +165,7 @@ }, "packages/contracts": { "name": "@t3tools/contracts", - "version": "0.0.23", + "version": "0.0.24", "dependencies": { "effect": "catalog:", }, @@ -272,7 +272,7 @@ "node-pty", ], "patchedDependencies": { - "effect@4.0.0-beta.59": "patches/effect@4.0.0-beta.59.patch", + "effect@4.0.0-beta.73": "patches/effect@4.0.0-beta.73.patch", }, "overrides": { "@effect/atom-react": "catalog:", @@ -285,18 +285,18 @@ "vite": "^8.0.0", }, "catalog": { - "@effect/atom-react": "4.0.0-beta.59", + "@effect/atom-react": "4.0.0-beta.73", "@effect/language-service": "0.84.2", - "@effect/openapi-generator": "4.0.0-beta.59", - "@effect/platform-bun": "4.0.0-beta.59", - "@effect/platform-node": "4.0.0-beta.59", - "@effect/platform-node-shared": "4.0.0-beta.59", - "@effect/sql-sqlite-bun": "4.0.0-beta.59", - "@effect/vitest": "4.0.0-beta.59", + "@effect/openapi-generator": "4.0.0-beta.73", + "@effect/platform-bun": "4.0.0-beta.73", + "@effect/platform-node": "4.0.0-beta.73", + "@effect/platform-node-shared": "4.0.0-beta.73", + "@effect/sql-sqlite-bun": "4.0.0-beta.73", + "@effect/vitest": "4.0.0-beta.73", "@pierre/diffs": "1.1.20", "@types/bun": "^1.3.11", "@types/node": "^24.10.13", - "effect": "4.0.0-beta.59", + "effect": "4.0.0-beta.73", "tsdown": "^0.20.3", "typescript": "^5.7.3", "vitest": "^4.0.0", @@ -384,21 +384,21 @@ "@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=="], - "@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=="], + "@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=="], "@effect/language-service": ["@effect/language-service@0.84.2", "", { "bin": { "effect-language-service": "cli.js" } }, "sha512-l04qNxpiA8rY5yXWckRPJ7Mk5MNerXuNymSFf+IdflfI5i8jgL1bpBNLuP6ijg7wgjdHc/KmTnCj2kT0SCntuA=="], - "@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=="], + "@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=="], - "@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=="], + "@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=="], - "@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=="], + "@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=="], - "@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=="], + "@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=="], - "@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=="], + "@effect/sql-sqlite-bun": ["@effect/sql-sqlite-bun@4.0.0-beta.73", "", { "peerDependencies": { "effect": "^4.0.0-beta.73" } }, "sha512-dkTDDeBLjaKo7h1IE049Oj2BDKQOT+Kc57KbBVaSk36dRdsEmVspRMP7OGbZj3etJU3lL1GJi1BIfnb4N3uMRw=="], - "@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=="], + "@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=="], "@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=="], @@ -608,17 +608,17 @@ "@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=="], - "@msgpackr-extract/msgpackr-extract-darwin-arm64": ["@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw=="], + "@msgpackr-extract/msgpackr-extract-darwin-arm64": ["@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-LCkGo6JDfaBhgST7UpPWgNgLINpcpabaHfyz5OBx75nUYxBsaEPxjnyNjWpeb/xBup/682QnBfRBy2/LvPutZQ=="], - "@msgpackr-extract/msgpackr-extract-darwin-x64": ["@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw=="], + "@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=="], - "@msgpackr-extract/msgpackr-extract-linux-arm": ["@msgpackr-extract/msgpackr-extract-linux-arm@3.0.3", "", { "os": "linux", "cpu": "arm" }, "sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw=="], + "@msgpackr-extract/msgpackr-extract-linux-arm": ["@msgpackr-extract/msgpackr-extract-linux-arm@3.0.4", "", { "os": "linux", "cpu": "arm" }, "sha512-Tg3yX65f5GbtXLkrYEHE5oibZG9epyYWas7FogTTEJeDEF9JlXJzKgXaNhT3UXlTOeA+AfZpYZYZ0uPj7Cfquw=="], - "@msgpackr-extract/msgpackr-extract-linux-arm64": ["@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg=="], + "@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=="], - "@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=="], + "@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=="], - "@msgpackr-extract/msgpackr-extract-win32-x64": ["@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3", "", { "os": "win32", "cpu": "x64" }, "sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ=="], + "@msgpackr-extract/msgpackr-extract-win32-x64": ["@msgpackr-extract/msgpackr-extract-win32-x64@3.0.4", "", { "os": "win32", "cpu": "x64" }, "sha512-CmCXPQrkbwExx3j946/PtHWHbYJiCRBRDl4BlkRQcJB/YOwQxJRTpoo7aTsortjgoJ1x7opzTSxn7C+ASSLVjQ=="], "@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=="], @@ -1160,7 +1160,7 @@ "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], - "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=="], + "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=="], "effect-acp": ["effect-acp@workspace:packages/effect-acp"], @@ -1230,7 +1230,7 @@ "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=="], - "fast-check": ["fast-check@4.6.0", "", { "dependencies": { "pure-rand": "^8.0.0" } }, "sha512-h7H6Dm0Fy+H4ciQYFxFjXnXkzR2kr9Fb22c0UBpHnm59K2zpr2t13aPTHlltFiNT6zuxp6HMPAVVvgur4BLdpA=="], + "fast-check": ["fast-check@4.8.0", "", { "dependencies": { "pure-rand": "^8.0.0" } }, "sha512-GOJ158CUMnN6cSahsv4+ExARvIDuzzinFjkp0E9WtiBa5zcVeLozVkWaE4IzFcc+Y48Wp1EDlUZsXRyAztQcSg=="], "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], @@ -1346,7 +1346,7 @@ "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], - "ini": ["ini@6.0.0", "", {}, "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ=="], + "ini": ["ini@7.0.0", "", {}, "sha512-ifK0CgjALofS5bkrcTy4RaQ9Vx2Knf/eLeIO+NaswQEpH1UblrtTSCIvN71qQDMq0PeQ/SSPojvEJp9vvvfr+w=="], "inline-style-parser": ["inline-style-parser@0.2.7", "", {}, "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA=="], @@ -1590,9 +1590,9 @@ "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - "msgpackr": ["msgpackr@1.11.9", "", { "optionalDependencies": { "msgpackr-extract": "^3.0.2" } }, "sha512-FkoAAyyA6HM8wL882EcEyFZ9s7hVADSwG9xrVx3dxxNQAtgADTrJoEWivID82Iv1zWDsv/OtbrrcZAzGzOMdNw=="], + "msgpackr": ["msgpackr@2.0.2", "", { "optionalDependencies": { "msgpackr-extract": "^3.0.4" } }, "sha512-c5hYOXFbP79Slh6Dzd2wzk+jnV7mX1UxfMYtilnY1NmalXPqG8DGb5cYCMBrW4AsH3zekBBZd4QrKz9NhtvYLQ=="], - "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=="], + "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=="], "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=="], @@ -2022,7 +2022,7 @@ "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=="], - "uuid": ["uuid@13.0.0", "", { "bin": { "uuid": "dist-node/bin/uuid" } }, "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w=="], + "uuid": ["uuid@14.0.0", "", { "bin": { "uuid": "dist-node/bin/uuid" } }, "sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg=="], "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], @@ -2096,7 +2096,7 @@ "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], - "yaml": ["yaml@2.8.3", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg=="], + "yaml": ["yaml@2.9.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="], "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=="], diff --git a/package.json b/package.json index a1aa5d0b1cd..7cdcc2c7004 100644 --- a/package.json +++ b/package.json @@ -9,14 +9,14 @@ "scripts" ], "catalog": { - "effect": "4.0.0-beta.59", - "@effect/atom-react": "4.0.0-beta.59", - "@effect/openapi-generator": "4.0.0-beta.59", - "@effect/platform-bun": "4.0.0-beta.59", - "@effect/platform-node": "4.0.0-beta.59", - "@effect/platform-node-shared": "4.0.0-beta.59", - "@effect/sql-sqlite-bun": "4.0.0-beta.59", - "@effect/vitest": "4.0.0-beta.59", + "effect": "4.0.0-beta.73", + "@effect/atom-react": "4.0.0-beta.73", + "@effect/openapi-generator": "4.0.0-beta.73", + "@effect/platform-bun": "4.0.0-beta.73", + "@effect/platform-node": "4.0.0-beta.73", + "@effect/platform-node-shared": "4.0.0-beta.73", + "@effect/sql-sqlite-bun": "4.0.0-beta.73", + "@effect/vitest": "4.0.0-beta.73", "@effect/language-service": "0.84.2", "@pierre/diffs": "1.1.20", "@types/bun": "^1.3.11", @@ -91,7 +91,7 @@ ] }, "patchedDependencies": { - "effect@4.0.0-beta.59": "patches/effect@4.0.0-beta.59.patch" + "effect@4.0.0-beta.73": "patches/effect@4.0.0-beta.73.patch" }, "trustedDependencies": [ "node-pty", diff --git a/packages/shared/src/git.test.ts b/packages/shared/src/git.test.ts index 5eb9fd51244..8638f750e0b 100644 --- a/packages/shared/src/git.test.ts +++ b/packages/shared/src/git.test.ts @@ -55,7 +55,14 @@ describe("parseGitHubRepositoryNameWithOwnerFromRemoteUrl", () => { describe("isTemporaryWorktreeBranch", () => { it("matches the generated temporary worktree refName format", () => { - expect(isTemporaryWorktreeBranch(buildTemporaryWorktreeBranchName())).toBe(true); + expect( + isTemporaryWorktreeBranch( + buildTemporaryWorktreeBranchName((byteLength) => { + expect(byteLength).toBe(4); + return "DEADBEEF"; + }), + ), + ).toBe(true); }); it("matches generated temporary worktree refs", () => { diff --git a/packages/shared/src/git.ts b/packages/shared/src/git.ts index 1ad17f52bd4..a813cfe4637 100644 --- a/packages/shared/src/git.ts +++ b/packages/shared/src/git.ts @@ -6,8 +6,6 @@ import type { VcsStatusResult, VcsStatusStreamEvent, } from "@t3tools/contracts"; -import * as Effect from "effect/Effect"; -import * as Random from "effect/Random"; import { detectSourceControlProviderFromRemoteUrl } from "./sourceControl.ts"; export const WORKTREE_BRANCH_PREFIX = "t3code"; @@ -86,8 +84,10 @@ export function deriveLocalBranchNameFromRemoteRef(branchName: string): string { return branchName.slice(firstSeparatorIndex + 1); } -export function buildTemporaryWorktreeBranchName(): string { - const token = Effect.runSync(Random.nextUUIDv4).replace(/-/g, "").slice(0, 8).toLowerCase(); +export function buildTemporaryWorktreeBranchName( + randomHex: (byteLength: number) => string, +): string { + const token = randomHex(4).toLowerCase(); return `${WORKTREE_BRANCH_PREFIX}/${token}`; } diff --git a/patches/effect@4.0.0-beta.59.patch b/patches/effect@4.0.0-beta.73.patch similarity index 84% rename from patches/effect@4.0.0-beta.59.patch rename to patches/effect@4.0.0-beta.73.patch index 479004acf33..e4bd08b186d 100644 --- a/patches/effect@4.0.0-beta.59.patch +++ b/patches/effect@4.0.0-beta.73.patch @@ -1,5 +1,5 @@ diff --git a/dist/unstable/rpc/RpcClient.d.ts b/dist/unstable/rpc/RpcClient.d.ts -index 7c5970042df67a25b4145df017960b3e6febed9c..7af3374632d53ad59f24e80835aa6741853b1f2b 100644 +index b0f61df..ead663e 100644 --- a/dist/unstable/rpc/RpcClient.d.ts +++ b/dist/unstable/rpc/RpcClient.d.ts @@ -2,6 +2,7 @@ import * as Cause from "../../Cause.ts"; @@ -10,7 +10,7 @@ index 7c5970042df67a25b4145df017960b3e6febed9c..7af3374632d53ad59f24e80835aa6741 import * as Layer from "../../Layer.ts"; import * as Queue from "../../Queue.ts"; import * as Schedule from "../../Schedule.ts"; -@@ -192,9 +193,40 @@ export declare const layerProtocolWorker: (options: { +@@ -260,9 +261,40 @@ export declare const layerProtocolWorker: (options: { readonly targetUtilization?: number | undefined; readonly timeToLive: Duration.Input; }) => Layer.Layer; @@ -50,12 +50,19 @@ index 7c5970042df67a25b4145df017960b3e6febed9c..7af3374632d53ad59f24e80835aa6741 + readonly onPingTimeout?: Effect.Effect | undefined; }>; /** - * @since 4.0.0 + * Represents optional client protocol hooks that run when a transport connects +@@ -279,4 +311,4 @@ declare const ConnectionHooks_base: Context.ServiceClass { isShutdown = true; -@@ -79,6 +80,11 @@ export const makeNoSerialization = /*#__PURE__*/Effect.fnUntraced(function* (gro +@@ -83,6 +84,11 @@ export const makeNoSerialization = /*#__PURE__*/Effect.fnUntraced(function* (gro return Effect.interrupt; } const id = generateRequestId(); @@ -75,7 +82,7 @@ index 951dfc2d667a9136cfa714dcd3195e6575917fde..599c0345873bedd72bc93afe80eab0c6 const send = middleware(message => options.onFromClient({ message, context, -@@ -96,7 +102,7 @@ export const makeNoSerialization = /*#__PURE__*/Effect.fnUntraced(function* (gro +@@ -100,7 +106,7 @@ export const makeNoSerialization = /*#__PURE__*/Effect.fnUntraced(function* (gro headers: Headers.merge(parentFiber.getRef(CurrentHeaders), headers) }); if (discard) { @@ -84,7 +91,7 @@ index 951dfc2d667a9136cfa714dcd3195e6575917fde..599c0345873bedd72bc93afe80eab0c6 } let fiber; return Effect.onInterrupt(Effect.callback(resume => { -@@ -114,7 +120,7 @@ export const makeNoSerialization = /*#__PURE__*/Effect.fnUntraced(function* (gro +@@ -118,7 +124,7 @@ export const makeNoSerialization = /*#__PURE__*/Effect.fnUntraced(function* (gro } }; entries.set(id, entry); @@ -93,7 +100,7 @@ index 951dfc2d667a9136cfa714dcd3195e6575917fde..599c0345873bedd72bc93afe80eab0c6 captureStackTrace: false }) : identity, Effect.runForkWith(parentFiber.context)); fiber.addObserver(exit => { -@@ -123,8 +129,9 @@ export const makeNoSerialization = /*#__PURE__*/Effect.fnUntraced(function* (gro +@@ -127,8 +133,9 @@ export const makeNoSerialization = /*#__PURE__*/Effect.fnUntraced(function* (gro } }); }), interruptors => { @@ -104,7 +111,7 @@ index 951dfc2d667a9136cfa714dcd3195e6575917fde..599c0345873bedd72bc93afe80eab0c6 }); }); const onStreamRequest = Effect.fnUntraced(function* (rpc, middleware, payload, headers, streamBufferSize, context) { -@@ -136,11 +143,17 @@ export const makeNoSerialization = /*#__PURE__*/Effect.fnUntraced(function* (gro +@@ -140,11 +147,17 @@ export const makeNoSerialization = /*#__PURE__*/Effect.fnUntraced(function* (gro }); const fiber = Fiber.getCurrent(); const id = generateRequestId(); @@ -124,7 +131,7 @@ index 951dfc2d667a9136cfa714dcd3195e6575917fde..599c0345873bedd72bc93afe80eab0c6 }); const queue = yield* Queue.bounded(streamBufferSize); entries.set(id, { -@@ -148,9 +161,10 @@ export const makeNoSerialization = /*#__PURE__*/Effect.fnUntraced(function* (gro +@@ -152,9 +165,10 @@ export const makeNoSerialization = /*#__PURE__*/Effect.fnUntraced(function* (gro rpc, queue, scope, @@ -137,7 +144,7 @@ index 951dfc2d667a9136cfa714dcd3195e6575917fde..599c0345873bedd72bc93afe80eab0c6 message, context, discard: false -@@ -165,7 +179,7 @@ export const makeNoSerialization = /*#__PURE__*/Effect.fnUntraced(function* (gro +@@ -169,7 +183,7 @@ export const makeNoSerialization = /*#__PURE__*/Effect.fnUntraced(function* (gro sampled: span.sampled } : {}), headers: Headers.merge(fiber.getRef(CurrentHeaders), headers) @@ -146,7 +153,7 @@ index 951dfc2d667a9136cfa714dcd3195e6575917fde..599c0345873bedd72bc93afe80eab0c6 captureStackTrace: false }) : identity, Effect.catchCause(error => Queue.failCause(queue, error)), Effect.interruptible, Effect.forkIn(scope, { startImmediately: true -@@ -195,7 +209,10 @@ export const makeNoSerialization = /*#__PURE__*/Effect.fnUntraced(function* (gro +@@ -199,7 +213,10 @@ export const makeNoSerialization = /*#__PURE__*/Effect.fnUntraced(function* (gro }); }; }; @@ -158,7 +165,7 @@ index 951dfc2d667a9136cfa714dcd3195e6575917fde..599c0345873bedd72bc93afe80eab0c6 const parentFiber = Fiber.getCurrent(); const fiber = options.onFromClient({ message: { -@@ -209,7 +226,7 @@ export const makeNoSerialization = /*#__PURE__*/Effect.fnUntraced(function* (gro +@@ -213,7 +230,7 @@ export const makeNoSerialization = /*#__PURE__*/Effect.fnUntraced(function* (gro fiber.addObserver(() => { resume(Effect.void); }); @@ -167,7 +174,7 @@ index 951dfc2d667a9136cfa714dcd3195e6575917fde..599c0345873bedd72bc93afe80eab0c6 const write = message => { switch (message._tag) { case "Chunk": -@@ -217,7 +234,13 @@ export const makeNoSerialization = /*#__PURE__*/Effect.fnUntraced(function* (gro +@@ -221,7 +238,13 @@ export const makeNoSerialization = /*#__PURE__*/Effect.fnUntraced(function* (gro const requestId = message.requestId; const entry = entries.get(requestId); if (!entry || entry._tag !== "Queue") return Effect.void; @@ -182,7 +189,7 @@ index 951dfc2d667a9136cfa714dcd3195e6575917fde..599c0345873bedd72bc93afe80eab0c6 message: { _tag: "Ack", requestId: message.requestId -@@ -232,11 +255,18 @@ export const makeNoSerialization = /*#__PURE__*/Effect.fnUntraced(function* (gro +@@ -236,11 +259,18 @@ export const makeNoSerialization = /*#__PURE__*/Effect.fnUntraced(function* (gro const entry = entries.get(requestId); if (!entry) return Effect.void; entries.delete(requestId); @@ -204,7 +211,7 @@ index 951dfc2d667a9136cfa714dcd3195e6575917fde..599c0345873bedd72bc93afe80eab0c6 } case "Defect": { -@@ -530,7 +560,7 @@ export const makeProtocolSocket = options => Protocol.make(Effect.fnUntraced(fun +@@ -568,7 +598,7 @@ export const makeProtocolSocket = options => Protocol.make(Effect.fnUntraced(fun const requestClientMap = new Map(); const write = yield* socket.writer; let parser = serialization.makeUnsafe(); @@ -213,7 +220,7 @@ index 951dfc2d667a9136cfa714dcd3195e6575917fde..599c0345873bedd72bc93afe80eab0c6 let currentError; const onOpen = Effect.suspend(() => { currentError = undefined; -@@ -550,8 +580,7 @@ export const makeProtocolSocket = options => Protocol.make(Effect.fnUntraced(fun +@@ -588,8 +618,7 @@ export const makeProtocolSocket = options => Protocol.make(Effect.fnUntraced(fun body: () => { const response = responses[i++]; if (response._tag === "Pong") { @@ -223,7 +230,7 @@ index 951dfc2d667a9136cfa714dcd3195e6575917fde..599c0345873bedd72bc93afe80eab0c6 } if ("requestId" in response) { const clientId = requestClientMap.get(response.requestId); -@@ -579,12 +608,12 @@ export const makeProtocolSocket = options => Protocol.make(Effect.fnUntraced(fun +@@ -617,12 +646,12 @@ export const makeProtocolSocket = options => Protocol.make(Effect.fnUntraced(fun } }, { onOpen @@ -238,7 +245,7 @@ index 951dfc2d667a9136cfa714dcd3195e6575917fde..599c0345873bedd72bc93afe80eab0c6 }).pipe(Effect.flatMap(() => Effect.fail(new Socket.SocketError({ reason: new Socket.SocketCloseError({ code: 1000 -@@ -626,20 +655,20 @@ export const makeProtocolSocket = options => Protocol.make(Effect.fnUntraced(fun +@@ -664,20 +693,20 @@ export const makeProtocolSocket = options => Protocol.make(Effect.fnUntraced(fun }; })); const defaultRetryPolicy = /*#__PURE__*/Schedule.exponential(500, 1.5).pipe(/*#__PURE__*/Schedule.either(/*#__PURE__*/Schedule.spaced(5000))); @@ -263,8 +270,8 @@ index 951dfc2d667a9136cfa714dcd3195e6575917fde..599c0345873bedd72bc93afe80eab0c6 }).pipe(Effect.delay("5 seconds"), Effect.ignore, Effect.forever, Effect.interruptible, Effect.forkScoped); return { timeout: latch.await, -@@ -773,6 +802,11 @@ export const makeProtocolWorker = options => Protocol.make(Effect.fnUntraced(fun - * @category protocol +@@ -820,6 +849,11 @@ export const makeProtocolWorker = options => Protocol.make(Effect.fnUntraced(fun + * @since 4.0.0 */ export const layerProtocolWorker = /*#__PURE__*/flow(makeProtocolWorker, /*#__PURE__*/Layer.effect(Protocol)); +/** @@ -273,5 +280,12 @@ index 951dfc2d667a9136cfa714dcd3195e6575917fde..599c0345873bedd72bc93afe80eab0c6 + */ +export class RequestHooks extends /*#__PURE__*/Context.Service()("effect/rpc/RpcClient/RequestHooks") {} /** - * @since 4.0.0 - * @category ConnectionHooks + * Represents optional client protocol hooks that run when a transport connects + * and disconnects. +@@ -835,4 +869,4 @@ export const layerProtocolWorker = /*#__PURE__*/flow(makeProtocolWorker, /*#__PU + export class ConnectionHooks extends /*#__PURE__*/Context.Service()("effect/rpc/RpcClient/ConnectionHooks") {} + // internal + const decodeDefect = /*#__PURE__*/Schema.decodeSync(Schema.Defect); +-//# sourceMappingURL=RpcClient.js.map +\ No newline at end of file ++//# sourceMappingURL=RpcClient.js.map diff --git a/scripts/build-desktop-artifact.ts b/scripts/build-desktop-artifact.ts index 104cbef20c1..20b8c4565e7 100644 --- a/scripts/build-desktop-artifact.ts +++ b/scripts/build-desktop-artifact.ts @@ -278,7 +278,7 @@ export const resolveBuildOptions = Effect.fn("resolveBuildOptions")(function* ( ) { const path = yield* Path.Path; const repoRoot = yield* RepoRoot; - const env = yield* BuildEnvConfig.asEffect(); + const env = yield* BuildEnvConfig; const platform = mergeOptions( input.platform, diff --git a/scripts/dev-runner.ts b/scripts/dev-runner.ts index 58bbb1ac35e..cce408c827c 100644 --- a/scripts/dev-runner.ts +++ b/scripts/dev-runner.ts @@ -385,7 +385,7 @@ interface DevRunnerCliInput { export function runDevRunnerWithInput(input: DevRunnerCliInput) { return Effect.gen(function* () { - const { portOffset, devInstance } = yield* OffsetConfig.asEffect().pipe( + const { portOffset, devInstance } = yield* OffsetConfig.pipe( Effect.mapError( (cause) => new DevRunnerError({ diff --git a/scripts/mock-update-server.ts b/scripts/mock-update-server.ts index 27715b5c5ad..b1dde31e37e 100644 --- a/scripts/mock-update-server.ts +++ b/scripts/mock-update-server.ts @@ -24,7 +24,7 @@ const resolveMockUpdateServerConfig = Effect.gen(function* () { root: Config.string("T3CODE_DESKTOP_MOCK_UPDATE_SERVER_ROOT").pipe( Config.withDefault("../release-mock"), ), - }).asEffect(); + }); const resolvedRoot = path.resolve(import.meta.dirname, config.root);