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:
4646 - name: Install dependencies
4747 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
4957 - name: Format
5058 run: bun run fmt:check
5159
apps/desktop/src/app/DesktopApp.ts+3-2
......@@ -1,8 +1,8 @@
11import * as Cause from "effect/Cause";
2import * as Crypto from "effect/Crypto";
23import * as Data from "effect/Data";
34import * as Effect from "effect/Effect";
45import * as Option from "effect/Option";
5import * as Random from "effect/Random";
66import * as Ref from "effect/Ref";
77
88import * as NetService from "@t3tools/shared/Net";
......@@ -26,7 +26,8 @@ const DEFAULT_DESKTOP_BACKEND_PORT = 3773;
2626const MAX_TCP_PORT = 65_535;
2727const 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),
3031 Effect.map((value) => value.replaceAll("-", "").slice(0, 12)),
3132);
3233
apps/desktop/src/backend/DesktopBackendConfiguration.ts+19-20
......@@ -1,10 +1,12 @@
11import { parsePersistedServerObservabilitySettings } from "@t3tools/shared/serverSettings";
22import * as Context from "effect/Context";
3import * as Crypto from "effect/Crypto";
34import * as Effect from "effect/Effect";
5import * as Encoding from "effect/Encoding";
46import * as FileSystem from "effect/FileSystem";
57import * as Layer from "effect/Layer";
68import * as Option from "effect/Option";
7import * as Random from "effect/Random";
9import * as PlatformError from "effect/PlatformError";
810import * as Ref from "effect/Ref";
911
1012import * as DesktopBackendManager from "./DesktopBackendManager.ts";
......@@ -13,7 +15,10 @@ import * as DesktopObservability from "../app/DesktopObservability.ts";
1315import * as DesktopServerExposure from "./DesktopServerExposure.ts";
1416
1517export interface DesktopBackendConfigurationShape {
16 readonly resolve: Effect.Effect<DesktopBackendManager.DesktopBackendStartConfig>;
18 readonly resolve: Effect.Effect<
19 DesktopBackendManager.DesktopBackendStartConfig,
20 PlatformError.PlatformError
21 >;
1722}
1823
1924export class DesktopBackendConfiguration extends Context.Service<
......@@ -80,23 +85,6 @@ const readPersistedBackendObservabilitySettings: Effect.Effect<
8085 };
8186});
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
10088const resolveBackendStartConfig = Effect.fn("desktop.backendConfiguration.resolveStartConfig")(
10189 function* (input: {
10290 readonly bootstrapToken: string;
......@@ -148,11 +136,22 @@ export const layer = Layer.effect(
148136 const environment = yield* DesktopEnvironment.DesktopEnvironment;
149137 const fileSystem = yield* FileSystem.FileSystem;
150138 const serverExposure = yield* DesktopServerExposure.DesktopServerExposure;
139 const crypto = yield* Crypto.Crypto;
151140 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
153152 return DesktopBackendConfiguration.of({
154153 resolve: Effect.gen(function* () {
155 const bootstrapToken = yield* getOrCreateBootstrapToken(tokenRef);
154 const bootstrapToken = yield* getOrCreateBootstrapToken;
156155 const observabilitySettings = yield* readPersistedBackendObservabilitySettings.pipe(
157156 Effect.provideService(FileSystem.FileSystem, fileSystem),
158157 Effect.provideService(DesktopEnvironment.DesktopEnvironment, environment),
apps/desktop/src/backend/DesktopBackendManager.ts+16-6
......@@ -329,9 +329,19 @@ const makeDesktopBackendManager = Effect.fn("makeDesktopBackendManager")(functio
329329 }
330330
331331 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 }
333343 const entryExists = yield* fileSystem
334 .exists(config.entryPath)
344 .exists(config.value.entryPath)
335345 .pipe(Effect.orElseSucceed(() => false));
336346
337347 yield* cancelRestart;
......@@ -339,11 +349,11 @@ const makeDesktopBackendManager = Effect.fn("makeDesktopBackendManager")(functio
339349 ...latest,
340350 desiredRunning: true,
341351 ready: false,
342 config: Option.some(config),
352 config: Option.some(config.value),
343353 }));
344354
345355 if (!entryExists) {
346 yield* scheduleRestart(`missing server entry at ${config.entryPath}`);
356 yield* scheduleRestart(`missing server entry at ${config.value.entryPath}`);
347357 return;
348358 }
349359
......@@ -425,7 +435,7 @@ const makeDesktopBackendManager = Effect.fn("makeDesktopBackendManager")(functio
425435 });
426436
427437 const program = runBackendProcess({
428 ...config,
438 ...config.value,
429439 onStarted: Effect.fn("desktop.backendManager.onStarted")(function* (pid) {
430440 yield* updateActiveRun(runId, (run) => ({
431441 ...run,
......@@ -433,7 +443,7 @@ const makeDesktopBackendManager = Effect.fn("makeDesktopBackendManager")(functio
433443 }));
434444 yield* backendOutputLog.writeSessionBoundary({
435445 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}`,
437447 });
438448 }),
439449 onReady: Effect.fn("desktop.backendManager.onReady")(function* () {
apps/desktop/src/settings/DesktopAppSettings.ts+16-10
......@@ -6,6 +6,7 @@ import {
66} from "@t3tools/contracts";
77import { fromLenientJson } from "@t3tools/shared/schemaJson";
88import * as Context from "effect/Context";
9import * as Crypto from "effect/Crypto";
910import * as Data from "effect/Data";
1011import * as Effect from "effect/Effect";
1112import * as FileSystem from "effect/FileSystem";
......@@ -13,7 +14,6 @@ import * as Layer from "effect/Layer";
1314import * as Option from "effect/Option";
1415import * as Path from "effect/Path";
1516import * as PlatformError from "effect/PlatformError";
16import * as Random from "effect/Random";
1717import * as Schema from "effect/Schema";
1818import * as SynchronizedRef from "effect/SynchronizedRef";
1919
......@@ -222,10 +222,10 @@ const writeSettings = Effect.fn("desktop.settings.writeSettings")(function* (inp
222222 readonly settingsPath: string;
223223 readonly settings: DesktopSettings;
224224 readonly defaultSettings: DesktopSettings;
225 readonly suffix: string;
225226}): Effect.fn.Return<void, PlatformError.PlatformError | Schema.SchemaError> {
226227 const directory = input.path.dirname(input.settingsPath);
227 const suffix = (yield* Random.nextUUIDv4).replace(/-/g, "");
228 const tempPath = `${input.settingsPath}.${process.pid}.${suffix}.tmp`;
228 const tempPath = `${input.settingsPath}.${process.pid}.${input.suffix}.tmp`;
229229 const encoded = yield* encodeDesktopSettingsJson(
230230 toDesktopSettingsDocument(input.settings, input.defaultSettings),
231231 );
......@@ -240,6 +240,7 @@ export const layer = Layer.effect(
240240 const environment = yield* DesktopEnvironment.DesktopEnvironment;
241241 const fileSystem = yield* FileSystem.FileSystem;
242242 const path = yield* Path.Path;
243 const crypto = yield* Crypto.Crypto;
243244 const settingsRef = yield* SynchronizedRef.make(environment.defaultDesktopSettings);
244245
245246 const persist = (
......@@ -251,13 +252,18 @@ export const layer = Layer.effect(
251252 return Effect.succeed([settingsChange(settings, false), settings] as const);
252253 }
253254
254 return writeSettings({
255 fileSystem,
256 path,
257 settingsPath: environment.desktopSettingsPath,
258 settings: nextSettings,
259 defaultSettings: environment.defaultDesktopSettings,
260 }).pipe(
255 return crypto.randomUUIDv4.pipe(
256 Effect.map((uuid) => uuid.replace(/-/g, "")),
257 Effect.flatMap((suffix) =>
258 writeSettings({
259 fileSystem,
260 path,
261 settingsPath: environment.desktopSettingsPath,
262 settings: nextSettings,
263 defaultSettings: environment.defaultDesktopSettings,
264 suffix,
265 }),
266 ),
261267 Effect.mapError((cause) => new DesktopSettingsWriteError({ cause })),
262268 Effect.as([settingsChange(nextSettings, true), nextSettings] as const),
263269 );
apps/desktop/src/settings/DesktopClientSettings.ts+15-9
......@@ -1,6 +1,7 @@
11import { ClientSettingsSchema, type ClientSettings } from "@t3tools/contracts";
22import { fromLenientJson } from "@t3tools/shared/schemaJson";
33import * as Context from "effect/Context";
4import * as Crypto from "effect/Crypto";
45import * as Data from "effect/Data";
56import * as Effect from "effect/Effect";
67import * as FileSystem from "effect/FileSystem";
......@@ -8,7 +9,6 @@ import * as Layer from "effect/Layer";
89import * as Option from "effect/Option";
910import * as Path from "effect/Path";
1011import * as PlatformError from "effect/PlatformError";
11import * as Random from "effect/Random";
1212import * as Schema from "effect/Schema";
1313import * as Ref from "effect/Ref";
1414
......@@ -74,10 +74,10 @@ const writeClientSettings = Effect.fnUntraced(function* (input: {
7474 readonly path: Path.Path;
7575 readonly settingsPath: string;
7676 readonly settings: ClientSettings;
77 readonly suffix: string;
7778}): Effect.fn.Return<void, PlatformError.PlatformError | Schema.SchemaError> {
7879 const directory = input.path.dirname(input.settingsPath);
79 const suffix = (yield* Random.nextUUIDv4).replace(/-/g, "");
80 const tempPath = `${input.settingsPath}.${process.pid}.${suffix}.tmp`;
80 const tempPath = `${input.settingsPath}.${process.pid}.${input.suffix}.tmp`;
8181 const encoded = yield* encodeClientSettingsJson(input.settings);
8282 yield* input.fileSystem.makeDirectory(directory, { recursive: true });
8383 yield* input.fileSystem.writeFileString(tempPath, `${encoded}\n`);
......@@ -90,18 +90,24 @@ export const layer = Layer.effect(
9090 const environment = yield* DesktopEnvironment.DesktopEnvironment;
9191 const fileSystem = yield* FileSystem.FileSystem;
9292 const path = yield* Path.Path;
93 const crypto = yield* Crypto.Crypto;
9394
9495 return DesktopClientSettings.of({
9596 get: readClientSettings(fileSystem, environment.clientSettingsPath).pipe(
9697 Effect.withSpan("desktop.clientSettings.get"),
9798 ),
9899 set: (settings) =>
99 writeClientSettings({
100 fileSystem,
101 path,
102 settingsPath: environment.clientSettingsPath,
103 settings,
104 }).pipe(
100 crypto.randomUUIDv4.pipe(
101 Effect.map((uuid) => uuid.replace(/-/g, "")),
102 Effect.flatMap((suffix) =>
103 writeClientSettings({
104 fileSystem,
105 path,
106 settingsPath: environment.clientSettingsPath,
107 settings,
108 suffix,
109 }),
110 ),
105111 Effect.mapError((cause) => new DesktopClientSettingsWriteError({ cause })),
106112 Effect.withSpan("desktop.clientSettings.set"),
107113 ),
apps/desktop/src/settings/DesktopSavedEnvironments.ts+17-9
......@@ -1,6 +1,7 @@
11import { EnvironmentId, type PersistedSavedEnvironmentRecord } from "@t3tools/contracts";
22import { fromLenientJson } from "@t3tools/shared/schemaJson";
33import * as Context from "effect/Context";
4import * as Crypto from "effect/Crypto";
45import * as Data from "effect/Data";
56import * as Effect from "effect/Effect";
67import * as Encoding from "effect/Encoding";
......@@ -9,7 +10,6 @@ import * as Layer from "effect/Layer";
910import * as Option from "effect/Option";
1011import * as Path from "effect/Path";
1112import * as PlatformError from "effect/PlatformError";
12import * as Random from "effect/Random";
1313import * as Schema from "effect/Schema";
1414import * as Ref from "effect/Ref";
1515
......@@ -200,10 +200,10 @@ const writeRegistryDocument = Effect.fn("desktop.savedEnvironments.writeRegistry
200200 readonly path: Path.Path;
201201 readonly registryPath: string;
202202 readonly document: SavedEnvironmentRegistryDocument;
203 readonly suffix: string;
203204 }): Effect.fn.Return<void, PlatformError.PlatformError | Schema.SchemaError> {
204205 const directory = input.path.dirname(input.registryPath);
205 const suffix = (yield* Random.nextUUIDv4).replace(/-/g, "");
206 const tempPath = `${input.registryPath}.${process.pid}.${suffix}.tmp`;
206 const tempPath = `${input.registryPath}.${process.pid}.${input.suffix}.tmp`;
207207 const encoded = yield* encodeSavedEnvironmentRegistryDocumentJson(input.document);
208208 yield* input.fileSystem.makeDirectory(directory, { recursive: true });
209209 yield* input.fileSystem.writeFileString(tempPath, `${encoded}\n`);
......@@ -247,14 +247,22 @@ export const layer = Layer.effect(
247247 const fileSystem = yield* FileSystem.FileSystem;
248248 const path = yield* Path.Path;
249249 const safeStorage = yield* ElectronSafeStorage.ElectronSafeStorage;
250 const crypto = yield* Crypto.Crypto;
250251
251252 const writeDocument = (document: SavedEnvironmentRegistryDocument) =>
252 writeRegistryDocument({
253 fileSystem,
254 path,
255 registryPath: environment.savedEnvironmentRegistryPath,
256 document,
257 }).pipe(Effect.mapError((cause) => new DesktopSavedEnvironmentsWriteError({ cause })));
253 crypto.randomUUIDv4.pipe(
254 Effect.map((uuid) => uuid.replace(/-/g, "")),
255 Effect.flatMap((suffix) =>
256 writeRegistryDocument({
257 fileSystem,
258 path,
259 registryPath: environment.savedEnvironmentRegistryPath,
260 document,
261 suffix,
262 }),
263 ),
264 Effect.mapError((cause) => new DesktopSavedEnvironmentsWriteError({ cause })),
265 );
258266
259267 return DesktopSavedEnvironments.of({
260268 getRegistry: readRegistryDocument(fileSystem, environment.savedEnvironmentRegistryPath).pipe(
apps/desktop/src/ssh/DesktopSshPasswordPrompts.test.ts+2
......@@ -1,4 +1,5 @@
11import { assert, describe, it } from "@effect/vitest";
2import * as NodeServices from "@effect/platform-node/NodeServices";
23import * as Duration from "effect/Duration";
34import * as Effect from "effect/Effect";
45import * as Fiber from "effect/Fiber";
......@@ -86,6 +87,7 @@ function makeElectronWindowLayer(window: ReturnType<typeof makeTestWindow>["wind
8687function makeLayer(window: ReturnType<typeof makeTestWindow>["window"]) {
8788 return DesktopSshPasswordPrompts.layer({ passwordPromptTimeoutMs: 1_000 }).pipe(
8889 Layer.provide(makeElectronWindowLayer(window)),
90 Layer.provide(NodeServices.layer),
8991 Layer.provideMerge(TestClock.layer()),
9092 );
9193}
apps/desktop/src/ssh/DesktopSshPasswordPrompts.ts+7-2
......@@ -2,6 +2,7 @@ import type { DesktopSshPasswordPromptRequest } from "@t3tools/contracts";
22import { DesktopSshPasswordPromptResolutionInputSchema } from "@t3tools/contracts";
33import type { SshPasswordRequest } from "@t3tools/ssh/auth";
44import * as Context from "effect/Context";
5import * as Crypto from "effect/Crypto";
56import * as Data from "effect/Data";
67import * as DateTime from "effect/DateTime";
78import * as Deferred from "effect/Deferred";
......@@ -9,7 +10,6 @@ import * as Duration from "effect/Duration";
910import * as Effect from "effect/Effect";
1011import * as Layer from "effect/Layer";
1112import * as Option from "effect/Option";
12import * as Random from "effect/Random";
1313import * as Ref from "effect/Ref";
1414
1515import * as IpcChannels from "../ipc/channels.ts";
......@@ -163,6 +163,7 @@ const failPending = (
163163
164164const make = Effect.fn("desktop.sshPasswordPrompts.make")(function* (options: LayerOptions = {}) {
165165 const electronWindow = yield* ElectronWindow.ElectronWindow;
166 const crypto = yield* Crypto.Crypto;
166167 const pendingRef = yield* Ref.make(new Map<string, PendingSshPasswordPrompt>());
167168 const passwordPromptTimeoutMs =
168169 options.passwordPromptTimeoutMs ?? DEFAULT_SSH_PASSWORD_PROMPT_TIMEOUT_MS;
......@@ -230,7 +231,11 @@ const make = Effect.fn("desktop.sshPasswordPrompts.make")(function* (options: La
230231 });
231232 }
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 );
234239 const now = yield* DateTime.now;
235240 const expiresAt = DateTime.formatIso(
236241 DateTime.add(now, { milliseconds: passwordPromptTimeoutMs }),
apps/server/integration/TestProviderAdapter.integration.ts+16-3
......@@ -11,8 +11,8 @@ import {
1111 ProviderDriverKind,
1212} from "@t3tools/contracts";
1313import * as Effect from "effect/Effect";
14import * as Crypto from "effect/Crypto";
1415import * as Queue from "effect/Queue";
15import * as Random from "effect/Random";
1616import * as Stream from "effect/Stream";
1717
1818import {
......@@ -226,6 +226,7 @@ function missingSessionEffect(
226226export const makeTestProviderAdapterHarness = (options?: MakeTestProviderAdapterHarnessOptions) =>
227227 Effect.gen(function* () {
228228 const provider = options?.provider ?? ProviderDriverKind.make("codex");
229 const crypto = yield* Crypto.Crypto;
229230 const runtimeEvents = yield* Queue.unbounded<ProviderRuntimeEvent>();
230231 let sessionCount = 0;
231232 const sessions = new Map<ThreadId, SessionState>();
......@@ -241,6 +242,18 @@ export const makeTestProviderAdapterHarness = (options?: MakeTestProviderAdapter
241242 >();
242243
243244 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
245258 const startSession: ProviderAdapterShape<ProviderAdapterError>["startSession"] = (input) =>
246259 Effect.gen(function* () {
......@@ -309,7 +322,7 @@ export const makeTestProviderAdapterHarness = (options?: MakeTestProviderAdapter
309322 for (const fixtureEvent of response.events) {
310323 const rawEvent: Record<string, unknown> = {
311324 ...(fixtureEvent as Record<string, unknown>),
312 eventId: yield* Random.nextUUIDv4,
325 eventId: yield* randomUUIDv4(input.threadId),
313326 provider,
314327 sessionId: RuntimeSessionId.make(String(input.threadId)),
315328 };
......@@ -366,7 +379,7 @@ export const makeTestProviderAdapterHarness = (options?: MakeTestProviderAdapter
366379 if (deferredTurnCompletedEvents.length === 0) {
367380 yield* emit({
368381 type: "turn.completed",
369 eventId: EventId.make(yield* Random.nextUUIDv4),
382 eventId: EventId.make(yield* randomUUIDv4(input.threadId)),
370383 provider,
371384 createdAt: nowIso(),
372385 threadId: state.snapshot.threadId,
apps/server/src/atomicWrite.ts+1-3
......@@ -1,7 +1,6 @@
11import * as Effect from "effect/Effect";
22import * as FileSystem from "effect/FileSystem";
33import * as Path from "effect/Path";
4import * as Random from "effect/Random";
54
65export const writeFileStringAtomically = (input: {
76 readonly filePath: string;
......@@ -11,7 +10,6 @@ export const writeFileStringAtomically = (input: {
1110 Effect.gen(function* () {
1211 const fs = yield* FileSystem.FileSystem;
1312 const path = yield* Path.Path;
14 const tempFileId = yield* Random.nextUUIDv4;
1513 const targetDirectory = path.dirname(input.filePath);
1614
1715 yield* fs.makeDirectory(targetDirectory, { recursive: true });
......@@ -19,7 +17,7 @@ export const writeFileStringAtomically = (input: {
1917 directory: targetDirectory,
2018 prefix: `${path.basename(input.filePath)}.`,
2119 });
22 const tempPath = path.join(tempDirectory, `${tempFileId}.tmp`);
20 const tempPath = path.join(tempDirectory, "contents.tmp");
2321
2422 yield* fs.writeFileString(tempPath, input.contents);
2523 yield* fs.rename(tempPath, input.filePath);
apps/server/src/cli/project.ts+16-5
......@@ -5,6 +5,7 @@ import {
55 type ClientOrchestrationCommand,
66} from "@t3tools/contracts";
77import * as Console from "effect/Console";
8import * as Crypto from "effect/Crypto";
89import * as Data from "effect/Data";
910import * as DateTime from "effect/DateTime";
1011import * as Duration from "effect/Duration";
......@@ -58,6 +59,16 @@ class ProjectCommandError extends Data.TaggedError("ProjectCommandError")<{
5859 readonly message: string;
5960}> {}
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
6172const ProjectCliRuntimeLive = Layer.mergeAll(
6273 WorkspacePathsLive,
6374 OrchestrationLayerLive.pipe(
......@@ -260,7 +271,7 @@ const runProjectMutation = Effect.fn("runProjectMutation")(function* (
260271 }) => Effect.Effect<
261272 string,
262273 Error,
263 FileSystem.FileSystem | HttpClient.HttpClient | Path.Path | WorkspacePaths
274 Crypto.Crypto | FileSystem.FileSystem | HttpClient.HttpClient | Path.Path | WorkspacePaths
264275 >,
265276) {
266277 const logLevel = yield* GlobalFlag.LogLevel;
......@@ -343,10 +354,10 @@ const projectAddCommand = Command.make("add", {
343354 }
344355
345356 const title = yield* resolveProjectTitle(workspaceRoot, Option.getOrUndefined(flags.title));
346 const projectId = ProjectId.make(crypto.randomUUID());
357 const projectId = ProjectId.make(yield* projectCommandUuid);
347358 yield* dispatch({
348359 type: "project.create",
349 commandId: CommandId.make(crypto.randomUUID()),
360 commandId: CommandId.make(yield* projectCommandUuid),
350361 projectId,
351362 title,
352363 workspaceRoot,
......@@ -384,7 +395,7 @@ const projectRemoveCommand = Command.make("remove", {
384395 });
385396 yield* dispatch({
386397 type: "project.delete",
387 commandId: CommandId.make(crypto.randomUUID()),
398 commandId: CommandId.make(yield* projectCommandUuid),
388399 projectId: project.id,
389400 });
390401 return `Removed project ${project.id} (${project.title}).`;
......@@ -424,7 +435,7 @@ const projectRenameCommand = Command.make("rename", {
424435
425436 yield* dispatch({
426437 type: "project.meta.update",
427 commandId: CommandId.make(crypto.randomUUID()),
438 commandId: CommandId.make(yield* projectCommandUuid),
428439 projectId: project.id,
429440 title: nextTitle,
430441 });
apps/server/src/environment/Layers/ServerEnvironment.ts+3-2
......@@ -1,9 +1,9 @@
11import { EnvironmentId, type ExecutionEnvironmentDescriptor } from "@t3tools/contracts";
2import * as Crypto from "effect/Crypto";
23import * as Effect from "effect/Effect";
34import * as FileSystem from "effect/FileSystem";
45import * as Layer from "effect/Layer";
56import * as Path from "effect/Path";
6import * as Random from "effect/Random";
77
88import { ServerConfig } from "../../config.ts";
99import { layer as ProcessRunnerLive } from "../../processRunner.ts";
......@@ -39,6 +39,7 @@ export const makeServerEnvironment = Effect.fn("makeServerEnvironment")(function
3939 const fileSystem = yield* FileSystem.FileSystem;
4040 const path = yield* Path.Path;
4141 const serverConfig = yield* ServerConfig;
42 const crypto = yield* Crypto.Crypto;
4243
4344 const readPersistedEnvironmentId = Effect.gen(function* () {
4445 const exists = yield* fileSystem
......@@ -64,7 +65,7 @@ export const makeServerEnvironment = Effect.fn("makeServerEnvironment")(function
6465 return persisted;
6566 }
6667
67 const generated = yield* Random.nextUUIDv4;
68 const generated = yield* crypto.randomUUIDv4;
6869 yield* persistEnvironmentId(generated);
6970 return generated;
7071 });
apps/server/src/git/GitManager.ts+28-22
......@@ -1,8 +1,7 @@
1import { randomUUID } from "node:crypto";
2
31import * as Arr from "effect/Array";
42import * as Cache from "effect/Cache";
53import * as Context from "effect/Context";
4import * as Crypto from "effect/Crypto";
65import * as DateTime from "effect/DateTime";
76import * as Duration from "effect/Duration";
87import * as Effect from "effect/Effect";
......@@ -532,32 +531,39 @@ export const makeGitManager = Effect.fn("makeGitManager")(function* () {
532531 const sourceControlProviders = yield* SourceControlProviderRegistry;
533532 const textGeneration = yield* TextGeneration;
534533 const projectSetupScriptRunner = yield* ProjectSetupScriptRunner;
534 const crypto = yield* Crypto.Crypto;
535535
536536 const sourceControlProvider = (cwd: string) => sourceControlProviders.resolve({ cwd });
537537 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
539544 const createProgressEmitter = (
540545 input: { cwd: string; action: GitStackedAction },
541546 options?: GitRunStackedActionOptions,
542 ) => {
543 const actionId = options?.actionId ?? randomUUID();
544 const reporter = options?.progressReporter;
545
546 const emit = (event: GitActionProgressPayload) =>
547 reporter
548 ? reporter.publish({
549 actionId,
550 cwd: input.cwd,
551 action: input.action,
552 ...event,
553 } as GitActionProgressEvent)
554 : Effect.void;
547 ) =>
548 (options?.actionId === undefined ? randomUUIDv4 : Effect.succeed(options.actionId)).pipe(
549 Effect.map((actionId) => {
550 const reporter = options?.progressReporter;
551 const emit = (event: GitActionProgressPayload) =>
552 reporter
553 ? reporter.publish({
554 actionId,
555 cwd: input.cwd,
556 action: input.action,
557 ...event,
558 } as GitActionProgressEvent)
559 : Effect.void;
555560
556 return {
557 actionId,
558 emit,
559 };
560 };
561 return {
562 actionId,
563 emit,
564 };
565 }),
566 );
561567
562568 const configurePullRequestHeadUpstreamBase = Effect.fn("configurePullRequestHeadUpstream")(
563569 function* (
......@@ -1301,7 +1307,7 @@ export const makeGitManager = Effect.fn("makeGitManager")(function* () {
13011307 modelSelection,
13021308 });
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`);
13051311 yield* fileSystem
13061312 .writeFileString(bodyFile, generated.body)
13071313 .pipe(
......@@ -1591,7 +1597,7 @@ export const makeGitManager = Effect.fn("makeGitManager")(function* () {
15911597
15921598 const runStackedAction: GitManagerShape["runStackedAction"] = Effect.fn("runStackedAction")(
15931599 function* (input, options) {
1594 const progress = createProgressEmitter(input, options);
1600 const progress = yield* createProgressEmitter(input, options);
15951601 const currentPhase = yield* Ref.make<Option.Option<GitActionProgressPhase>>(Option.none());
15961602
15971603 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", () => {
211211 Layer.provide(Layer.succeed(OrchestrationEventStore, eventStore)),
212212 Layer.provide(OrchestrationCommandReceiptRepositoryLive),
213213 Layer.provide(SqlitePersistenceMemory),
214 Layer.provideMerge(NodeServices.layer),
214215 );
215216
216217 const runtime = ManagedRuntime.make(layer);
apps/server/src/orchestration/Layers/OrchestrationEngine.ts+14-1
......@@ -7,6 +7,7 @@ import type {
77import { OrchestrationCommand } from "@t3tools/contracts";
88import * as Cause from "effect/Cause";
99import * as Clock from "effect/Clock";
10import * as Crypto from "effect/Crypto";
1011import * as DateTime from "effect/DateTime";
1112import * as Deferred from "effect/Deferred";
1213import * as Duration from "effect/Duration";
......@@ -81,6 +82,7 @@ const makeOrchestrationEngine = Effect.gen(function* () {
8182 const commandReceiptRepository = yield* OrchestrationCommandReceiptRepository;
8283 const projectionPipeline = yield* OrchestrationProjectionPipeline;
8384 const projectionSnapshotQuery = yield* ProjectionSnapshotQuery;
85 const crypto = yield* Crypto.Crypto;
8486
8587 const nowIso = Effect.map(DateTime.now, DateTime.formatIso);
8688 let commandReadModel = createEmptyReadModel(yield* nowIso);
......@@ -151,7 +153,18 @@ const makeOrchestrationEngine = Effect.gen(function* () {
151153 const eventBase = yield* decideOrchestrationCommand({
152154 command: envelope.command,
153155 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 );
155168 const eventBases = Array.isArray(eventBase) ? eventBase : [eventBase];
156169 const committedCommand = yield* sql
157170 .withTransaction(
apps/server/src/orchestration/Layers/ProviderCommandReactor.ts+42-29
......@@ -15,6 +15,7 @@ import {
1515import { isTemporaryWorktreeBranch, WORKTREE_BRANCH_PREFIX } from "@t3tools/shared/git";
1616import * as Cache from "effect/Cache";
1717import * as Cause from "effect/Cause";
18import * as Crypto from "effect/Crypto";
1819import * as Duration from "effect/Duration";
1920import * as Effect from "effect/Effect";
2021import * as Equal from "effect/Equal";
......@@ -81,9 +82,6 @@ function mapProviderSessionStatusToOrchestrationStatus(
8182const turnStartKeyForEvent = (event: ProviderIntentEvent): string =>
8283 event.commandId !== null ? `command:${event.commandId}` : `event:${event.eventId}`;
8384
84const serverCommandId = (tag: string): CommandId =>
85 CommandId.make(`server:${tag}:${crypto.randomUUID()}`);
86
8785const HANDLED_TURN_START_KEY_MAX = 10_000;
8886const HANDLED_TURN_START_KEY_TTL = Duration.minutes(30);
8987const DEFAULT_RUNTIME_MODE: RuntimeMode = "full-access";
......@@ -178,6 +176,7 @@ function buildGeneratedWorktreeBranchName(raw: string): string {
178176}
179177
180178const make = Effect.gen(function* () {
179 const crypto = yield* Crypto.Crypto;
181180 const orchestrationEngine = yield* OrchestrationEngineService;
182181 const projectionSnapshotQuery = yield* ProjectionSnapshotQuery;
183182 const providerService = yield* ProviderService;
......@@ -185,6 +184,9 @@ const make = Effect.gen(function* () {
185184 const vcsStatusBroadcaster = yield* VcsStatusBroadcaster;
186185 const textGeneration = yield* TextGeneration;
187186 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));
188190 const handledTurnStartKeys = yield* Cache.make<string, true>({
189191 capacity: HANDLED_TURN_START_KEY_MAX,
190192 timeToLive: HANDLED_TURN_START_KEY_TTL,
......@@ -214,24 +216,31 @@ const make = Effect.gen(function* () {
214216 readonly createdAt: string;
215217 readonly requestId?: string;
216218 }) =>
217 orchestrationEngine.dispatch({
218 type: "thread.activity.append",
219 Effect.all({
219220 commandId: serverCommandId("provider-failure-activity"),
220 threadId: input.threadId,
221 activity: {
222 id: EventId.make(crypto.randomUUID()),
223 tone: "error",
224 kind: input.kind,
225 summary: input.summary,
226 payload: {
227 detail: input.detail,
228 ...(input.requestId ? { requestId: input.requestId } : {}),
229 },
230 turnId: input.turnId,
231 createdAt: input.createdAt,
232 },
233 createdAt: input.createdAt,
234 });
221 eventId: serverEventId(),
222 }).pipe(
223 Effect.flatMap(({ commandId, eventId }) =>
224 orchestrationEngine.dispatch({
225 type: "thread.activity.append",
226 commandId,
227 threadId: input.threadId,
228 activity: {
229 id: eventId,
230 tone: "error",
231 kind: input.kind,
232 summary: input.summary,
233 payload: {
234 detail: input.detail,
235 ...(input.requestId ? { requestId: input.requestId } : {}),
236 },
237 turnId: input.turnId,
238 createdAt: input.createdAt,
239 },
240 createdAt: input.createdAt,
241 }),
242 ),
243 );
235244
236245 const formatFailureDetail = (cause: Cause.Cause<unknown>): string => {
237246 const failReason = cause.reasons.find(Cause.isFailReason);
......@@ -249,13 +258,17 @@ const make = Effect.gen(function* () {
249258 readonly session: OrchestrationSession;
250259 readonly createdAt: string;
251260 }) =>
252 orchestrationEngine.dispatch({
253 type: "thread.session.set",
254 commandId: serverCommandId("provider-session-set"),
255 threadId: input.threadId,
256 session: input.session,
257 createdAt: input.createdAt,
258 });
261 serverCommandId("provider-session-set").pipe(
262 Effect.flatMap((commandId) =>
263 orchestrationEngine.dispatch({
264 type: "thread.session.set",
265 commandId,
266 threadId: input.threadId,
267 session: input.session,
268 createdAt: input.createdAt,
269 }),
270 ),
271 );
259272
260273 const setThreadSessionErrorOnTurnStartFailure = Effect.fnUntraced(function* (input: {
261274 readonly threadId: ThreadId;
......@@ -610,7 +623,7 @@ const make = Effect.gen(function* () {
610623 const renamed = yield* gitWorkflow.renameBranch({ cwd, oldBranch, newBranch: targetBranch });
611624 yield* orchestrationEngine.dispatch({
612625 type: "thread.meta.update",
613 commandId: serverCommandId("worktree-branch-rename"),
626 commandId: yield* serverCommandId("worktree-branch-rename"),
614627 threadId: input.threadId,
615628 branch: renamed.branch,
616629 worktreePath: cwd,
......@@ -657,7 +670,7 @@ const make = Effect.gen(function* () {
657670
658671 yield* orchestrationEngine.dispatch({
659672 type: "thread.meta.update",
660 commandId: serverCommandId("thread-title-rename"),
673 commandId: yield* serverCommandId("thread-title-rename"),
661674 threadId: input.threadId,
662675 title: generated.title,
663676 });
apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts+29-20
......@@ -19,6 +19,7 @@ import {
1919} from "@t3tools/contracts";
2020import * as Cache from "effect/Cache";
2121import * as Cause from "effect/Cause";
22import * as Crypto from "effect/Crypto";
2223import * as Duration from "effect/Duration";
2324import * as Effect from "effect/Effect";
2425import * as Layer from "effect/Layer";
......@@ -39,8 +40,6 @@ import {
3940import { ServerSettingsService } from "../../serverSettings.ts";
4041
4142const providerTurnKey = (threadId: ThreadId, turnId: TurnId) => `${threadId}:${turnId}`;
42const providerCommandId = (event: ProviderRuntimeEvent, tag: string): CommandId =>
43 CommandId.make(`provider:${event.eventId}:${tag}:${crypto.randomUUID()}`);
4443
4544interface AssistantSegmentState {
4645 baseKey: string;
......@@ -607,11 +606,16 @@ function runtimeEventToActivities(
607606}
608607
609608const make = Effect.gen(function* () {
609 const crypto = yield* Crypto.Crypto;
610610 const orchestrationEngine = yield* OrchestrationEngineService;
611611 const projectionSnapshotQuery = yield* ProjectionSnapshotQuery;
612612 const providerService = yield* ProviderService;
613613 const projectionTurnRepository = yield* ProjectionTurnRepository;
614614 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
616620 const turnMessageIdsByTurnKey = yield* Cache.make<string, Set<MessageId>>({
617621 capacity: TURN_MESSAGE_IDS_BY_TURN_CACHE_CAPACITY,
......@@ -848,7 +852,7 @@ const make = Effect.gen(function* () {
848852
849853 yield* orchestrationEngine.dispatch({
850854 type: "thread.message.assistant.delta",
851 commandId: providerCommandId(input.event, input.commandTag),
855 commandId: yield* providerCommandId(input.event, input.commandTag),
852856 threadId: input.threadId,
853857 messageId: input.messageId,
854858 delta: bufferedText,
......@@ -915,7 +919,7 @@ const make = Effect.gen(function* () {
915919 if (hasRenderableText) {
916920 yield* orchestrationEngine.dispatch({
917921 type: "thread.message.assistant.delta",
918 commandId: providerCommandId(input.event, input.finalDeltaCommandTag),
922 commandId: yield* providerCommandId(input.event, input.finalDeltaCommandTag),
919923 threadId: input.threadId,
920924 messageId: input.messageId,
921925 delta: text,
......@@ -927,7 +931,7 @@ const make = Effect.gen(function* () {
927931 if (input.hasProjectedMessage || hasRenderableText) {
928932 yield* orchestrationEngine.dispatch({
929933 type: "thread.message.assistant.complete",
930 commandId: providerCommandId(input.event, input.commandTag),
934 commandId: yield* providerCommandId(input.event, input.commandTag),
931935 threadId: input.threadId,
932936 messageId: input.messageId,
933937 ...(input.turnId ? { turnId: input.turnId } : {}),
......@@ -1003,7 +1007,7 @@ const make = Effect.gen(function* () {
10031007 const existingPlan = findProposedPlanById(input.threadProposedPlans, input.planId);
10041008 yield* orchestrationEngine.dispatch({
10051009 type: "thread.proposed-plan.upsert",
1006 commandId: providerCommandId(input.event, "proposed-plan-upsert"),
1010 commandId: yield* providerCommandId(input.event, "proposed-plan-upsert"),
10071011 threadId: input.threadId,
10081012 proposedPlan: {
10091013 id: input.planId,
......@@ -1159,10 +1163,11 @@ const make = Effect.gen(function* () {
11591163 return;
11601164 }
11611165
1166 const commandUuid = yield* crypto.randomUUIDv4;
11621167 yield* orchestrationEngine.dispatch({
11631168 type: "thread.proposed-plan.upsert",
11641169 commandId: CommandId.make(
1165 `provider:source-proposed-plan-implemented:${implementationThreadId}:${crypto.randomUUID()}`,
1170 `provider:source-proposed-plan-implemented:${implementationThreadId}:${commandUuid}`,
11661171 ),
11671172 threadId: sourceThread.id,
11681173 proposedPlan: {
......@@ -1296,7 +1301,7 @@ const make = Effect.gen(function* () {
12961301
12971302 yield* orchestrationEngine.dispatch({
12981303 type: "thread.session.set",
1299 commandId: providerCommandId(event, "thread-session-set"),
1304 commandId: yield* providerCommandId(event, "thread-session-set"),
13001305 threadId: thread.id,
13011306 session: {
13021307 threadId: thread.id,
......@@ -1342,7 +1347,7 @@ const make = Effect.gen(function* () {
13421347 if (spillChunk.length > 0) {
13431348 yield* orchestrationEngine.dispatch({
13441349 type: "thread.message.assistant.delta",
1345 commandId: providerCommandId(event, "assistant-delta-buffer-spill"),
1350 commandId: yield* providerCommandId(event, "assistant-delta-buffer-spill"),
13461351 threadId: thread.id,
13471352 messageId: assistantMessageId,
13481353 delta: spillChunk,
......@@ -1353,7 +1358,7 @@ const make = Effect.gen(function* () {
13531358 } else {
13541359 yield* orchestrationEngine.dispatch({
13551360 type: "thread.message.assistant.delta",
1356 commandId: providerCommandId(event, "assistant-delta"),
1361 commandId: yield* providerCommandId(event, "assistant-delta"),
13571362 threadId: thread.id,
13581363 messageId: assistantMessageId,
13591364 delta: assistantDelta,
......@@ -1546,7 +1551,7 @@ const make = Effect.gen(function* () {
15461551 if (shouldApplyRuntimeError) {
15471552 yield* orchestrationEngine.dispatch({
15481553 type: "thread.session.set",
1549 commandId: providerCommandId(event, "runtime-error-session-set"),
1554 commandId: yield* providerCommandId(event, "runtime-error-session-set"),
15501555 threadId: thread.id,
15511556 session: {
15521557 threadId: thread.id,
......@@ -1568,7 +1573,7 @@ const make = Effect.gen(function* () {
15681573 if (event.type === "thread.metadata.updated" && event.payload.name) {
15691574 yield* orchestrationEngine.dispatch({
15701575 type: "thread.meta.update",
1571 commandId: providerCommandId(event, "thread-meta-update"),
1576 commandId: yield* providerCommandId(event, "thread-meta-update"),
15721577 threadId: thread.id,
15731578 title: event.payload.name,
15741579 });
......@@ -1596,7 +1601,7 @@ const make = Effect.gen(function* () {
15961601 );
15971602 yield* orchestrationEngine.dispatch({
15981603 type: "thread.turn.diff.complete",
1599 commandId: providerCommandId(event, "thread-turn-diff-complete"),
1604 commandId: yield* providerCommandId(event, "thread-turn-diff-complete"),
16001605 threadId: thread.id,
16011606 turnId,
16021607 completedAt: now,
......@@ -1613,13 +1618,17 @@ const make = Effect.gen(function* () {
16131618
16141619 const activities = runtimeEventToActivities(event);
16151620 yield* Effect.forEach(activities, (activity) =>
1616 orchestrationEngine.dispatch({
1617 type: "thread.activity.append",
1618 commandId: providerCommandId(event, "thread-activity-append"),
1619 threadId: thread.id,
1620 activity,
1621 createdAt: activity.createdAt,
1622 }),
1621 providerCommandId(event, "thread-activity-append").pipe(
1622 Effect.flatMap((commandId) =>
1623 orchestrationEngine.dispatch({
1624 type: "thread.activity.append",
1625 commandId,
1626 threadId: thread.id,
1627 activity,
1628 createdAt: activity.createdAt,
1629 }),
1630 ),
1631 ),
16231632 ).pipe(Effect.asVoid);
16241633 });
16251634
apps/server/src/orchestration/decider.delete.test.ts+135-145
......@@ -6,11 +6,11 @@ import {
66 ThreadId,
77 type OrchestrationCommand,
88 type OrchestrationEvent,
9 type OrchestrationReadModel,
109 ProviderInstanceId,
1110} from "@t3tools/contracts";
1211import * 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
1515import { decideOrchestrationCommand } from "./decider.ts";
1616import { createEmptyReadModel, projectEvent } from "./projector.ts";
......@@ -20,93 +20,87 @@ const asEventId = (value: string): EventId => EventId.make(value);
2020const asProjectId = (value: string): ProjectId => ProjectId.make(value);
2121const asThreadId = (value: string): ThreadId => ThreadId.make(value);
2222
23async function seedReadModel(): Promise<OrchestrationReadModel> {
23const seedReadModel = Effect.gen(function* () {
2424 const now = "2026-01-01T00:00:00.000Z";
2525 const initial = createEmptyReadModel(now);
26 const withProject = await Effect.runPromise(
27 projectEvent(initial, {
28 sequence: 1,
29 eventId: asEventId("evt-project-create"),
30 aggregateKind: "project",
31 aggregateId: asProjectId("project-delete"),
32 type: "project.created",
33 occurredAt: now,
34 commandId: asCommandId("cmd-project-create"),
35 causationEventId: null,
36 correlationId: asCommandId("cmd-project-create"),
37 metadata: {},
38 payload: {
39 projectId: asProjectId("project-delete"),
40 title: "Project Delete",
41 workspaceRoot: "/tmp/project-delete",
42 defaultModelSelection: null,
43 scripts: [],
44 createdAt: now,
45 updatedAt: now,
46 },
47 }),
48 );
26 const withProject = yield* projectEvent(initial, {
27 sequence: 1,
28 eventId: asEventId("evt-project-create"),
29 aggregateKind: "project",
30 aggregateId: asProjectId("project-delete"),
31 type: "project.created",
32 occurredAt: now,
33 commandId: asCommandId("cmd-project-create"),
34 causationEventId: null,
35 correlationId: asCommandId("cmd-project-create"),
36 metadata: {},
37 payload: {
38 projectId: asProjectId("project-delete"),
39 title: "Project Delete",
40 workspaceRoot: "/tmp/project-delete",
41 defaultModelSelection: null,
42 scripts: [],
43 createdAt: now,
44 updatedAt: now,
45 },
46 });
4947
50 const withFirstThread = await Effect.runPromise(
51 projectEvent(withProject, {
52 sequence: 2,
53 eventId: asEventId("evt-thread-create-1"),
54 aggregateKind: "thread",
55 aggregateId: asThreadId("thread-delete-1"),
56 type: "thread.created",
57 occurredAt: now,
58 commandId: asCommandId("cmd-thread-create-1"),
59 causationEventId: null,
60 correlationId: asCommandId("cmd-thread-create-1"),
61 metadata: {},
62 payload: {
63 threadId: asThreadId("thread-delete-1"),
64 projectId: asProjectId("project-delete"),
65 title: "Thread Delete 1",
66 modelSelection: {
67 instanceId: ProviderInstanceId.make("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,
48 const withFirstThread = yield* projectEvent(withProject, {
49 sequence: 2,
50 eventId: asEventId("evt-thread-create-1"),
51 aggregateKind: "thread",
52 aggregateId: asThreadId("thread-delete-1"),
53 type: "thread.created",
54 occurredAt: now,
55 commandId: asCommandId("cmd-thread-create-1"),
56 causationEventId: null,
57 correlationId: asCommandId("cmd-thread-create-1"),
58 metadata: {},
59 payload: {
60 threadId: asThreadId("thread-delete-1"),
61 projectId: asProjectId("project-delete"),
62 title: "Thread Delete 1",
63 modelSelection: {
64 instanceId: ProviderInstanceId.make("codex"),
65 model: "gpt-5-codex",
7666 },
77 }),
78 );
67 interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE,
68 runtimeMode: "approval-required",
69 branch: null,
70 worktreePath: null,
71 createdAt: now,
72 updatedAt: now,
73 },
74 });
7975
80 return Effect.runPromise(
81 projectEvent(withFirstThread, {
82 sequence: 3,
83 eventId: asEventId("evt-thread-create-2"),
84 aggregateKind: "thread",
85 aggregateId: asThreadId("thread-delete-2"),
86 type: "thread.created",
87 occurredAt: now,
88 commandId: asCommandId("cmd-thread-create-2"),
89 causationEventId: null,
90 correlationId: asCommandId("cmd-thread-create-2"),
91 metadata: {},
92 payload: {
93 threadId: asThreadId("thread-delete-2"),
94 projectId: asProjectId("project-delete"),
95 title: "Thread Delete 2",
96 modelSelection: {
97 instanceId: ProviderInstanceId.make("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,
76 return yield* projectEvent(withFirstThread, {
77 sequence: 3,
78 eventId: asEventId("evt-thread-create-2"),
79 aggregateKind: "thread",
80 aggregateId: asThreadId("thread-delete-2"),
81 type: "thread.created",
82 occurredAt: now,
83 commandId: asCommandId("cmd-thread-create-2"),
84 causationEventId: null,
85 correlationId: asCommandId("cmd-thread-create-2"),
86 metadata: {},
87 payload: {
88 threadId: asThreadId("thread-delete-2"),
89 projectId: asProjectId("project-delete"),
90 title: "Thread Delete 2",
91 modelSelection: {
92 instanceId: ProviderInstanceId.make("codex"),
93 model: "gpt-5-codex",
10694 },
107 }),
108 );
109}
95 interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE,
96 runtimeMode: "approval-required",
97 branch: null,
98 worktreePath: null,
99 createdAt: now,
100 updatedAt: now,
101 },
102 });
103});
110104
111105type PlannedEvent = Omit<OrchestrationEvent, "sequence">;
112106
......@@ -142,12 +136,11 @@ function normalizeDeleteEvent(event: PlannedEvent | ReadonlyArray<PlannedEvent>)
142136 });
143137}
144138
145describe("decider deletion flows", () => {
146 it("rejects deleting a non-empty project without force", async () => {
147 const readModel = await seedReadModel();
148
149 await expect(
150 Effect.runPromise(
139it.layer(NodeServices.layer)("decider deletion flows", (it) => {
140 it.effect("rejects deleting a non-empty project without force", () =>
141 Effect.gen(function* () {
142 const readModel = yield* seedReadModel;
143 const error = yield* Effect.flip(
151144 decideOrchestrationCommand({
152145 command: {
153146 type: "project.delete",
......@@ -156,72 +149,69 @@ describe("decider deletion flows", () => {
156149 },
157150 readModel,
158151 }),
159 ),
160 ).rejects.toThrow("cannot be deleted without force=true");
161 });
152 );
153 expect(error.message).toContain("cannot be deleted without force=true");
154 }),
155 );
162156
163 it("reuses thread.delete semantics when force-deleting a non-empty project", async () => {
164 const readModel = await seedReadModel();
165 const projectDeleteCommand: Extract<OrchestrationCommand, { type: "project.delete" }> = {
166 type: "project.delete",
167 commandId: asCommandId("cmd-project-delete-force"),
168 projectId: asProjectId("project-delete"),
169 force: true,
170 };
157 it.effect("reuses thread.delete semantics when force-deleting a non-empty project", () =>
158 Effect.gen(function* () {
159 const readModel = yield* seedReadModel;
160 const projectDeleteCommand: Extract<OrchestrationCommand, { type: "project.delete" }> = {
161 type: "project.delete",
162 commandId: asCommandId("cmd-project-delete-force"),
163 projectId: asProjectId("project-delete"),
164 force: true,
165 };
171166
172 const forcedResult = await Effect.runPromise(
173 decideOrchestrationCommand({
167 const forcedResult = yield* decideOrchestrationCommand({
174168 command: projectDeleteCommand,
175169 readModel,
176 }),
177 );
178 const forcedEvents = Array.isArray(forcedResult) ? forcedResult : [forcedResult];
170 });
171 const forcedEvents = Array.isArray(forcedResult) ? forcedResult : [forcedResult];
179172
180 expect(forcedEvents.map((event) => event.type)).toEqual([
181 "thread.deleted",
182 "thread.deleted",
183 "project.deleted",
184 ]);
173 expect(forcedEvents.map((event) => event.type)).toEqual([
174 "thread.deleted",
175 "thread.deleted",
176 "project.deleted",
177 ]);
185178
186 let sequentialReadModel = readModel;
187 let nextSequence = readModel.snapshotSequence;
188 const sequentialEvents: PlannedEvent[] = [];
189 for (const nextCommand of [
190 {
191 type: "thread.delete",
192 commandId: projectDeleteCommand.commandId,
193 threadId: asThreadId("thread-delete-1"),
194 },
195 {
196 type: "thread.delete",
197 commandId: projectDeleteCommand.commandId,
198 threadId: asThreadId("thread-delete-2"),
199 },
200 {
201 type: "project.delete",
202 commandId: projectDeleteCommand.commandId,
203 projectId: asProjectId("project-delete"),
204 },
205 ] satisfies ReadonlyArray<OrchestrationCommand>) {
206 const decided = await Effect.runPromise(
207 decideOrchestrationCommand({
179 let sequentialReadModel = readModel;
180 let nextSequence = readModel.snapshotSequence;
181 const sequentialEvents: PlannedEvent[] = [];
182 for (const nextCommand of [
183 {
184 type: "thread.delete",
185 commandId: projectDeleteCommand.commandId,
186 threadId: asThreadId("thread-delete-1"),
187 },
188 {
189 type: "thread.delete",
190 commandId: projectDeleteCommand.commandId,
191 threadId: asThreadId("thread-delete-2"),
192 },
193 {
194 type: "project.delete",
195 commandId: projectDeleteCommand.commandId,
196 projectId: asProjectId("project-delete"),
197 },
198 ] satisfies ReadonlyArray<OrchestrationCommand>) {
199 const decided = yield* decideOrchestrationCommand({
208200 command: nextCommand,
209201 readModel: sequentialReadModel,
210 }),
211 );
212 const nextEvents = Array.isArray(decided) ? decided : [decided];
213 sequentialEvents.push(...nextEvents);
214 for (const nextEvent of nextEvents) {
215 nextSequence += 1;
216 sequentialReadModel = await Effect.runPromise(
217 projectEvent(sequentialReadModel, {
202 });
203 const nextEvents = Array.isArray(decided) ? decided : [decided];
204 sequentialEvents.push(...nextEvents);
205 for (const nextEvent of nextEvents) {
206 nextSequence += 1;
207 sequentialReadModel = yield* projectEvent(sequentialReadModel, {
218208 ...nextEvent,
219209 sequence: nextSequence,
220 }),
221 );
210 });
211 }
222212 }
223 }
224213
225 expect(normalizeDeleteEvent(forcedResult)).toEqual(normalizeDeleteEvent(sequentialEvents));
226 });
214 expect(normalizeDeleteEvent(forcedResult)).toEqual(normalizeDeleteEvent(sequentialEvents));
215 }),
216 );
227217});
apps/server/src/orchestration/decider.projectScripts.test.ts+113-127
......@@ -8,8 +8,9 @@ import {
88 ProviderInstanceId,
99} from "@t3tools/contracts";
1010import { createModelSelection } from "@t3tools/shared/model";
11import { describe, expect, it } from "vitest";
11import { expect, it } from "@effect/vitest";
1212import * as Effect from "effect/Effect";
13import * as NodeServices from "@effect/platform-node/NodeServices";
1314
1415import { decideOrchestrationCommand } from "./decider.ts";
1516import { createEmptyReadModel, projectEvent } from "./projector.ts";
......@@ -17,14 +18,13 @@ import { createEmptyReadModel, projectEvent } from "./projector.ts";
1718const asEventId = (value: string): EventId => EventId.make(value);
1819const asProjectId = (value: string): ProjectId => ProjectId.make(value);
1920const 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", () => {
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({
27 const result = yield* decideOrchestrationCommand({
2828 command: {
2929 type: "project.create",
3030 commandId: CommandId.make("cmd-project-create-scripts"),
......@@ -34,19 +34,19 @@ describe("decider project scripts", () => {
3434 createdAt: now,
3535 },
3636 readModel,
37 }),
38 );
37 });
3938
40 const event = Array.isArray(result) ? result[0] : result;
41 expect(event.type).toBe("project.created");
42 expect((event.payload as { scripts: unknown[] }).scripts).toEqual([]);
43 });
39 const event = Array.isArray(result) ? result[0] : result;
40 expect(event.type).toBe("project.created");
41 expect((event.payload as { scripts: unknown[] }).scripts).toEqual([]);
42 }),
43 );
4444
45 it("propagates scripts in project.meta.update payload", async () => {
46 const now = "2026-01-01T00:00:00.000Z";
47 const initial = createEmptyReadModel(now);
48 const readModel = await Effect.runPromise(
49 projectEvent(initial, {
45 it.effect("propagates scripts in project.meta.update payload", () =>
46 Effect.gen(function* () {
47 const now = "2026-01-01T00:00:00.000Z";
48 const initial = createEmptyReadModel(now);
49 const readModel = yield* projectEvent(initial, {
5050 sequence: 1,
5151 eventId: asEventId("evt-project-create-scripts"),
5252 aggregateKind: "project",
......@@ -66,21 +66,19 @@ describe("decider project scripts", () => {
6666 createdAt: now,
6767 updatedAt: now,
6868 },
69 }),
70 );
69 });
7170
72 const scripts = [
73 {
74 id: "lint",
75 name: "Lint",
76 command: "bun run lint",
77 icon: "lint",
78 runOnWorktreeCreate: false,
79 },
80 ] as const;
71 const scripts = [
72 {
73 id: "lint",
74 name: "Lint",
75 command: "bun run lint",
76 icon: "lint",
77 runOnWorktreeCreate: false,
78 },
79 ] as const;
8180
82 const result = await Effect.runPromise(
83 decideOrchestrationCommand({
81 const result = yield* decideOrchestrationCommand({
8482 command: {
8583 type: "project.meta.update",
8684 commandId: CommandId.make("cmd-project-update-scripts"),
......@@ -88,19 +86,19 @@ describe("decider project scripts", () => {
8886 scripts: Array.from(scripts),
8987 },
9088 readModel,
91 }),
92 );
89 });
9390
94 const event = Array.isArray(result) ? result[0] : result;
95 expect(event.type).toBe("project.meta-updated");
96 expect((event.payload as { scripts?: unknown[] }).scripts).toEqual(scripts);
97 });
91 const event = Array.isArray(result) ? result[0] : result;
92 expect(event.type).toBe("project.meta-updated");
93 expect((event.payload as { scripts?: unknown[] }).scripts).toEqual(scripts);
94 }),
95 );
9896
99 it("emits user message and turn-start-requested events for thread.turn.start", async () => {
100 const now = "2026-01-01T00:00:00.000Z";
101 const initial = createEmptyReadModel(now);
102 const withProject = await Effect.runPromise(
103 projectEvent(initial, {
97 it.effect("emits user message and turn-start-requested events for thread.turn.start", () =>
98 Effect.gen(function* () {
99 const now = "2026-01-01T00:00:00.000Z";
100 const initial = createEmptyReadModel(now);
101 const withProject = yield* projectEvent(initial, {
104102 sequence: 1,
105103 eventId: asEventId("evt-project-create"),
106104 aggregateKind: "project",
......@@ -120,10 +118,8 @@ describe("decider project scripts", () => {
120118 createdAt: now,
121119 updatedAt: now,
122120 },
123 }),
124 );
125 const readModel = await Effect.runPromise(
126 projectEvent(withProject, {
121 });
122 const readModel = yield* projectEvent(withProject, {
127123 sequence: 2,
128124 eventId: asEventId("evt-thread-create"),
129125 aggregateKind: "thread",
......@@ -149,11 +145,9 @@ describe("decider project scripts", () => {
149145 createdAt: now,
150146 updatedAt: now,
151147 },
152 }),
153 );
148 });
154149
155 const result = await Effect.runPromise(
156 decideOrchestrationCommand({
150 const result = yield* decideOrchestrationCommand({
157151 command: {
158152 type: "thread.turn.start",
159153 commandId: CommandId.make("cmd-turn-start"),
......@@ -173,35 +167,35 @@ describe("decider project scripts", () => {
173167 createdAt: now,
174168 },
175169 readModel,
176 }),
177 );
170 });
178171
179 expect(Array.isArray(result)).toBe(true);
180 const events = Array.isArray(result) ? result : [result];
181 expect(events).toHaveLength(2);
182 expect(events[0]?.type).toBe("thread.message-sent");
183 const turnStartEvent = events[1];
184 expect(turnStartEvent?.type).toBe("thread.turn-start-requested");
185 expect(turnStartEvent?.causationEventId).toBe(events[0]?.eventId ?? null);
186 if (turnStartEvent?.type !== "thread.turn-start-requested") {
187 return;
188 }
189 expect(turnStartEvent.payload).toMatchObject({
190 threadId: ThreadId.make("thread-1"),
191 messageId: asMessageId("message-user-1"),
192 modelSelection: createModelSelection(ProviderInstanceId.make("codex"), "gpt-5.3-codex", [
193 { id: "reasoningEffort", value: "high" },
194 { id: "fastMode", value: true },
195 ]),
196 runtimeMode: "approval-required",
197 });
198 });
172 expect(Array.isArray(result)).toBe(true);
173 const events = Array.isArray(result) ? result : [result];
174 expect(events).toHaveLength(2);
175 expect(events[0]?.type).toBe("thread.message-sent");
176 const turnStartEvent = events[1];
177 expect(turnStartEvent?.type).toBe("thread.turn-start-requested");
178 expect(turnStartEvent?.causationEventId).toBe(events[0]?.eventId ?? null);
179 if (turnStartEvent?.type !== "thread.turn-start-requested") {
180 return;
181 }
182 expect(turnStartEvent.payload).toMatchObject({
183 threadId: ThreadId.make("thread-1"),
184 messageId: asMessageId("message-user-1"),
185 modelSelection: createModelSelection(ProviderInstanceId.make("codex"), "gpt-5.3-codex", [
186 { id: "reasoningEffort", value: "high" },
187 { id: "fastMode", value: true },
188 ]),
189 runtimeMode: "approval-required",
190 });
191 }),
192 );
199193
200 it("emits thread.runtime-mode-set from thread.runtime-mode.set", async () => {
201 const now = "2026-01-01T00:00:00.000Z";
202 const initial = createEmptyReadModel(now);
203 const withProject = await Effect.runPromise(
204 projectEvent(initial, {
194 it.effect("emits thread.runtime-mode-set from thread.runtime-mode.set", () =>
195 Effect.gen(function* () {
196 const now = "2026-01-01T00:00:00.000Z";
197 const initial = createEmptyReadModel(now);
198 const withProject = yield* projectEvent(initial, {
205199 sequence: 1,
206200 eventId: asEventId("evt-project-create"),
207201 aggregateKind: "project",
......@@ -221,10 +215,8 @@ describe("decider project scripts", () => {
221215 createdAt: now,
222216 updatedAt: now,
223217 },
224 }),
225 );
226 const readModel = await Effect.runPromise(
227 projectEvent(withProject, {
218 });
219 const readModel = yield* projectEvent(withProject, {
228220 sequence: 2,
229221 eventId: asEventId("evt-thread-create"),
230222 aggregateKind: "thread",
......@@ -250,11 +242,9 @@ describe("decider project scripts", () => {
250242 createdAt: now,
251243 updatedAt: now,
252244 },
253 }),
254 );
245 });
255246
256 const result = await Effect.runPromise(
257 decideOrchestrationCommand({
247 const result = yield* decideOrchestrationCommand({
258248 command: {
259249 type: "thread.runtime-mode.set",
260250 commandId: CommandId.make("cmd-runtime-mode-set"),
......@@ -263,27 +253,27 @@ describe("decider project scripts", () => {
263253 createdAt: now,
264254 },
265255 readModel,
266 }),
267 );
256 });
268257
269 const singleResult = Array.isArray(result) ? null : result;
270 if (singleResult === null) {
271 throw new Error("Expected a single runtime-mode-set event.");
272 }
273 expect(singleResult).toMatchObject({
274 type: "thread.runtime-mode-set",
275 payload: {
276 threadId: ThreadId.make("thread-1"),
277 runtimeMode: "approval-required",
278 },
279 });
280 });
258 const singleResult = Array.isArray(result) ? null : result;
259 if (singleResult === null) {
260 throw new Error("Expected a single runtime-mode-set event.");
261 }
262 expect(singleResult).toMatchObject({
263 type: "thread.runtime-mode-set",
264 payload: {
265 threadId: ThreadId.make("thread-1"),
266 runtimeMode: "approval-required",
267 },
268 });
269 }),
270 );
281271
282 it("emits thread.interaction-mode-set from thread.interaction-mode.set", async () => {
283 const now = "2026-01-01T00:00:00.000Z";
284 const initial = createEmptyReadModel(now);
285 const withProject = await Effect.runPromise(
286 projectEvent(initial, {
272 it.effect("emits thread.interaction-mode-set from thread.interaction-mode.set", () =>
273 Effect.gen(function* () {
274 const now = "2026-01-01T00:00:00.000Z";
275 const initial = createEmptyReadModel(now);
276 const withProject = yield* projectEvent(initial, {
287277 sequence: 1,
288278 eventId: asEventId("evt-project-create"),
289279 aggregateKind: "project",
......@@ -303,10 +293,8 @@ describe("decider project scripts", () => {
303293 createdAt: now,
304294 updatedAt: now,
305295 },
306 }),
307 );
308 const readModel = await Effect.runPromise(
309 projectEvent(withProject, {
296 });
297 const readModel = yield* projectEvent(withProject, {
310298 sequence: 2,
311299 eventId: asEventId("evt-thread-create"),
312300 aggregateKind: "thread",
......@@ -332,11 +320,9 @@ describe("decider project scripts", () => {
332320 createdAt: now,
333321 updatedAt: now,
334322 },
335 }),
336 );
323 });
337324
338 const result = await Effect.runPromise(
339 decideOrchestrationCommand({
325 const result = yield* decideOrchestrationCommand({
340326 command: {
341327 type: "thread.interaction-mode.set",
342328 commandId: CommandId.make("cmd-interaction-mode-set"),
......@@ -345,19 +331,19 @@ describe("decider project scripts", () => {
345331 createdAt: now,
346332 },
347333 readModel,
348 }),
349 );
334 });
350335
351 const singleResult = Array.isArray(result) ? null : result;
352 if (singleResult === null) {
353 throw new Error("Expected a single interaction-mode-set event.");
354 }
355 expect(singleResult).toMatchObject({
356 type: "thread.interaction-mode-set",
357 payload: {
358 threadId: ThreadId.make("thread-1"),
359 interactionMode: "plan",
360 },
361 });
362 });
336 const singleResult = Array.isArray(result) ? null : result;
337 if (singleResult === null) {
338 throw new Error("Expected a single interaction-mode-set event.");
339 }
340 expect(singleResult).toMatchObject({
341 type: "thread.interaction-mode-set",
342 payload: {
343 threadId: ThreadId.make("thread-1"),
344 interactionMode: "plan",
345 },
346 });
347 }),
348 );
363349});
apps/server/src/orchestration/decider.ts+86-65
......@@ -1,10 +1,13 @@
1import type {
2 OrchestrationCommand,
3 OrchestrationEvent,
4 OrchestrationReadModel,
1import {
2 EventId,
3 type OrchestrationCommand,
4 type OrchestrationEvent,
5 type OrchestrationReadModel,
56} from "@t3tools/contracts";
67import * as DateTime from "effect/DateTime";
8import * as Crypto from "effect/Crypto";
79import * as Effect from "effect/Effect";
10import type * as PlatformError from "effect/PlatformError";
811
912import { OrchestrationCommandInvariantError } from "./Errors.ts";
1013import {
......@@ -27,17 +30,27 @@ function withEventBase(
2730 readonly occurredAt: string;
2831 readonly metadata?: OrchestrationEvent["metadata"];
2932 },
30): Omit<OrchestrationEvent, "sequence" | "type" | "payload"> {
31 return {
32 eventId: crypto.randomUUID() as OrchestrationEvent["eventId"],
33 aggregateKind: input.aggregateKind,
34 aggregateId: input.aggregateId,
35 occurredAt: input.occurredAt,
36 commandId: input.commandId,
37 causationEventId: null,
38 correlationId: input.commandId,
39 metadata: input.metadata ?? {},
40 };
33): Effect.Effect<
34 Omit<OrchestrationEvent, "sequence" | "type" | "payload">,
35 PlatformError.PlatformError,
36 Crypto.Crypto
37> {
38 return Crypto.Crypto.pipe(
39 Effect.flatMap((crypto) =>
40 crypto.randomUUIDv4.pipe(
41 Effect.map((eventId) => ({
42 eventId: EventId.make(eventId),
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 );
4154}
4255
4356type PlannedOrchestrationEvent = Omit<OrchestrationEvent, "sequence">;
......@@ -52,7 +65,11 @@ const decideCommandSequence = Effect.fn("decideCommandSequence")(function* ({
5265}: {
5366 readonly commands: ReadonlyArray<OrchestrationCommand>;
5467 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> {
5673 let nextReadModel = readModel;
5774 let nextSequence = readModel.snapshotSequence;
5875 const plannedEvents: PlannedOrchestrationEvent[] = [];
......@@ -82,7 +99,11 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand"
8299}: {
83100 readonly command: OrchestrationCommand;
84101 readonly readModel: OrchestrationReadModel;
85}): Effect.fn.Return<DecideOrchestrationCommandResult, OrchestrationCommandInvariantError> {
102}): Effect.fn.Return<
103 DecideOrchestrationCommandResult,
104 OrchestrationCommandInvariantError | PlatformError.PlatformError,
105 Crypto.Crypto
106> {
86107 switch (command.type) {
87108 case "project.create": {
88109 yield* requireProjectAbsent({
......@@ -92,12 +113,12 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand"
92113 });
93114
94115 return {
95 ...withEventBase({
116 ...(yield* withEventBase({
96117 aggregateKind: "project",
97118 aggregateId: command.projectId,
98119 occurredAt: command.createdAt,
99120 commandId: command.commandId,
100 }),
121 })),
101122 type: "project.created",
102123 payload: {
103124 projectId: command.projectId,
......@@ -119,12 +140,12 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand"
119140 });
120141 const occurredAt = yield* nowIso;
121142 return {
122 ...withEventBase({
143 ...(yield* withEventBase({
123144 aggregateKind: "project",
124145 aggregateId: command.projectId,
125146 occurredAt,
126147 commandId: command.commandId,
127 }),
148 })),
128149 type: "project.meta-updated",
129150 payload: {
130151 projectId: command.projectId,
......@@ -176,12 +197,12 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand"
176197
177198 const occurredAt = yield* nowIso;
178199 return {
179 ...withEventBase({
200 ...(yield* withEventBase({
180201 aggregateKind: "project",
181202 aggregateId: command.projectId,
182203 occurredAt,
183204 commandId: command.commandId,
184 }),
205 })),
185206 type: "project.deleted" as const,
186207 payload: {
187208 projectId: command.projectId,
......@@ -202,12 +223,12 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand"
202223 threadId: command.threadId,
203224 });
204225 return {
205 ...withEventBase({
226 ...(yield* withEventBase({
206227 aggregateKind: "thread",
207228 aggregateId: command.threadId,
208229 occurredAt: command.createdAt,
209230 commandId: command.commandId,
210 }),
231 })),
211232 type: "thread.created",
212233 payload: {
213234 threadId: command.threadId,
......@@ -232,12 +253,12 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand"
232253 });
233254 const occurredAt = yield* nowIso;
234255 return {
235 ...withEventBase({
256 ...(yield* withEventBase({
236257 aggregateKind: "thread",
237258 aggregateId: command.threadId,
238259 occurredAt,
239260 commandId: command.commandId,
240 }),
261 })),
241262 type: "thread.deleted",
242263 payload: {
243264 threadId: command.threadId,
......@@ -254,12 +275,12 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand"
254275 });
255276 const occurredAt = yield* nowIso;
256277 return {
257 ...withEventBase({
278 ...(yield* withEventBase({
258279 aggregateKind: "thread",
259280 aggregateId: command.threadId,
260281 occurredAt,
261282 commandId: command.commandId,
262 }),
283 })),
263284 type: "thread.archived",
264285 payload: {
265286 threadId: command.threadId,
......@@ -277,12 +298,12 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand"
277298 });
278299 const occurredAt = yield* nowIso;
279300 return {
280 ...withEventBase({
301 ...(yield* withEventBase({
281302 aggregateKind: "thread",
282303 aggregateId: command.threadId,
283304 occurredAt,
284305 commandId: command.commandId,
285 }),
306 })),
286307 type: "thread.unarchived",
287308 payload: {
288309 threadId: command.threadId,
......@@ -299,12 +320,12 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand"
299320 });
300321 const occurredAt = yield* nowIso;
301322 return {
302 ...withEventBase({
323 ...(yield* withEventBase({
303324 aggregateKind: "thread",
304325 aggregateId: command.threadId,
305326 occurredAt,
306327 commandId: command.commandId,
307 }),
328 })),
308329 type: "thread.meta-updated",
309330 payload: {
310331 threadId: command.threadId,
......@@ -327,12 +348,12 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand"
327348 });
328349 const occurredAt = yield* nowIso;
329350 return {
330 ...withEventBase({
351 ...(yield* withEventBase({
331352 aggregateKind: "thread",
332353 aggregateId: command.threadId,
333354 occurredAt,
334355 commandId: command.commandId,
335 }),
356 })),
336357 type: "thread.runtime-mode-set",
337358 payload: {
338359 threadId: command.threadId,
......@@ -350,12 +371,12 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand"
350371 });
351372 const occurredAt = yield* nowIso;
352373 return {
353 ...withEventBase({
374 ...(yield* withEventBase({
354375 aggregateKind: "thread",
355376 aggregateId: command.threadId,
356377 occurredAt,
357378 commandId: command.commandId,
358 }),
379 })),
359380 type: "thread.interaction-mode-set",
360381 payload: {
361382 threadId: command.threadId,
......@@ -396,12 +417,12 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand"
396417 });
397418 }
398419 const userMessageEvent: Omit<OrchestrationEvent, "sequence"> = {
399 ...withEventBase({
420 ...(yield* withEventBase({
400421 aggregateKind: "thread",
401422 aggregateId: command.threadId,
402423 occurredAt: command.createdAt,
403424 commandId: command.commandId,
404 }),
425 })),
405426 type: "thread.message-sent",
406427 payload: {
407428 threadId: command.threadId,
......@@ -416,12 +437,12 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand"
416437 },
417438 };
418439 const turnStartRequestedEvent: Omit<OrchestrationEvent, "sequence"> = {
419 ...withEventBase({
440 ...(yield* withEventBase({
420441 aggregateKind: "thread",
421442 aggregateId: command.threadId,
422443 occurredAt: command.createdAt,
423444 commandId: command.commandId,
424 }),
445 })),
425446 causationEventId: userMessageEvent.eventId,
426447 type: "thread.turn-start-requested",
427448 payload: {
......@@ -447,12 +468,12 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand"
447468 threadId: command.threadId,
448469 });
449470 return {
450 ...withEventBase({
471 ...(yield* withEventBase({
451472 aggregateKind: "thread",
452473 aggregateId: command.threadId,
453474 occurredAt: command.createdAt,
454475 commandId: command.commandId,
455 }),
476 })),
456477 type: "thread.turn-interrupt-requested",
457478 payload: {
458479 threadId: command.threadId,
......@@ -469,7 +490,7 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand"
469490 threadId: command.threadId,
470491 });
471492 return {
472 ...withEventBase({
493 ...(yield* withEventBase({
473494 aggregateKind: "thread",
474495 aggregateId: command.threadId,
475496 occurredAt: command.createdAt,
......@@ -477,7 +498,7 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand"
477498 metadata: {
478499 requestId: command.requestId,
479500 },
480 }),
501 })),
481502 type: "thread.approval-response-requested",
482503 payload: {
483504 threadId: command.threadId,
......@@ -495,7 +516,7 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand"
495516 threadId: command.threadId,
496517 });
497518 return {
498 ...withEventBase({
519 ...(yield* withEventBase({
499520 aggregateKind: "thread",
500521 aggregateId: command.threadId,
501522 occurredAt: command.createdAt,
......@@ -503,7 +524,7 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand"
503524 metadata: {
504525 requestId: command.requestId,
505526 },
506 }),
527 })),
507528 type: "thread.user-input-response-requested",
508529 payload: {
509530 threadId: command.threadId,
......@@ -521,12 +542,12 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand"
521542 threadId: command.threadId,
522543 });
523544 return {
524 ...withEventBase({
545 ...(yield* withEventBase({
525546 aggregateKind: "thread",
526547 aggregateId: command.threadId,
527548 occurredAt: command.createdAt,
528549 commandId: command.commandId,
529 }),
550 })),
530551 type: "thread.checkpoint-revert-requested",
531552 payload: {
532553 threadId: command.threadId,
......@@ -543,12 +564,12 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand"
543564 threadId: command.threadId,
544565 });
545566 return {
546 ...withEventBase({
567 ...(yield* withEventBase({
547568 aggregateKind: "thread",
548569 aggregateId: command.threadId,
549570 occurredAt: command.createdAt,
550571 commandId: command.commandId,
551 }),
572 })),
552573 type: "thread.session-stop-requested",
553574 payload: {
554575 threadId: command.threadId,
......@@ -564,13 +585,13 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand"
564585 threadId: command.threadId,
565586 });
566587 return {
567 ...withEventBase({
588 ...(yield* withEventBase({
568589 aggregateKind: "thread",
569590 aggregateId: command.threadId,
570591 occurredAt: command.createdAt,
571592 commandId: command.commandId,
572593 metadata: {},
573 }),
594 })),
574595 type: "thread.session-set",
575596 payload: {
576597 threadId: command.threadId,
......@@ -586,12 +607,12 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand"
586607 threadId: command.threadId,
587608 });
588609 return {
589 ...withEventBase({
610 ...(yield* withEventBase({
590611 aggregateKind: "thread",
591612 aggregateId: command.threadId,
592613 occurredAt: command.createdAt,
593614 commandId: command.commandId,
594 }),
615 })),
595616 type: "thread.message-sent",
596617 payload: {
597618 threadId: command.threadId,
......@@ -613,12 +634,12 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand"
613634 threadId: command.threadId,
614635 });
615636 return {
616 ...withEventBase({
637 ...(yield* withEventBase({
617638 aggregateKind: "thread",
618639 aggregateId: command.threadId,
619640 occurredAt: command.createdAt,
620641 commandId: command.commandId,
621 }),
642 })),
622643 type: "thread.message-sent",
623644 payload: {
624645 threadId: command.threadId,
......@@ -640,12 +661,12 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand"
640661 threadId: command.threadId,
641662 });
642663 return {
643 ...withEventBase({
664 ...(yield* withEventBase({
644665 aggregateKind: "thread",
645666 aggregateId: command.threadId,
646667 occurredAt: command.createdAt,
647668 commandId: command.commandId,
648 }),
669 })),
649670 type: "thread.proposed-plan-upserted",
650671 payload: {
651672 threadId: command.threadId,
......@@ -661,12 +682,12 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand"
661682 threadId: command.threadId,
662683 });
663684 return {
664 ...withEventBase({
685 ...(yield* withEventBase({
665686 aggregateKind: "thread",
666687 aggregateId: command.threadId,
667688 occurredAt: command.createdAt,
668689 commandId: command.commandId,
669 }),
690 })),
670691 type: "thread.turn-diff-completed",
671692 payload: {
672693 threadId: command.threadId,
......@@ -688,12 +709,12 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand"
688709 threadId: command.threadId,
689710 });
690711 return {
691 ...withEventBase({
712 ...(yield* withEventBase({
692713 aggregateKind: "thread",
693714 aggregateId: command.threadId,
694715 occurredAt: command.createdAt,
695716 commandId: command.commandId,
696 }),
717 })),
697718 type: "thread.reverted",
698719 payload: {
699720 threadId: command.threadId,
......@@ -717,13 +738,13 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand"
717738 .requestId as OrchestrationEvent["metadata"]["requestId"])
718739 : undefined;
719740 return {
720 ...withEventBase({
741 ...(yield* withEventBase({
721742 aggregateKind: "thread",
722743 aggregateId: command.threadId,
723744 occurredAt: command.createdAt,
724745 commandId: command.commandId,
725746 ...(requestId !== undefined ? { metadata: { requestId } } : {}),
726 }),
747 })),
727748 type: "thread.activity-appended",
728749 payload: {
729750 threadId: command.threadId,
apps/server/src/persistence/NodeSqliteClient.ts+5-7
......@@ -252,14 +252,12 @@ export const layerConfig = (
252252 config: Config.Wrap<SqliteClientConfig>,
253253): Layer.Layer<Client.SqlClient, Config.ConfigError> =>
254254 Layer.effectContext(
255 Config.unwrap(config)
256 .asEffect()
257 .pipe(
258 Effect.flatMap(make),
259 Effect.map((client) =>
260 Context.make(SqliteClient, client).pipe(Context.add(Client.SqlClient, client)),
261 ),
255 Config.unwrap(config).pipe(
256 Effect.flatMap(make),
257 Effect.map((client) =>
258 Context.make(SqliteClient, client).pipe(Context.add(Client.SqlClient, client)),
262259 ),
260 ),
263261 ).pipe(Layer.provide(Reactivity.layer));
264262
265263export const layer = (config: SqliteClientConfig): Layer.Layer<Client.SqlClient> =>
apps/server/src/process/externalLauncher.test.ts+4-2
......@@ -1,12 +1,12 @@
11import * as NodeServices from "@effect/platform-node/NodeServices";
22import { assert, it } from "@effect/vitest";
33import { assertSuccess } from "@effect/vitest/utils";
4import * as Crypto from "effect/Crypto";
45import * as Effect from "effect/Effect";
56import * as Encoding from "effect/Encoding";
67import * as FileSystem from "effect/FileSystem";
78import * as Layer from "effect/Layer";
89import * as Path from "effect/Path";
9import * as Random from "effect/Random";
1010import * as Sink from "effect/Sink";
1111import * as Stream from "effect/Stream";
1212import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
......@@ -651,7 +651,9 @@ it.layer(NodeServices.layer)("launchEditorProcess", (it) => {
651651 Effect.gen(function* () {
652652 const spawnerLayer = Layer.mock(ChildProcessSpawner.ChildProcessSpawner, {});
653653 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 )}`,
655657 args: [],
656658 }).pipe(Effect.provide(spawnerLayer), Effect.result);
657659 assert.equal(result._tag, "Failure");
apps/server/src/provider/Drivers/ClaudeDriver.ts+2
......@@ -15,6 +15,7 @@
1515import { ClaudeSettings, ProviderDriverKind, type ServerProvider } from "@t3tools/contracts";
1616import * as Cache from "effect/Cache";
1717import * as Duration from "effect/Duration";
18import * as Crypto from "effect/Crypto";
1819import * as Effect from "effect/Effect";
1920import * as FileSystem from "effect/FileSystem";
2021import * as Path from "effect/Path";
......@@ -77,6 +78,7 @@ const UPDATE = makePackageManagedProviderMaintenanceResolver({
7778
7879export type ClaudeDriverEnv =
7980 | ChildProcessSpawner.ChildProcessSpawner
81 | Crypto.Crypto
8082 | FileSystem.FileSystem
8183 | HttpClient.HttpClient
8284 | Path.Path
apps/server/src/provider/Drivers/CodexDriver.ts+2
......@@ -23,6 +23,7 @@
2323 */
2424import { CodexSettings, ProviderDriverKind, type ServerProvider } from "@t3tools/contracts";
2525import * as Duration from "effect/Duration";
26import * as Crypto from "effect/Crypto";
2627import * as Effect from "effect/Effect";
2728import * as FileSystem from "effect/FileSystem";
2829import * as Path from "effect/Path";
......@@ -69,6 +70,7 @@ const UPDATE = makePackageManagedProviderMaintenanceResolver({
6970 */
7071export type CodexDriverEnv =
7172 | ChildProcessSpawner.ChildProcessSpawner
73 | Crypto.Crypto
7274 | FileSystem.FileSystem
7375 | HttpClient.HttpClient
7476 | Path.Path
apps/server/src/provider/Drivers/CursorDriver.ts+2
......@@ -14,6 +14,7 @@
1414 */
1515import { CursorSettings, ProviderDriverKind, type ServerProvider } from "@t3tools/contracts";
1616import * as Duration from "effect/Duration";
17import * as Crypto from "effect/Crypto";
1718import * as Effect from "effect/Effect";
1819import * as FileSystem from "effect/FileSystem";
1920import * as Path from "effect/Path";
......@@ -61,6 +62,7 @@ const UPDATE = makeStaticProviderMaintenanceResolver(
6162
6263export type CursorDriverEnv =
6364 | ChildProcessSpawner.ChildProcessSpawner
65 | Crypto.Crypto
6466 | FileSystem.FileSystem
6567 | HttpClient.HttpClient
6668 | Path.Path
apps/server/src/provider/Drivers/OpenCodeDriver.ts+2
......@@ -14,6 +14,7 @@
1414 */
1515import { OpenCodeSettings, ProviderDriverKind, type ServerProvider } from "@t3tools/contracts";
1616import * as Duration from "effect/Duration";
17import * as Crypto from "effect/Crypto";
1718import * as Effect from "effect/Effect";
1819import * as FileSystem from "effect/FileSystem";
1920import * as Path from "effect/Path";
......@@ -73,6 +74,7 @@ const UPDATE = makePackageManagedProviderMaintenanceResolver({
7374
7475export type OpenCodeDriverEnv =
7576 | ChildProcessSpawner.ChildProcessSpawner
77 | Crypto.Crypto
7678 | FileSystem.FileSystem
7779 | HttpClient.HttpClient
7880 | OpenCodeRuntime
apps/server/src/provider/Layers/ClaudeAdapter.ts+43-13
......@@ -52,6 +52,7 @@ import {
5252 resolvePromptInjectedEffort,
5353} from "@t3tools/shared/model";
5454import * as Cause from "effect/Cause";
55import * as Crypto from "effect/Crypto";
5556import * as DateTime from "effect/DateTime";
5657import * as Deferred from "effect/Deferred";
5758import * as Effect from "effect/Effect";
......@@ -60,7 +61,6 @@ import * as FileSystem from "effect/FileSystem";
6061import * as Fiber from "effect/Fiber";
6162import * as Path from "effect/Path";
6263import * as Queue from "effect/Queue";
63import * as Random from "effect/Random";
6464import * as Ref from "effect/Ref";
6565import * as Schema from "effect/Schema";
6666import * as Stream from "effect/Stream";
......@@ -998,6 +998,7 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (
998998 const fileSystem = yield* FileSystem.FileSystem;
999999 const path = yield* Path.Path;
10001000 const serverConfig = yield* ServerConfig;
1001 const crypto = yield* Crypto.Crypto;
10011002 const claudeEnvironment = yield* makeClaudeEnvironment(claudeSettings, options?.environment).pipe(
10021003 Effect.provideService(Path.Path, path),
10031004 );
......@@ -1024,7 +1025,18 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (
10241025 const runtimeEventQueue = yield* Queue.unbounded<ProviderRuntimeEvent>();
10251026
10261027 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));
10281040 const makeEventStamp = () => Effect.all({ eventId: nextEventId, createdAt: nowIso });
10291041
10301042 const offerRuntimeEvent = (event: ProviderRuntimeEvent): Effect.Effect<void> =>
......@@ -1048,7 +1060,7 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (
10481060 id:
10491061 "uuid" in message && typeof message.uuid === "string"
10501062 ? message.uuid
1051 : yield* Random.nextUUIDv4,
1063 : yield* randomUUIDv4,
10521064 kind: "notification",
10531065 provider: PROVIDER,
10541066 createdAt: observedAt,
......@@ -1132,7 +1144,7 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (
11321144 }
11331145
11341146 const block: AssistantTextBlockState = {
1135 itemId: yield* Random.nextUUIDv4,
1147 itemId: yield* randomUUIDv4,
11361148 blockIndex,
11371149 emittedTextDelta: false,
11381150 fallbackText: options?.fallbackText ?? "",
......@@ -1965,7 +1977,7 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (
19651977 // Auto-start a synthetic turn for assistant messages that arrive without
19661978 // an active turn (e.g., background agent/subagent responses between user prompts).
19671979 if (!context.turnState) {
1968 const turnId = TurnId.make(yield* Random.nextUUIDv4);
1980 const turnId = TurnId.make(yield* randomUUIDv4);
19691981 const startedAt = yield* nowIso;
19701982 context.turnState = {
19711983 turnId,
......@@ -2378,7 +2390,17 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (
23782390 toProcessError(cause, "Claude runtime stream failed.", context.session.threadId),
23792391 ).pipe(
23802392 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 ),
23822404 );
23832405
23842406 const handleStreamExit = Effect.fn("handleStreamExit")(function* (
......@@ -2552,8 +2574,7 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (
25522574 const resumeState = readClaudeResumeState(input.resumeCursor);
25532575 const threadId = input.threadId;
25542576 const existingResumeSessionId = resumeState?.resume;
2555 const newSessionId =
2556 existingResumeSessionId === undefined ? yield* Random.nextUUIDv4 : undefined;
2577 const newSessionId = existingResumeSessionId === undefined ? yield* randomUUIDv4 : undefined;
25572578 const sessionId = existingResumeSessionId ?? newSessionId;
25582579
25592580 const runtimeContext = yield* Effect.context<never>();
......@@ -2588,7 +2609,7 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (
25882609 readonly toolUseID?: string;
25892610 },
25902611 ) {
2591 const requestId = ApprovalRequestId.make(yield* Random.nextUUIDv4);
2612 const requestId = ApprovalRequestId.make(yield* randomUUIDv4);
25922613
25932614 // Parse questions from the SDK's AskUserQuestion input.
25942615 // `id` MUST equal the full question text — Claude SDK >= 2.1.121 looks
......@@ -2758,7 +2779,7 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (
27582779 } satisfies PermissionResult;
27592780 }
27602781
2761 const requestId = ApprovalRequestId.make(yield* Random.nextUUIDv4);
2782 const requestId = ApprovalRequestId.make(yield* randomUUIDv4);
27622783 const requestType = classifyRequestType(toolName);
27632784 const detail = summarizeToolRequest(toolName, toolInput);
27642785 const decisionDeferred = yield* Deferred.make<ProviderApprovalDecision>();
......@@ -3059,7 +3080,11 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (
30593080 if (context.streamFiber === streamFiber) {
30603081 context.streamFiber = undefined;
30613082 }
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 );
30633088 }),
30643089 ),
30653090 );
......@@ -3120,7 +3145,7 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (
31203145 });
31213146 }
31223147
3123 const turnId = TurnId.make(yield* Random.nextUUIDv4);
3148 const turnId = TurnId.make(yield* randomUUIDv4);
31243149 const turnState: ClaudeTurnState = {
31253150 turnId,
31263151 startedAt: yield* nowIso,
......@@ -3269,7 +3294,12 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (
32693294 emitExitEvent: false,
32703295 }),
32713296 { 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 ),
32733303 );
32743304
32753305 return {
apps/server/src/provider/Layers/CodexAdapter.ts+3
......@@ -25,6 +25,7 @@ import {
2525 ProviderSendTurnInput,
2626} from "@t3tools/contracts";
2727import * as Effect from "effect/Effect";
28import * as Crypto from "effect/Crypto";
2829import * as Exit from "effect/Exit";
2930import * as Fiber from "effect/Fiber";
3031import * as FileSystem from "effect/FileSystem";
......@@ -1349,6 +1350,7 @@ export const makeCodexAdapter = Effect.fn("makeCodexAdapter")(function* (
13491350 const boundInstanceId = options?.instanceId ?? ProviderInstanceId.make("codex");
13501351 const fileSystem = yield* FileSystem.FileSystem;
13511352 const childProcessSpawner = yield* ChildProcessSpawner.ChildProcessSpawner;
1353 const crypto = yield* Crypto.Crypto;
13521354 const serverConfig = yield* Effect.service(ServerConfig);
13531355 const nativeEventLogger =
13541356 options?.nativeEventLogger ??
......@@ -1406,6 +1408,7 @@ export const makeCodexAdapter = Effect.fn("makeCodexAdapter")(function* (
14061408 const runtime = yield* createRuntime(runtimeInput).pipe(
14071409 Effect.provideService(Scope.Scope, sessionScope),
14081410 Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, childProcessSpawner),
1411 Effect.provideService(Crypto.Crypto, crypto),
14091412 Effect.mapError(
14101413 (cause) =>
14111414 new ProviderAdapterProcessError({
apps/server/src/provider/Layers/CodexSessionRuntime.ts+21-7
......@@ -17,6 +17,7 @@ import {
1717 TurnId,
1818} from "@t3tools/contracts";
1919import { normalizeModelSlug } from "@t3tools/shared/model";
20import * as Crypto from "effect/Crypto";
2021import * as DateTime from "effect/DateTime";
2122import * as Deferred from "effect/Deferred";
2223import * as Effect from "effect/Effect";
......@@ -25,7 +26,6 @@ import * as Layer from "effect/Layer";
2526import * as Queue from "effect/Queue";
2627import * as Ref from "effect/Ref";
2728import * as Scope from "effect/Scope";
28import * as Random from "effect/Random";
2929import * as Schema from "effect/Schema";
3030import * as Stream from "effect/Stream";
3131import * as SchemaIssue from "effect/SchemaIssue";
......@@ -697,11 +697,12 @@ export const makeCodexSessionRuntime = (
697697): Effect.Effect<
698698 CodexSessionRuntimeShape,
699699 CodexErrors.CodexAppServerError,
700 ChildProcessSpawner.ChildProcessSpawner | Scope.Scope
700 ChildProcessSpawner.ChildProcessSpawner | Crypto.Crypto | Scope.Scope
701701> =>
702702 Effect.gen(function* () {
703703 const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
704704 const runtimeScope = yield* Scope.Scope;
705 const crypto = yield* Crypto.Crypto;
705706 const events = yield* Queue.unbounded<ProviderEvent>();
706707 const pendingApprovalsRef = yield* Ref.make(new Map<ApprovalRequestId, PendingApproval>());
707708 const approvalCorrelationsRef = yield* Ref.make(new Map<string, ApprovalCorrelation>());
......@@ -746,6 +747,15 @@ export const makeCodexSessionRuntime = (
746747 );
747748 const serverNotifications = yield* Queue.unbounded<CodexServerNotification>();
748749 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
750760 const sessionCreatedAt = yield* nowIso;
751761 const initialSession = {
......@@ -765,7 +775,7 @@ export const makeCodexSessionRuntime = (
765775
766776 const emitEvent = (event: Omit<ProviderEvent, "id" | "provider" | "createdAt">) =>
767777 Effect.gen(function* () {
768 const id = yield* Random.nextUUIDv4;
778 const id = yield* randomUUIDv4;
769779 return yield* offerEvent({
770780 id: EventId.make(id),
771781 provider: PROVIDER,
......@@ -933,7 +943,7 @@ export const makeCodexSessionRuntime = (
933943
934944 yield* client.handleServerRequest("item/commandExecution/requestApproval", (payload) =>
935945 Effect.gen(function* () {
936 const requestId = ApprovalRequestId.make(yield* Random.nextUUIDv4);
946 const requestId = ApprovalRequestId.make(yield* randomUUIDv4);
937947 const turnId = TurnId.make(payload.turnId);
938948 const itemId = ProviderItemId.make(payload.itemId);
939949 const decision = yield* Deferred.make<ProviderApprovalDecision>();
......@@ -989,7 +999,7 @@ export const makeCodexSessionRuntime = (
989999
9901000 yield* client.handleServerRequest("item/fileChange/requestApproval", (payload) =>
9911001 Effect.gen(function* () {
992 const requestId = ApprovalRequestId.make(yield* Random.nextUUIDv4);
1002 const requestId = ApprovalRequestId.make(yield* randomUUIDv4);
9931003 const turnId = TurnId.make(payload.turnId);
9941004 const itemId = ProviderItemId.make(payload.itemId);
9951005 const decision = yield* Deferred.make<ProviderApprovalDecision>();
......@@ -1045,7 +1055,7 @@ export const makeCodexSessionRuntime = (
10451055
10461056 yield* client.handleServerRequest("item/tool/requestUserInput", (payload) =>
10471057 Effect.gen(function* () {
1048 const requestId = ApprovalRequestId.make(yield* Random.nextUUIDv4);
1058 const requestId = ApprovalRequestId.make(yield* randomUUIDv4);
10491059 const turnId = TurnId.make(payload.turnId);
10501060 const itemId = ProviderItemId.make(payload.itemId);
10511061 const answers = yield* Deferred.make<ProviderUserInputAnswers>();
......@@ -1229,7 +1239,11 @@ export const makeCodexSessionRuntime = (
12291239 status: "closed",
12301240 activeTurnId: undefined,
12311241 });
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 );
12331247 yield* Scope.close(runtimeScope, Exit.void);
12341248 yield* Queue.shutdown(serverNotifications);
12351249 yield* Queue.shutdown(events);
apps/server/src/provider/Layers/CursorAdapter.ts+182-142
......@@ -22,6 +22,7 @@ import {
2222 TurnId,
2323} from "@t3tools/contracts";
2424import * as DateTime from "effect/DateTime";
25import * as Crypto from "effect/Crypto";
2526import * as Deferred from "effect/Deferred";
2627import * as Effect from "effect/Effect";
2728import * as Exit from "effect/Exit";
......@@ -30,13 +31,13 @@ import * as FileSystem from "effect/FileSystem";
3031import * as Option from "effect/Option";
3132import * as Path from "effect/Path";
3233import * as PubSub from "effect/PubSub";
33import * as Random from "effect/Random";
3434import * as Schema from "effect/Schema";
3535import * as Scope from "effect/Scope";
3636import * as Semaphore from "effect/Semaphore";
3737import * as Stream from "effect/Stream";
3838import * as SynchronizedRef from "effect/SynchronizedRef";
3939import { ChildProcessSpawner } from "effect/unstable/process";
40import * as EffectAcpErrors from "effect-acp/errors";
4041import type * as EffectAcpSchema from "effect-acp/schema";
4142
4243import { resolveAttachmentPath } from "../../attachmentStore.ts";
......@@ -62,7 +63,7 @@ import {
6263 type AcpSessionModeState,
6364 parsePermissionRequest,
6465} from "../acp/AcpRuntimeModel.ts";
65import { makeAcpNativeLoggers } from "../acp/AcpNativeLogging.ts";
66import { makeAcpNativeLoggerFactory } from "../acp/AcpNativeLogging.ts";
6667import { applyCursorAcpModelSelection, makeCursorAcpRuntime } from "../acp/CursorAcpSupport.ts";
6768import {
6869 CursorAskQuestionRequest,
......@@ -314,6 +315,7 @@ export function makeCursorAdapter(
314315 const path = yield* Path.Path;
315316 const childProcessSpawner = yield* ChildProcessSpawner.ChildProcessSpawner;
316317 const serverConfig = yield* Effect.service(ServerConfig);
318 const crypto = yield* Crypto.Crypto;
317319 const nativeEventLogger =
318320 options?.nativeEventLogger ??
319321 (options?.nativeEventLogPath !== undefined
......@@ -323,14 +325,36 @@ export function makeCursorAdapter(
323325 : undefined);
324326 const managedNativeEventLogger =
325327 options?.nativeEventLogger === undefined ? nativeEventLogger : undefined;
328 const makeAcpNativeLoggers = yield* makeAcpNativeLoggerFactory();
326329
327330 const sessions = new Map<ThreadId, CursorSessionContext>();
328331 const threadLocksRef = yield* SynchronizedRef.make(new Map<string, Semaphore.Semaphore>());
329332 const runtimeEventPubSub = yield* PubSub.unbounded<ProviderRuntimeEvent>();
330333
331334 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));
333347 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
335359 const offerRuntimeEvent = (event: ProviderRuntimeEvent) =>
336360 PubSub.publish(runtimeEventPubSub, event).pipe(Effect.asVoid);
......@@ -369,7 +393,7 @@ export function makeCursorAdapter(
369393 {
370394 observedAt,
371395 event: {
372 id: yield* Random.nextUUIDv4,
396 id: yield* randomUUIDv4,
373397 kind: "notification",
374398 provider: PROVIDER,
375399 createdAt: observedAt,
......@@ -524,159 +548,167 @@ export function makeCursorAdapter(
524548 );
525549 const started = yield* Effect.gen(function* () {
526550 yield* acp.handleExtRequest("cursor/ask_question", CursorAskQuestionRequest, (params) =>
527 Effect.gen(function* () {
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) =>
551 mapExtensionFailure(
594552 Effect.gen(function* () {
595553 yield* logNative(
596554 input.threadId,
597 "cursor/update_todos",
555 "cursor/ask_question",
598556 params,
599557 "acp.cursor.extension",
600558 );
601 if (ctx) {
602 yield* emitPlanUpdate(
603 ctx,
604 extractTodosAsPlan(params),
605 params,
606 "acp.cursor.extension",
607 "cursor/update_todos",
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(),
559 const requestId = ApprovalRequestId.make(yield* randomUUIDv4);
560 const runtimeRequestId = RuntimeRequestId.make(requestId);
561 const answers = yield* Deferred.make<ProviderUserInputAnswers>();
562 pendingUserInputs.set(requestId, { answers });
563 yield* offerRuntimeEvent({
564 type: "user-input.requested",
565 ...(yield* makeEventStamp()),
642566 provider: PROVIDER,
643567 threadId: input.threadId,
644568 turnId: ctx?.activeTurnId,
645569 requestId: runtimeRequestId,
646 permissionRequest,
647 detail:
648 permissionRequest.detail ??
649 encodeJsonStringForDiagnostics(params)?.slice(0, 2000) ??
650 "[unserializable params]",
651 args: params,
652 source: "acp.jsonrpc",
653 method: "session/request_permission",
654 rawPayload: params,
655 }),
656 );
657 const resolved = yield* Deferred.await(decision);
658 pendingApprovals.delete(requestId);
659 yield* offerRuntimeEvent(
660 makeAcpRequestResolvedEvent({
661 stamp: yield* makeEventStamp(),
570 payload: { questions: extractAskQuestions(params) },
571 raw: {
572 source: "acp.cursor.extension",
573 method: "cursor/ask_question",
574 payload: params,
575 },
576 });
577 const resolved = yield* Deferred.await(answers);
578 pendingUserInputs.delete(requestId);
579 yield* offerRuntimeEvent({
580 type: "user-input.resolved",
581 ...(yield* makeEventStamp()),
662582 provider: PROVIDER,
663583 threadId: input.threadId,
664584 turnId: ctx?.activeTurnId,
665585 requestId: runtimeRequestId,
666 permissionRequest,
667 decision: resolved,
586 payload: { answers: 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 }
668639 }),
669 );
670 return {
671 outcome:
672 resolved === "cancel"
673 ? ({ outcome: "cancelled" } as const)
674 : {
640 ),
641 );
642 yield* acp.handleRequestPermission((params) =>
643 mapExtensionFailure(
644 Effect.gen(function* () {
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: {
675656 outcome: "selected" as const,
676 optionId: acpPermissionOutcome(resolved),
657 optionId: autoApprovedOptionId,
677658 },
678 };
679 }),
659 };
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 ),
680712 );
681713 return yield* acp.start();
682714 }).pipe(
......@@ -810,7 +842,12 @@ export function makeCursorAdapter(
810842 }
811843 }),
812844 ),
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
815852 ctx.notificationFiber = nf;
816853 sessions.set(input.threadId, ctx);
......@@ -845,7 +882,7 @@ export function makeCursorAdapter(
845882 const sendTurn: CursorAdapterShape["sendTurn"] = (input) =>
846883 Effect.gen(function* () {
847884 const ctx = yield* requireSession(input.threadId);
848 const turnId = TurnId.make(crypto.randomUUID());
885 const turnId = TurnId.make(yield* randomUUIDv4);
849886 const turnModelSelection =
850887 input.modelSelection?.instanceId === boundInstanceId ? input.modelSelection : undefined;
851888 const model = turnModelSelection?.model ?? ctx.session.model;
......@@ -1056,6 +1093,9 @@ export function makeCursorAdapter(
10561093
10571094 yield* Effect.addFinalizer(() =>
10581095 Effect.forEach(sessions.values(), stopSessionInternal, { discard: true }).pipe(
1096 Effect.catch((cause) =>
1097 Effect.logError("Failed to emit Cursor session shutdown event.", { cause }),
1098 ),
10591099 Effect.tap(() => PubSub.shutdown(runtimeEventPubSub)),
10601100 Effect.tap(() => managedNativeEventLogger?.close() ?? Effect.void),
10611101 ),
apps/server/src/provider/Layers/OpenCodeAdapter.ts+39-30
......@@ -13,11 +13,11 @@ import {
1313 type UserInputQuestion,
1414} from "@t3tools/contracts";
1515import * as Cause from "effect/Cause";
16import * as Crypto from "effect/Crypto";
1617import * as DateTime from "effect/DateTime";
1718import * as Effect from "effect/Effect";
1819import * as Exit from "effect/Exit";
1920import * as Queue from "effect/Queue";
20import * as Random from "effect/Random";
2121import * as Ref from "effect/Ref";
2222import * as Scope from "effect/Scope";
2323import * as Stream from "effect/Stream";
......@@ -134,40 +134,14 @@ const toProcessError = (threadId: ThreadId, cause: unknown): ProviderAdapterProc
134134 cause,
135135 });
136136
137const buildEventBase = (input: {
137type EventBaseInput = {
138138 readonly threadId: ThreadId;
139139 readonly turnId?: TurnId | undefined;
140140 readonly itemId?: string | undefined;
141141 readonly requestId?: string | undefined;
142142 readonly createdAt?: string | undefined;
143143 readonly raw?: unknown;
144}): Effect.Effect<
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 });
144};
171145
172146function toToolLifecycleItemType(toolName: string): ToolLifecycleItemType {
173147 const normalized = toolName.toLowerCase();
......@@ -457,6 +431,7 @@ export function makeOpenCodeAdapter(
457431 const boundInstanceId = options?.instanceId ?? ProviderInstanceId.make("opencode");
458432 const serverConfig = yield* ServerConfig;
459433 const openCodeRuntime = yield* OpenCodeRuntime;
434 const crypto = yield* Crypto.Crypto;
460435 const nativeEventLogger =
461436 options?.nativeEventLogger ??
462437 (options?.nativeEventLogPath !== undefined
......@@ -470,6 +445,40 @@ export function makeOpenCodeAdapter(
470445 options?.nativeEventLogger === undefined ? nativeEventLogger : undefined;
471446 const runtimeEvents = yield* Queue.unbounded<ProviderRuntimeEvent>();
472447 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
474483 // Layer-level finalizer: when the adapter layer shuts down, stop every
475484 // session. Each session's `Scope.close` tears down its spawned OpenCode
......@@ -1142,7 +1151,7 @@ export function makeOpenCodeAdapter(
11421151
11431152 const sendTurn: OpenCodeAdapterShape["sendTurn"] = Effect.fn("sendTurn")(function* (input) {
11441153 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}`);
11461155 const modelSelection =
11471156 input.modelSelection ??
11481157 (context.session.model
apps/server/src/provider/acp/AcpNativeLogging.ts+60-59
......@@ -1,40 +1,13 @@
11import type { ProviderDriverKind, ThreadId } from "@t3tools/contracts";
22import * as Cause from "effect/Cause";
3import * as Crypto from "effect/Crypto";
34import * as DateTime from "effect/DateTime";
45import * as Effect from "effect/Effect";
5import * as Random from "effect/Random";
66import type * as EffectAcpProtocol from "effect-acp/protocol";
77
88import type { EventNdjsonLogger } from "../Layers/EventNdjsonLogger.ts";
99import 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
3811function formatRequestLogPayload(event: AcpSessionRequestLogEvent) {
3912 return {
4013 method: event.method,
......@@ -45,35 +18,63 @@ function formatRequestLogPayload(event: AcpSessionRequestLogEvent) {
4518 };
4619}
4720
48export function makeAcpNativeLoggers(input: {
49 readonly nativeEventLogger: EventNdjsonLogger | undefined;
50 readonly provider: ProviderDriverKind;
51 readonly threadId: ThreadId;
52}): Pick<AcpSessionRuntimeOptions, "requestLogger" | "protocolLogging"> {
53 return {
54 requestLogger: (event) =>
55 writeNativeAcpLog({
56 nativeEventLogger: input.nativeEventLogger,
57 provider: input.provider,
58 threadId: input.threadId,
59 kind: "request",
60 payload: formatRequestLogPayload(event),
61 }),
62 ...(input.nativeEventLogger
63 ? {
64 protocolLogging: {
65 logIncoming: true,
66 logOutgoing: true,
67 logger: (event: EffectAcpProtocol.AcpProtocolLogEvent) =>
68 writeNativeAcpLog({
69 nativeEventLogger: input.nativeEventLogger,
70 provider: input.provider,
71 threadId: input.threadId,
72 kind: "protocol",
73 payload: event,
74 }),
75 } satisfies NonNullable<AcpSessionRuntimeOptions["protocolLogging"]>,
76 }
77 : {}),
21export const makeAcpNativeLoggerFactory = Effect.fn("makeAcpNativeLoggerFactory")(function* () {
22 const crypto = yield* Crypto.Crypto;
23 return (input: {
24 readonly nativeEventLogger: EventNdjsonLogger | undefined;
25 readonly provider: ProviderDriverKind;
26 readonly threadId: ThreadId;
27 }): Pick<AcpSessionRuntimeOptions, "requestLogger" | "protocolLogging"> => {
28 const writeNativeAcpLog = (logInput: {
29 readonly kind: "request" | "protocol";
30 readonly payload: unknown;
31 }) =>
32 Effect.gen(function* () {
33 if (!input.nativeEventLogger) return;
34 const observedAt = DateTime.formatIso(yield* DateTime.now);
35 yield* input.nativeEventLogger.write(
36 {
37 observedAt,
38 event: {
39 id: yield* crypto.randomUUIDv4,
40 kind: logInput.kind,
41 provider: input.provider,
42 createdAt: observedAt,
43 threadId: input.threadId,
44 payload: logInput.payload,
45 },
46 },
47 input.threadId,
48 );
49 }).pipe(
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 };
7879 };
79}
80});
apps/server/src/provider/providerMaintenance.test.ts+10-9
......@@ -1,12 +1,12 @@
11// @effect-diagnostics nodeBuiltinImport:off
2import { afterEach, describe, expect, it } from "@effect/vitest";
2import { afterEach, expect, it } from "@effect/vitest";
33import { chmodSync, mkdirSync, symlinkSync, writeFileSync } from "node:fs";
44import * as NodeServices from "@effect/platform-node/NodeServices";
55import os from "node:os";
66import path from "node:path";
77import { ProviderDriverKind } from "@t3tools/contracts";
8import * as Crypto from "effect/Crypto";
89import * as Effect from "effect/Effect";
9import * as Random from "effect/Random";
1010import {
1111 clearLatestProviderVersionCacheForTests,
1212 createProviderVersionAdvisory,
......@@ -18,10 +18,11 @@ import {
1818} from "./providerMaintenance.ts";
1919
2020const driver = (value: string) => ProviderDriverKind.make(value);
21const makeTempDir = Effect.fn("makeTempDir")(function* (name: string) {
22 const id = yield* Random.nextUUIDv4;
23 return path.join(os.tmpdir(), `${name}-${id}`);
24});
21const makeTempDir = (name: string) =>
22 Crypto.Crypto.pipe(
23 Effect.flatMap((crypto) => crypto.randomUUIDv4),
24 Effect.map((id) => path.join(os.tmpdir(), `${name}-${id}`)),
25 );
2526const isNativeTestCommandPath =
2627 (expectedPathSegment: string) =>
2728 (commandPath: string): boolean =>
......@@ -68,7 +69,7 @@ afterEach(() => {
6869 clearLatestProviderVersionCacheForTests();
6970});
7071
71describe("providerMaintenance", () => {
72it.layer(NodeServices.layer)("providerMaintenance", (it) => {
7273 it("marks providers with unknown current versions as unknown", () => {
7374 expect(
7475 createProviderVersionAdvisory({
......@@ -404,7 +405,7 @@ describe("providerMaintenance", () => {
404405 env: {
405406 PATH: "",
406407 },
407 }).pipe(Effect.provide(NodeServices.layer));
408 });
408409
409410 expect(capabilities).toEqual({
410411 provider: driver("packageTool"),
......@@ -452,7 +453,7 @@ describe("providerMaintenance", () => {
452453 env: {
453454 PATH: "",
454455 },
455 }).pipe(Effect.provide(NodeServices.layer));
456 });
456457
457458 expect(capabilities).toEqual({
458459 provider: driver("packageTool"),
apps/server/src/telemetry/Identify.ts+17-15
......@@ -1,9 +1,9 @@
1import * as Crypto from "effect/Crypto";
12import * as Effect from "effect/Effect";
3import * as Encoding from "effect/Encoding";
24import * as FileSystem from "effect/FileSystem";
35import * as Path from "effect/Path";
4import * as Random from "effect/Random";
56import * as Schema from "effect/Schema";
6import * as Crypto from "node:crypto";
77import { homedir } from "node:os";
88import { ServerConfig } from "../config.ts";
99
......@@ -23,14 +23,17 @@ class IdentifyUserError extends Schema.TaggedErrorClass<IdentifyUserError>()("Id
2323}) {}
2424
2525const hash = (value: string) =>
26 Effect.try({
27 try: () => Crypto.createHash("sha256").update(value).digest("hex"),
28 catch: (error) =>
29 new IdentifyUserError({
30 message: "Failed to hash identifier",
31 cause: error,
32 }),
33 });
26 Crypto.Crypto.pipe(
27 Effect.flatMap((crypto) => crypto.digest("SHA-256", new TextEncoder().encode(value))),
28 Effect.map(Encoding.encodeHex),
29 Effect.mapError(
30 (cause) =>
31 new IdentifyUserError({
32 message: "Failed to hash identifier",
33 cause,
34 }),
35 ),
36 );
3437
3538const getCodexAccountId = Effect.gen(function* () {
3639 const fileSystem = yield* FileSystem.FileSystem;
......@@ -64,11 +67,10 @@ const upsertAnonymousId = Effect.gen(function* () {
6467
6568 const anonymousId = yield* fileSystem.readFileString(anonymousIdPath).pipe(
6669 Effect.catch(() =>
67 Effect.gen(function* () {
68 const randomId = yield* Random.nextUUIDv4;
69 yield* fileSystem.writeFileString(anonymousIdPath, randomId);
70 return randomId;
71 }),
70 Crypto.Crypto.pipe(
71 Effect.flatMap((crypto) => crypto.randomUUIDv4),
72 Effect.tap((randomId) => fileSystem.writeFileString(anonymousIdPath, randomId)),
73 ),
7274 ),
7375 );
7476
apps/server/src/telemetry/Layers/AnalyticsService.ts+1-1
......@@ -40,7 +40,7 @@ const TelemetryEnvConfig = Config.all({
4040});
4141
4242const makeAnalyticsService = Effect.gen(function* () {
43 const telemetryConfig = yield* TelemetryEnvConfig.asEffect();
43 const telemetryConfig = yield* TelemetryEnvConfig;
4444 const httpClient = yield* HttpClient.HttpClient;
4545 const serverConfig = yield* ServerConfig;
4646 const identifier = yield* getTelemetryIdentifier;
apps/server/src/textGeneration/CodexTextGeneration.ts+1-3
......@@ -2,7 +2,6 @@ import * as Effect from "effect/Effect";
22import * as FileSystem from "effect/FileSystem";
33import * as Option from "effect/Option";
44import * as Path from "effect/Path";
5import * as Random from "effect/Random";
65import * as Schema from "effect/Schema";
76import * as Scope from "effect/Scope";
87import * as Stream from "effect/Stream";
......@@ -79,10 +78,9 @@ export const makeCodexTextGeneration = Effect.fn("makeCodexTextGeneration")(func
7978 content: string,
8079 ): Effect.Effect<string, TextGenerationError, Scope.Scope> => {
8180 return Effect.gen(function* () {
82 const tempFileId = yield* Random.nextUUIDv4;
8381 return yield* fileSystem
8482 .makeTempFileScoped({
85 prefix: `t3code-${prefix}-${process.pid}-${tempFileId}.tmp`,
83 prefix: `t3code-${prefix}-${process.pid}-`,
8684 })
8785 .pipe(Effect.tap((filePath) => fileSystem.writeFileString(filePath, content)));
8886 }).pipe(
apps/web/src/components/ChatView.tsx+2-2
......@@ -105,7 +105,7 @@ import { resolveShortcutCommand, shortcutLabelForCommand } from "../keybindings"
105105import PlanSidebar from "./PlanSidebar";
106106import ThreadTerminalDrawer from "./ThreadTerminalDrawer";
107107import { ChevronDownIcon, TriangleAlertIcon, WifiOffIcon } from "lucide-react";
108import { cn, randomUUID } from "~/lib/utils";
108import { cn, randomHex, randomUUID } from "~/lib/utils";
109109import { stackedThreadToast, toastManager } from "./ui/toast";
110110import { decodeProjectScriptKeybindingRule } from "~/lib/projectScriptKeybindings";
111111import { type NewProjectScriptInput } from "./ProjectScriptsControl";
......@@ -2846,7 +2846,7 @@ export default function ChatView(props: ChatViewProps) {
28462846 prepareWorktree: {
28472847 projectCwd: activeProject.cwd,
28482848 baseBranch: baseBranchForWorktree,
2849 branch: buildTemporaryWorktreeBranchName(),
2849 branch: buildTemporaryWorktreeBranchName(randomHex),
28502850 },
28512851 runSetupScript: true,
28522852 }
apps/web/src/lib/utils.ts+10-6
......@@ -1,8 +1,7 @@
11import { CommandId, MessageId, ProjectId, ThreadId } from "@t3tools/contracts";
22import { type CxOptions, cx } from "class-variance-authority";
3import * as Encoding from "effect/Encoding";
34import { twMerge } from "tailwind-merge";
4import * as Random from "effect/Random";
5import * as Effect from "effect/Effect";
65import { DraftId } from "../composerDraftStore";
76
87export function cn(...inputs: CxOptions) {
......@@ -21,11 +20,16 @@ export function isLinuxPlatform(platform: string): boolean {
2120 return /linux/i.test(platform);
2221}
2322
23export function randomHex(byteLength: number): string {
24 return Encoding.encodeHex(globalThis.crypto.getRandomValues(new Uint8Array(byteLength)));
25}
26
2427export function randomUUID(): string {
25 if (typeof crypto.randomUUID === "function") {
26 return crypto.randomUUID();
27 }
28 return Effect.runSync(Random.nextUUIDv4);
28 const bytes = globalThis.crypto.getRandomValues(new Uint8Array(16));
29 bytes[6] = (bytes[6]! & 0x0f) | 0x40;
30 bytes[8] = (bytes[8]! & 0x3f) | 0x80;
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)}`;
2933}
3034
3135export const newCommandId = (): CommandId => CommandId.make(randomUUID());
bun.lock+33-33
......@@ -17,7 +17,7 @@
1717 },
1818 "apps/desktop": {
1919 "name": "@t3tools/desktop",
20 "version": "0.0.23",
20 "version": "0.0.24",
2121 "dependencies": {
2222 "@effect/platform-node": "catalog:",
2323 "@t3tools/contracts": "workspace:*",
......@@ -50,7 +50,7 @@
5050 },
5151 "apps/server": {
5252 "name": "t3",
53 "version": "0.0.23",
53 "version": "0.0.24",
5454 "bin": {
5555 "t3": "./dist/bin.mjs",
5656 },
......@@ -83,7 +83,7 @@
8383 },
8484 "apps/web": {
8585 "name": "@t3tools/web",
86 "version": "0.0.23",
86 "version": "0.0.24",
8787 "dependencies": {
8888 "@base-ui/react": "^1.4.1",
8989 "@dnd-kit/core": "^6.3.1",
......@@ -165,7 +165,7 @@
165165 },
166166 "packages/contracts": {
167167 "name": "@t3tools/contracts",
168 "version": "0.0.23",
168 "version": "0.0.24",
169169 "dependencies": {
170170 "effect": "catalog:",
171171 },
......@@ -272,7 +272,7 @@
272272 "node-pty",
273273 ],
274274 "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",
276276 },
277277 "overrides": {
278278 "@effect/atom-react": "catalog:",
......@@ -285,18 +285,18 @@
285285 "vite": "^8.0.0",
286286 },
287287 "catalog": {
288 "@effect/atom-react": "4.0.0-beta.59",
288 "@effect/atom-react": "4.0.0-beta.73",
289289 "@effect/language-service": "0.84.2",
290 "@effect/openapi-generator": "4.0.0-beta.59",
291 "@effect/platform-bun": "4.0.0-beta.59",
292 "@effect/platform-node": "4.0.0-beta.59",
293 "@effect/platform-node-shared": "4.0.0-beta.59",
294 "@effect/sql-sqlite-bun": "4.0.0-beta.59",
295 "@effect/vitest": "4.0.0-beta.59",
290 "@effect/openapi-generator": "4.0.0-beta.73",
291 "@effect/platform-bun": "4.0.0-beta.73",
292 "@effect/platform-node": "4.0.0-beta.73",
293 "@effect/platform-node-shared": "4.0.0-beta.73",
294 "@effect/sql-sqlite-bun": "4.0.0-beta.73",
295 "@effect/vitest": "4.0.0-beta.73",
296296 "@pierre/diffs": "1.1.20",
297297 "@types/bun": "^1.3.11",
298298 "@types/node": "^24.10.13",
299 "effect": "4.0.0-beta.59",
299 "effect": "4.0.0-beta.73",
300300 "tsdown": "^0.20.3",
301301 "typescript": "^5.7.3",
302302 "vitest": "^4.0.0",
......@@ -384,21 +384,21 @@
384384
385385 "@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
389389 "@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
403403 "@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 @@
608608
609609 "@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
623623 "@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 @@
11601160
11611161 "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
11651165 "effect-acp": ["effect-acp@workspace:packages/effect-acp"],
11661166
......@@ -1230,7 +1230,7 @@
12301230
12311231 "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
12351235 "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
12361236
......@@ -1346,7 +1346,7 @@
13461346
13471347 "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
13511351 "inline-style-parser": ["inline-style-parser@0.2.7", "", {}, "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA=="],
13521352
......@@ -1590,9 +1590,9 @@
15901590
15911591 "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
15971597 "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 @@
20222022
20232023 "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
20272027 "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
20282028
......@@ -2096,7 +2096,7 @@
20962096
20972097 "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
21012101 "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 @@
99 "scripts"
1010 ],
1111 "catalog": {
12 "effect": "4.0.0-beta.59",
13 "@effect/atom-react": "4.0.0-beta.59",
14 "@effect/openapi-generator": "4.0.0-beta.59",
15 "@effect/platform-bun": "4.0.0-beta.59",
16 "@effect/platform-node": "4.0.0-beta.59",
17 "@effect/platform-node-shared": "4.0.0-beta.59",
18 "@effect/sql-sqlite-bun": "4.0.0-beta.59",
19 "@effect/vitest": "4.0.0-beta.59",
12 "effect": "4.0.0-beta.73",
13 "@effect/atom-react": "4.0.0-beta.73",
14 "@effect/openapi-generator": "4.0.0-beta.73",
15 "@effect/platform-bun": "4.0.0-beta.73",
16 "@effect/platform-node": "4.0.0-beta.73",
17 "@effect/platform-node-shared": "4.0.0-beta.73",
18 "@effect/sql-sqlite-bun": "4.0.0-beta.73",
19 "@effect/vitest": "4.0.0-beta.73",
2020 "@effect/language-service": "0.84.2",
2121 "@pierre/diffs": "1.1.20",
2222 "@types/bun": "^1.3.11",
......@@ -91,7 +91,7 @@
9191 ]
9292 },
9393 "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"
9595 },
9696 "trustedDependencies": [
9797 "node-pty",
packages/shared/src/git.test.ts+8-1
......@@ -55,7 +55,14 @@ describe("parseGitHubRepositoryNameWithOwnerFromRemoteUrl", () => {
5555
5656describe("isTemporaryWorktreeBranch", () => {
5757 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);
5966 });
6067
6168 it("matches generated temporary worktree refs", () => {
packages/shared/src/git.ts+4-4
......@@ -6,8 +6,6 @@ import type {
66 VcsStatusResult,
77 VcsStatusStreamEvent,
88} from "@t3tools/contracts";
9import * as Effect from "effect/Effect";
10import * as Random from "effect/Random";
119import { detectSourceControlProviderFromRemoteUrl } from "./sourceControl.ts";
1210
1311export const WORKTREE_BRANCH_PREFIX = "t3code";
......@@ -86,8 +84,10 @@ export function deriveLocalBranchNameFromRemoteRef(branchName: string): string {
8684 return branchName.slice(firstSeparatorIndex + 1);
8785}
8886
89export function buildTemporaryWorktreeBranchName(): string {
90 const token = Effect.runSync(Random.nextUUIDv4).replace(/-/g, "").slice(0, 8).toLowerCase();
87export function buildTemporaryWorktreeBranchName(
88 randomHex: (byteLength: number) => string,
89): string {
90 const token = randomHex(4).toLowerCase();
9191 return `${WORKTREE_BRANCH_PREFIX}/${token}`;
9292}
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* (
278278) {
279279 const path = yield* Path.Path;
280280 const repoRoot = yield* RepoRoot;
281 const env = yield* BuildEnvConfig.asEffect();
281 const env = yield* BuildEnvConfig;
282282
283283 const platform = mergeOptions(
284284 input.platform,
scripts/dev-runner.ts+1-1
......@@ -385,7 +385,7 @@ interface DevRunnerCliInput {
385385
386386export function runDevRunnerWithInput(input: DevRunnerCliInput) {
387387 return Effect.gen(function* () {
388 const { portOffset, devInstance } = yield* OffsetConfig.asEffect().pipe(
388 const { portOffset, devInstance } = yield* OffsetConfig.pipe(
389389 Effect.mapError(
390390 (cause) =>
391391 new DevRunnerError({
scripts/mock-update-server.ts+1-1
......@@ -24,7 +24,7 @@ const resolveMockUpdateServerConfig = Effect.gen(function* () {
2424 root: Config.string("T3CODE_DESKTOP_MOCK_UPDATE_SERVER_ROOT").pipe(
2525 Config.withDefault("../release-mock"),
2626 ),
27 }).asEffect();
27 });
2828
2929 const resolvedRoot = path.resolve(import.meta.dirname, config.root);
3030