authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-05-28 11:13:50-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-05-28 11:13:50-07:00
loge6330ead82319d2be53beb5de3aaf4eda0d91a70
tree88eee34595f8f62acbd219e810a6a08acf1d60b6
parent4f0f24f055fe5f5346f7e73372e8cdc167e052f9
signature Signed by PGP key B5690EEEBB952194

[codex] Bump Effect to beta.73 and migrate compatibility APIs (#2840)

Co-authored-by: codex <codex@users.noreply.github.com>

48 files changed, 1341 insertions(+), 1103 deletions(-)

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