authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-05-08 09:58:29-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-05-08 09:58:29-07:00
log25b02f4ba28d9c6a1adff3475625e5b3fdacce4b
treecf015483f76067409dc4afcfe3edceea84494560
parent131234be3fa53d9b0daff3e8116138d5bad079ba
signature Signed by PGP key B5690EEEBB952194

Enable stricter Effect LSP rules (#2596)

Co-authored-by: Cursor Agent <cursoragent@cursor.com>

409 files changed, 3120 insertions(+), 2111 deletions(-)

apps/desktop/src/backend/DesktopBackendManager.test.ts+1-1
......@@ -15,7 +15,7 @@ import * as Schema from "effect/Schema";
1515import * as Sink from "effect/Sink";
1616import * as Scope from "effect/Scope";
1717import * as Stream from "effect/Stream";
18import { TestClock } from "effect/testing";
18import * as TestClock from "effect/testing/TestClock";
1919import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http";
2020import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
2121
apps/desktop/src/ssh/DesktopSshEnvironment.ts+3-3
......@@ -3,7 +3,7 @@ import type {
33 DesktopSshEnvironmentBootstrap,
44 DesktopSshEnvironmentTarget,
55} from "@t3tools/contracts";
6import { type NetError, NetService } from "@t3tools/shared/Net";
6import * as NetService from "@t3tools/shared/Net";
77import {
88 SshPasswordPrompt,
99 type SshPasswordPromptShape,
......@@ -35,7 +35,7 @@ export type DesktopSshEnvironmentRuntimeServices =
3535 | FileSystem.FileSystem
3636 | Path.Path
3737 | HttpClient.HttpClient
38 | NetService;
38 | NetService.NetService;
3939
4040export type DesktopSshEnvironmentOperationError =
4141 | SshCommandError
......@@ -44,7 +44,7 @@ export type DesktopSshEnvironmentOperationError =
4444 | SshPairingError
4545 | SshReadinessError
4646 | SshPasswordPromptError
47 | NetError;
47 | NetService.NetError;
4848
4949export type DesktopSshEnvironmentDiscoverError = SshHostDiscoveryError;
5050
apps/desktop/src/ssh/DesktopSshPasswordPrompts.test.ts+1-1
......@@ -4,7 +4,7 @@ import * as Effect from "effect/Effect";
44import * as Fiber from "effect/Fiber";
55import * as Layer from "effect/Layer";
66import * as Option from "effect/Option";
7import { TestClock } from "effect/testing";
7import * as TestClock from "effect/testing/TestClock";
88import type * as Electron from "electron";
99
1010import * as ElectronWindow from "../electron/ElectronWindow.ts";
apps/desktop/src/updates/DesktopUpdates.test.ts+1-1
......@@ -8,7 +8,7 @@ import * as Effect from "effect/Effect";
88import * as Fiber from "effect/Fiber";
99import * as Layer from "effect/Layer";
1010import * as Option from "effect/Option";
11import { TestClock } from "effect/testing";
11import * as TestClock from "effect/testing/TestClock";
1212
1313import * as DesktopBackendManager from "../backend/DesktopBackendManager.ts";
1414import * as DesktopConfig from "../app/DesktopConfig.ts";
apps/desktop/tsconfig.json+1-13
......@@ -3,19 +3,7 @@
33 "compilerOptions": {
44 "composite": true,
55 "types": ["node", "electron"],
6 "lib": ["ESNext", "DOM", "esnext.disposable"],
7 "plugins": [
8 {
9 "name": "@effect/language-service",
10 "namespaceImportPackages": ["@effect/platform-node", "effect"],
11 "diagnosticSeverity": {
12 "importFromBarrel": "error",
13 "anyUnknownInErrorContext": "error",
14 "instanceOfSchema": "warning",
15 "deterministicKeys": "warning"
16 }
17 }
18 ]
6 "lib": ["ESNext", "DOM", "esnext.disposable"]
197 },
208 "include": ["src", "tsdown.config.ts"]
219}
apps/server/integration/OrchestrationEngineHarness.integration.ts+13-14
......@@ -1,3 +1,4 @@
1// @effect-diagnostics nodeBuiltinImport:off
12import { execFileSync } from "node:child_process";
23
34import * as NodeServices from "@effect/platform-node/NodeServices";
......@@ -8,20 +9,18 @@ import {
89 type OrchestrationEvent,
910 type OrchestrationThread,
1011} from "@t3tools/contracts";
11import {
12 Effect,
13 Exit,
14 FileSystem,
15 Layer,
16 ManagedRuntime,
17 Option,
18 Path,
19 Ref,
20 Schedule,
21 Schema,
22 Scope,
23 Stream,
24} from "effect";
12import * as Effect from "effect/Effect";
13import * as Exit from "effect/Exit";
14import * as FileSystem from "effect/FileSystem";
15import * as Layer from "effect/Layer";
16import * as ManagedRuntime from "effect/ManagedRuntime";
17import * as Option from "effect/Option";
18import * as Path from "effect/Path";
19import * as Ref from "effect/Ref";
20import * as Schedule from "effect/Schedule";
21import * as Schema from "effect/Schema";
22import * as Scope from "effect/Scope";
23import * as Stream from "effect/Stream";
2524
2625import { CheckpointStoreLive } from "../src/checkpointing/Layers/CheckpointStore.ts";
2726import { CheckpointStore } from "../src/checkpointing/Services/CheckpointStore.ts";
apps/server/integration/TestProviderAdapter.integration.ts+7-7
......@@ -1,5 +1,3 @@
1import { randomUUID } from "node:crypto";
2
31import {
42 ApprovalRequestId,
53 EventId,
......@@ -12,7 +10,10 @@ import {
1210 TurnId,
1311 ProviderDriverKind,
1412} from "@t3tools/contracts";
15import { Effect, Queue, Stream } from "effect";
13import * as Effect from "effect/Effect";
14import * as Queue from "effect/Queue";
15import * as Random from "effect/Random";
16import * as Stream from "effect/Stream";
1617
1718import {
1819 ProviderAdapterSessionNotFoundError,
......@@ -202,7 +203,7 @@ interface MakeTestProviderAdapterHarnessOptions {
202203}
203204
204205function nowIso(): string {
205 return new Date().toISOString();
206 return "2026-01-01T00:00:00.000Z";
206207}
207208
208209function sessionNotFound(
......@@ -308,10 +309,9 @@ export const makeTestProviderAdapterHarness = (options?: MakeTestProviderAdapter
308309 for (const fixtureEvent of response.events) {
309310 const rawEvent: Record<string, unknown> = {
310311 ...(fixtureEvent as Record<string, unknown>),
311 eventId: randomUUID(),
312 eventId: yield* Random.nextUUIDv4,
312313 provider,
313314 sessionId: RuntimeSessionId.make(String(input.threadId)),
314 createdAt: nowIso(),
315315 };
316316 rawEvent.threadId = state.snapshot.threadId;
317317 if (Object.hasOwn(rawEvent, "turnId")) {
......@@ -366,7 +366,7 @@ export const makeTestProviderAdapterHarness = (options?: MakeTestProviderAdapter
366366 if (deferredTurnCompletedEvents.length === 0) {
367367 yield* emit({
368368 type: "turn.completed",
369 eventId: EventId.make(randomUUID()),
369 eventId: EventId.make(yield* Random.nextUUIDv4),
370370 provider,
371371 createdAt: nowIso(),
372372 threadId: state.snapshot.threadId,
apps/server/integration/orchestrationEngine.integration.test.ts+12-5
......@@ -1,3 +1,4 @@
1// @effect-diagnostics nodeBuiltinImport:off
12import fs from "node:fs";
23import path from "node:path";
34
......@@ -17,7 +18,10 @@ import {
1718 ProviderInstanceId,
1819} from "@t3tools/contracts";
1920import { assert, it } from "@effect/vitest";
20import { Effect, Option, Schema } from "effect";
21import * as Clock from "effect/Clock";
22import * as Effect from "effect/Effect";
23import * as Option from "effect/Option";
24import * as Schema from "effect/Schema";
2125
2226import type { TestTurnResponse } from "./TestProviderAdapter.integration.ts";
2327import {
......@@ -47,7 +51,7 @@ const CODEX_PROVIDER = ProviderDriverKind.make("codex");
4751const CLAUDE_AGENT_PROVIDER = ProviderDriverKind.make("claudeAgent");
4852
4953function nowIso() {
50 return new Date().toISOString();
54 return "2026-05-01T00:00:00.000Z";
5155}
5256
5357class IntegrationWaitTimeoutError extends Schema.TaggedErrorClass<IntegrationWaitTimeoutError>()(
......@@ -64,14 +68,14 @@ function waitForSync<A>(
6468 timeoutMs = 10_000,
6569): Effect.Effect<A, never> {
6670 return Effect.gen(function* () {
67 const deadline = Date.now() + timeoutMs;
71 const deadline = (yield* Clock.currentTimeMillis) + timeoutMs;
6872
6973 while (true) {
7074 const value = read();
7175 if (predicate(value)) {
7276 return value;
7377 }
74 if (Date.now() >= deadline) {
78 if ((yield* Clock.currentTimeMillis) >= deadline) {
7579 return yield* Effect.die(new IntegrationWaitTimeoutError({ description }));
7680 }
7781 yield* Effect.sleep(10);
......@@ -156,6 +160,7 @@ const startTurn = (input: {
156160 readonly messageId: string;
157161 readonly text: string;
158162 readonly modelSelection?: ModelSelection;
163 readonly createdAt?: string;
159164}) =>
160165 input.harness.engine.dispatch({
161166 type: "thread.turn.start",
......@@ -174,7 +179,7 @@ const startTurn = (input: {
174179 : {}),
175180 interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE,
176181 runtimeMode: "approval-required",
177 createdAt: nowIso(),
182 createdAt: input.createdAt ?? nowIso(),
178183 });
179184
180185it.live("runs a single turn end-to-end and persists checkpoint state in sqlite + git", () =>
......@@ -755,6 +760,7 @@ it.live("reverts to an earlier checkpoint and trims checkpoint projections + git
755760 commandId: "cmd-turn-start-revert-1",
756761 messageId: "msg-user-revert-1",
757762 text: "First edit",
763 createdAt: "2026-02-24T10:04:59.900Z",
758764 });
759765
760766 yield* harness.waitForThread(
......@@ -813,6 +819,7 @@ it.live("reverts to an earlier checkpoint and trims checkpoint projections + git
813819 commandId: "cmd-turn-start-revert-2",
814820 messageId: "msg-user-revert-2",
815821 text: "Second edit",
822 createdAt: "2026-02-24T10:05:00.900Z",
816823 });
817824
818825 yield* harness.waitForThread(
apps/server/integration/providerService.integration.test.ts+6-1
......@@ -3,7 +3,12 @@ import { ProviderDriverKind, ProviderInstanceId, ThreadId } from "@t3tools/contr
33import { DEFAULT_SERVER_SETTINGS } from "@t3tools/contracts/settings";
44import * as NodeServices from "@effect/platform-node/NodeServices";
55import { it, assert } from "@effect/vitest";
6import { Effect, FileSystem, Layer, Path, Queue, Stream } from "effect";
6import * as Effect from "effect/Effect";
7import * as FileSystem from "effect/FileSystem";
8import * as Layer from "effect/Layer";
9import * as Path from "effect/Path";
10import * as Queue from "effect/Queue";
11import * as Stream from "effect/Stream";
712
813import { ProviderAdapterRegistry } from "../src/provider/Services/ProviderAdapterRegistry.ts";
914import { makeAdapterRegistryMock } from "../src/provider/testUtils/providerAdapterRegistryMock.ts";
apps/server/scripts/acp-mock-agent.ts+1
......@@ -1,4 +1,5 @@
11#!/usr/bin/env bun
2// @effect-diagnostics nodeBuiltinImport:off
23import { appendFileSync } from "node:fs";
34
45import * as Effect from "effect/Effect";
apps/server/scripts/cli.ts+13-3
......@@ -1,8 +1,13 @@
11#!/usr/bin/env node
2
32import * as NodeRuntime from "@effect/platform-node/NodeRuntime";
43import * as NodeServices from "@effect/platform-node/NodeServices";
5import { Data, Effect, FileSystem, Logger, Option, Path } from "effect";
4import * as Data from "effect/Data";
5import * as Effect from "effect/Effect";
6import * as FileSystem from "effect/FileSystem";
7import * as Logger from "effect/Logger";
8import * as Option from "effect/Option";
9import * as Path from "effect/Path";
10import * as Schema from "effect/Schema";
611import { Command, Flag } from "effect/unstable/cli";
712import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
813
......@@ -11,6 +16,7 @@ import {
1116 PUBLISH_ICON_OVERRIDES,
1217} from "../../../scripts/lib/brand-assets.ts";
1318import { resolveCatalogDependencies } from "../../../scripts/lib/resolve-catalog.ts";
19import { fromJsonStringPretty } from "@t3tools/shared/schemaJson";
1420import rootPackageJson from "../../../package.json" with { type: "json" };
1521import serverPackageJson from "../package.json" with { type: "json" };
1622
......@@ -30,6 +36,9 @@ interface PackageJson {
3036 overrides: Record<string, string>;
3137}
3238
39const PackageJsonPrettyJson = fromJsonStringPretty(Schema.Unknown);
40const encodePackageJson = Schema.encodeEffect(PackageJsonPrettyJson);
41
3342class CliError extends Data.TaggedError("CliError")<{
3443 readonly message: string;
3544 readonly cause?: unknown;
......@@ -227,8 +236,9 @@ const publishCmd = Command.make(
227236 };
228237
229238 const original = yield* fs.readFileString(packageJsonPath);
239 const packageJsonString = yield* encodePackageJson(pkg);
230240 yield* fs.writeFileString(backupPath, original);
231 yield* fs.writeFileString(packageJsonPath, `${JSON.stringify(pkg, null, 2)}\n`);
241 yield* fs.writeFileString(packageJsonPath, `${packageJsonString}\n`);
232242 yield* Effect.log("[cli] Prepared package.json for publish");
233243
234244 const iconBackups = yield* applyPublishIconOverrides(repoRoot, serverDir);
apps/server/scripts/cursor-acp-model-mismatch-probe.ts+8-4
......@@ -1,6 +1,8 @@
1// @effect-diagnostics nodeBuiltinImport:off
12import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process";
23import process from "node:process";
34import readline from "node:readline";
5import * as NodeTimers from "node:timers";
46
57type JsonPrimitive = null | boolean | number | string;
68type JsonValue = JsonPrimitive | JsonValue[] | { [key: string]: JsonValue };
......@@ -114,7 +116,8 @@ function matchesKeyword(option: SessionConfigOption, keyword: string): boolean {
114116
115117function sleep(ms: number) {
116118 return new Promise<void>((resolve) => {
117 setTimeout(resolve, ms);
119 // @effect-diagnostics-next-line globalTimers:off - Standalone Node probe script, not an Effect runtime test.
120 NodeTimers.setTimeout(resolve, ms);
118121 });
119122}
120123
......@@ -177,7 +180,8 @@ class JsonRpcChild {
177180 const id = this.nextId++;
178181
179182 const responsePromise = new Promise<JsonValue | undefined>((resolve, reject) => {
180 const timeout = setTimeout(() => {
183 // @effect-diagnostics-next-line globalTimers:off - Standalone Node probe script request timeout.
184 const timeout = NodeTimers.setTimeout(() => {
181185 this.pending.delete(id);
182186 reject(new Error(`Timed out waiting for ${method} response after ${timeoutMs}ms.`));
183187 }, timeoutMs);
......@@ -185,11 +189,11 @@ class JsonRpcChild {
185189 this.pending.set(id, {
186190 method,
187191 resolve: (value) => {
188 clearTimeout(timeout);
192 NodeTimers.clearTimeout(timeout);
189193 resolve(value);
190194 },
191195 reject: (error) => {
192 clearTimeout(timeout);
196 NodeTimers.clearTimeout(timeout);
193197 reject(error);
194198 },
195199 });
apps/server/src/atomicWrite.ts+3-1
......@@ -1,4 +1,6 @@
1import { Effect, FileSystem, Path } from "effect";
1import * as Effect from "effect/Effect";
2import * as FileSystem from "effect/FileSystem";
3import * as Path from "effect/Path";
24import * as Random from "effect/Random";
35
46export const writeFileStringAtomically = (input: {
apps/server/src/attachmentPaths.ts+6-5
......@@ -1,9 +1,10 @@
1import path from "node:path";
1// @effect-diagnostics nodeBuiltinImport:off
2import NodePath from "node:path";
23
34export const ATTACHMENTS_ROUTE_PREFIX = "/attachments";
45
56export function normalizeAttachmentRelativePath(rawRelativePath: string): string | null {
6 const normalized = path.normalize(rawRelativePath).replace(/^[/\\]+/, "");
7 const normalized = NodePath.normalize(rawRelativePath).replace(/^[/\\]+/, "");
78 if (normalized.length === 0 || normalized.startsWith("..") || normalized.includes("\0")) {
89 return null;
910 }
......@@ -19,9 +20,9 @@ export function resolveAttachmentRelativePath(input: {
1920 return null;
2021 }
2122
22 const attachmentsRoot = path.resolve(input.attachmentsDir);
23 const filePath = path.resolve(path.join(attachmentsRoot, normalizedRelativePath));
24 if (!filePath.startsWith(`${attachmentsRoot}${path.sep}`)) {
23 const attachmentsRoot = NodePath.resolve(input.attachmentsDir);
24 const filePath = NodePath.resolve(NodePath.join(attachmentsRoot, normalizedRelativePath));
25 if (!filePath.startsWith(`${attachmentsRoot}${NodePath.sep}`)) {
2526 return null;
2627 }
2728 return filePath;
apps/server/src/attachmentStore.test.ts+1
......@@ -1,3 +1,4 @@
1// @effect-diagnostics nodeBuiltinImport:off
12import fs from "node:fs";
23import os from "node:os";
34import path from "node:path";
apps/server/src/attachmentStore.ts+1
......@@ -1,3 +1,4 @@
1// @effect-diagnostics nodeBuiltinImport:off
12import { randomUUID } from "node:crypto";
23import { existsSync } from "node:fs";
34
apps/server/src/auth/Layers/AuthControlPlane.test.ts+2-1
......@@ -1,6 +1,7 @@
11import * as NodeServices from "@effect/platform-node/NodeServices";
22import { expect, it } from "@effect/vitest";
3import { Effect, Layer } from "effect";
3import * as Effect from "effect/Effect";
4import * as Layer from "effect/Layer";
45
56import type { ServerConfigShape } from "../../config.ts";
67import { ServerConfig } from "../../config.ts";
apps/server/src/auth/Layers/AuthControlPlane.ts+3-1
......@@ -1,5 +1,7 @@
11import type { AuthClientSession, AuthPairingLink } from "@t3tools/contracts";
2import { DateTime, Effect, Layer } from "effect";
2import * as DateTime from "effect/DateTime";
3import * as Effect from "effect/Effect";
4import * as Layer from "effect/Layer";
35
46import { BootstrapCredentialServiceLive } from "./BootstrapCredentialService.ts";
57import { ServerSecretStoreLive } from "./ServerSecretStore.ts";
apps/server/src/auth/Layers/BootstrapCredentialService.test.ts+4-2
......@@ -1,7 +1,9 @@
11import * as NodeServices from "@effect/platform-node/NodeServices";
22import { expect, it } from "@effect/vitest";
3import { Duration, Effect, Layer } from "effect";
4import { TestClock } from "effect/testing";
3import * as Duration from "effect/Duration";
4import * as Effect from "effect/Effect";
5import * as Layer from "effect/Layer";
6import * as TestClock from "effect/testing/TestClock";
57
68import type { ServerConfigShape } from "../../config.ts";
79import { ServerConfig } from "../../config.ts";
apps/server/src/auth/Layers/BootstrapCredentialService.ts+8-2
......@@ -1,6 +1,12 @@
11import type { AuthPairingLink } from "@t3tools/contracts";
2import { DateTime, Duration, Effect, Layer, PubSub, Ref, Stream } from "effect";
3import { Option } from "effect";
2import * as DateTime from "effect/DateTime";
3import * as Duration from "effect/Duration";
4import * as Effect from "effect/Effect";
5import * as Layer from "effect/Layer";
6import * as PubSub from "effect/PubSub";
7import * as Ref from "effect/Ref";
8import * as Stream from "effect/Stream";
9import * as Option from "effect/Option";
410
511import { ServerConfig } from "../../config.ts";
612import { AuthPairingLinkRepositoryLive } from "../../persistence/Layers/AuthPairingLinks.ts";
apps/server/src/auth/Layers/ServerAuth.test.ts+2-1
......@@ -1,6 +1,7 @@
11import * as NodeServices from "@effect/platform-node/NodeServices";
22import { expect, it } from "@effect/vitest";
3import { Effect, Layer } from "effect";
3import * as Effect from "effect/Effect";
4import * as Layer from "effect/Layer";
45
56import type { ServerConfigShape } from "../../config.ts";
67import { ServerConfig } from "../../config.ts";
apps/server/src/auth/Layers/ServerAuth.ts+4-1
......@@ -6,7 +6,10 @@ import {
66 type AuthSessionState,
77 type AuthWebSocketTokenResult,
88} from "@t3tools/contracts";
9import { DateTime, Effect, Layer, Option } from "effect";
9import * as DateTime from "effect/DateTime";
10import * as Effect from "effect/Effect";
11import * as Layer from "effect/Layer";
12import * as Option from "effect/Option";
1013import * as HttpServerRequest from "effect/unstable/http/HttpServerRequest";
1114
1215import { AuthControlPlane } from "../Services/AuthControlPlane.ts";
apps/server/src/auth/Layers/ServerAuthPolicy.test.ts+2-1
......@@ -1,6 +1,7 @@
11import * as NodeServices from "@effect/platform-node/NodeServices";
22import { expect, it } from "@effect/vitest";
3import { Effect, Layer } from "effect";
3import * as Effect from "effect/Effect";
4import * as Layer from "effect/Layer";
45
56import type { ServerConfigShape } from "../../config.ts";
67import { ServerConfig } from "../../config.ts";
apps/server/src/auth/Layers/ServerAuthPolicy.ts+2-1
......@@ -1,5 +1,6 @@
11import type { ServerAuthDescriptor } from "@t3tools/contracts";
2import { Effect, Layer } from "effect";
2import * as Effect from "effect/Effect";
3import * as Layer from "effect/Layer";
34
45import { ServerConfig } from "../../config.ts";
56import { ServerAuthPolicy, type ServerAuthPolicyShape } from "../Services/ServerAuthPolicy.ts";
apps/server/src/auth/Layers/ServerSecretStore.test.ts+6-1
......@@ -1,6 +1,11 @@
11import * as NodeServices from "@effect/platform-node/NodeServices";
22import { expect, it } from "@effect/vitest";
3import { Cause, Deferred, Effect, FileSystem, Layer, Ref } from "effect";
3import * as Cause from "effect/Cause";
4import * as Deferred from "effect/Deferred";
5import * as Effect from "effect/Effect";
6import * as FileSystem from "effect/FileSystem";
7import * as Layer from "effect/Layer";
8import * as Ref from "effect/Ref";
49import * as PlatformError from "effect/PlatformError";
510
611import { ServerConfig } from "../../config.ts";
apps/server/src/auth/Layers/ServerSecretStore.ts+5-1
......@@ -1,6 +1,10 @@
11import * as Crypto from "node:crypto";
22
3import { Effect, FileSystem, Layer, Path, Predicate } from "effect";
3import * as Effect from "effect/Effect";
4import * as FileSystem from "effect/FileSystem";
5import * as Layer from "effect/Layer";
6import * as Path from "effect/Path";
7import * as Predicate from "effect/Predicate";
48import * as PlatformError from "effect/PlatformError";
59
610import { ServerConfig } from "../../config.ts";
apps/server/src/auth/Layers/SessionCredentialService.test.ts+4-2
......@@ -1,7 +1,9 @@
11import * as NodeServices from "@effect/platform-node/NodeServices";
22import { expect, it } from "@effect/vitest";
3import { Duration, Effect, Layer } from "effect";
4import { TestClock } from "effect/testing";
3import * as Duration from "effect/Duration";
4import * as Effect from "effect/Effect";
5import * as Layer from "effect/Layer";
6import * as TestClock from "effect/testing/TestClock";
57
68import type { ServerConfigShape } from "../../config.ts";
79import { ServerConfig } from "../../config.ts";
apps/server/src/auth/Layers/SessionCredentialService.ts+33-5
......@@ -1,6 +1,14 @@
11import { AuthSessionId, type AuthClientMetadata, type AuthClientSession } from "@t3tools/contracts";
2import { Clock, DateTime, Duration, Effect, Layer, PubSub, Ref, Schema, Stream } from "effect";
3import { Option } from "effect";
2import * as Clock from "effect/Clock";
3import * as DateTime from "effect/DateTime";
4import * as Duration from "effect/Duration";
5import * as Effect from "effect/Effect";
6import * as Layer from "effect/Layer";
7import * as PubSub from "effect/PubSub";
8import * as Ref from "effect/Ref";
9import * as Schema from "effect/Schema";
10import * as Stream from "effect/Stream";
11import * as Option from "effect/Option";
412
513import { ServerConfig } from "../../config.ts";
614import { AuthSessionRepositoryLive } from "../../persistence/Layers/AuthSessions.ts";
......@@ -192,6 +200,7 @@ export const makeSessionCredentialService = Effect.gen(function* () {
192200 ),
193201 );
194202
203 const encodeClaims = Schema.encodeEffect(Schema.fromJsonString(SessionClaims));
195204 const issue: SessionCredentialServiceShape["issue"] = (input) =>
196205 Effect.gen(function* () {
197206 const sessionId = AuthSessionId.make(crypto.randomUUID());
......@@ -209,7 +218,13 @@ export const makeSessionCredentialService = Effect.gen(function* () {
209218 iat: issuedAt.epochMilliseconds,
210219 exp: expiresAt.epochMilliseconds,
211220 };
212 const encodedPayload = base64UrlEncode(JSON.stringify(claims));
221
222 const encodedPayload = yield* encodeClaims(claims).pipe(
223 Effect.map(base64UrlEncode),
224 Effect.mapError(
225 (cause) => new SessionCredentialError({ message: "Failed to encode claims", cause }),
226 ),
227 );
213228 const signature = signPayload(encodedPayload, signingSecret);
214229 const client = input?.client ?? createDefaultClientMetadata();
215230 yield* authSessions.create({
......@@ -297,12 +312,19 @@ export const makeSessionCredentialService = Effect.gen(function* () {
297312 });
298313 }
299314
315 const expiresAt = DateTime.make(claims.exp);
316 if (Option.isNone(expiresAt)) {
317 return yield* new SessionCredentialError({
318 message: "Invalid `exp` claim",
319 });
320 }
321
300322 return {
301323 sessionId: claims.sid,
302324 token,
303325 method: claims.method,
304326 client: toClientMetadata(row.value.client),
305 expiresAt: DateTime.makeUnsafe(claims.exp),
327 expiresAt: expiresAt.value,
306328 subject: claims.sub,
307329 role: claims.role,
308330 } satisfies VerifiedSession;
......@@ -317,6 +339,7 @@ export const makeSessionCredentialService = Effect.gen(function* () {
317339 ),
318340 );
319341
342 const encodeWsClaims = Schema.encodeEffect(Schema.fromJsonString(WebSocketClaims));
320343 const issueWebSocketToken: SessionCredentialServiceShape["issueWebSocketToken"] = (
321344 sessionId,
322345 input,
......@@ -333,7 +356,12 @@ export const makeSessionCredentialService = Effect.gen(function* () {
333356 iat: issuedAt.epochMilliseconds,
334357 exp: expiresAt.epochMilliseconds,
335358 };
336 const encodedPayload = base64UrlEncode(JSON.stringify(claims));
359 const encodedPayload = yield* encodeWsClaims(claims).pipe(
360 Effect.map(base64UrlEncode),
361 Effect.mapError(
362 (cause) => new SessionCredentialError({ message: "Failed to encode claims", cause }),
363 ),
364 );
337365 const signature = signPayload(encodedPayload, signingSecret);
338366 return {
339367 token: `${encodedPayload}.${signature}`,
apps/server/src/auth/Services/AuthControlPlane.ts+5-1
......@@ -4,7 +4,11 @@ import type {
44 AuthPairingLink,
55 AuthSessionId,
66} from "@t3tools/contracts";
7import { Data, DateTime, Duration, Effect, Context } from "effect";
7import * as Data from "effect/Data";
8import * as DateTime from "effect/DateTime";
9import * as Duration from "effect/Duration";
10import * as Effect from "effect/Effect";
11import * as Context from "effect/Context";
812import type { SessionRole } from "./SessionCredentialService.ts";
913
1014export const DEFAULT_SESSION_SUBJECT = "cli-issued-session";
apps/server/src/auth/Services/BootstrapCredentialService.ts+6-2
......@@ -1,6 +1,10 @@
11import type { AuthPairingLink, ServerAuthBootstrapMethod } from "@t3tools/contracts";
2import { Data, DateTime, Duration, Context } from "effect";
3import type { Effect, Stream } from "effect";
2import * as Data from "effect/Data";
3import * as DateTime from "effect/DateTime";
4import * as Duration from "effect/Duration";
5import * as Context from "effect/Context";
6import type * as Effect from "effect/Effect";
7import type * as Stream from "effect/Stream";
48
59export type BootstrapCredentialRole = "owner" | "client";
610
apps/server/src/auth/Services/ServerAuth.ts+4-2
......@@ -12,8 +12,10 @@ import type {
1212 ServerAuthSessionMethod,
1313 AuthWebSocketTokenResult,
1414} from "@t3tools/contracts";
15import { Data, DateTime, Context } from "effect";
16import type { Effect } from "effect";
15import * as Data from "effect/Data";
16import * as DateTime from "effect/DateTime";
17import * as Context from "effect/Context";
18import type * as Effect from "effect/Effect";
1719import type * as HttpServerRequest from "effect/unstable/http/HttpServerRequest";
1820import type { SessionRole } from "./SessionCredentialService.ts";
1921
apps/server/src/auth/Services/ServerAuthPolicy.ts+2-2
......@@ -1,6 +1,6 @@
11import type { ServerAuthDescriptor } from "@t3tools/contracts";
2import { Context } from "effect";
3import type { Effect } from "effect";
2import * as Context from "effect/Context";
3import type * as Effect from "effect/Effect";
44
55export interface ServerAuthPolicyShape {
66 readonly getDescriptor: () => Effect.Effect<ServerAuthDescriptor>;
apps/server/src/auth/Services/ServerSecretStore.ts+3-2
......@@ -1,5 +1,6 @@
1import { Data, Context } from "effect";
2import type { Effect } from "effect";
1import * as Data from "effect/Data";
2import * as Context from "effect/Context";
3import type * as Effect from "effect/Effect";
34
45export class SecretStoreError extends Data.TaggedError("SecretStoreError")<{
56 readonly message: string;
apps/server/src/auth/Services/SessionCredentialService.ts+6-2
......@@ -4,8 +4,12 @@ import type {
44 AuthSessionId,
55 ServerAuthSessionMethod,
66} from "@t3tools/contracts";
7import { Data, DateTime, Duration, Context } from "effect";
8import type { Effect, Stream } from "effect";
7import * as Data from "effect/Data";
8import * as DateTime from "effect/DateTime";
9import * as Duration from "effect/Duration";
10import * as Context from "effect/Context";
11import type * as Effect from "effect/Effect";
12import type * as Stream from "effect/Stream";
913
1014export type SessionRole = "owner" | "client";
1115
apps/server/src/auth/http.ts+3-1
......@@ -6,7 +6,9 @@ import {
66 AuthRevokePairingLinkInput,
77 type AuthWebSocketTokenResult,
88} from "@t3tools/contracts";
9import { DateTime, Effect, Schema } from "effect";
9import * as DateTime from "effect/DateTime";
10import * as Effect from "effect/Effect";
11import * as Schema from "effect/Schema";
1012import { HttpRouter, HttpServerRequest, HttpServerResponse } from "effect/unstable/http";
1113
1214import { AuthError, ServerAuth } from "./Services/ServerAuth.ts";
apps/server/src/bin.test.ts+7-2
......@@ -1,3 +1,4 @@
1// @effect-diagnostics-next-line nodeBuiltinImport:off - NodeHttpServer.layer takes `NodeHttp.createServer` as arg
12import * as NodeHttp from "node:http";
23import { mkdtempSync } from "node:fs";
34import { tmpdir } from "node:os";
......@@ -5,7 +6,7 @@ import { join } from "node:path";
56
67import * as NodeHttpServer from "@effect/platform-node/NodeHttpServer";
78import * as NodeServices from "@effect/platform-node/NodeServices";
8import { NetService } from "@t3tools/shared/Net";
9import * as NetService from "@t3tools/shared/Net";
910import { assert, it } from "@effect/vitest";
1011import * as Effect from "effect/Effect";
1112import * as Layer from "effect/Layer";
......@@ -138,7 +139,7 @@ const withLiveProjectCliServer = <A, E, R>(baseDir: string, run: () => Effect.Ef
138139 }
139140 yield* persistServerRuntimeState({
140141 path: config.serverRuntimeStatePath,
141 state: makePersistedServerRuntimeState({
142 state: yield* makePersistedServerRuntimeState({
142143 config,
143144 port: address.port,
144145 }),
......@@ -179,6 +180,7 @@ it.layer(NodeServices.layer)("bin cli parsing", (it) => {
179180 const createdOutput = yield* captureStdout(
180181 runCli(["auth", "pairing", "create", "--base-dir", baseDir, "--json"]),
181182 );
183 // @effect-diagnostics-next-line preferSchemaOverJson:off
182184 const created = JSON.parse(createdOutput.output) as {
183185 readonly id: string;
184186 readonly credential: string;
......@@ -186,6 +188,7 @@ it.layer(NodeServices.layer)("bin cli parsing", (it) => {
186188 const listedOutput = yield* captureStdout(
187189 runCli(["auth", "pairing", "list", "--base-dir", baseDir, "--json"]),
188190 );
191 // @effect-diagnostics-next-line preferSchemaOverJson:off
189192 const listed = JSON.parse(listedOutput.output) as ReadonlyArray<{
190193 readonly id: string;
191194 readonly credential?: string;
......@@ -207,6 +210,7 @@ it.layer(NodeServices.layer)("bin cli parsing", (it) => {
207210 const issuedOutput = yield* captureStdout(
208211 runCli(["auth", "session", "issue", "--base-dir", baseDir, "--json"]),
209212 );
213 // @effect-diagnostics-next-line preferSchemaOverJson:off
210214 const issued = JSON.parse(issuedOutput.output) as {
211215 readonly sessionId: string;
212216 readonly token: string;
......@@ -215,6 +219,7 @@ it.layer(NodeServices.layer)("bin cli parsing", (it) => {
215219 const listedOutput = yield* captureStdout(
216220 runCli(["auth", "session", "list", "--base-dir", baseDir, "--json"]),
217221 );
222 // @effect-diagnostics-next-line preferSchemaOverJson:off
218223 const listed = JSON.parse(listedOutput.output) as ReadonlyArray<{
219224 readonly sessionId: string;
220225 readonly token?: string;
apps/server/src/bin.ts+1-1
......@@ -4,7 +4,7 @@ import * as Effect from "effect/Effect";
44import * as Layer from "effect/Layer";
55import { Command } from "effect/unstable/cli";
66
7import { NetService } from "@t3tools/shared/Net";
7import * as NetService from "@t3tools/shared/Net";
88import packageJson from "../package.json" with { type: "json" };
99import { authCommand } from "./cli/auth.ts";
1010import { sharedServerCommandFlags } from "./cli/config.ts";
apps/server/src/bootstrap.test.ts+4-2
......@@ -1,13 +1,15 @@
1// @effect-diagnostics nodeBuiltinImport:off
12import * as NFS from "node:fs";
23import * as path from "node:path";
34import { execFileSync, spawn } from "node:child_process";
45import * as NodeServices from "@effect/platform-node/NodeServices";
56import { assert, it } from "@effect/vitest";
6import { FileSystem, Schema } from "effect";
7import * as FileSystem from "effect/FileSystem";
8import * as Schema from "effect/Schema";
79import * as Duration from "effect/Duration";
810import * as Effect from "effect/Effect";
911import * as Fiber from "effect/Fiber";
10import { TestClock } from "effect/testing";
12import * as TestClock from "effect/testing/TestClock";
1113import { vi } from "vitest";
1214
1315import { readBootstrapEnvelope, resolveFdPath } from "./bootstrap.ts";
apps/server/src/bootstrap.ts+7-1
......@@ -1,9 +1,15 @@
1// @effect-diagnostics nodeBuiltinImport:off
12import * as NFS from "node:fs";
23import * as Net from "node:net";
34import * as readline from "node:readline";
45import type { Readable } from "node:stream";
56
6import { Data, Effect, Option, Predicate, Result, Schema } from "effect";
7import * as Data from "effect/Data";
8import * as Effect from "effect/Effect";
9import * as Option from "effect/Option";
10import * as Predicate from "effect/Predicate";
11import * as Result from "effect/Result";
12import * as Schema from "effect/Schema";
713import { decodeJsonResult } from "@t3tools/shared/schemaJson";
814
915class BootstrapError extends Data.TaggedError("BootstrapError")<{
apps/server/src/checkpointing/Errors.ts+1-1
......@@ -1,4 +1,4 @@
1import { Schema } from "effect";
1import * as Schema from "effect/Schema";
22import type { ProjectionRepositoryError } from "../persistence/Errors.ts";
33import type { VcsError } from "@t3tools/contracts";
44
apps/server/src/checkpointing/Layers/CheckpointDiffQuery.test.ts+3-1
......@@ -1,5 +1,7 @@
11import { CheckpointRef, ProjectId, ThreadId, TurnId } from "@t3tools/contracts";
2import { Effect, Layer, Option } from "effect";
2import * as Effect from "effect/Effect";
3import * as Layer from "effect/Layer";
4import * as Option from "effect/Option";
35import { describe, expect, it } from "vitest";
46
57import {
apps/server/src/checkpointing/Layers/CheckpointDiffQuery.ts+4-1
......@@ -4,7 +4,10 @@ import {
44 type OrchestrationGetFullThreadDiffResult,
55 type OrchestrationGetTurnDiffResult as OrchestrationGetTurnDiffResultType,
66} from "@t3tools/contracts";
7import { Effect, Layer, Option, Schema } from "effect";
7import * as Effect from "effect/Effect";
8import * as Layer from "effect/Layer";
9import * as Option from "effect/Option";
10import * as Schema from "effect/Schema";
811
912import { ProjectionSnapshotQuery } from "../../orchestration/Services/ProjectionSnapshotQuery.ts";
1013import { CheckpointInvariantError, CheckpointUnavailableError } from "../Errors.ts";
apps/server/src/checkpointing/Layers/CheckpointStore.test.ts+6-1
......@@ -1,8 +1,13 @@
1// @effect-diagnostics nodeBuiltinImport:off
12import path from "node:path";
23
34import * as NodeServices from "@effect/platform-node/NodeServices";
45import { it } from "@effect/vitest";
5import { Effect, FileSystem, Layer, PlatformError, Scope } from "effect";
6import * as Effect from "effect/Effect";
7import * as FileSystem from "effect/FileSystem";
8import * as Layer from "effect/Layer";
9import * as PlatformError from "effect/PlatformError";
10import * as Scope from "effect/Scope";
611import { describe, expect } from "vitest";
712
813import { checkpointRefForThreadTurn } from "../Utils.ts";
apps/server/src/checkpointing/Layers/CheckpointStore.ts+4-1
......@@ -11,7 +11,10 @@
1111 */
1212import { randomUUID } from "node:crypto";
1313
14import { Effect, Layer, FileSystem, Path } from "effect";
14import * as Effect from "effect/Effect";
15import * as Layer from "effect/Layer";
16import * as FileSystem from "effect/FileSystem";
17import * as Path from "effect/Path";
1518
1619import { CheckpointInvariantError } from "../Errors.ts";
1720import { VcsProcessExitError } from "@t3tools/contracts";
apps/server/src/checkpointing/Services/CheckpointDiffQuery.ts+2-2
......@@ -12,8 +12,8 @@ import type {
1212 OrchestrationGetTurnDiffInput,
1313 OrchestrationGetTurnDiffResult,
1414} from "@t3tools/contracts";
15import { Context } from "effect";
16import type { Effect } from "effect";
15import * as Context from "effect/Context";
16import type * as Effect from "effect/Effect";
1717
1818import type { CheckpointServiceError } from "../Errors.ts";
1919
apps/server/src/checkpointing/Services/CheckpointStore.ts+2-2
......@@ -10,8 +10,8 @@
1010 *
1111 * @module CheckpointStore
1212 */
13import { Context } from "effect";
14import type { Effect } from "effect";
13import * as Context from "effect/Context";
14import type * as Effect from "effect/Effect";
1515
1616import type { CheckpointStoreError } from "../Errors.ts";
1717import { CheckpointRef } from "@t3tools/contracts";
apps/server/src/checkpointing/Utils.ts+1-1
......@@ -1,4 +1,4 @@
1import { Encoding } from "effect";
1import * as Encoding from "effect/Encoding";
22import { CheckpointRef, ProjectId, type ThreadId } from "@t3tools/contracts";
33
44export const CHECKPOINT_REFS_PREFIX = "refs/t3/checkpoints";
apps/server/src/cli/auth.ts+5-1
......@@ -1,5 +1,9 @@
11import { AuthSessionId } from "@t3tools/contracts";
2import { Console, Effect, Layer, Option, References } from "effect";
2import * as Console from "effect/Console";
3import * as Effect from "effect/Effect";
4import * as Layer from "effect/Layer";
5import * as Option from "effect/Option";
6import * as References from "effect/References";
37import { Argument, Command, Flag, GlobalFlag } from "effect/unstable/cli";
48
59import { AuthControlPlaneRuntimeLive } from "../auth/Layers/AuthControlPlane.ts";
apps/server/src/cli/config.test.ts+16-9
......@@ -1,13 +1,19 @@
1import os from "node:os";
1import NodeOS from "node:os";
22
33import { assert, expect, it } from "@effect/vitest";
4import { ConfigProvider, Effect, FileSystem, Layer, Option, Path, Schema } from "effect";
4import * as ConfigProvider from "effect/ConfigProvider";
5import * as Effect from "effect/Effect";
6import * as FileSystem from "effect/FileSystem";
7import * as Layer from "effect/Layer";
8import * as Option from "effect/Option";
9import * as Path from "effect/Path";
10import * as Schema from "effect/Schema";
511
612import {
713 DesktopBackendBootstrap,
814 type DesktopBackendBootstrap as DesktopBackendBootstrapValue,
915} from "@t3tools/contracts";
10import { NetService } from "@t3tools/shared/Net";
16import * as NetService from "@t3tools/shared/Net";
1117import * as NodeServices from "@effect/platform-node/NodeServices";
1218import { deriveServerPaths } from "../config.ts";
1319import { resolveServerConfig } from "./config.ts";
......@@ -53,7 +59,7 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => {
5359 it.effect("falls back to effect/config values when flags are omitted", () =>
5460 Effect.gen(function* () {
5561 const { join } = yield* Path.Path;
56 const baseDir = join(os.tmpdir(), "t3-cli-config-env-base");
62 const baseDir = join(NodeOS.tmpdir(), "t3-cli-config-env-base");
5763 const derivedPaths = yield* deriveServerPaths(baseDir, new URL("http://127.0.0.1:5173"));
5864 const resolved = yield* resolveServerConfig(
5965 {
......@@ -119,7 +125,7 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => {
119125 it.effect("uses CLI flags when provided", () =>
120126 Effect.gen(function* () {
121127 const { join } = yield* Path.Path;
122 const baseDir = join(os.tmpdir(), "t3-cli-config-flags-base");
128 const baseDir = join(NodeOS.tmpdir(), "t3-cli-config-flags-base");
123129 const derivedPaths = yield* deriveServerPaths(baseDir, new URL("http://127.0.0.1:4173"));
124130 const resolved = yield* resolveServerConfig(
125131 {
......@@ -147,7 +153,7 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => {
147153 T3CODE_MODE: "desktop",
148154 T3CODE_PORT: "4001",
149155 T3CODE_HOST: "0.0.0.0",
150 T3CODE_HOME: join(os.tmpdir(), "ignored-base"),
156 T3CODE_HOME: join(NodeOS.tmpdir(), "ignored-base"),
151157 VITE_DEV_SERVER_URL: "http://127.0.0.1:5173",
152158 T3CODE_NO_BROWSER: "false",
153159 T3CODE_AUTO_BOOTSTRAP_PROJECT_FROM_CWD: "false",
......@@ -185,7 +191,7 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => {
185191 it.effect("preserves explicit false CLI boolean flags over env and bootstrap values", () =>
186192 Effect.gen(function* () {
187193 const { join } = yield* Path.Path;
188 const baseDir = join(os.tmpdir(), "t3-cli-config-false-flags");
194 const baseDir = join(NodeOS.tmpdir(), "t3-cli-config-false-flags");
189195 const fd = yield* openBootstrapFd(
190196 makeDesktopBootstrap({
191197 noBrowser: true,
......@@ -378,7 +384,7 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => {
378384 it.effect("applies flag then env precedence over bootstrap envelope values", () =>
379385 Effect.gen(function* () {
380386 const { join } = yield* Path.Path;
381 const baseDir = join(os.tmpdir(), "t3-cli-config-env-wins");
387 const baseDir = join(NodeOS.tmpdir(), "t3-cli-config-env-wins");
382388 const fd = yield* openBootstrapFd(
383389 makeDesktopBootstrap({
384390 port: 4888,
......@@ -459,6 +465,7 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => {
459465 yield* fs.makeDirectory(path.dirname(derivedPaths.settingsPath), { recursive: true });
460466 yield* fs.writeFileString(
461467 derivedPaths.settingsPath,
468 // @effect-diagnostics-next-line preferSchemaOverJson:off
462469 `${JSON.stringify({
463470 observability: {
464471 otlpTracesUrl: "http://localhost:4318/v1/traces",
......@@ -521,7 +528,7 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => {
521528 it.effect("forces noBrowser and disables auto-bootstrap for headless startup presentation", () =>
522529 Effect.gen(function* () {
523530 const { join } = yield* Path.Path;
524 const baseDir = join(os.tmpdir(), "t3-cli-config-headless-base");
531 const baseDir = join(NodeOS.tmpdir(), "t3-cli-config-headless-base");
525532 const derivedPaths = yield* deriveServerPaths(baseDir, undefined);
526533
527534 const resolved = yield* resolveServerConfig(
apps/server/src/cli/config.ts+12-14
......@@ -1,18 +1,16 @@
1import { NetService } from "@t3tools/shared/Net";
1import * as NetService from "@t3tools/shared/Net";
22import { parsePersistedServerObservabilitySettings } from "@t3tools/shared/serverSettings";
33import { DesktopBackendBootstrap, PortSchema } from "@t3tools/contracts";
4import {
5 Config,
6 Duration,
7 Effect,
8 FileSystem,
9 LogLevel,
10 Option,
11 Path,
12 Schema,
13 SchemaIssue,
14 SchemaTransformation,
15} from "effect";
4import * as Config from "effect/Config";
5import * as Duration from "effect/Duration";
6import * as Effect from "effect/Effect";
7import * as FileSystem from "effect/FileSystem";
8import * as LogLevel from "effect/LogLevel";
9import * as Option from "effect/Option";
10import * as Path from "effect/Path";
11import * as Schema from "effect/Schema";
12import * as SchemaIssue from "effect/SchemaIssue";
13import * as SchemaTransformation from "effect/SchemaTransformation";
1614import { Argument, Flag } from "effect/unstable/cli";
1715
1816import { readBootstrapEnvelope } from "../bootstrap.ts";
......@@ -215,7 +213,7 @@ export const resolveServerConfig = (
215213 },
216214) =>
217215 Effect.gen(function* () {
218 const { findAvailablePort } = yield* NetService;
216 const { findAvailablePort } = yield* NetService.NetService;
219217 const path = yield* Path.Path;
220218 const fs = yield* FileSystem.FileSystem;
221219 const env = yield* EnvServerConfig;
apps/server/src/cli/project.ts+27-21
......@@ -4,18 +4,18 @@ import {
44 ProjectId,
55 type ClientOrchestrationCommand,
66} from "@t3tools/contracts";
7import {
8 Console,
9 Duration,
10 Effect,
11 Exit,
12 FileSystem,
13 Layer,
14 Option,
15 Path,
16 References,
17 Schema,
18} from "effect";
7import * as Console from "effect/Console";
8import * as Data from "effect/Data";
9import * as DateTime from "effect/DateTime";
10import * as Duration from "effect/Duration";
11import * as Effect from "effect/Effect";
12import * as Exit from "effect/Exit";
13import * as FileSystem from "effect/FileSystem";
14import * as Layer from "effect/Layer";
15import * as Option from "effect/Option";
16import * as Path from "effect/Path";
17import * as References from "effect/References";
18import * as Schema from "effect/Schema";
1919import { Argument, Command, Flag, GlobalFlag } from "effect/unstable/cli";
2020import {
2121 FetchHttpClient,
......@@ -54,6 +54,10 @@ type ProjectCliDispatchCommand = Extract<
5454 { type: "project.create" | "project.meta.update" | "project.delete" }
5555>;
5656
57class ProjectCommandError extends Data.TaggedError("ProjectCommandError")<{
58 readonly message: string;
59}> {}
60
5761const ProjectCliRuntimeLive = Layer.mergeAll(
5862 WorkspacePathsLive,
5963 OrchestrationLayerLive.pipe(
......@@ -124,7 +128,7 @@ const resolveProjectTitle = Effect.fn("resolveProjectTitle")(function* (
124128 if (trimmed.length > 0) {
125129 return trimmed;
126130 }
127 return yield* Effect.fail(new Error("Project title cannot be empty."));
131 return yield* new ProjectCommandError({ message: "Project title cannot be empty." });
128132 }
129133
130134 const path = yield* Path.Path;
......@@ -138,7 +142,7 @@ const findActiveProjectTarget = Effect.fn("findActiveProjectTarget")(function* (
138142}) {
139143 const trimmedIdentifier = input.identifier.trim();
140144 if (trimmedIdentifier.length === 0) {
141 return yield* Effect.fail(new Error("Project identifier cannot be empty."));
145 return yield* new ProjectCommandError({ message: "Project identifier cannot be empty." });
142146 }
143147
144148 const activeProjects = input.snapshot.projects.filter((project) => project.deletedAt === null);
......@@ -165,7 +169,9 @@ const findActiveProjectTarget = Effect.fn("findActiveProjectTarget")(function* (
165169
166170 const resolved = exactWorkspaceMatch;
167171 if (!resolved) {
168 return yield* Effect.fail(new Error(`No active project found for '${trimmedIdentifier}'.`));
172 return yield* new ProjectCommandError({
173 message: `No active project found for '${trimmedIdentifier}'.`,
174 });
169175 }
170176
171177 return {
......@@ -185,7 +191,7 @@ const fetchLiveOrchestrationSnapshot = (origin: string, bearerToken: string) =>
185191 "2xx": decodeOrchestrationReadModelResponse,
186192 orElse: (response) =>
187193 readErrorMessageFromResponse(response).pipe(
188 Effect.flatMap((message) => Effect.fail(new Error(message))),
194 Effect.flatMap((message) => Effect.fail(new ProjectCommandError({ message }))),
189195 ),
190196 }),
191197 );
......@@ -206,7 +212,7 @@ const dispatchLiveOrchestrationCommand = (
206212 "2xx": () => Effect.void,
207213 orElse: (response) =>
208214 readErrorMessageFromResponse(response).pipe(
209 Effect.flatMap((message) => Effect.fail(new Error(message))),
215 Effect.flatMap((message) => Effect.fail(new ProjectCommandError({ message }))),
210216 ),
211217 }),
212218 ),
......@@ -331,9 +337,9 @@ const projectAddCommand = Command.make("add", {
331337 (project) => project.deletedAt === null && project.workspaceRoot === workspaceRoot,
332338 );
333339 if (existingProject) {
334 return yield* Effect.fail(
335 new Error(`An active project already exists for '${workspaceRoot}'.`),
336 );
340 return yield* new ProjectCommandError({
341 message: `An active project already exists for '${workspaceRoot}'.`,
342 });
337343 }
338344
339345 const title = yield* resolveProjectTitle(workspaceRoot, Option.getOrUndefined(flags.title));
......@@ -345,7 +351,7 @@ const projectAddCommand = Command.make("add", {
345351 title,
346352 workspaceRoot,
347353 defaultModelSelection: getAutoBootstrapDefaultModelSelection(),
348 createdAt: new Date().toISOString(),
354 createdAt: DateTime.formatIso(yield* DateTime.now),
349355 });
350356 return `Added project ${projectId} (${title}) at ${workspaceRoot}.`;
351357 }),
apps/server/src/cli/server.ts+1-1
......@@ -1,4 +1,4 @@
1import { Effect } from "effect";
1import * as Effect from "effect/Effect";
22import { Command, GlobalFlag } from "effect/unstable/cli";
33
44import { ServerConfig, type StartupPresentation } from "../config.ts";
apps/server/src/cliAuthFormat.test.ts+8-8
......@@ -1,5 +1,5 @@
11import { expect, it } from "@effect/vitest";
2import { DateTime } from "effect";
2import * as DateTime from "effect/DateTime";
33
44import {
55 formatIssuedPairingCredential,
......@@ -15,8 +15,8 @@ it("formats issued pairing credentials with the secret and optional pair URL", (
1515 credential: "secret-pairing-token",
1616 role: "client",
1717 subject: "one-time-token",
18 createdAt: DateTime.fromDateUnsafe(new Date("2026-04-08T09:00:00.000Z")),
19 expiresAt: DateTime.fromDateUnsafe(new Date("2026-04-08T10:00:00.000Z")),
18 createdAt: DateTime.makeUnsafe("2026-04-08T09:00:00.000Z"),
19 expiresAt: DateTime.makeUnsafe("2026-04-08T10:00:00.000Z"),
2020 },
2121 { baseUrl: "https://example.com", json: false },
2222 );
......@@ -34,8 +34,8 @@ it("formats pairing listings without exposing the secret token", () => {
3434 subject: "one-time-token",
3535 label: "Phone",
3636 role: "client",
37 createdAt: DateTime.fromDateUnsafe(new Date("2026-04-08T09:00:00.000Z")),
38 expiresAt: DateTime.fromDateUnsafe(new Date("2026-04-08T10:00:00.000Z")),
37 createdAt: DateTime.makeUnsafe("2026-04-08T09:00:00.000Z"),
38 expiresAt: DateTime.makeUnsafe("2026-04-08T10:00:00.000Z"),
3939 },
4040 ],
4141 { json: false },
......@@ -57,7 +57,7 @@ it("formats issued sessions with the bearer token but omits tokens from listings
5757 label: "deploy-bot",
5858 deviceType: "bot",
5959 },
60 expiresAt: DateTime.fromDateUnsafe(new Date("2026-04-08T10:00:00.000Z")),
60 expiresAt: DateTime.makeUnsafe("2026-04-08T10:00:00.000Z"),
6161 },
6262 { json: false },
6363 );
......@@ -75,8 +75,8 @@ it("formats issued sessions with the bearer token but omits tokens from listings
7575 },
7676 connected: false,
7777 current: false,
78 issuedAt: DateTime.fromDateUnsafe(new Date("2026-04-08T09:00:00.000Z")),
79 expiresAt: DateTime.fromDateUnsafe(new Date("2026-04-08T10:00:00.000Z")),
78 issuedAt: DateTime.makeUnsafe("2026-04-08T09:00:00.000Z"),
79 expiresAt: DateTime.makeUnsafe("2026-04-08T10:00:00.000Z"),
8080 lastConnectedAt: null,
8181 },
8282 ],
apps/server/src/cliAuthFormat.ts+1-1
......@@ -1,5 +1,5 @@
11import type { AuthClientMetadata, AuthClientSession, AuthPairingLink } from "@t3tools/contracts";
2import { DateTime } from "effect";
2import * as DateTime from "effect/DateTime";
33
44import type { IssuedBearerSession, IssuedPairingLink } from "./auth/Services/AuthControlPlane.ts";
55
apps/server/src/config.ts+7-1
......@@ -6,7 +6,13 @@
66 *
77 * @module ServerConfig
88 */
9import { Effect, FileSystem, Layer, LogLevel, Path, Schema, Context } from "effect";
9import * as Effect from "effect/Effect";
10import * as FileSystem from "effect/FileSystem";
11import * as Layer from "effect/Layer";
12import * as LogLevel from "effect/LogLevel";
13import * as Path from "effect/Path";
14import * as Schema from "effect/Schema";
15import * as Context from "effect/Context";
1016
1117export const DEFAULT_PORT = 3773;
1218
apps/server/src/diagnostics/TraceDiagnostics.ts+5-14
......@@ -89,24 +89,14 @@ function toNumberValue(value: unknown): number | null {
8989function unixNanoToDateTime(value: unknown): DateTime.Utc | null {
9090 const text = toStringValue(value);
9191 if (!text) return null;
92
9392 try {
9493 const millis = Number(BigInt(text) / 1_000_000n);
95 if (!Number.isFinite(millis)) return null;
96 return DateTime.makeUnsafe(millis);
94 return Option.getOrNull(DateTime.make(millis));
9795 } catch {
9896 return null;
9997 }
10098}
10199
102function isAfter(left: DateTime.Utc, right: DateTime.Utc): boolean {
103 return DateTime.toEpochMillis(left) > DateTime.toEpochMillis(right);
104}
105
106function isBefore(left: DateTime.Utc, right: DateTime.Utc): boolean {
107 return DateTime.toEpochMillis(left) < DateTime.toEpochMillis(right);
108}
109
110100function readExitTag(exit: unknown): string | null {
111101 if (!isRecordObject(exit) || !("_tag" in exit)) return null;
112102 return toStringValue(exit._tag);
......@@ -254,10 +244,11 @@ export function aggregateTraceDiagnostics(
254244
255245 recordCount += 1;
256246 firstSpanAt =
257 startedAt && (firstSpanAt === null || isBefore(startedAt, firstSpanAt))
247 startedAt && (firstSpanAt === null || DateTime.isLessThan(startedAt, firstSpanAt))
258248 ? startedAt
259249 : firstSpanAt;
260 lastSpanAt = lastSpanAt === null || isAfter(endedAt, lastSpanAt) ? endedAt : lastSpanAt;
250 lastSpanAt =
251 lastSpanAt === null || DateTime.isGreaterThan(endedAt, lastSpanAt) ? endedAt : lastSpanAt;
261252
262253 const exitTag = readExitTag(parsed.exit);
263254 const isFailure = exitTag === "Failure";
......@@ -289,7 +280,7 @@ export function aggregateTraceDiagnostics(
289280
290281 const failureKey = `${name}\0${cause}`;
291282 const existing = failuresByKey.get(failureKey);
292 const isLatestFailure = !existing || isAfter(endedAt, existing.lastSeenAt);
283 const isLatestFailure = !existing || DateTime.isGreaterThan(endedAt, existing.lastSeenAt);
293284 failuresByKey.set(failureKey, {
294285 name,
295286 cause,
apps/server/src/environment/Layers/ServerEnvironment.test.ts+6-1
......@@ -1,7 +1,12 @@
1// @effect-diagnostics nodeBuiltinImport:off
12import * as nodePath from "node:path";
23import * as NodeServices from "@effect/platform-node/NodeServices";
34import { expect, it } from "@effect/vitest";
4import { Effect, Exit, FileSystem, Layer, PlatformError } from "effect";
5import * as Effect from "effect/Effect";
6import * as Exit from "effect/Exit";
7import * as FileSystem from "effect/FileSystem";
8import * as Layer from "effect/Layer";
9import * as PlatformError from "effect/PlatformError";
510
611import { deriveServerPaths, ServerConfig, type ServerConfigShape } from "../../config.ts";
712import { ServerEnvironment } from "../Services/ServerEnvironment.ts";
apps/server/src/environment/Layers/ServerEnvironment.ts+5-1
......@@ -1,5 +1,9 @@
11import { EnvironmentId, type ExecutionEnvironmentDescriptor } from "@t3tools/contracts";
2import { Effect, FileSystem, Layer, Path, Random } from "effect";
2import * as Effect from "effect/Effect";
3import * as FileSystem from "effect/FileSystem";
4import * as Layer from "effect/Layer";
5import * as Path from "effect/Path";
6import * as Random from "effect/Random";
37
48import { ServerConfig } from "../../config.ts";
59import { ServerEnvironment, type ServerEnvironmentShape } from "../Services/ServerEnvironment.ts";
apps/server/src/environment/Layers/ServerEnvironmentLabel.test.ts+2-1
......@@ -1,5 +1,6 @@
11import { afterEach, describe, expect, it } from "@effect/vitest";
2import { Effect, FileSystem } from "effect";
2import * as Effect from "effect/Effect";
3import * as FileSystem from "effect/FileSystem";
34import { vi } from "vitest";
45
56vi.mock("../../processRunner.ts", () => ({
apps/server/src/environment/Layers/ServerEnvironmentLabel.ts+2-1
......@@ -1,6 +1,7 @@
11import * as OS from "node:os";
22
3import { Effect, FileSystem } from "effect";
3import * as Effect from "effect/Effect";
4import * as FileSystem from "effect/FileSystem";
45
56import { runProcess } from "../../processRunner.ts";
67
apps/server/src/environment/Services/ServerEnvironment.ts+2-2
......@@ -1,6 +1,6 @@
11import type { EnvironmentId, ExecutionEnvironmentDescriptor } from "@t3tools/contracts";
2import { Context } from "effect";
3import type { Effect } from "effect";
2import * as Context from "effect/Context";
3import type * as Effect from "effect/Effect";
44
55export interface ServerEnvironmentShape {
66 readonly getEnvironmentId: Effect.Effect<EnvironmentId>;
apps/server/src/git/GitManager.test.ts+58-4
......@@ -1,10 +1,15 @@
1// @effect-diagnostics nodeBuiltinImport:off
12import fs from "node:fs";
23import path from "node:path";
34import { spawnSync } from "node:child_process";
45
56import * as NodeServices from "@effect/platform-node/NodeServices";
67import { it } from "@effect/vitest";
7import { Effect, FileSystem, Layer, PlatformError, Scope } from "effect";
8import * as Effect from "effect/Effect";
9import * as FileSystem from "effect/FileSystem";
10import * as Layer from "effect/Layer";
11import * as PlatformError from "effect/PlatformError";
12import * as Scope from "effect/Scope";
813import { ChildProcessSpawner } from "effect/unstable/process";
914import { expect } from "vitest";
1015import type {
......@@ -32,6 +37,7 @@ import { ServerConfig } from "../config.ts";
3237import { ServerSettingsService } from "../serverSettings.ts";
3338import {
3439 ProjectSetupScriptRunner,
40 ProjectSetupScriptRunnerError,
3541 type ProjectSetupScriptRunnerInput,
3642 type ProjectSetupScriptRunnerShape,
3743} from "../project/Services/ProjectSetupScriptRunner.ts";
......@@ -713,6 +719,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
713719 const { manager } = yield* makeManager({
714720 ghScenario: {
715721 prListSequence: [
722 // @effect-diagnostics-next-line preferSchemaOverJson:off
716723 JSON.stringify([
717724 {
718725 number: 13,
......@@ -754,6 +761,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
754761 const { manager } = yield* makeManager({
755762 ghScenario: {
756763 prListSequence: [
764 // @effect-diagnostics-next-line preferSchemaOverJson:off
757765 JSON.stringify([
758766 {
759767 number: 14,
......@@ -792,6 +800,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
792800 const { manager } = yield* makeManager({
793801 ghScenario: {
794802 prListSequence: [
803 // @effect-diagnostics-next-line preferSchemaOverJson:off
795804 JSON.stringify([
796805 {
797806 number: 0,
......@@ -843,6 +852,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
843852 const { manager } = yield* makeManager({
844853 ghScenario: {
845854 prListSequence: [
855 // @effect-diagnostics-next-line preferSchemaOverJson:off
846856 JSON.stringify([
847857 {
848858 number: 16,
......@@ -955,6 +965,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
955965 };
956966 const { manager, ghCalls } = yield* makeManager({
957967 ghScenario: {
968 // @effect-diagnostics-next-line preferSchemaOverJson:off
958969 prListSequence: [JSON.stringify([existingPr]), JSON.stringify([existingPr])],
959970 },
960971 });
......@@ -981,6 +992,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
981992 const { manager } = yield* makeManager({
982993 ghScenario: {
983994 prListSequence: [
995 // @effect-diagnostics-next-line preferSchemaOverJson:off
984996 JSON.stringify([
985997 {
986998 number: 1661,
......@@ -1034,8 +1046,11 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
10341046 const { manager, ghCalls } = yield* makeManager({
10351047 ghScenario: {
10361048 prListSequence: [
1049 // @effect-diagnostics-next-line preferSchemaOverJson:off
10371050 JSON.stringify([]),
1051 // @effect-diagnostics-next-line preferSchemaOverJson:off
10381052 JSON.stringify([]),
1053 // @effect-diagnostics-next-line preferSchemaOverJson:off
10391054 JSON.stringify([
10401055 {
10411056 number: 488,
......@@ -1110,6 +1125,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
11101125 const { manager, ghCalls } = yield* makeManager({
11111126 ghScenario: {
11121127 prListByHeadSelector: {
1128 // @effect-diagnostics-next-line preferSchemaOverJson:off
11131129 "effect-atom": JSON.stringify([
11141130 {
11151131 number: 1618,
......@@ -1121,6 +1137,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
11211137 updatedAt: "2026-03-01T10:00:00Z",
11221138 },
11231139 ]),
1140 // @effect-diagnostics-next-line preferSchemaOverJson:off
11241141 "upstream/effect-atom": JSON.stringify([
11251142 {
11261143 number: 1518,
......@@ -1132,8 +1149,11 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
11321149 updatedAt: "2026-04-01T10:00:00Z",
11331150 },
11341151 ]),
1152 // @effect-diagnostics-next-line preferSchemaOverJson:off
11351153 "pingdotgg:effect-atom": JSON.stringify([]),
1154 // @effect-diagnostics-next-line preferSchemaOverJson:off
11361155 "my-org/upstream:effect-atom": JSON.stringify([]),
1156 // @effect-diagnostics-next-line preferSchemaOverJson:off
11371157 "pingdotgg:upstream/effect-atom": JSON.stringify([
11381158 {
11391159 number: 1518,
......@@ -1145,6 +1165,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
11451165 updatedAt: "2026-04-01T10:00:00Z",
11461166 },
11471167 ]),
1168 // @effect-diagnostics-next-line preferSchemaOverJson:off
11481169 "my-org/upstream:upstream/effect-atom": JSON.stringify([
11491170 {
11501171 number: 1518,
......@@ -1194,6 +1215,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
11941215 const { manager } = yield* makeManager({
11951216 ghScenario: {
11961217 prListSequence: [
1218 // @effect-diagnostics-next-line preferSchemaOverJson:off
11971219 JSON.stringify([
11981220 {
11991221 number: 22,
......@@ -1231,6 +1253,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
12311253 const { manager } = yield* makeManager({
12321254 ghScenario: {
12331255 prListSequence: [
1256 // @effect-diagnostics-next-line preferSchemaOverJson:off
12341257 JSON.stringify([
12351258 {
12361259 number: 23,
......@@ -1262,6 +1285,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
12621285 const { manager } = yield* makeManager({
12631286 ghScenario: {
12641287 prListSequence: [
1288 // @effect-diagnostics-next-line preferSchemaOverJson:off
12651289 JSON.stringify([
12661290 {
12671291 number: 45,
......@@ -1612,6 +1636,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
16121636 ghScenario: {
16131637 prListSequence: [
16141638 "[]",
1639 // @effect-diagnostics-next-line preferSchemaOverJson:off
16151640 JSON.stringify([
16161641 {
16171642 number: 77,
......@@ -1747,6 +1772,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
17471772 ghScenario: {
17481773 prListSequence: [
17491774 "[]",
1775 // @effect-diagnostics-next-line preferSchemaOverJson:off
17501776 JSON.stringify([
17511777 {
17521778 number: 303,
......@@ -1793,6 +1819,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
17931819 ghScenario: {
17941820 prListSequence: [
17951821 "[]",
1822 // @effect-diagnostics-next-line preferSchemaOverJson:off
17961823 JSON.stringify([
17971824 {
17981825 number: 404,
......@@ -1833,6 +1860,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
18331860 const { manager, ghCalls } = yield* makeManager({
18341861 ghScenario: {
18351862 prListSequence: [
1863 // @effect-diagnostics-next-line preferSchemaOverJson:off
18361864 JSON.stringify([
18371865 {
18381866 number: 42,
......@@ -1886,7 +1914,9 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
18861914 const { manager, ghCalls } = yield* makeManager({
18871915 ghScenario: {
18881916 prListSequence: [
1917 // @effect-diagnostics-next-line preferSchemaOverJson:off
18891918 JSON.stringify([]),
1919 // @effect-diagnostics-next-line preferSchemaOverJson:off
18901920 JSON.stringify([
18911921 {
18921922 number: 142,
......@@ -1963,6 +1993,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
19631993 const { manager, ghCalls } = yield* makeManager({
19641994 ghScenario: {
19651995 prListByHeadSelector: {
1996 // @effect-diagnostics-next-line preferSchemaOverJson:off
19661997 "effect-atom": JSON.stringify([
19671998 {
19681999 number: 1618,
......@@ -1972,6 +2003,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
19722003 headRefName: "effect-atom",
19732004 },
19742005 ]),
2006 // @effect-diagnostics-next-line preferSchemaOverJson:off
19752007 "upstream/effect-atom": JSON.stringify([
19762008 {
19772009 number: 1518,
......@@ -1981,8 +2013,11 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
19812013 headRefName: "upstream/effect-atom",
19822014 },
19832015 ]),
2016 // @effect-diagnostics-next-line preferSchemaOverJson:off
19842017 "pingdotgg:effect-atom": JSON.stringify([]),
2018 // @effect-diagnostics-next-line preferSchemaOverJson:off
19852019 "my-org/upstream:effect-atom": JSON.stringify([]),
2020 // @effect-diagnostics-next-line preferSchemaOverJson:off
19862021 "pingdotgg:upstream/effect-atom": JSON.stringify([
19872022 {
19882023 number: 1518,
......@@ -1992,6 +2027,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
19922027 headRefName: "upstream/effect-atom",
19932028 },
19942029 ]),
2030 // @effect-diagnostics-next-line preferSchemaOverJson:off
19952031 "my-org/upstream:upstream/effect-atom": JSON.stringify([
19962032 {
19972033 number: 1518,
......@@ -2041,7 +2077,9 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
20412077 const { manager, ghCalls } = yield* makeManager({
20422078 ghScenario: {
20432079 prListByHeadSelector: {
2080 // @effect-diagnostics-next-line preferSchemaOverJson:off
20442081 "t3code/pr-142/statemachine": JSON.stringify([]),
2082 // @effect-diagnostics-next-line preferSchemaOverJson:off
20452083 statemachine: JSON.stringify([
20462084 {
20472085 number: 41,
......@@ -2051,6 +2089,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
20512089 headRefName: "statemachine",
20522090 },
20532091 ]),
2092 // @effect-diagnostics-next-line preferSchemaOverJson:off
20542093 "octocat:statemachine": JSON.stringify([
20552094 {
20562095 number: 142,
......@@ -2068,6 +2107,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
20682107 },
20692108 },
20702109 ]),
2110 // @effect-diagnostics-next-line preferSchemaOverJson:off
20712111 "fork-seed:statemachine": JSON.stringify([]),
20722112 },
20732113 },
......@@ -2112,6 +2152,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
21122152 const { manager, ghCalls } = yield* makeManager({
21132153 ghScenario: {
21142154 prListByHeadSelector: {
2155 // @effect-diagnostics-next-line preferSchemaOverJson:off
21152156 "octocat:statemachine": JSON.stringify([
21162157 {
21172158 number: 142,
......@@ -2129,8 +2170,11 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
21292170 },
21302171 },
21312172 ]),
2173 // @effect-diagnostics-next-line preferSchemaOverJson:off
21322174 "fork-seed:statemachine": JSON.stringify([]),
2175 // @effect-diagnostics-next-line preferSchemaOverJson:off
21332176 "t3code/pr-142/statemachine": JSON.stringify([]),
2177 // @effect-diagnostics-next-line preferSchemaOverJson:off
21342178 statemachine: JSON.stringify([]),
21352179 },
21362180 },
......@@ -2170,6 +2214,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
21702214 ghScenario: {
21712215 prListSequence: [
21722216 "[]",
2217 // @effect-diagnostics-next-line preferSchemaOverJson:off
21732218 JSON.stringify([
21742219 {
21752220 number: 88,
......@@ -2215,6 +2260,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
22152260 const { manager, ghCalls } = yield* makeManager({
22162261 ghScenario: {
22172262 prListSequence: [
2263 // @effect-diagnostics-next-line preferSchemaOverJson:off
22182264 JSON.stringify([
22192265 {
22202266 number: 1661,
......@@ -2232,6 +2278,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
22322278 },
22332279 },
22342280 ]),
2281 // @effect-diagnostics-next-line preferSchemaOverJson:off
22352282 JSON.stringify([
22362283 {
22372284 number: 188,
......@@ -2294,7 +2341,9 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
22942341 ghScenario: {
22952342 prListSequenceByHeadSelector: {
22962343 "octocat:statemachine": [
2344 // @effect-diagnostics-next-line preferSchemaOverJson:off
22972345 JSON.stringify([]),
2346 // @effect-diagnostics-next-line preferSchemaOverJson:off
22982347 JSON.stringify([
22992348 {
23002349 number: 188,
......@@ -2313,7 +2362,9 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
23132362 },
23142363 ]),
23152364 ],
2365 // @effect-diagnostics-next-line preferSchemaOverJson:off
23162366 "fork-seed:statemachine": [JSON.stringify([])],
2367 // @effect-diagnostics-next-line preferSchemaOverJson:off
23172368 statemachine: [JSON.stringify([])],
23182369 },
23192370 },
......@@ -2870,7 +2921,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
28702921 yield* runGit(repoDir, ["add", "existing.txt"]);
28712922 yield* runGit(repoDir, ["commit", "-m", "Existing worktree branch"]);
28722923 yield* runGit(repoDir, ["checkout", "main"]);
2873 const worktreePath = path.join(repoDir, "..", `pr-existing-${Date.now()}`);
2924 const worktreePath = path.join(repoDir, "..", `pr-existing-${path.basename(repoDir)}`);
28742925 yield* runGit(repoDir, ["worktree", "add", worktreePath, "feature/pr-existing-worktree"]);
28752926
28762927 const setupCalls: ProjectSetupScriptRunnerInput[] = [];
......@@ -3044,7 +3095,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
30443095 yield* runGit(repoDir, ["commit", "-m", "Reused fork PR branch"]);
30453096 yield* runGit(repoDir, ["push", "-u", "fork-seed", "feature/pr-reused-fork"]);
30463097 yield* runGit(repoDir, ["checkout", "main"]);
3047 const worktreePath = path.join(repoDir, "..", `pr-reused-fork-${Date.now()}`);
3098 const worktreePath = path.join(repoDir, "..", `pr-reused-fork-${path.basename(repoDir)}`);
30483099 yield* runGit(repoDir, ["worktree", "add", worktreePath, "feature/pr-reused-fork"]);
30493100 yield* runGit(worktreePath, ["branch", "--unset-upstream"], true);
30503101
......@@ -3112,7 +3163,8 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
31123163 },
31133164 },
31143165 setupScriptRunner: {
3115 runForThread: () => Effect.fail(new Error("terminal start failed")),
3166 runForThread: () =>
3167 Effect.fail(new ProjectSetupScriptRunnerError({ message: "terminal start failed" })),
31163168 },
31173169 });
31183170
......@@ -3289,7 +3341,9 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
32893341 const { manager } = yield* makeManager({
32903342 ghScenario: {
32913343 prListSequence: [
3344 // @effect-diagnostics-next-line preferSchemaOverJson:off
32923345 JSON.stringify([]),
3346 // @effect-diagnostics-next-line preferSchemaOverJson:off
32933347 JSON.stringify([
32943348 {
32953349 number: 201,
apps/server/src/git/GitManager.ts+13-15
......@@ -1,20 +1,18 @@
11import { randomUUID } from "node:crypto";
22
3import {
4 Array as Arr,
5 Cache,
6 Context,
7 DateTime,
8 Duration,
9 Effect,
10 Exit,
11 FileSystem,
12 Layer,
13 Option,
14 Order,
15 Path,
16 Ref,
17} from "effect";
3import * as Arr from "effect/Array";
4import * as Cache from "effect/Cache";
5import * as Context from "effect/Context";
6import * as DateTime from "effect/DateTime";
7import * as Duration from "effect/Duration";
8import * as Effect from "effect/Effect";
9import * as Exit from "effect/Exit";
10import * as FileSystem from "effect/FileSystem";
11import * as Layer from "effect/Layer";
12import * as Option from "effect/Option";
13import * as Order from "effect/Order";
14import * as Path from "effect/Path";
15import * as Ref from "effect/Ref";
1816import {
1917 GitActionProgressEvent,
2018 GitActionProgressPhase,
apps/server/src/git/GitWorkflowService.test.ts+2-1
......@@ -1,5 +1,6 @@
11import { assert, describe, it, vi } from "@effect/vitest";
2import { Effect, Layer } from "effect";
2import * as Effect from "effect/Effect";
3import * as Layer from "effect/Layer";
34
45import * as GitManager from "./GitManager.ts";
56import * as GitWorkflowService from "./GitWorkflowService.ts";
apps/server/src/git/GitWorkflowService.ts+3-1
......@@ -1,4 +1,6 @@
1import { Context, Effect, Layer } from "effect";
1import * as Context from "effect/Context";
2import * as Effect from "effect/Effect";
3import * as Layer from "effect/Layer";
24
35import {
46 GitManagerError,
apps/server/src/git/Utils.ts+1
......@@ -1,3 +1,4 @@
1// @effect-diagnostics nodeBuiltinImport:off
12import { existsSync } from "node:fs";
23import { join } from "node:path";
34
apps/server/src/http.ts+5-1
......@@ -1,6 +1,10 @@
11import Mime from "@effect/platform-node/Mime";
22import { decodeOtlpTraceRecords } from "@t3tools/shared/observability";
3import { Data, Effect, FileSystem, Option, Path } from "effect";
3import * as Data from "effect/Data";
4import * as Effect from "effect/Effect";
5import * as FileSystem from "effect/FileSystem";
6import * as Option from "effect/Option";
7import * as Path from "effect/Path";
48import { cast } from "effect/Function";
59import {
610 HttpBody,
apps/server/src/keybindings.test.ts+8-1
......@@ -2,7 +2,13 @@ import { KeybindingCommand, KeybindingRule, KeybindingsConfig } from "@t3tools/c
22import * as NodeServices from "@effect/platform-node/NodeServices";
33import { assert, it } from "@effect/vitest";
44import { assertFailure } from "@effect/vitest/utils";
5import { Cause, Effect, FileSystem, Layer, Logger, Path, Schema } from "effect";
5import * as Cause from "effect/Cause";
6import * as Effect from "effect/Effect";
7import * as FileSystem from "effect/FileSystem";
8import * as Layer from "effect/Layer";
9import * as Logger from "effect/Logger";
10import * as Path from "effect/Path";
11import * as Schema from "effect/Schema";
612import { ServerConfig } from "./config.ts";
713
814import {
......@@ -229,6 +235,7 @@ it.layer(NodeServices.layer)("keybindings", (it) => {
229235 const { keybindingsConfigPath } = yield* ServerConfig;
230236 yield* fs.writeFileString(
231237 keybindingsConfigPath,
238 // @effect-diagnostics-next-line preferSchemaOverJson:off
232239 JSON.stringify([
233240 { key: "mod+j", command: "terminal.toggle" },
234241 { key: "mod+shift+d+o", command: "terminal.new" },
apps/server/src/keybindings.ts+22-34
......@@ -19,33 +19,30 @@ import {
1919 type ServerUpsertKeybindingInput,
2020 type ServerConfigIssue,
2121} from "@t3tools/contracts";
22import {
23 Array,
24 Cache,
25 Cause,
26 Deferred,
27 Duration,
28 Effect,
29 Exit,
30 FileSystem,
31 Path,
32 Layer,
33 Option,
34 Predicate,
35 PubSub,
36 Schema,
37 SchemaGetter,
38 SchemaIssue,
39 SchemaTransformation,
40 Ref,
41 Context,
42 Scope,
43 Stream,
44} from "effect";
22import * as Array from "effect/Array";
23import * as Cache from "effect/Cache";
24import * as Cause from "effect/Cause";
25import * as Deferred from "effect/Deferred";
26import * as Duration from "effect/Duration";
27import * as Effect from "effect/Effect";
28import * as Exit from "effect/Exit";
29import * as FileSystem from "effect/FileSystem";
30import * as Path from "effect/Path";
31import * as Layer from "effect/Layer";
32import * as Option from "effect/Option";
33import * as Predicate from "effect/Predicate";
34import * as PubSub from "effect/PubSub";
35import * as Schema from "effect/Schema";
36import * as SchemaIssue from "effect/SchemaIssue";
37import * as SchemaTransformation from "effect/SchemaTransformation";
38import * as Ref from "effect/Ref";
39import * as Context from "effect/Context";
40import * as Scope from "effect/Scope";
41import * as Stream from "effect/Stream";
4542import * as Semaphore from "effect/Semaphore";
4643import { ServerConfig } from "./config.ts";
4744import { writeFileStringAtomically } from "./atomicWrite.ts";
48import { fromLenientJson } from "@t3tools/shared/schemaJson";
45import { fromJsonStringPretty, fromLenientJson } from "@t3tools/shared/schemaJson";
4946import {
5047 DEFAULT_KEYBINDINGS,
5148 DEFAULT_RESOLVED_KEYBINDINGS,
......@@ -172,16 +169,7 @@ function encodeWhenAst(node: KeybindingWhenNode): string {
172169}
173170
174171const RawKeybindingsEntries = fromLenientJson(Schema.Array(Schema.Unknown));
175const KeybindingsConfigJson = Schema.fromJsonString(KeybindingsConfig);
176const PrettyJsonString = SchemaGetter.parseJson<string>().compose(
177 SchemaGetter.stringifyJson({ space: 2 }),
178);
179const KeybindingsConfigPrettyJson = KeybindingsConfigJson.pipe(
180 Schema.encode({
181 decode: PrettyJsonString,
182 encode: PrettyJsonString,
183 }),
184);
172const KeybindingsConfigPrettyJson = fromJsonStringPretty(KeybindingsConfig);
185173
186174export interface KeybindingsConfigState {
187175 readonly keybindings: ResolvedKeybindingsConfig;
apps/server/src/observability/Attributes.ts+2-1
......@@ -1,4 +1,5 @@
1import { Cause, Exit } from "effect";
1import * as Cause from "effect/Cause";
2import * as Exit from "effect/Exit";
23
34export type MetricAttributeValue = string;
45export type MetricAttributes = Readonly<Record<string, MetricAttributeValue>>;
apps/server/src/observability/Layers/Observability.ts+4-1
......@@ -1,5 +1,8 @@
11import { makeLocalFileTracer, makeTraceSink } from "@t3tools/shared/observability";
2import { Effect, Layer, References, Tracer } from "effect";
2import * as Effect from "effect/Effect";
3import * as Layer from "effect/Layer";
4import * as References from "effect/References";
5import * as Tracer from "effect/Tracer";
36import { OtlpMetrics, OtlpSerialization, OtlpTracer } from "effect/unstable/observability";
47
58import { ServerConfig } from "../../config.ts";
apps/server/src/observability/Metrics.test.ts+5-2
......@@ -1,7 +1,10 @@
11import { assert, describe, it } from "@effect/vitest";
22import { ProviderDriverKind } from "@t3tools/contracts";
3import { Duration, Effect, Fiber, Metric } from "effect";
4import { TestClock } from "effect/testing";
3import * as Duration from "effect/Duration";
4import * as Effect from "effect/Effect";
5import * as Fiber from "effect/Fiber";
6import * as Metric from "effect/Metric";
7import * as TestClock from "effect/testing/TestClock";
58
69import { withMetrics } from "./Metrics.ts";
710
apps/server/src/observability/Metrics.ts+5-1
......@@ -1,4 +1,8 @@
1import { Clock, Duration, Effect, Exit, Metric } from "effect";
1import * as Clock from "effect/Clock";
2import * as Duration from "effect/Duration";
3import * as Effect from "effect/Effect";
4import * as Exit from "effect/Exit";
5import * as Metric from "effect/Metric";
26import { dual } from "effect/Function";
37
48import {
apps/server/src/observability/RpcInstrumentation.test.ts+8-2
......@@ -1,7 +1,13 @@
11import { assert, describe, it } from "@effect/vitest";
22import { WS_METHODS } from "@t3tools/contracts";
3import { Duration, Effect, Exit, Fiber, Metric, Stream, Tracer } from "effect";
4import { TestClock } from "effect/testing";
3import * as Duration from "effect/Duration";
4import * as Effect from "effect/Effect";
5import * as Exit from "effect/Exit";
6import * as Fiber from "effect/Fiber";
7import * as Metric from "effect/Metric";
8import * as Stream from "effect/Stream";
9import * as Tracer from "effect/Tracer";
10import * as TestClock from "effect/testing/TestClock";
511
612import {
713 observeRpcEffect,
apps/server/src/observability/RpcInstrumentation.ts+7-1
......@@ -1,5 +1,11 @@
11import { WS_METHODS } from "@t3tools/contracts";
2import { Clock, Duration, Effect, Exit, Metric, References, Stream } from "effect";
2import * as Clock from "effect/Clock";
3import * as Duration from "effect/Duration";
4import * as Effect from "effect/Effect";
5import * as Exit from "effect/Exit";
6import * as Metric from "effect/Metric";
7import * as References from "effect/References";
8import * as Stream from "effect/Stream";
39
410import { outcomeFromExit } from "./Attributes.ts";
511import { metricAttributes, rpcRequestDuration, rpcRequestsTotal, withMetrics } from "./Metrics.ts";
apps/server/src/observability/Services/BrowserTraceCollector.ts+2-2
......@@ -1,6 +1,6 @@
11import type { TraceRecord } from "@t3tools/shared/observability";
2import { Context } from "effect";
3import type { Effect } from "effect";
2import * as Context from "effect/Context";
3import type * as Effect from "effect/Effect";
44
55export interface BrowserTraceCollectorShape {
66 readonly record: (records: ReadonlyArray<TraceRecord>) => Effect.Effect<void>;
apps/server/src/open.test.ts+5-2
......@@ -1,7 +1,10 @@
11import * as NodeServices from "@effect/platform-node/NodeServices";
22import { assert, it } from "@effect/vitest";
33import { assertSuccess } from "@effect/vitest/utils";
4import { FileSystem, Path, Effect } from "effect";
4import * as Effect from "effect/Effect";
5import * as FileSystem from "effect/FileSystem";
6import * as Path from "effect/Path";
7import * as Random from "effect/Random";
58
69import {
710 isCommandAvailable,
......@@ -488,7 +491,7 @@ it.layer(NodeServices.layer)("launchDetached", (it) => {
488491 it.effect("rejects when command does not exist", () =>
489492 Effect.gen(function* () {
490493 const result = yield* launchDetached({
491 command: `t3code-no-such-command-${Date.now()}`,
494 command: `t3code-no-such-command-${yield* Random.nextUUIDv4}`,
492495 args: [],
493496 }).pipe(Effect.result);
494497 assert.equal(result._tag, "Failure");
apps/server/src/open.ts+4-1
......@@ -1,3 +1,4 @@
1// @effect-diagnostics nodeBuiltinImport:off
12/**
23 * Open - Browser/editor launch service interface.
34 *
......@@ -10,7 +11,9 @@ import { spawn } from "node:child_process";
1011
1112import { EDITORS, OpenError, type EditorId } from "@t3tools/contracts";
1213import { isCommandAvailable, type CommandAvailabilityOptions } from "@t3tools/shared/shell";
13import { Context, Effect, Layer } from "effect";
14import * as Context from "effect/Context";
15import * as Effect from "effect/Effect";
16import * as Layer from "effect/Layer";
1417
1518// ==============================
1619// Definitions
apps/server/src/orchestration/Errors.ts+2-1
......@@ -1,4 +1,5 @@
1import { SchemaIssue, Schema } from "effect";
1import * as SchemaIssue from "effect/SchemaIssue";
2import * as Schema from "effect/Schema";
23
34import type { ProjectionRepositoryError } from "../persistence/Errors.ts";
45
apps/server/src/orchestration/Layers/CheckpointReactor.test.ts+45-37
......@@ -1,3 +1,4 @@
1// @effect-diagnostics nodeBuiltinImport:off
12import fs from "node:fs";
23import os from "node:os";
34import path from "node:path";
......@@ -19,7 +20,14 @@ import {
1920 TurnId,
2021} from "@t3tools/contracts";
2122import * as NodeServices from "@effect/platform-node/NodeServices";
22import { Effect, Exit, Layer, ManagedRuntime, PubSub, Scope, Stream } from "effect";
23import * as Clock from "effect/Clock";
24import * as Effect from "effect/Effect";
25import * as Exit from "effect/Exit";
26import * as Layer from "effect/Layer";
27import * as ManagedRuntime from "effect/ManagedRuntime";
28import * as PubSub from "effect/PubSub";
29import * as Scope from "effect/Scope";
30import * as Stream from "effect/Stream";
2331import { afterEach, describe, expect, it, vi } from "vitest";
2432
2533import { CheckpointStoreLive } from "../../checkpointing/Layers/CheckpointStore.ts";
......@@ -73,7 +81,7 @@ function createProviderServiceHarness(
7381 sessionCwd = cwd,
7482 providerName: ProviderSession["provider"] = ProviderDriverKind.make("codex"),
7583) {
76 const now = new Date().toISOString();
84 const now = "2026-01-01T00:00:00.000Z";
7785 const runtimeEventPubSub = Effect.runSync(PubSub.unbounded<ProviderRuntimeEvent>());
7886 const rollbackConversation = vi.fn(
7987 (_input: { readonly threadId: ThreadId; readonly numTurns: number }) => Effect.void,
......@@ -148,7 +156,7 @@ async function waitForThread(
148156 }) => boolean,
149157 timeoutMs = 15_000,
150158) {
151 const deadline = Date.now() + timeoutMs;
159 const deadline = (await Effect.runPromise(Clock.currentTimeMillis)) + timeoutMs;
152160 const poll = async (): Promise<{
153161 latestTurn: { turnId: string } | null;
154162 checkpoints: ReadonlyArray<{ checkpointTurnCount: number }>;
......@@ -159,10 +167,10 @@ async function waitForThread(
159167 if (thread && predicate(thread)) {
160168 return thread;
161169 }
162 if (Date.now() >= deadline) {
170 if ((await Effect.runPromise(Clock.currentTimeMillis)) >= deadline) {
163171 throw new Error("Timed out waiting for thread state.");
164172 }
165 await new Promise((resolve) => setTimeout(resolve, 10));
173 await Effect.runPromise(Effect.sleep("10 millis"));
166174 return poll();
167175 };
168176 return poll();
......@@ -173,7 +181,7 @@ async function waitForEvent(
173181 predicate: (event: { type: string }) => boolean,
174182 timeoutMs = 15_000,
175183) {
176 const deadline = Date.now() + timeoutMs;
184 const deadline = (await Effect.runPromise(Clock.currentTimeMillis)) + timeoutMs;
177185 const poll = async () => {
178186 const events = await Effect.runPromise(
179187 Stream.runCollect(engine.readEvents(0)).pipe(Effect.map((chunk) => Array.from(chunk))),
......@@ -181,10 +189,10 @@ async function waitForEvent(
181189 if (events.some(predicate)) {
182190 return events;
183191 }
184 if (Date.now() >= deadline) {
192 if ((await Effect.runPromise(Clock.currentTimeMillis)) >= deadline) {
185193 throw new Error("Timed out waiting for orchestration event.");
186194 }
187 await new Promise((resolve) => setTimeout(resolve, 10));
195 await Effect.runPromise(Effect.sleep("10 millis"));
188196 return poll();
189197 };
190198 return poll();
......@@ -223,15 +231,15 @@ function gitShowFileAtRef(cwd: string, ref: string, filePath: string): string {
223231}
224232
225233async function waitForGitRefExists(cwd: string, ref: string, timeoutMs = 15_000) {
226 const deadline = Date.now() + timeoutMs;
234 const deadline = (await Effect.runPromise(Clock.currentTimeMillis)) + timeoutMs;
227235 const poll = async (): Promise<void> => {
228236 if (gitRefExists(cwd, ref)) {
229237 return;
230238 }
231 if (Date.now() >= deadline) {
239 if ((await Effect.runPromise(Clock.currentTimeMillis)) >= deadline) {
232240 throw new Error(`Timed out waiting for git ref '${ref}'.`);
233241 }
234 await new Promise((resolve) => setTimeout(resolve, 10));
242 await Effect.runPromise(Effect.sleep("10 millis"));
235243 return poll();
236244 };
237245 return poll();
......@@ -342,7 +350,7 @@ describe("CheckpointReactor", () => {
342350 await Effect.runPromise(reactor.start().pipe(Scope.provide(scope)));
343351 const drain = () => Effect.runPromise(reactor.drain);
344352
345 const createdAt = new Date().toISOString();
353 const createdAt = "2026-01-01T00:00:00.000Z";
346354 await Effect.runPromise(
347355 engine.dispatch({
348356 type: "project.create",
......@@ -410,7 +418,7 @@ describe("CheckpointReactor", () => {
410418
411419 it("captures pre-turn baseline on turn.started and post-turn checkpoint on turn.completed", async () => {
412420 const harness = await createHarness({ seedFilesystemCheckpoints: false });
413 const createdAt = new Date().toISOString();
421 const createdAt = "2026-01-01T00:00:00.000Z";
414422
415423 await Effect.runPromise(
416424 harness.engine.dispatch({
......@@ -435,7 +443,7 @@ describe("CheckpointReactor", () => {
435443 eventId: EventId.make("evt-turn-started-1"),
436444 provider: ProviderDriverKind.make("codex"),
437445
438 createdAt: new Date().toISOString(),
446 createdAt: "2026-01-01T00:00:00.000Z",
439447 threadId: ThreadId.make("thread-1"),
440448 turnId: asTurnId("turn-1"),
441449 });
......@@ -450,7 +458,7 @@ describe("CheckpointReactor", () => {
450458 eventId: EventId.make("evt-turn-completed-1"),
451459 provider: ProviderDriverKind.make("codex"),
452460
453 createdAt: new Date().toISOString(),
461 createdAt: "2026-01-01T00:00:00.000Z",
454462 threadId: ThreadId.make("thread-1"),
455463 turnId: asTurnId("turn-1"),
456464 payload: { state: "completed" },
......@@ -495,7 +503,7 @@ describe("CheckpointReactor", () => {
495503 type: "turn.completed",
496504 eventId: EventId.make("evt-turn-completed-refresh-local-status"),
497505 provider: ProviderDriverKind.make("codex"),
498 createdAt: new Date().toISOString(),
506 createdAt: "2026-01-01T00:00:00.000Z",
499507 threadId: ThreadId.make("thread-1"),
500508 turnId: asTurnId("turn-refresh-local-status"),
501509 payload: { state: "completed" },
......@@ -508,7 +516,7 @@ describe("CheckpointReactor", () => {
508516
509517 it("ignores auxiliary thread turn completion while primary turn is active", async () => {
510518 const harness = await createHarness({ seedFilesystemCheckpoints: false });
511 const createdAt = new Date().toISOString();
519 const createdAt = "2026-01-01T00:00:00.000Z";
512520
513521 await Effect.runPromise(
514522 harness.engine.dispatch({
......@@ -533,7 +541,7 @@ describe("CheckpointReactor", () => {
533541 eventId: EventId.make("evt-turn-started-main"),
534542 provider: ProviderDriverKind.make("codex"),
535543
536 createdAt: new Date().toISOString(),
544 createdAt: "2026-01-01T00:00:00.000Z",
537545 threadId: ThreadId.make("thread-1"),
538546 turnId: asTurnId("turn-main"),
539547 });
......@@ -549,7 +557,7 @@ describe("CheckpointReactor", () => {
549557 eventId: EventId.make("evt-turn-completed-aux"),
550558 provider: ProviderDriverKind.make("codex"),
551559
552 createdAt: new Date().toISOString(),
560 createdAt: "2026-01-01T00:00:00.000Z",
553561 threadId: ThreadId.make("thread-1"),
554562 turnId: asTurnId("turn-aux"),
555563 payload: { state: "completed" },
......@@ -565,7 +573,7 @@ describe("CheckpointReactor", () => {
565573 eventId: EventId.make("evt-turn-completed-main"),
566574 provider: ProviderDriverKind.make("codex"),
567575
568 createdAt: new Date().toISOString(),
576 createdAt: "2026-01-01T00:00:00.000Z",
569577 threadId: ThreadId.make("thread-1"),
570578 turnId: asTurnId("turn-main"),
571579 payload: { state: "completed" },
......@@ -583,7 +591,7 @@ describe("CheckpointReactor", () => {
583591 seedFilesystemCheckpoints: false,
584592 providerName: ProviderDriverKind.make("claudeAgent"),
585593 });
586 const createdAt = new Date().toISOString();
594 const createdAt = "2026-01-01T00:00:00.000Z";
587595
588596 await Effect.runPromise(
589597 harness.engine.dispatch({
......@@ -607,7 +615,7 @@ describe("CheckpointReactor", () => {
607615 type: "turn.started",
608616 eventId: EventId.make("evt-turn-started-claude-1"),
609617 provider: ProviderDriverKind.make("claudeAgent"),
610 createdAt: new Date().toISOString(),
618 createdAt: "2026-01-01T00:00:00.000Z",
611619 threadId: ThreadId.make("thread-1"),
612620 turnId: asTurnId("turn-claude-1"),
613621 });
......@@ -621,7 +629,7 @@ describe("CheckpointReactor", () => {
621629 type: "turn.completed",
622630 eventId: EventId.make("evt-turn-completed-claude-1"),
623631 provider: ProviderDriverKind.make("claudeAgent"),
624 createdAt: new Date().toISOString(),
632 createdAt: "2026-01-01T00:00:00.000Z",
625633 threadId: ThreadId.make("thread-1"),
626634 turnId: asTurnId("turn-claude-1"),
627635 payload: { state: "completed" },
......@@ -641,7 +649,7 @@ describe("CheckpointReactor", () => {
641649
642650 it("appends capture failure activity when turn diff summary cannot be derived", async () => {
643651 const harness = await createHarness({ seedFilesystemCheckpoints: false });
644 const createdAt = new Date().toISOString();
652 const createdAt = "2026-01-01T00:00:00.000Z";
645653
646654 await Effect.runPromise(
647655 harness.engine.dispatch({
......@@ -666,7 +674,7 @@ describe("CheckpointReactor", () => {
666674 eventId: EventId.make("evt-turn-completed-missing-baseline"),
667675 provider: ProviderDriverKind.make("codex"),
668676
669 createdAt: new Date().toISOString(),
677 createdAt: "2026-01-01T00:00:00.000Z",
670678 threadId: ThreadId.make("thread-1"),
671679 turnId: asTurnId("turn-missing-baseline"),
672680 payload: { state: "completed" },
......@@ -706,7 +714,7 @@ describe("CheckpointReactor", () => {
706714 },
707715 interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE,
708716 runtimeMode: "approval-required",
709 createdAt: new Date().toISOString(),
717 createdAt: "2026-01-01T00:00:00.000Z",
710718 }),
711719 );
712720
......@@ -729,7 +737,7 @@ describe("CheckpointReactor", () => {
729737 seedFilesystemCheckpoints: false,
730738 threadWorktreePath: null,
731739 });
732 const createdAt = new Date().toISOString();
740 const createdAt = "2026-01-01T00:00:00.000Z";
733741
734742 await Effect.runPromise(
735743 harness.engine.dispatch({
......@@ -755,7 +763,7 @@ describe("CheckpointReactor", () => {
755763 eventId: EventId.make("evt-turn-completed-missing-provider-cwd"),
756764 provider: ProviderDriverKind.make("codex"),
757765
758 createdAt: new Date().toISOString(),
766 createdAt: "2026-01-01T00:00:00.000Z",
759767 threadId: ThreadId.make("thread-1"),
760768 turnId: asTurnId("turn-missing-cwd"),
761769 payload: { state: "completed" },
......@@ -776,7 +784,7 @@ describe("CheckpointReactor", () => {
776784
777785 it("ignores non-v2 checkpoint.captured runtime events", async () => {
778786 const harness = await createHarness();
779 const createdAt = new Date().toISOString();
787 const createdAt = "2026-01-01T00:00:00.000Z";
780788
781789 await Effect.runPromise(
782790 harness.engine.dispatch({
......@@ -801,7 +809,7 @@ describe("CheckpointReactor", () => {
801809 eventId: EventId.make("evt-checkpoint-captured-3"),
802810 provider: ProviderDriverKind.make("codex"),
803811
804 createdAt: new Date().toISOString(),
812 createdAt: "2026-01-01T00:00:00.000Z",
805813 threadId: ThreadId.make("thread-1"),
806814 turnId: asTurnId("turn-3"),
807815 turnCount: 3,
......@@ -826,7 +834,7 @@ describe("CheckpointReactor", () => {
826834 seedFilesystemCheckpoints: false,
827835 providerSessionCwd: nonRepositorySessionCwd,
828836 });
829 const createdAt = new Date().toISOString();
837 const createdAt = "2026-01-01T00:00:00.000Z";
830838
831839 await Effect.runPromise(
832840 harness.engine.dispatch({
......@@ -851,7 +859,7 @@ describe("CheckpointReactor", () => {
851859 eventId: EventId.make("evt-runtime-capture-failure"),
852860 provider: ProviderDriverKind.make("codex"),
853861
854 createdAt: new Date().toISOString(),
862 createdAt: "2026-01-01T00:00:00.000Z",
855863 threadId: ThreadId.make("thread-1"),
856864 turnId: asTurnId("turn-runtime-failure"),
857865 payload: { state: "completed" },
......@@ -862,7 +870,7 @@ describe("CheckpointReactor", () => {
862870 eventId: EventId.make("evt-turn-started-after-runtime-failure"),
863871 provider: ProviderDriverKind.make("codex"),
864872
865 createdAt: new Date().toISOString(),
873 createdAt: "2026-01-01T00:00:00.000Z",
866874 threadId: ThreadId.make("thread-1"),
867875 turnId: asTurnId("turn-after-runtime-failure"),
868876 });
......@@ -878,7 +886,7 @@ describe("CheckpointReactor", () => {
878886
879887 it("executes provider revert and emits thread.reverted for checkpoint revert requests", async () => {
880888 const harness = await createHarness();
881 const createdAt = new Date().toISOString();
889 const createdAt = "2026-01-01T00:00:00.000Z";
882890
883891 await Effect.runPromise(
884892 harness.engine.dispatch({
......@@ -959,7 +967,7 @@ describe("CheckpointReactor", () => {
959967
960968 it("executes provider revert and emits thread.reverted for claude sessions", async () => {
961969 const harness = await createHarness({ providerName: ProviderDriverKind.make("claudeAgent") });
962 const createdAt = new Date().toISOString();
970 const createdAt = "2026-01-01T00:00:00.000Z";
963971
964972 await Effect.runPromise(
965973 harness.engine.dispatch({
......@@ -1028,7 +1036,7 @@ describe("CheckpointReactor", () => {
10281036
10291037 it("processes consecutive revert requests with deterministic rollback sequencing", async () => {
10301038 const harness = await createHarness();
1031 const createdAt = new Date().toISOString();
1039 const createdAt = "2026-01-01T00:00:00.000Z";
10321040
10331041 await Effect.runPromise(
10341042 harness.engine.dispatch({
......@@ -1111,7 +1119,7 @@ describe("CheckpointReactor", () => {
11111119
11121120 it("appends an error activity when revert is requested without an active session", async () => {
11131121 const harness = await createHarness({ hasSession: false });
1114 const createdAt = new Date().toISOString();
1122 const createdAt = "2026-01-01T00:00:00.000Z";
11151123
11161124 await Effect.runPromise(
11171125 harness.engine.dispatch({
apps/server/src/orchestration/Layers/CheckpointReactor.ts+33-20
......@@ -8,7 +8,12 @@ import {
88 type OrchestrationEvent,
99 type ProviderRuntimeEvent,
1010} from "@t3tools/contracts";
11import { Cause, Effect, Layer, Option, Stream } from "effect";
11import * as Cause from "effect/Cause";
12import * as DateTime from "effect/DateTime";
13import * as Effect from "effect/Effect";
14import * as Layer from "effect/Layer";
15import * as Option from "effect/Option";
16import * as Stream from "effect/Stream";
1217import { makeDrainableWorker } from "@t3tools/shared/DrainableWorker";
1318
1419import { parseTurnDiffFilesFromUnifiedDiff } from "../../checkpointing/Diffs.ts";
......@@ -28,6 +33,8 @@ import { isGitRepository } from "../../git/Utils.ts";
2833import { VcsStatusBroadcaster } from "../../vcs/VcsStatusBroadcaster.ts";
2934import { WorkspaceEntries } from "../../workspace/Services/WorkspaceEntries.ts";
3035
36const nowIso = Effect.map(DateTime.now, DateTime.formatIso);
37
3138type ReactorInput =
3239 | {
3340 readonly source: "runtime";
......@@ -584,7 +591,7 @@ const make = Effect.gen(function* () {
584591 const handleRevertRequested = Effect.fn("handleRevertRequested")(function* (
585592 event: Extract<OrchestrationEvent, { type: "thread.checkpoint-revert-requested" }>,
586593 ) {
587 const now = new Date().toISOString();
594 const now = DateTime.formatIso(yield* DateTime.now);
588595
589596 const thread = yield* resolveThreadDetail(event.payload.threadId);
590597 if (!thread) {
......@@ -717,12 +724,14 @@ const make = Effect.gen(function* () {
717724 if (event.type === "thread.checkpoint-revert-requested") {
718725 yield* handleRevertRequested(event).pipe(
719726 Effect.catch((error) =>
720 appendRevertFailureActivity({
721 threadId: event.payload.threadId,
722 turnCount: event.payload.turnCount,
723 detail: error.message,
724 createdAt: new Date().toISOString(),
725 }),
727 Effect.flatMap(nowIso, (createdAt) =>
728 appendRevertFailureActivity({
729 threadId: event.payload.threadId,
730 turnCount: event.payload.turnCount,
731 detail: error.message,
732 createdAt,
733 }),
734 ),
726735 ),
727736 );
728737 return;
......@@ -736,12 +745,14 @@ const make = Effect.gen(function* () {
736745 if (event.type === "thread.turn-diff-completed") {
737746 yield* captureCheckpointFromPlaceholder(event).pipe(
738747 Effect.catch((error) =>
739 appendCaptureFailureActivity({
740 threadId: event.payload.threadId,
741 turnId: event.payload.turnId,
742 detail: error.message,
743 createdAt: new Date().toISOString(),
744 }).pipe(Effect.catch(() => Effect.void)),
748 Effect.flatMap(nowIso, (createdAt) =>
749 appendCaptureFailureActivity({
750 threadId: event.payload.threadId,
751 turnId: event.payload.turnId,
752 detail: error.message,
753 createdAt,
754 }).pipe(Effect.catch(() => Effect.void)),
755 ),
745756 ),
746757 );
747758 }
......@@ -760,12 +771,14 @@ const make = Effect.gen(function* () {
760771 yield* refreshLocalGitStatusFromTurnCompletion(event);
761772 yield* captureCheckpointFromTurnCompletion(event).pipe(
762773 Effect.catch((error) =>
763 appendCaptureFailureActivity({
764 threadId: event.threadId,
765 turnId,
766 detail: error.message,
767 createdAt: new Date().toISOString(),
768 }).pipe(Effect.catch(() => Effect.void)),
774 Effect.flatMap(nowIso, (createdAt) =>
775 appendCaptureFailureActivity({
776 threadId: event.threadId,
777 turnId,
778 detail: error.message,
779 createdAt,
780 }).pipe(Effect.catch(() => Effect.void)),
781 ),
769782 ),
770783 );
771784 return;
apps/server/src/orchestration/Layers/OrchestrationEngine.test.ts+8-3
......@@ -8,9 +8,14 @@ import {
88 TurnId,
99 type OrchestrationEvent,
1010 ProviderInstanceId,
11 type OrchestrationReadModel,
1211} from "@t3tools/contracts";
13import { Effect, Layer, ManagedRuntime, Metric, Option, Queue, Stream } from "effect";
12import * as Effect from "effect/Effect";
13import * as Layer from "effect/Layer";
14import * as ManagedRuntime from "effect/ManagedRuntime";
15import * as Metric from "effect/Metric";
16import * as Option from "effect/Option";
17import * as Queue from "effect/Queue";
18import * as Stream from "effect/Stream";
1419import { describe, expect, it } from "vitest";
1520
1621import { PersistenceSqlError } from "../../persistence/Errors.ts";
......@@ -69,7 +74,7 @@ async function createOrchestrationSystem() {
6974}
7075
7176function now() {
72 return new Date().toISOString();
77 return "2026-01-01T00:00:00.000Z";
7378}
7479
7580const hasMetricSnapshot = (
apps/server/src/orchestration/Layers/OrchestrationEngine.ts+26-20
......@@ -5,20 +5,20 @@ import type {
55 ThreadId,
66} from "@t3tools/contracts";
77import { OrchestrationCommand } from "@t3tools/contracts";
8import {
9 Cause,
10 Deferred,
11 Duration,
12 Effect,
13 Exit,
14 Layer,
15 Metric,
16 Option,
17 PubSub,
18 Queue,
19 Schema,
20 Stream,
21} from "effect";
8import * as Cause from "effect/Cause";
9import * as Clock from "effect/Clock";
10import * as DateTime from "effect/DateTime";
11import * as Deferred from "effect/Deferred";
12import * as Duration from "effect/Duration";
13import * as Effect from "effect/Effect";
14import * as Exit from "effect/Exit";
15import * as Layer from "effect/Layer";
16import * as Metric from "effect/Metric";
17import * as Option from "effect/Option";
18import * as PubSub from "effect/PubSub";
19import * as Queue from "effect/Queue";
20import * as Schema from "effect/Schema";
21import * as Stream from "effect/Stream";
2222import * as SqlClient from "effect/unstable/sql/SqlClient";
2323
2424import {
......@@ -78,7 +78,8 @@ const makeOrchestrationEngine = Effect.gen(function* () {
7878 const projectionPipeline = yield* OrchestrationProjectionPipeline;
7979 const projectionSnapshotQuery = yield* ProjectionSnapshotQuery;
8080
81 let commandReadModel = createEmptyReadModel(new Date().toISOString());
81 const nowIso = Effect.map(DateTime.now, DateTime.formatIso);
82 let commandReadModel = createEmptyReadModel(yield* nowIso);
8283
8384 const commandQueue = yield* Queue.unbounded<CommandEnvelope>();
8485 const eventPubSub = yield* PubSub.unbounded<OrchestrationEvent>();
......@@ -97,7 +98,7 @@ const makeOrchestrationEngine = Effect.gen(function* () {
9798
9899 const processEnvelope = (envelope: CommandEnvelope): Effect.Effect<void> => {
99100 const dispatchStartSequence = commandReadModel.snapshotSequence;
100 const processingStartedAtMs = Date.now();
101 let processingStartedAtMs = 0;
101102 const aggregateRef = commandToAggregateRef(envelope.command);
102103 const baseMetricAttributes = {
103104 commandType: envelope.command.type,
......@@ -120,6 +121,7 @@ const makeOrchestrationEngine = Effect.gen(function* () {
120121
121122 return Effect.exit(
122123 Effect.gen(function* () {
124 processingStartedAtMs = yield* Clock.currentTimeMillis;
123125 yield* Effect.annotateCurrentSpan({
124126 "orchestration.command_id": envelope.command.commandId,
125127 "orchestration.command_type": envelope.command.type,
......@@ -205,7 +207,7 @@ const makeOrchestrationEngine = Effect.gen(function* () {
205207 ackEventType: event.type,
206208 }),
207209 ),
208 Duration.millis(Math.max(0, Date.now() - envelope.startedAtMs)),
210 Duration.millis(Math.max(0, (yield* Clock.currentTimeMillis) - envelope.startedAtMs)),
209211 );
210212 }
211213 }
......@@ -224,7 +226,7 @@ const makeOrchestrationEngine = Effect.gen(function* () {
224226 orchestrationCommandDuration,
225227 metricAttributes(baseMetricAttributes),
226228 ),
227 Duration.millis(Math.max(0, Date.now() - processingStartedAtMs)),
229 Duration.millis(Math.max(0, (yield* Clock.currentTimeMillis) - processingStartedAtMs)),
228230 );
229231 yield* Metric.update(
230232 Metric.withAttributes(
......@@ -263,7 +265,7 @@ const makeOrchestrationEngine = Effect.gen(function* () {
263265 commandId: envelope.command.commandId,
264266 aggregateKind: aggregateRef.aggregateKind,
265267 aggregateId: aggregateRef.aggregateId,
266 acceptedAt: new Date().toISOString(),
268 acceptedAt: yield* nowIso,
267269 resultSequence: commandReadModel.snapshotSequence,
268270 status: "rejected",
269271 error: error.message,
......@@ -293,7 +295,11 @@ const makeOrchestrationEngine = Effect.gen(function* () {
293295 const dispatch: OrchestrationEngineShape["dispatch"] = (command) =>
294296 Effect.gen(function* () {
295297 const result = yield* Deferred.make<{ sequence: number }, OrchestrationDispatchError>();
296 yield* Queue.offer(commandQueue, { command, result, startedAtMs: Date.now() });
298 yield* Queue.offer(commandQueue, {
299 command,
300 result,
301 startedAtMs: yield* Clock.currentTimeMillis,
302 });
297303 return yield* Deferred.await(result);
298304 });
299305
apps/server/src/orchestration/Layers/OrchestrationReactor.test.ts+5-1
......@@ -1,4 +1,8 @@
1import { Effect, Exit, Layer, ManagedRuntime, Scope } from "effect";
1import * as Effect from "effect/Effect";
2import * as Exit from "effect/Exit";
3import * as Layer from "effect/Layer";
4import * as ManagedRuntime from "effect/ManagedRuntime";
5import * as Scope from "effect/Scope";
26import { afterEach, describe, expect, it } from "vitest";
37
48import { CheckpointReactor } from "../Services/CheckpointReactor.ts";
apps/server/src/orchestration/Layers/OrchestrationReactor.ts+2-1
......@@ -1,4 +1,5 @@
1import { Effect, Layer } from "effect";
1import * as Effect from "effect/Effect";
2import * as Layer from "effect/Layer";
23
34import {
45 OrchestrationReactor,
apps/server/src/orchestration/Layers/ProjectionPipeline.test.ts+23-16
......@@ -11,7 +11,10 @@ import {
1111} from "@t3tools/contracts";
1212import * as NodeServices from "@effect/platform-node/NodeServices";
1313import { assert, it } from "@effect/vitest";
14import { Effect, FileSystem, Layer, Path } from "effect";
14import * as Effect from "effect/Effect";
15import * as FileSystem from "effect/FileSystem";
16import * as Layer from "effect/Layer";
17import * as Path from "effect/Path";
1518import * as SqlClient from "effect/unstable/sql/SqlClient";
1619
1720import { OrchestrationCommandReceiptRepositoryLive } from "../../persistence/Layers/OrchestrationCommandReceipts.ts";
......@@ -55,7 +58,7 @@ it.layer(BaseTestLayer)("OrchestrationProjectionPipeline", (it) => {
5558 const projectionPipeline = yield* OrchestrationProjectionPipeline;
5659 const eventStore = yield* OrchestrationEventStore;
5760 const sql = yield* SqlClient.SqlClient;
58 const now = new Date().toISOString();
61 const now = "2026-01-01T00:00:00.000Z";
5962
6063 yield* eventStore.append({
6164 type: "project.created",
......@@ -180,7 +183,7 @@ it.layer(Layer.fresh(makeProjectionPipelinePrefixedTestLayer("t3-base-")))(
180183 const projectionPipeline = yield* OrchestrationProjectionPipeline;
181184 const eventStore = yield* OrchestrationEventStore;
182185 const sql = yield* SqlClient.SqlClient;
183 const now = new Date().toISOString();
186 const now = "2026-01-01T00:00:00.000Z";
184187
185188 yield* eventStore.append({
186189 type: "thread.message-sent",
......@@ -224,6 +227,7 @@ it.layer(Layer.fresh(makeProjectionPipelinePrefixedTestLayer("t3-base-")))(
224227 WHERE message_id = 'message-attachments'
225228 `;
226229 assert.equal(rows.length, 1);
230 // @effect-diagnostics-next-line preferSchemaOverJson:off
227231 assert.deepEqual(JSON.parse(rows[0]?.attachmentsJson ?? "null"), [
228232 {
229233 type: "image",
......@@ -246,7 +250,7 @@ it.layer(Layer.fresh(makeProjectionPipelinePrefixedTestLayer("t3-projection-atta
246250 const projectionPipeline = yield* OrchestrationProjectionPipeline;
247251 const eventStore = yield* OrchestrationEventStore;
248252 const sql = yield* SqlClient.SqlClient;
249 const now = new Date().toISOString();
253 const now = "2026-01-01T00:00:00.000Z";
250254
251255 yield* eventStore.append({
252256 type: "thread.message-sent",
......@@ -297,6 +301,7 @@ it.layer(Layer.fresh(makeProjectionPipelinePrefixedTestLayer("t3-projection-atta
297301 WHERE message_id = 'message-attachments-safe'
298302 `;
299303 assert.equal(rows.length, 1);
304 // @effect-diagnostics-next-line preferSchemaOverJson:off
300305 assert.deepEqual(JSON.parse(rows[0]?.attachmentsJson ?? "null"), [
301306 {
302307 type: "image",
......@@ -326,8 +331,8 @@ it.layer(BaseTestLayer)("OrchestrationProjectionPipeline", (it) => {
326331 const projectionPipeline = yield* OrchestrationProjectionPipeline;
327332 const eventStore = yield* OrchestrationEventStore;
328333 const sql = yield* SqlClient.SqlClient;
329 const now = new Date().toISOString();
330 const later = new Date(Date.now() + 1_000).toISOString();
334 const now = "2026-01-01T00:00:00.000Z";
335 const later = "2026-01-01T00:00:01.000Z";
331336
332337 yield* eventStore.append({
333338 type: "project.created",
......@@ -441,6 +446,7 @@ it.layer(BaseTestLayer)("OrchestrationProjectionPipeline", (it) => {
441446 WHERE message_id = 'message-clear-attachments'
442447 `;
443448 assert.equal(rows.length, 1);
449 // @effect-diagnostics-next-line preferSchemaOverJson:off
444450 assert.deepEqual(JSON.parse(rows[0]?.attachmentsJson ?? "null"), []);
445451 }),
446452 );
......@@ -454,8 +460,8 @@ it.layer(
454460 const projectionPipeline = yield* OrchestrationProjectionPipeline;
455461 const eventStore = yield* OrchestrationEventStore;
456462 const sql = yield* SqlClient.SqlClient;
457 const now = new Date().toISOString();
458 const later = new Date(Date.now() + 1_000).toISOString();
463 const now = "2026-01-01T00:00:00.000Z";
464 const later = "2026-01-01T00:00:01.000Z";
459465
460466 yield* eventStore.append({
461467 type: "project.created",
......@@ -576,6 +582,7 @@ it.layer(
576582 WHERE message_id = 'message-overwrite'
577583 `;
578584 assert.equal(rows.length, 1);
585 // @effect-diagnostics-next-line preferSchemaOverJson:off
579586 assert.deepEqual(JSON.parse(rows[0]?.attachmentsJson ?? "null"), [
580587 {
581588 type: "image",
......@@ -598,7 +605,7 @@ it.layer(
598605 const eventStore = yield* OrchestrationEventStore;
599606 const path = yield* Path.Path;
600607 const sql = yield* SqlClient.SqlClient;
601 const now = new Date().toISOString();
608 const now = "2026-01-01T00:00:00.000Z";
602609
603610 const appendAndProject = (event: Parameters<typeof eventStore.append>[0]) =>
604611 eventStore
......@@ -722,7 +729,7 @@ it.layer(
722729 const projectionPipeline = yield* OrchestrationProjectionPipeline;
723730 const eventStore = yield* OrchestrationEventStore;
724731 const { attachmentsDir } = yield* ServerConfig;
725 const now = new Date().toISOString();
732 const now = "2026-01-01T00:00:00.000Z";
726733 const threadId = ThreadId.make("Thread Revert.Files");
727734 const keepAttachmentId = "thread-revert-files-00000000-0000-4000-8000-000000000001";
728735 const removeAttachmentId = "thread-revert-files-00000000-0000-4000-8000-000000000002";
......@@ -931,7 +938,7 @@ it.layer(Layer.fresh(makeProjectionPipelinePrefixedTestLayer("t3-projection-atta
931938 const projectionPipeline = yield* OrchestrationProjectionPipeline;
932939 const eventStore = yield* OrchestrationEventStore;
933940 const { attachmentsDir } = yield* ServerConfig;
934 const now = new Date().toISOString();
941 const now = "2026-01-01T00:00:00.000Z";
935942 const threadId = ThreadId.make("Thread Delete.Files");
936943 const attachmentId = "thread-delete-files-00000000-0000-4000-8000-000000000001";
937944 const otherThreadAttachmentId =
......@@ -1063,7 +1070,7 @@ it.layer(Layer.fresh(makeProjectionPipelinePrefixedTestLayer("t3-projection-atta
10631070 const path = yield* Path.Path;
10641071 const projectionPipeline = yield* OrchestrationProjectionPipeline;
10651072 const eventStore = yield* OrchestrationEventStore;
1066 const now = new Date().toISOString();
1073 const now = "2026-01-01T00:00:00.000Z";
10671074 const { attachmentsDir: attachmentsRootDir, stateDir } = yield* ServerConfig;
10681075 const attachmentsSentinelPath = path.join(attachmentsRootDir, "sentinel.txt");
10691076 const stateDirSentinelPath = path.join(stateDir, "state-sentinel.txt");
......@@ -1103,7 +1110,7 @@ it.layer(BaseTestLayer)("OrchestrationProjectionPipeline", (it) => {
11031110 const projectionPipeline = yield* OrchestrationProjectionPipeline;
11041111 const eventStore = yield* OrchestrationEventStore;
11051112 const sql = yield* SqlClient.SqlClient;
1106 const now = new Date().toISOString();
1113 const now = "2026-01-01T00:00:00.000Z";
11071114
11081115 yield* eventStore.append({
11091116 type: "project.created",
......@@ -1230,7 +1237,7 @@ it.layer(BaseTestLayer)("OrchestrationProjectionPipeline", (it) => {
12301237 const projectionPipeline = yield* OrchestrationProjectionPipeline;
12311238 const eventStore = yield* OrchestrationEventStore;
12321239 const sql = yield* SqlClient.SqlClient;
1233 const now = new Date().toISOString();
1240 const now = "2026-01-01T00:00:00.000Z";
12341241
12351242 yield* eventStore.append({
12361243 type: "project.created",
......@@ -2187,7 +2194,7 @@ engineLayer("OrchestrationProjectionPipeline via engine dispatch", (it) => {
21872194 Effect.gen(function* () {
21882195 const engine = yield* OrchestrationEngineService;
21892196 const sql = yield* SqlClient.SqlClient;
2190 const createdAt = new Date().toISOString();
2197 const createdAt = "2026-01-01T00:00:00.000Z";
21912198
21922199 yield* engine.dispatch({
21932200 type: "project.create",
......@@ -2225,7 +2232,7 @@ engineLayer("OrchestrationProjectionPipeline via engine dispatch", (it) => {
22252232 Effect.gen(function* () {
22262233 const engine = yield* OrchestrationEngineService;
22272234 const sql = yield* SqlClient.SqlClient;
2228 const createdAt = new Date().toISOString();
2235 const createdAt = "2026-01-01T00:00:00.000Z";
22292236
22302237 yield* engine.dispatch({
22312238 type: "project.create",
apps/server/src/orchestration/Layers/ProjectionPipeline.ts+6-1
......@@ -4,7 +4,12 @@ import {
44 type OrchestrationEvent,
55 ThreadId,
66} from "@t3tools/contracts";
7import { Effect, FileSystem, Layer, Option, Path, Stream } from "effect";
7import * as Effect from "effect/Effect";
8import * as FileSystem from "effect/FileSystem";
9import * as Layer from "effect/Layer";
10import * as Option from "effect/Option";
11import * as Path from "effect/Path";
12import * as Stream from "effect/Stream";
813import * as SqlClient from "effect/unstable/sql/SqlClient";
914
1015import { toPersistenceSqlError, type ProjectionRepositoryError } from "../../persistence/Errors.ts";
apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.test.ts+2-1
......@@ -8,7 +8,8 @@ import {
88 ProviderInstanceId,
99} from "@t3tools/contracts";
1010import { assert, it } from "@effect/vitest";
11import { Effect, Layer } from "effect";
11import * as Effect from "effect/Effect";
12import * as Layer from "effect/Layer";
1213import * as SqlClient from "effect/unstable/sql/SqlClient";
1314
1415import { SqlitePersistenceMemory } from "../../persistence/Layers/Sqlite.ts";
apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts+8-4
......@@ -23,7 +23,11 @@ import {
2323 ProjectId,
2424 ThreadId,
2525} from "@t3tools/contracts";
26import { Effect, Layer, Option, Schema, Struct } from "effect";
26import * as Effect from "effect/Effect";
27import * as Layer from "effect/Layer";
28import * as Option from "effect/Option";
29import * as Schema from "effect/Schema";
30import * as Struct from "effect/Struct";
2731import * as SqlClient from "effect/unstable/sql/SqlClient";
2832import * as SqlSchema from "effect/unstable/sql/SqlSchema";
2933
......@@ -1005,7 +1009,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
10051009 snapshotSequence: computeSnapshotSequence(stateRows),
10061010 projects,
10071011 threads,
1008 updatedAt: updatedAt ?? new Date(0).toISOString(),
1012 updatedAt: updatedAt ?? "1970-01-01T00:00:00.000Z",
10091013 };
10101014
10111015 return yield* decodeReadModel(snapshot).pipe(
......@@ -1204,7 +1208,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
12041208 snapshotSequence: computeSnapshotSequence(stateRows),
12051209 projects,
12061210 threads,
1207 updatedAt: updatedAt ?? new Date(0).toISOString(),
1211 updatedAt: updatedAt ?? "1970-01-01T00:00:00.000Z",
12081212 } satisfies OrchestrationReadModel;
12091213 }),
12101214 ),
......@@ -1326,7 +1330,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
13261330 hasActionableProposedPlan: row.hasActionableProposedPlan > 0,
13271331 }),
13281332 ),
1329 updatedAt: updatedAt ?? new Date(0).toISOString(),
1333 updatedAt: updatedAt ?? "1970-01-01T00:00:00.000Z",
13301334 };
13311335
13321336 return yield* decodeShellSnapshot(snapshot).pipe(
apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts+43-35
......@@ -1,3 +1,4 @@
1// @effect-diagnostics nodeBuiltinImport:off
12import fs from "node:fs";
23import os from "node:os";
34import path from "node:path";
......@@ -20,7 +21,13 @@ import {
2021 ThreadId,
2122 TurnId,
2223} from "@t3tools/contracts";
23import { Effect, Exit, Layer, ManagedRuntime, PubSub, Scope, Stream } from "effect";
24import * as Effect from "effect/Effect";
25import * as Exit from "effect/Exit";
26import * as Layer from "effect/Layer";
27import * as ManagedRuntime from "effect/ManagedRuntime";
28import * as PubSub from "effect/PubSub";
29import * as Scope from "effect/Scope";
30import * as Stream from "effect/Stream";
2431import { afterEach, describe, expect, it, vi } from "vitest";
2532
2633import { deriveServerPaths, ServerConfig } from "../../config.ts";
......@@ -47,6 +54,7 @@ import { OrchestrationEngineService } from "../Services/OrchestrationEngine.ts";
4754import { ProviderCommandReactor } from "../Services/ProviderCommandReactor.ts";
4855import { ProjectionSnapshotQuery } from "../Services/ProjectionSnapshotQuery.ts";
4956import * as NodeServices from "@effect/platform-node/NodeServices";
57import * as Clock from "effect/Clock";
5058import { ServerSettingsService } from "../../serverSettings.ts";
5159import { VcsStatusBroadcaster } from "../../vcs/VcsStatusBroadcaster.ts";
5260import { GitWorkflowService, type GitWorkflowServiceShape } from "../../git/GitWorkflowService.ts";
......@@ -63,15 +71,15 @@ async function waitFor(
6371 predicate: () => boolean | Promise<boolean>,
6472 timeoutMs = 2000,
6573): Promise<void> {
66 const deadline = Date.now() + timeoutMs;
74 const deadline = (await Effect.runPromise(Clock.currentTimeMillis)) + timeoutMs;
6775 const poll = async (): Promise<void> => {
6876 if (await predicate()) {
6977 return;
7078 }
71 if (Date.now() >= deadline) {
79 if ((await Effect.runPromise(Clock.currentTimeMillis)) >= deadline) {
7280 throw new Error("Timed out waiting for expectation.");
7381 }
74 await new Promise((resolve) => setTimeout(resolve, 10));
82 await Effect.runPromise(Effect.yieldNow);
7583 return poll();
7684 };
7785
......@@ -135,7 +143,7 @@ describe("ProviderCommandReactor", () => {
135143 readonly threadModelSelection?: ModelSelection;
136144 readonly sessionModelSwitch?: "unsupported" | "in-session";
137145 }) {
138 const now = new Date().toISOString();
146 const now = "2026-01-01T00:00:00.000Z";
139147 const baseDir = input?.baseDir ?? fs.mkdtempSync(path.join(os.tmpdir(), "t3code-reactor-"));
140148 createdBaseDirs.add(baseDir);
141149 const { stateDir } = deriveServerPathsSync(baseDir, undefined);
......@@ -408,7 +416,7 @@ describe("ProviderCommandReactor", () => {
408416
409417 it("reacts to thread.turn.start by ensuring session and sending provider turn", async () => {
410418 const harness = await createHarness();
411 const now = new Date().toISOString();
419 const now = "2026-01-01T00:00:00.000Z";
412420
413421 await Effect.runPromise(
414422 harness.engine.dispatch({
......@@ -447,7 +455,7 @@ describe("ProviderCommandReactor", () => {
447455
448456 it("generates a thread title on the first turn", async () => {
449457 const harness = await createHarness();
450 const now = new Date().toISOString();
458 const now = "2026-01-01T00:00:00.000Z";
451459 const seededTitle = "Please investigate reconnect failures after restar...";
452460 harness.generateThreadTitle.mockReturnValue(Effect.succeed({ title: "Generated title" }));
453461
......@@ -497,7 +505,7 @@ describe("ProviderCommandReactor", () => {
497505
498506 it("does not overwrite an existing custom thread title on the first turn", async () => {
499507 const harness = await createHarness();
500 const now = new Date().toISOString();
508 const now = "2026-01-01T00:00:00.000Z";
501509 const seededTitle = "Please investigate reconnect failures after restar...";
502510
503511 await Effect.runPromise(
......@@ -537,7 +545,7 @@ describe("ProviderCommandReactor", () => {
537545
538546 it("matches the client-seeded title even when the outgoing prompt is reformatted", async () => {
539547 const harness = await createHarness();
540 const now = new Date().toISOString();
548 const now = "2026-01-01T00:00:00.000Z";
541549 const seededTitle = "Fix reconnect spinner on resume";
542550 harness.generateThreadTitle.mockReturnValue(
543551 Effect.succeed({
......@@ -588,7 +596,7 @@ describe("ProviderCommandReactor", () => {
588596
589597 it("generates a worktree branch name for the first turn", async () => {
590598 const harness = await createHarness();
591 const now = new Date().toISOString();
599 const now = "2026-01-01T00:00:00.000Z";
592600
593601 await Effect.runPromise(
594602 harness.engine.dispatch({
......@@ -642,7 +650,7 @@ describe("ProviderCommandReactor", () => {
642650
643651 it("forwards codex model options through session start and turn send", async () => {
644652 const harness = await createHarness();
645 const now = new Date().toISOString();
653 const now = "2026-01-01T00:00:00.000Z";
646654
647655 await Effect.runPromise(
648656 harness.engine.dispatch({
......@@ -689,7 +697,7 @@ describe("ProviderCommandReactor", () => {
689697 model: "claude-sonnet-4-6",
690698 },
691699 });
692 const now = new Date().toISOString();
700 const now = "2026-01-01T00:00:00.000Z";
693701
694702 await Effect.runPromise(
695703 harness.engine.dispatch({
......@@ -739,7 +747,7 @@ describe("ProviderCommandReactor", () => {
739747 model: "claude-opus-4-6",
740748 },
741749 });
742 const now = new Date().toISOString();
750 const now = "2026-01-01T00:00:00.000Z";
743751
744752 await Effect.runPromise(
745753 harness.engine.dispatch({
......@@ -784,7 +792,7 @@ describe("ProviderCommandReactor", () => {
784792
785793 it("forwards plan interaction mode to the provider turn request", async () => {
786794 const harness = await createHarness();
787 const now = new Date().toISOString();
795 const now = "2026-01-01T00:00:00.000Z";
788796
789797 await Effect.runPromise(
790798 harness.engine.dispatch({
......@@ -822,7 +830,7 @@ describe("ProviderCommandReactor", () => {
822830
823831 it("preserves the active session model when in-session model switching is unsupported", async () => {
824832 const harness = await createHarness({ sessionModelSwitch: "unsupported" });
825 const now = new Date().toISOString();
833 const now = "2026-01-01T00:00:00.000Z";
826834
827835 await Effect.runPromise(
828836 harness.engine.dispatch({
......@@ -875,7 +883,7 @@ describe("ProviderCommandReactor", () => {
875883 const harness = await createHarness({
876884 threadModelSelection: { instanceId: ProviderInstanceId.make("codex"), model: "gpt-5-codex" },
877885 });
878 const now = new Date().toISOString();
886 const now = "2026-01-01T00:00:00.000Z";
879887
880888 await Effect.runPromise(
881889 harness.engine.dispatch({
......@@ -921,7 +929,7 @@ describe("ProviderCommandReactor", () => {
921929
922930 it("reuses the same provider session when runtime mode is unchanged", async () => {
923931 const harness = await createHarness();
924 const now = new Date().toISOString();
932 const now = "2026-01-01T00:00:00.000Z";
925933
926934 await Effect.runPromise(
927935 harness.engine.dispatch({
......@@ -967,7 +975,7 @@ describe("ProviderCommandReactor", () => {
967975
968976 it("restarts an existing Codex thread on a compatible requested instance", async () => {
969977 const harness = await createHarness();
970 const now = new Date().toISOString();
978 const now = "2026-01-01T00:00:00.000Z";
971979
972980 await Effect.runPromise(
973981 harness.engine.dispatch({
......@@ -1009,7 +1017,7 @@ describe("ProviderCommandReactor", () => {
10091017 },
10101018 interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE,
10111019 runtimeMode: "approval-required",
1012 createdAt: new Date().toISOString(),
1020 createdAt: "2026-01-01T00:00:00.000Z",
10131021 }),
10141022 );
10151023
......@@ -1034,7 +1042,7 @@ describe("ProviderCommandReactor", () => {
10341042 model: "claude-sonnet-4-6",
10351043 },
10361044 });
1037 const now = new Date().toISOString();
1045 const now = "2026-01-01T00:00:00.000Z";
10381046
10391047 await Effect.runPromise(
10401048 harness.engine.dispatch({
......@@ -1107,7 +1115,7 @@ describe("ProviderCommandReactor", () => {
11071115 model: "claude-sonnet-4-6",
11081116 },
11091117 });
1110 const now = new Date().toISOString();
1118 const now = "2026-01-01T00:00:00.000Z";
11111119
11121120 await Effect.runPromise(
11131121 harness.engine.dispatch({
......@@ -1170,7 +1178,7 @@ describe("ProviderCommandReactor", () => {
11701178
11711179 it("restarts the provider session when runtime mode is updated on the thread", async () => {
11721180 const harness = await createHarness();
1173 const now = new Date().toISOString();
1181 const now = "2026-01-01T00:00:00.000Z";
11741182
11751183 await Effect.runPromise(
11761184 harness.engine.dispatch({
......@@ -1260,7 +1268,7 @@ describe("ProviderCommandReactor", () => {
12601268 model: "claude-opus-4-6",
12611269 },
12621270 });
1263 const now = new Date().toISOString();
1271 const now = "2026-01-01T00:00:00.000Z";
12641272
12651273 await Effect.runPromise(
12661274 harness.engine.dispatch({
......@@ -1303,7 +1311,7 @@ describe("ProviderCommandReactor", () => {
13031311
13041312 it("does not stop the active session when restart fails before rebind", async () => {
13051313 const harness = await createHarness();
1306 const now = new Date().toISOString();
1314 const now = "2026-01-01T00:00:00.000Z";
13071315
13081316 await Effect.runPromise(
13091317 harness.engine.dispatch({
......@@ -1336,7 +1344,7 @@ describe("ProviderCommandReactor", () => {
13361344 await waitFor(() => harness.sendTurn.mock.calls.length === 1);
13371345
13381346 harness.startSession.mockImplementationOnce(
1339 (_: unknown, __: unknown) => Effect.fail(new Error("simulated restart failure")) as never,
1347 (_: unknown, __: unknown) => Effect.fail("simulated restart failure") as never,
13401348 );
13411349
13421350 await Effect.runPromise(
......@@ -1368,7 +1376,7 @@ describe("ProviderCommandReactor", () => {
13681376
13691377 it("rejects provider changes after a thread is already bound to a session provider", async () => {
13701378 const harness = await createHarness();
1371 const now = new Date().toISOString();
1379 const now = "2026-01-01T00:00:00.000Z";
13721380
13731381 await Effect.runPromise(
13741382 harness.engine.dispatch({
......@@ -1440,7 +1448,7 @@ describe("ProviderCommandReactor", () => {
14401448
14411449 it("rejects cross-driver provider changes after the existing thread session has stopped", async () => {
14421450 const harness = await createHarness();
1443 const now = new Date().toISOString();
1451 const now = "2026-01-01T00:00:00.000Z";
14441452
14451453 await Effect.runPromise(
14461454 harness.engine.dispatch({
......@@ -1506,7 +1514,7 @@ describe("ProviderCommandReactor", () => {
15061514
15071515 it("reacts to thread.turn.interrupt-requested by calling provider interrupt", async () => {
15081516 const harness = await createHarness();
1509 const now = new Date().toISOString();
1517 const now = "2026-01-01T00:00:00.000Z";
15101518
15111519 await Effect.runPromise(
15121520 harness.engine.dispatch({
......@@ -1544,7 +1552,7 @@ describe("ProviderCommandReactor", () => {
15441552
15451553 it("starts a fresh session when only projected session state exists", async () => {
15461554 const harness = await createHarness();
1547 const now = new Date().toISOString();
1555 const now = "2026-01-01T00:00:00.000Z";
15481556
15491557 await Effect.runPromise(
15501558 harness.engine.dispatch({
......@@ -1599,7 +1607,7 @@ describe("ProviderCommandReactor", () => {
15991607
16001608 it("rejects active runtime sessions that are missing provider instance ids", async () => {
16011609 const harness = await createHarness();
1602 const now = new Date().toISOString();
1610 const now = "2026-01-01T00:00:00.000Z";
16031611
16041612 await Effect.runPromise(
16051613 harness.engine.dispatch({
......@@ -1670,7 +1678,7 @@ describe("ProviderCommandReactor", () => {
16701678
16711679 it("reacts to thread.approval.respond by forwarding provider approval response", async () => {
16721680 const harness = await createHarness();
1673 const now = new Date().toISOString();
1681 const now = "2026-01-01T00:00:00.000Z";
16741682
16751683 await Effect.runPromise(
16761684 harness.engine.dispatch({
......@@ -1711,7 +1719,7 @@ describe("ProviderCommandReactor", () => {
17111719
17121720 it("reacts to thread.user-input.respond by forwarding structured user input answers", async () => {
17131721 const harness = await createHarness();
1714 const now = new Date().toISOString();
1722 const now = "2026-01-01T00:00:00.000Z";
17151723
17161724 await Effect.runPromise(
17171725 harness.engine.dispatch({
......@@ -1756,7 +1764,7 @@ describe("ProviderCommandReactor", () => {
17561764
17571765 it("surfaces stale provider approval request failures without faking approval resolution", async () => {
17581766 const harness = await createHarness();
1759 const now = new Date().toISOString();
1767 const now = "2026-01-01T00:00:00.000Z";
17601768 harness.respondToRequest.mockImplementation(() =>
17611769 Effect.fail(
17621770 new ProviderAdapterRequestError({
......@@ -1851,7 +1859,7 @@ describe("ProviderCommandReactor", () => {
18511859
18521860 it("surfaces stale provider user-input failures without faking user-input resolution", async () => {
18531861 const harness = await createHarness();
1854 const now = new Date().toISOString();
1862 const now = "2026-01-01T00:00:00.000Z";
18551863 harness.respondToUserInput.mockImplementation(() =>
18561864 Effect.fail(
18571865 new ProviderAdapterRequestError({
......@@ -1960,7 +1968,7 @@ describe("ProviderCommandReactor", () => {
19601968
19611969 it("reacts to thread.session.stop by stopping provider session and clearing thread session state", async () => {
19621970 const harness = await createHarness();
1963 const now = new Date().toISOString();
1971 const now = "2026-01-01T00:00:00.000Z";
19641972
19651973 await Effect.runPromise(
19661974 harness.engine.dispatch({
apps/server/src/orchestration/Layers/ProviderCommandReactor.ts+9-1
......@@ -13,7 +13,15 @@ import {
1313 type TurnId,
1414} from "@t3tools/contracts";
1515import { isTemporaryWorktreeBranch, WORKTREE_BRANCH_PREFIX } from "@t3tools/shared/git";
16import { Cache, Cause, Duration, Effect, Equal, Layer, Option, Schema, Stream } from "effect";
16import * as Cache from "effect/Cache";
17import * as Cause from "effect/Cause";
18import * as Duration from "effect/Duration";
19import * as Effect from "effect/Effect";
20import * as Equal from "effect/Equal";
21import * as Layer from "effect/Layer";
22import * as Option from "effect/Option";
23import * as Schema from "effect/Schema";
24import * as Stream from "effect/Stream";
1725import { makeDrainableWorker } from "@t3tools/shared/DrainableWorker";
1826
1927import { resolveThreadWorkspaceCwd } from "../../checkpointing/Utils.ts";
apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts+69-61
......@@ -1,3 +1,4 @@
1// @effect-diagnostics nodeBuiltinImport:off
12import fs from "node:fs";
23import os from "node:os";
34import path from "node:path";
......@@ -21,7 +22,14 @@ import {
2122 ThreadId,
2223 TurnId,
2324} from "@t3tools/contracts";
24import { Effect, Exit, Layer, ManagedRuntime, PubSub, Scope, Stream } from "effect";
25import * as Clock from "effect/Clock";
26import * as Effect from "effect/Effect";
27import * as Exit from "effect/Exit";
28import * as Layer from "effect/Layer";
29import * as ManagedRuntime from "effect/ManagedRuntime";
30import * as PubSub from "effect/PubSub";
31import * as Scope from "effect/Scope";
32import * as Stream from "effect/Stream";
2533import { afterEach, describe, expect, it } from "vitest";
2634
2735import { OrchestrationEventStoreLive } from "../../persistence/Layers/OrchestrationEventStore.ts";
......@@ -165,17 +173,17 @@ async function waitForThread(
165173 timeoutMs = 2000,
166174 threadId: ThreadId = asThreadId("thread-1"),
167175) {
168 const deadline = Date.now() + timeoutMs;
176 const deadline = (await Effect.runPromise(Clock.currentTimeMillis)) + timeoutMs;
169177 const poll = async (): Promise<ProviderRuntimeTestThread> => {
170178 const snapshot = await readModel();
171179 const thread = snapshot.threads.find((entry) => entry.id === threadId);
172180 if (thread && predicate(thread)) {
173181 return thread;
174182 }
175 if (Date.now() >= deadline) {
183 if ((await Effect.runPromise(Clock.currentTimeMillis)) >= deadline) {
176184 throw new Error("Timed out waiting for thread state");
177185 }
178 await new Promise((resolve) => setTimeout(resolve, 10));
186 await Effect.runPromise(Effect.yieldNow);
179187 return poll();
180188 };
181189 return poll();
......@@ -242,7 +250,7 @@ describe("ProviderRuntimeIngestion", () => {
242250 await Effect.runPromise(ingestion.start().pipe(Scope.provide(scope)));
243251 const drain = () => Effect.runPromise(ingestion.drain);
244252
245 const createdAt = new Date().toISOString();
253 const createdAt = "2026-01-01T00:00:00.000Z";
246254 await Effect.runPromise(
247255 engine.dispatch({
248256 type: "project.create",
......@@ -312,7 +320,7 @@ describe("ProviderRuntimeIngestion", () => {
312320
313321 it("maps turn started/completed events into thread session updates", async () => {
314322 const harness = await createHarness();
315 const now = new Date().toISOString();
323 const now = "2026-01-01T00:00:00.000Z";
316324
317325 harness.emit({
318326 type: "turn.started",
......@@ -333,7 +341,7 @@ describe("ProviderRuntimeIngestion", () => {
333341 eventId: asEventId("evt-turn-completed"),
334342 provider: ProviderDriverKind.make("codex"),
335343 threadId: asThreadId("thread-1"),
336 createdAt: new Date().toISOString(),
344 createdAt: "2026-01-01T00:00:00.000Z",
337345 turnId: asTurnId("turn-1"),
338346 payload: {
339347 state: "failed",
......@@ -354,7 +362,7 @@ describe("ProviderRuntimeIngestion", () => {
354362
355363 it("applies provider session.state.changed transitions directly", async () => {
356364 const harness = await createHarness();
357 const waitingAt = new Date().toISOString();
365 const waitingAt = "2026-01-01T00:00:00.000Z";
358366
359367 harness.emit({
360368 type: "session.state.changed",
......@@ -380,7 +388,7 @@ describe("ProviderRuntimeIngestion", () => {
380388 eventId: asEventId("evt-session-state-error"),
381389 provider: ProviderDriverKind.make("codex"),
382390 threadId: asThreadId("thread-1"),
383 createdAt: new Date().toISOString(),
391 createdAt: "2026-01-01T00:00:00.000Z",
384392 payload: {
385393 state: "error",
386394 reason: "provider crashed",
......@@ -402,7 +410,7 @@ describe("ProviderRuntimeIngestion", () => {
402410 eventId: asEventId("evt-session-state-stopped"),
403411 provider: ProviderDriverKind.make("codex"),
404412 threadId: asThreadId("thread-1"),
405 createdAt: new Date().toISOString(),
413 createdAt: "2026-01-01T00:00:00.000Z",
406414 payload: {
407415 state: "stopped",
408416 },
......@@ -423,7 +431,7 @@ describe("ProviderRuntimeIngestion", () => {
423431 eventId: asEventId("evt-session-state-ready"),
424432 provider: ProviderDriverKind.make("codex"),
425433 threadId: asThreadId("thread-1"),
426 createdAt: new Date().toISOString(),
434 createdAt: "2026-01-01T00:00:00.000Z",
427435 payload: {
428436 state: "ready",
429437 },
......@@ -442,7 +450,7 @@ describe("ProviderRuntimeIngestion", () => {
442450
443451 it("does not clear active turn when session/thread started arrives mid-turn", async () => {
444452 const harness = await createHarness();
445 const now = new Date().toISOString();
453 const now = "2026-01-01T00:00:00.000Z";
446454
447455 harness.emit({
448456 type: "turn.started",
......@@ -464,14 +472,14 @@ describe("ProviderRuntimeIngestion", () => {
464472 type: "thread.started",
465473 eventId: asEventId("evt-thread-started-midturn-lifecycle"),
466474 provider: ProviderDriverKind.make("codex"),
467 createdAt: new Date().toISOString(),
475 createdAt: "2026-01-01T00:00:00.000Z",
468476 threadId: asThreadId("thread-1"),
469477 });
470478 harness.emit({
471479 type: "session.started",
472480 eventId: asEventId("evt-session-started-midturn-lifecycle"),
473481 provider: ProviderDriverKind.make("codex"),
474 createdAt: new Date().toISOString(),
482 createdAt: "2026-01-01T00:00:00.000Z",
475483 threadId: asThreadId("thread-1"),
476484 });
477485
......@@ -485,7 +493,7 @@ describe("ProviderRuntimeIngestion", () => {
485493 type: "turn.completed",
486494 eventId: asEventId("evt-turn-completed-midturn-lifecycle"),
487495 provider: ProviderDriverKind.make("codex"),
488 createdAt: new Date().toISOString(),
496 createdAt: "2026-01-01T00:00:00.000Z",
489497 threadId: asThreadId("thread-1"),
490498 turnId: asTurnId("turn-midturn-lifecycle"),
491499 status: "completed",
......@@ -499,7 +507,7 @@ describe("ProviderRuntimeIngestion", () => {
499507
500508 it("accepts claude turn lifecycle when seeded thread id is a synthetic placeholder", async () => {
501509 const harness = await createHarness();
502 const seededAt = new Date().toISOString();
510 const seededAt = "2026-01-01T00:00:00.000Z";
503511
504512 await Effect.runPromise(
505513 harness.engine.dispatch({
......@@ -523,7 +531,7 @@ describe("ProviderRuntimeIngestion", () => {
523531 type: "turn.started",
524532 eventId: asEventId("evt-turn-started-claude-placeholder"),
525533 provider: ProviderDriverKind.make("claudeAgent"),
526 createdAt: new Date().toISOString(),
534 createdAt: "2026-01-01T00:00:00.000Z",
527535 threadId: asThreadId("thread-1"),
528536 turnId: asTurnId("turn-claude-placeholder"),
529537 });
......@@ -539,7 +547,7 @@ describe("ProviderRuntimeIngestion", () => {
539547 type: "turn.completed",
540548 eventId: asEventId("evt-turn-completed-claude-placeholder"),
541549 provider: ProviderDriverKind.make("claudeAgent"),
542 createdAt: new Date().toISOString(),
550 createdAt: "2026-01-01T00:00:00.000Z",
543551 threadId: asThreadId("thread-1"),
544552 turnId: asTurnId("turn-claude-placeholder"),
545553 status: "completed",
......@@ -553,7 +561,7 @@ describe("ProviderRuntimeIngestion", () => {
553561
554562 it("ignores auxiliary turn completions from a different provider thread", async () => {
555563 const harness = await createHarness();
556 const now = new Date().toISOString();
564 const now = "2026-01-01T00:00:00.000Z";
557565
558566 harness.emit({
559567 type: "turn.started",
......@@ -574,7 +582,7 @@ describe("ProviderRuntimeIngestion", () => {
574582 type: "turn.completed",
575583 eventId: asEventId("evt-turn-completed-aux"),
576584 provider: ProviderDriverKind.make("codex"),
577 createdAt: new Date().toISOString(),
585 createdAt: "2026-01-01T00:00:00.000Z",
578586 threadId: asThreadId("thread-1"),
579587 turnId: asTurnId("turn-aux"),
580588 status: "completed",
......@@ -590,7 +598,7 @@ describe("ProviderRuntimeIngestion", () => {
590598 type: "turn.completed",
591599 eventId: asEventId("evt-turn-completed-primary"),
592600 provider: ProviderDriverKind.make("codex"),
593 createdAt: new Date().toISOString(),
601 createdAt: "2026-01-01T00:00:00.000Z",
594602 threadId: asThreadId("thread-1"),
595603 turnId: asTurnId("turn-primary"),
596604 status: "completed",
......@@ -604,7 +612,7 @@ describe("ProviderRuntimeIngestion", () => {
604612
605613 it("ignores non-active turn completion when runtime omits thread id", async () => {
606614 const harness = await createHarness();
607 const now = new Date().toISOString();
615 const now = "2026-01-01T00:00:00.000Z";
608616
609617 harness.emit({
610618 type: "turn.started",
......@@ -626,7 +634,7 @@ describe("ProviderRuntimeIngestion", () => {
626634 type: "turn.completed",
627635 eventId: asEventId("evt-turn-completed-guarded-other"),
628636 provider: ProviderDriverKind.make("codex"),
629 createdAt: new Date().toISOString(),
637 createdAt: "2026-01-01T00:00:00.000Z",
630638 threadId: asThreadId("thread-1"),
631639 turnId: asTurnId("turn-guarded-other"),
632640 status: "completed",
......@@ -642,7 +650,7 @@ describe("ProviderRuntimeIngestion", () => {
642650 type: "turn.completed",
643651 eventId: asEventId("evt-turn-completed-guarded-main"),
644652 provider: ProviderDriverKind.make("codex"),
645 createdAt: new Date().toISOString(),
653 createdAt: "2026-01-01T00:00:00.000Z",
646654 threadId: asThreadId("thread-1"),
647655 turnId: asTurnId("turn-guarded-main"),
648656 status: "completed",
......@@ -656,7 +664,7 @@ describe("ProviderRuntimeIngestion", () => {
656664
657665 it("maps canonical content delta/item completed into finalized assistant messages", async () => {
658666 const harness = await createHarness();
659 const now = new Date().toISOString();
667 const now = "2026-01-01T00:00:00.000Z";
660668
661669 harness.emit({
662670 type: "content.delta",
......@@ -713,7 +721,7 @@ describe("ProviderRuntimeIngestion", () => {
713721
714722 it("uses assistant item completion detail when no assistant deltas were streamed", async () => {
715723 const harness = await createHarness();
716 const now = new Date().toISOString();
724 const now = "2026-01-01T00:00:00.000Z";
717725
718726 harness.emit({
719727 type: "item.completed",
......@@ -745,7 +753,7 @@ describe("ProviderRuntimeIngestion", () => {
745753
746754 it("preserves completed tool metadata on projected tool activities", async () => {
747755 const harness = await createHarness();
748 const now = new Date().toISOString();
756 const now = "2026-01-01T00:00:00.000Z";
749757
750758 harness.emit({
751759 type: "item.completed",
......@@ -801,7 +809,7 @@ describe("ProviderRuntimeIngestion", () => {
801809
802810 it("normalizes command execution activities to ran-command summaries", async () => {
803811 const harness = await createHarness();
804 const now = new Date().toISOString();
812 const now = "2026-01-01T00:00:00.000Z";
805813
806814 harness.emit({
807815 type: "item.completed",
......@@ -843,7 +851,7 @@ describe("ProviderRuntimeIngestion", () => {
843851
844852 it("uses structured read-file paths when available", async () => {
845853 const harness = await createHarness();
846 const now = new Date().toISOString();
854 const now = "2026-01-01T00:00:00.000Z";
847855
848856 harness.emit({
849857 type: "item.completed",
......@@ -885,7 +893,7 @@ describe("ProviderRuntimeIngestion", () => {
885893
886894 it("projects completed plan items into first-class proposed plans", async () => {
887895 const harness = await createHarness();
888 const now = new Date().toISOString();
896 const now = "2026-01-01T00:00:00.000Z";
889897
890898 harness.emit({
891899 type: "turn.proposed.completed",
......@@ -919,7 +927,7 @@ describe("ProviderRuntimeIngestion", () => {
919927 const targetThreadId = asThreadId("thread-implement");
920928 const sourceTurnId = asTurnId("turn-plan-source");
921929 const targetTurnId = asTurnId("turn-plan-implement");
922 const createdAt = new Date().toISOString();
930 const createdAt = "2026-01-01T00:00:00.000Z";
923931
924932 await Effect.runPromise(
925933 harness.engine.dispatch({
......@@ -1050,7 +1058,7 @@ describe("ProviderRuntimeIngestion", () => {
10501058 },
10511059 interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE,
10521060 runtimeMode: "approval-required",
1053 createdAt: new Date().toISOString(),
1061 createdAt: "2026-01-01T00:00:00.000Z",
10541062 }),
10551063 );
10561064
......@@ -1075,7 +1083,7 @@ describe("ProviderRuntimeIngestion", () => {
10751083 type: "turn.started",
10761084 eventId: asEventId("evt-plan-target-started"),
10771085 provider: ProviderDriverKind.make("codex"),
1078 createdAt: new Date().toISOString(),
1086 createdAt: "2026-01-01T00:00:00.000Z",
10791087 threadId: targetThreadId,
10801088 turnId: targetTurnId,
10811089 });
......@@ -1106,7 +1114,7 @@ describe("ProviderRuntimeIngestion", () => {
11061114 const sourceTurnId = asTurnId("turn-plan-source");
11071115 const activeTurnId = asTurnId("turn-already-running");
11081116 const staleTurnId = asTurnId("turn-stale-start");
1109 const createdAt = new Date().toISOString();
1117 const createdAt = "2026-01-01T00:00:00.000Z";
11101118
11111119 await Effect.runPromise(
11121120 harness.engine.dispatch({
......@@ -1219,7 +1227,7 @@ describe("ProviderRuntimeIngestion", () => {
12191227 },
12201228 interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE,
12211229 runtimeMode: "approval-required",
1222 createdAt: new Date().toISOString(),
1230 createdAt: "2026-01-01T00:00:00.000Z",
12231231 }),
12241232 );
12251233
......@@ -1227,7 +1235,7 @@ describe("ProviderRuntimeIngestion", () => {
12271235 type: "turn.started",
12281236 eventId: asEventId("evt-turn-started-stale-plan-implementation"),
12291237 provider: ProviderDriverKind.make("codex"),
1230 createdAt: new Date().toISOString(),
1238 createdAt: "2026-01-01T00:00:00.000Z",
12311239 threadId: targetThreadId,
12321240 turnId: staleTurnId,
12331241 });
......@@ -1259,7 +1267,7 @@ describe("ProviderRuntimeIngestion", () => {
12591267 const sourceTurnId = asTurnId("turn-plan-source");
12601268 const expectedTurnId = asTurnId("turn-plan-implement");
12611269 const replayedTurnId = asTurnId("turn-replayed");
1262 const createdAt = new Date().toISOString();
1270 const createdAt = "2026-01-01T00:00:00.000Z";
12631271
12641272 await Effect.runPromise(
12651273 harness.engine.dispatch({
......@@ -1381,7 +1389,7 @@ describe("ProviderRuntimeIngestion", () => {
13811389 },
13821390 interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE,
13831391 runtimeMode: "approval-required",
1384 createdAt: new Date().toISOString(),
1392 createdAt: "2026-01-01T00:00:00.000Z",
13851393 }),
13861394 );
13871395
......@@ -1399,7 +1407,7 @@ describe("ProviderRuntimeIngestion", () => {
13991407 type: "turn.started",
14001408 eventId: asEventId("evt-turn-started-unrelated-plan-implementation"),
14011409 provider: ProviderDriverKind.make("codex"),
1402 createdAt: new Date().toISOString(),
1410 createdAt: "2026-01-01T00:00:00.000Z",
14031411 threadId: targetThreadId,
14041412 turnId: replayedTurnId,
14051413 });
......@@ -1420,7 +1428,7 @@ describe("ProviderRuntimeIngestion", () => {
14201428
14211429 it("finalizes buffered proposed-plan deltas into a first-class proposed plan on turn completion", async () => {
14221430 const harness = await createHarness();
1423 const now = new Date().toISOString();
1431 const now = "2026-01-01T00:00:00.000Z";
14241432
14251433 harness.emit({
14261434 type: "turn.started",
......@@ -1486,7 +1494,7 @@ describe("ProviderRuntimeIngestion", () => {
14861494
14871495 it("buffers assistant deltas by default until completion", async () => {
14881496 const harness = await createHarness();
1489 const now = new Date().toISOString();
1497 const now = "2026-01-01T00:00:00.000Z";
14901498
14911499 harness.emit({
14921500 type: "turn.started",
......@@ -1554,7 +1562,7 @@ describe("ProviderRuntimeIngestion", () => {
15541562
15551563 it("flushes and completes buffered assistant text when an approval request opens", async () => {
15561564 const harness = await createHarness();
1557 const now = new Date().toISOString();
1565 const now = "2026-01-01T00:00:00.000Z";
15581566
15591567 harness.emit({
15601568 type: "turn.started",
......@@ -1614,7 +1622,7 @@ describe("ProviderRuntimeIngestion", () => {
16141622
16151623 it("flushes and completes buffered assistant text when user input is requested", async () => {
16161624 const harness = await createHarness();
1617 const now = new Date().toISOString();
1625 const now = "2026-01-01T00:00:00.000Z";
16181626
16191627 harness.emit({
16201628 type: "turn.started",
......@@ -1980,7 +1988,7 @@ describe("ProviderRuntimeIngestion", () => {
19801988
19811989 it("streams assistant deltas when thread.turn.start requests streaming mode", async () => {
19821990 const harness = await createHarness({ serverSettings: { enableAssistantStreaming: true } });
1983 const now = new Date().toISOString();
1991 const now = "2026-01-01T00:00:00.000Z";
19841992
19851993 await Effect.runPromise(
19861994 harness.engine.dispatch({
......@@ -2072,7 +2080,7 @@ describe("ProviderRuntimeIngestion", () => {
20722080
20732081 it("spills oversized buffered deltas and still finalizes full assistant text", async () => {
20742082 const harness = await createHarness();
2075 const now = new Date().toISOString();
2083 const now = "2026-01-01T00:00:00.000Z";
20762084 const oversizedText = "x".repeat(40_000);
20772085
20782086 harness.emit({
......@@ -2133,7 +2141,7 @@ describe("ProviderRuntimeIngestion", () => {
21332141
21342142 it("does not duplicate assistant completion when item.completed is followed by turn.completed", async () => {
21352143 const harness = await createHarness();
2136 const now = new Date().toISOString();
2144 const now = "2026-01-01T00:00:00.000Z";
21372145
21382146 harness.emit({
21392147 type: "turn.started",
......@@ -2219,7 +2227,7 @@ describe("ProviderRuntimeIngestion", () => {
22192227
22202228 it("maps canonical request events into approval activities with requestKind", async () => {
22212229 const harness = await createHarness();
2222 const now = new Date().toISOString();
2230 const now = "2026-01-01T00:00:00.000Z";
22232231
22242232 harness.emit({
22252233 type: "request.opened",
......@@ -2285,7 +2293,7 @@ describe("ProviderRuntimeIngestion", () => {
22852293
22862294 it("maps runtime.error into errored session state", async () => {
22872295 const harness = await createHarness();
2288 const now = new Date().toISOString();
2296 const now = "2026-01-01T00:00:00.000Z";
22892297
22902298 harness.emit({
22912299 type: "runtime.error",
......@@ -2312,7 +2320,7 @@ describe("ProviderRuntimeIngestion", () => {
23122320
23132321 it("records runtime.error activities from the typed payload message", async () => {
23142322 const harness = await createHarness();
2315 const now = new Date().toISOString();
2323 const now = "2026-01-01T00:00:00.000Z";
23162324
23172325 harness.emit({
23182326 type: "runtime.error",
......@@ -2343,7 +2351,7 @@ describe("ProviderRuntimeIngestion", () => {
23432351
23442352 it("keeps the session running when a runtime.warning arrives during an active turn", async () => {
23452353 const harness = await createHarness();
2346 const now = new Date().toISOString();
2354 const now = "2026-01-01T00:00:00.000Z";
23472355
23482356 harness.emit({
23492357 type: "turn.started",
......@@ -2387,7 +2395,7 @@ describe("ProviderRuntimeIngestion", () => {
23872395
23882396 it("maps session/thread lifecycle and item.started into session/activity projections", async () => {
23892397 const harness = await createHarness();
2390 const now = new Date().toISOString();
2398 const now = "2026-01-01T00:00:00.000Z";
23912399
23922400 harness.emit({
23932401 type: "session.started",
......@@ -2439,7 +2447,7 @@ describe("ProviderRuntimeIngestion", () => {
24392447
24402448 it("consumes P1 runtime events into thread metadata, diff checkpoints, and activities", async () => {
24412449 const harness = await createHarness();
2442 const now = new Date().toISOString();
2450 const now = "2026-01-01T00:00:00.000Z";
24432451
24442452 harness.emit({
24452453 type: "thread.metadata.updated",
......@@ -2573,7 +2581,7 @@ describe("ProviderRuntimeIngestion", () => {
25732581
25742582 it("projects context window updates into normalized thread activities", async () => {
25752583 const harness = await createHarness();
2576 const now = new Date().toISOString();
2584 const now = "2026-01-01T00:00:00.000Z";
25772585
25782586 harness.emit({
25792587 type: "thread.token-usage.updated",
......@@ -2625,7 +2633,7 @@ describe("ProviderRuntimeIngestion", () => {
26252633
26262634 it("projects Codex camelCase token usage payloads into normalized thread activities", async () => {
26272635 const harness = await createHarness();
2628 const now = new Date().toISOString();
2636 const now = "2026-01-01T00:00:00.000Z";
26292637
26302638 harness.emit({
26312639 type: "thread.token-usage.updated",
......@@ -2678,7 +2686,7 @@ describe("ProviderRuntimeIngestion", () => {
26782686
26792687 it("projects Claude usage snapshots with context window into normalized thread activities", async () => {
26802688 const harness = await createHarness();
2681 const now = new Date().toISOString();
2689 const now = "2026-01-01T00:00:00.000Z";
26822690
26832691 harness.emit({
26842692 type: "thread.token-usage.updated",
......@@ -2722,7 +2730,7 @@ describe("ProviderRuntimeIngestion", () => {
27222730
27232731 it("projects compacted thread state into context compaction activities", async () => {
27242732 const harness = await createHarness();
2725 const now = new Date().toISOString();
2733 const now = "2026-01-01T00:00:00.000Z";
27262734
27272735 harness.emit({
27282736 type: "thread.state.changed",
......@@ -2752,7 +2760,7 @@ describe("ProviderRuntimeIngestion", () => {
27522760
27532761 it("projects Codex task lifecycle chunks into thread activities", async () => {
27542762 const harness = await createHarness();
2755 const now = new Date().toISOString();
2763 const now = "2026-01-01T00:00:00.000Z";
27562764
27572765 harness.emit({
27582766 type: "task.started",
......@@ -2855,7 +2863,7 @@ describe("ProviderRuntimeIngestion", () => {
28552863
28562864 it("projects structured user input request and resolution as thread activities", async () => {
28572865 const harness = await createHarness();
2858 const now = new Date().toISOString();
2866 const now = "2026-01-01T00:00:00.000Z";
28592867
28602868 harness.emit({
28612869 type: "user-input.requested",
......@@ -2886,7 +2894,7 @@ describe("ProviderRuntimeIngestion", () => {
28862894 type: "user-input.resolved",
28872895 eventId: asEventId("evt-user-input-resolved"),
28882896 provider: ProviderDriverKind.make("codex"),
2889 createdAt: new Date().toISOString(),
2897 createdAt: "2026-01-01T00:00:00.000Z",
28902898 threadId: asThreadId("thread-1"),
28912899 turnId: asTurnId("turn-user-input"),
28922900 requestId: ApprovalRequestId.make("req-user-input-1"),
......@@ -2928,7 +2936,7 @@ describe("ProviderRuntimeIngestion", () => {
29282936
29292937 it("continues processing runtime events after a single event handler failure", async () => {
29302938 const harness = await createHarness();
2931 const now = new Date().toISOString();
2939 const now = "2026-01-01T00:00:00.000Z";
29322940
29332941 harness.emit({
29342942 type: "content.delta",
......@@ -2948,7 +2956,7 @@ describe("ProviderRuntimeIngestion", () => {
29482956 type: "runtime.error",
29492957 eventId: asEventId("evt-runtime-error-after-failure"),
29502958 provider: ProviderDriverKind.make("codex"),
2951 createdAt: new Date().toISOString(),
2959 createdAt: "2026-01-01T00:00:00.000Z",
29522960 threadId: asThreadId("thread-1"),
29532961 turnId: asTurnId("turn-after-failure"),
29542962 payload: {
apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts+7-1
......@@ -17,7 +17,13 @@ import {
1717 type OrchestrationThreadActivity,
1818 type ProviderRuntimeEvent,
1919} from "@t3tools/contracts";
20import { Cache, Cause, Duration, Effect, Layer, Option, Stream } from "effect";
20import * as Cache from "effect/Cache";
21import * as Cause from "effect/Cause";
22import * as Duration from "effect/Duration";
23import * as Effect from "effect/Effect";
24import * as Layer from "effect/Layer";
25import * as Option from "effect/Option";
26import * as Stream from "effect/Stream";
2127import { makeDrainableWorker } from "@t3tools/shared/DrainableWorker";
2228
2329import { ProviderService } from "../../provider/Services/ProviderService.ts";
apps/server/src/orchestration/Layers/RuntimeReceiptBus.ts+4-1
......@@ -8,7 +8,10 @@
88 *
99 * @module RuntimeReceiptBus
1010 */
11import { Effect, Layer, PubSub, Stream } from "effect";
11import * as Effect from "effect/Effect";
12import * as Layer from "effect/Layer";
13import * as PubSub from "effect/PubSub";
14import * as Stream from "effect/Stream";
1215
1316import {
1417 RuntimeReceiptBus,
apps/server/src/orchestration/Layers/ThreadDeletionReactor.test.ts+3-1
......@@ -1,5 +1,7 @@
11import { ThreadId } from "@t3tools/contracts";
2import { Cause, Effect, Exit } from "effect";
2import * as Cause from "effect/Cause";
3import * as Effect from "effect/Effect";
4import * as Exit from "effect/Exit";
35import { describe, expect, it } from "vitest";
46
57import { logCleanupCauseUnlessInterrupted } from "./ThreadDeletionReactor.ts";
apps/server/src/orchestration/Layers/ThreadDeletionReactor.ts+4-1
......@@ -1,6 +1,9 @@
11import type { OrchestrationEvent } from "@t3tools/contracts";
22import { makeDrainableWorker } from "@t3tools/shared/DrainableWorker";
3import { Cause, Effect, Layer, Stream } from "effect";
3import * as Cause from "effect/Cause";
4import * as Effect from "effect/Effect";
5import * as Layer from "effect/Layer";
6import * as Stream from "effect/Stream";
47
58import { ProviderService } from "../../provider/Services/ProviderService.ts";
69import { TerminalManager } from "../../terminal/Services/Manager.ts";
apps/server/src/orchestration/Normalizer.ts+3-1
......@@ -1,4 +1,6 @@
1import { Effect, FileSystem, Path } from "effect";
1import * as Effect from "effect/Effect";
2import * as FileSystem from "effect/FileSystem";
3import * as Path from "effect/Path";
24import {
35 type ClientOrchestrationCommand,
46 type OrchestrationCommand,
apps/server/src/orchestration/Services/CheckpointReactor.ts+3-2
......@@ -6,8 +6,9 @@
66 *
77 * @module CheckpointReactor
88 */
9import { Context } from "effect";
10import type { Effect, Scope } from "effect";
9import * as Context from "effect/Context";
10import type * as Effect from "effect/Effect";
11import type * as Scope from "effect/Scope";
1112
1213/**
1314 * CheckpointReactorShape - Service API for checkpoint reactor lifecycle.
apps/server/src/orchestration/Services/OrchestrationEngine.ts+3-2
......@@ -11,8 +11,9 @@
1111 * @module OrchestrationEngineService
1212 */
1313import type { OrchestrationCommand, OrchestrationEvent } from "@t3tools/contracts";
14import { Context } from "effect";
15import type { Effect, Stream } from "effect";
14import * as Context from "effect/Context";
15import type * as Effect from "effect/Effect";
16import type * as Stream from "effect/Stream";
1617
1718import type { OrchestrationDispatchError } from "../Errors.ts";
1819import type { OrchestrationEventStoreError } from "../../persistence/Errors.ts";
apps/server/src/orchestration/Services/OrchestrationReactor.ts+3-2
......@@ -6,8 +6,9 @@
66 *
77 * @module OrchestrationReactor
88 */
9import { Context } from "effect";
10import type { Effect, Scope } from "effect";
9import * as Context from "effect/Context";
10import type * as Effect from "effect/Effect";
11import type * as Scope from "effect/Scope";
1112
1213/**
1314 * OrchestrationReactorShape - Service API for orchestration reactor lifecycle.
apps/server/src/orchestration/Services/ProjectionPipeline.ts+2-2
......@@ -7,8 +7,8 @@
77 * @module OrchestrationProjectionPipeline
88 */
99import type { OrchestrationEvent } from "@t3tools/contracts";
10import { Context } from "effect";
11import type { Effect } from "effect";
10import * as Context from "effect/Context";
11import type * as Effect from "effect/Effect";
1212
1313import type { ProjectionRepositoryError } from "../../persistence/Errors.ts";
1414
apps/server/src/orchestration/Services/ProjectionSnapshotQuery.ts+3-3
......@@ -17,9 +17,9 @@ import type {
1717 ProjectId,
1818 ThreadId,
1919} from "@t3tools/contracts";
20import { Context } from "effect";
21import type { Option } from "effect";
22import type { Effect } from "effect";
20import * as Context from "effect/Context";
21import type * as Option from "effect/Option";
22import type * as Effect from "effect/Effect";
2323
2424import type { ProjectionRepositoryError } from "../../persistence/Errors.ts";
2525
apps/server/src/orchestration/Services/ProviderCommandReactor.ts+3-2
......@@ -6,8 +6,9 @@
66 *
77 * @module ProviderCommandReactor
88 */
9import { Context } from "effect";
10import type { Effect, Scope } from "effect";
9import * as Context from "effect/Context";
10import type * as Effect from "effect/Effect";
11import type * as Scope from "effect/Scope";
1112
1213/**
1314 * ProviderCommandReactorShape - Service API for provider command reactors.
apps/server/src/orchestration/Services/ProviderRuntimeIngestion.ts+3-2
......@@ -6,8 +6,9 @@
66 *
77 * @module ProviderRuntimeIngestionService
88 */
9import { Context } from "effect";
10import type { Effect, Scope } from "effect";
9import * as Context from "effect/Context";
10import type * as Effect from "effect/Effect";
11import type * as Scope from "effect/Scope";
1112
1213/**
1314 * ProviderRuntimeIngestionShape - Service API for runtime ingestion lifecycle.
apps/server/src/orchestration/Services/RuntimeReceiptBus.ts+4-2
......@@ -15,8 +15,10 @@
1515 * @module RuntimeReceiptBus
1616 */
1717import { CheckpointRef, IsoDateTime, NonNegativeInt, ThreadId, TurnId } from "@t3tools/contracts";
18import { Schema, Context } from "effect";
19import type { Effect, Stream } from "effect";
18import * as Schema from "effect/Schema";
19import * as Context from "effect/Context";
20import type * as Effect from "effect/Effect";
21import type * as Stream from "effect/Stream";
2022
2123export const CheckpointBaselineCapturedReceipt = Schema.Struct({
2224 type: Schema.Literal("checkpoint.baseline.captured"),
apps/server/src/orchestration/Services/ThreadDeletionReactor.ts+3-2
......@@ -6,8 +6,9 @@
66 *
77 * @module ThreadDeletionReactor
88 */
9import { Context } from "effect";
10import type { Effect, Scope } from "effect";
9import * as Context from "effect/Context";
10import type * as Effect from "effect/Effect";
11import type * as Scope from "effect/Scope";
1112
1213/**
1314 * ThreadDeletionReactorShape - Service API for thread deletion cleanup.
apps/server/src/orchestration/commandInvariants.test.ts+2-2
......@@ -9,7 +9,7 @@ import {
99 type OrchestrationReadModel,
1010 ProviderInstanceId,
1111} from "@t3tools/contracts";
12import { Effect } from "effect";
12import * as Effect from "effect/Effect";
1313
1414import {
1515 findThreadById,
......@@ -19,7 +19,7 @@ import {
1919 requireThreadAbsent,
2020} from "./commandInvariants.ts";
2121
22const now = new Date().toISOString();
22const now = "2026-01-01T00:00:00.000Z";
2323
2424const readModel: OrchestrationReadModel = {
2525 snapshotSequence: 2,
apps/server/src/orchestration/commandInvariants.ts+1-1
......@@ -6,7 +6,7 @@ import type {
66 ProjectId,
77 ThreadId,
88} from "@t3tools/contracts";
9import { Effect } from "effect";
9import * as Effect from "effect/Effect";
1010
1111import { OrchestrationCommandInvariantError } from "./Errors.ts";
1212
apps/server/src/orchestration/decider.delete.test.ts+2-2
......@@ -9,7 +9,7 @@ import {
99 type OrchestrationReadModel,
1010 ProviderInstanceId,
1111} from "@t3tools/contracts";
12import { Effect } from "effect";
12import * as Effect from "effect/Effect";
1313import { describe, expect, it } from "vitest";
1414
1515import { decideOrchestrationCommand } from "./decider.ts";
......@@ -21,7 +21,7 @@ const asProjectId = (value: string): ProjectId => ProjectId.make(value);
2121const asThreadId = (value: string): ThreadId => ThreadId.make(value);
2222
2323async function seedReadModel(): Promise<OrchestrationReadModel> {
24 const now = new Date().toISOString();
24 const now = "2026-01-01T00:00:00.000Z";
2525 const initial = createEmptyReadModel(now);
2626 const withProject = await Effect.runPromise(
2727 projectEvent(initial, {
apps/server/src/orchestration/decider.projectScripts.test.ts+6-6
......@@ -9,7 +9,7 @@ import {
99} from "@t3tools/contracts";
1010import { createModelSelection } from "@t3tools/shared/model";
1111import { describe, expect, it } from "vitest";
12import { Effect } from "effect";
12import * as Effect from "effect/Effect";
1313
1414import { decideOrchestrationCommand } from "./decider.ts";
1515import { createEmptyReadModel, projectEvent } from "./projector.ts";
......@@ -20,7 +20,7 @@ const asMessageId = (value: string): MessageId => MessageId.make(value);
2020
2121describe("decider project scripts", () => {
2222 it("emits empty scripts on project.create", async () => {
23 const now = new Date().toISOString();
23 const now = "2026-01-01T00:00:00.000Z";
2424 const readModel = createEmptyReadModel(now);
2525
2626 const result = await Effect.runPromise(
......@@ -43,7 +43,7 @@ describe("decider project scripts", () => {
4343 });
4444
4545 it("propagates scripts in project.meta.update payload", async () => {
46 const now = new Date().toISOString();
46 const now = "2026-01-01T00:00:00.000Z";
4747 const initial = createEmptyReadModel(now);
4848 const readModel = await Effect.runPromise(
4949 projectEvent(initial, {
......@@ -97,7 +97,7 @@ describe("decider project scripts", () => {
9797 });
9898
9999 it("emits user message and turn-start-requested events for thread.turn.start", async () => {
100 const now = new Date().toISOString();
100 const now = "2026-01-01T00:00:00.000Z";
101101 const initial = createEmptyReadModel(now);
102102 const withProject = await Effect.runPromise(
103103 projectEvent(initial, {
......@@ -198,7 +198,7 @@ describe("decider project scripts", () => {
198198 });
199199
200200 it("emits thread.runtime-mode-set from thread.runtime-mode.set", async () => {
201 const now = new Date().toISOString();
201 const now = "2026-01-01T00:00:00.000Z";
202202 const initial = createEmptyReadModel(now);
203203 const withProject = await Effect.runPromise(
204204 projectEvent(initial, {
......@@ -280,7 +280,7 @@ describe("decider project scripts", () => {
280280 });
281281
282282 it("emits thread.interaction-mode-set from thread.interaction-mode.set", async () => {
283 const now = new Date().toISOString();
283 const now = "2026-01-01T00:00:00.000Z";
284284 const initial = createEmptyReadModel(now);
285285 const withProject = await Effect.runPromise(
286286 projectEvent(initial, {
apps/server/src/orchestration/decider.ts+11-10
......@@ -3,7 +3,8 @@ import type {
33 OrchestrationEvent,
44 OrchestrationReadModel,
55} from "@t3tools/contracts";
6import { DateTime, Effect } from "effect";
6import * as DateTime from "effect/DateTime";
7import * as Effect from "effect/Effect";
78
89import { OrchestrationCommandInvariantError } from "./Errors.ts";
910import {
......@@ -17,7 +18,7 @@ import {
1718} from "./commandInvariants.ts";
1819import { projectEvent } from "./projector.ts";
1920
20const currentIso = DateTime.now.pipe(Effect.map(DateTime.formatIso));
21const nowIso = Effect.map(DateTime.now, DateTime.formatIso);
2122
2223function withEventBase(
2324 input: Pick<OrchestrationCommand, "commandId"> & {
......@@ -116,7 +117,7 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand"
116117 command,
117118 projectId: command.projectId,
118119 });
119 const occurredAt = yield* currentIso;
120 const occurredAt = yield* nowIso;
120121 return {
121122 ...withEventBase({
122123 aggregateKind: "project",
......@@ -173,7 +174,7 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand"
173174 });
174175 }
175176
176 const occurredAt = yield* currentIso;
177 const occurredAt = yield* nowIso;
177178 return {
178179 ...withEventBase({
179180 aggregateKind: "project",
......@@ -229,7 +230,7 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand"
229230 command,
230231 threadId: command.threadId,
231232 });
232 const occurredAt = yield* currentIso;
233 const occurredAt = yield* nowIso;
233234 return {
234235 ...withEventBase({
235236 aggregateKind: "thread",
......@@ -251,7 +252,7 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand"
251252 command,
252253 threadId: command.threadId,
253254 });
254 const occurredAt = yield* currentIso;
255 const occurredAt = yield* nowIso;
255256 return {
256257 ...withEventBase({
257258 aggregateKind: "thread",
......@@ -274,7 +275,7 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand"
274275 command,
275276 threadId: command.threadId,
276277 });
277 const occurredAt = yield* currentIso;
278 const occurredAt = yield* nowIso;
278279 return {
279280 ...withEventBase({
280281 aggregateKind: "thread",
......@@ -296,7 +297,7 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand"
296297 command,
297298 threadId: command.threadId,
298299 });
299 const occurredAt = yield* currentIso;
300 const occurredAt = yield* nowIso;
300301 return {
301302 ...withEventBase({
302303 aggregateKind: "thread",
......@@ -324,7 +325,7 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand"
324325 command,
325326 threadId: command.threadId,
326327 });
327 const occurredAt = yield* currentIso;
328 const occurredAt = yield* nowIso;
328329 return {
329330 ...withEventBase({
330331 aggregateKind: "thread",
......@@ -347,7 +348,7 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand"
347348 command,
348349 threadId: command.threadId,
349350 });
350 const occurredAt = yield* currentIso;
351 const occurredAt = yield* nowIso;
351352 return {
352353 ...withEventBase({
353354 aggregateKind: "thread",
apps/server/src/orchestration/http.ts+1-1
......@@ -4,7 +4,7 @@ import {
44 OrchestrationGetSnapshotError,
55 type OrchestrationReadModel,
66} from "@t3tools/contracts";
7import { Effect } from "effect";
7import * as Effect from "effect/Effect";
88import { HttpRouter, HttpServerRequest, HttpServerResponse } from "effect/unstable/http";
99
1010import { ServerAuth } from "../auth/Services/ServerAuth.ts";
apps/server/src/orchestration/projector.test.ts+6-6
......@@ -6,7 +6,7 @@ import {
66 ThreadId,
77 type OrchestrationEvent,
88} from "@t3tools/contracts";
9import { Effect } from "effect";
9import * as Effect from "effect/Effect";
1010import { describe, expect, it } from "vitest";
1111
1212import { createEmptyReadModel, projectEvent } from "./projector.ts";
......@@ -40,7 +40,7 @@ function makeEvent(input: {
4040
4141describe("orchestration projector", () => {
4242 it("applies thread.created events", async () => {
43 const now = new Date().toISOString();
43 const now = "2026-01-01T00:00:00.000Z";
4444 const model = createEmptyReadModel(now);
4545
4646 const next = await Effect.runPromise(
......@@ -100,7 +100,7 @@ describe("orchestration projector", () => {
100100 });
101101
102102 it("fails when event payload cannot be decoded by runtime schema", async () => {
103 const now = new Date().toISOString();
103 const now = "2026-01-01T00:00:00.000Z";
104104 const model = createEmptyReadModel(now);
105105
106106 await expect(
......@@ -134,8 +134,8 @@ describe("orchestration projector", () => {
134134 });
135135
136136 it("applies thread.archived and thread.unarchived events", async () => {
137 const now = new Date().toISOString();
138 const later = new Date(Date.parse(now) + 1_000).toISOString();
137 const now = "2026-01-01T00:00:00.000Z";
138 const later = "2026-01-01T00:00:01.000Z";
139139 const created = await Effect.runPromise(
140140 projectEvent(
141141 createEmptyReadModel(now),
......@@ -206,7 +206,7 @@ describe("orchestration projector", () => {
206206 });
207207
208208 it("keeps projector forward-compatible for unhandled event types", async () => {
209 const now = new Date().toISOString();
209 const now = "2026-01-01T00:00:00.000Z";
210210 const model = createEmptyReadModel(now);
211211
212212 const next = await Effect.runPromise(
apps/server/src/orchestration/projector.ts+2-1
......@@ -5,7 +5,8 @@ import {
55 OrchestrationSession,
66 OrchestrationThread,
77} from "@t3tools/contracts";
8import { Effect, Schema } from "effect";
8import * as Effect from "effect/Effect";
9import * as Schema from "effect/Schema";
910
1011import { toProjectorDecodeError, type OrchestrationProjectorDecodeError } from "./Errors.ts";
1112import {
apps/server/src/orchestration/runtimeLayer.ts+1-1
......@@ -1,4 +1,4 @@
1import { Layer } from "effect";
1import * as Layer from "effect/Layer";
22
33import { OrchestrationCommandReceiptRepositoryLive } from "../persistence/Layers/OrchestrationCommandReceipts.ts";
44import { OrchestrationEventStoreLive } from "../persistence/Layers/OrchestrationEventStore.ts";
apps/server/src/os-jank.test.ts deleted-185
......@@ -1,185 +0,0 @@
1import { describe, expect, it, vi } from "vitest";
2
3import { fixPath } from "./os-jank.ts";
4
5describe("fixPath", () => {
6 it("hydrates PATH on linux using the resolved login shell", () => {
7 const env: NodeJS.ProcessEnv = {
8 SHELL: "/bin/zsh",
9 PATH: "/Users/test/.local/bin:/usr/bin",
10 };
11 const readPath = vi.fn(() => "/opt/homebrew/bin:/usr/bin");
12
13 fixPath({
14 env,
15 platform: "linux",
16 readPath,
17 });
18
19 expect(readPath).toHaveBeenCalledWith("/bin/zsh");
20 expect(env.PATH).toBe("/opt/homebrew/bin:/usr/bin:/Users/test/.local/bin");
21 });
22
23 it("falls back to launchctl PATH on macOS when shell probing fails", () => {
24 const env: NodeJS.ProcessEnv = {
25 SHELL: "/opt/homebrew/bin/nu",
26 PATH: "/usr/bin",
27 };
28 const readPath = vi
29 .fn()
30 .mockImplementationOnce(() => {
31 throw new Error("unknown flag");
32 })
33 .mockImplementationOnce(() => undefined);
34 const readLaunchctlPath = vi.fn(() => "/opt/homebrew/bin:/usr/bin");
35 const logWarning = vi.fn();
36
37 fixPath({
38 env,
39 platform: "darwin",
40 readPath,
41 readLaunchctlPath,
42 userShell: "/bin/zsh",
43 logWarning,
44 });
45
46 expect(readPath).toHaveBeenNthCalledWith(1, "/opt/homebrew/bin/nu");
47 expect(readPath).toHaveBeenNthCalledWith(2, "/bin/zsh");
48 expect(readLaunchctlPath).toHaveBeenCalledTimes(1);
49 expect(logWarning).toHaveBeenCalledWith(
50 "Failed to read PATH from login shell /opt/homebrew/bin/nu.",
51 expect.any(Error),
52 );
53 expect(env.PATH).toBe("/opt/homebrew/bin:/usr/bin");
54 });
55
56 it("repairs PATH on Windows by merging PowerShell PATH with inherited PATH", () => {
57 const env: NodeJS.ProcessEnv = {
58 PATH: "C:\\Windows\\System32",
59 APPDATA: "C:\\Users\\testuser\\AppData\\Roaming",
60 LOCALAPPDATA: "C:\\Users\\testuser\\AppData\\Local",
61 USERPROFILE: "C:\\Users\\testuser",
62 };
63 const readWindowsEnvironment = vi.fn(() => ({
64 PATH: "C:\\Custom\\Bin;C:\\Windows\\System32",
65 }));
66 const isWindowsCommandAvailable = vi.fn(() => true);
67
68 fixPath({
69 env,
70 platform: "win32",
71 readWindowsEnvironment,
72 isWindowsCommandAvailable,
73 });
74
75 expect(readWindowsEnvironment).toHaveBeenCalledWith(["PATH"], { loadProfile: false });
76 expect(env.PATH).toBe(
77 [
78 "C:\\Users\\testuser\\AppData\\Roaming\\npm",
79 "C:\\Users\\testuser\\AppData\\Local\\Programs\\nodejs",
80 "C:\\Users\\testuser\\AppData\\Local\\Volta\\bin",
81 "C:\\Users\\testuser\\AppData\\Local\\pnpm",
82 "C:\\Users\\testuser\\.bun\\bin",
83 "C:\\Users\\testuser\\scoop\\shims",
84 "C:\\Custom\\Bin",
85 "C:\\Windows\\System32",
86 ].join(";"),
87 );
88 });
89
90 it("applies profile-derived fnm variables on Windows when node is missing", () => {
91 const env: NodeJS.ProcessEnv = {
92 PATH: "C:\\Windows\\System32",
93 APPDATA: "C:\\Users\\testuser\\AppData\\Roaming",
94 LOCALAPPDATA: "C:\\Users\\testuser\\AppData\\Local",
95 USERPROFILE: "C:\\Users\\testuser",
96 };
97 const readWindowsEnvironment = vi.fn(
98 (_names: ReadonlyArray<string>, options?: { loadProfile?: boolean }) =>
99 options?.loadProfile
100 ? {
101 PATH: "C:\\Profile\\Node;C:\\Windows\\System32",
102 FNM_DIR: "C:\\Users\\testuser\\AppData\\Roaming\\fnm",
103 FNM_MULTISHELL_PATH: "C:\\Users\\testuser\\AppData\\Local\\fnm_multishells\\123",
104 }
105 : { PATH: "C:\\Custom\\Bin;C:\\Windows\\System32" },
106 );
107 const isWindowsCommandAvailable = vi.fn().mockReturnValueOnce(false).mockReturnValueOnce(true);
108
109 fixPath({
110 env,
111 platform: "win32",
112 readWindowsEnvironment,
113 isWindowsCommandAvailable,
114 });
115
116 expect(env.PATH).toBe(
117 [
118 "C:\\Profile\\Node",
119 "C:\\Windows\\System32",
120 "C:\\Users\\testuser\\AppData\\Roaming\\npm",
121 "C:\\Users\\testuser\\AppData\\Local\\Programs\\nodejs",
122 "C:\\Users\\testuser\\AppData\\Local\\Volta\\bin",
123 "C:\\Users\\testuser\\AppData\\Local\\pnpm",
124 "C:\\Users\\testuser\\.bun\\bin",
125 "C:\\Users\\testuser\\scoop\\shims",
126 "C:\\Custom\\Bin",
127 ].join(";"),
128 );
129 expect(env.FNM_DIR).toBe("C:\\Users\\testuser\\AppData\\Roaming\\fnm");
130 expect(env.FNM_MULTISHELL_PATH).toBe(
131 "C:\\Users\\testuser\\AppData\\Local\\fnm_multishells\\123",
132 );
133 });
134
135 it("preserves baseline PATH on Windows when the profile probe fails", () => {
136 const env: NodeJS.ProcessEnv = {
137 PATH: "C:\\Windows\\System32",
138 APPDATA: "C:\\Users\\testuser\\AppData\\Roaming",
139 USERPROFILE: "C:\\Users\\testuser",
140 };
141 const readWindowsEnvironment = vi.fn(
142 (_names: ReadonlyArray<string>, options?: { loadProfile?: boolean }) => {
143 if (options?.loadProfile) {
144 throw new Error("profile load failed");
145 }
146 return { PATH: "C:\\Custom\\Bin;C:\\Windows\\System32" };
147 },
148 );
149 const isWindowsCommandAvailable = vi.fn(() => false);
150
151 fixPath({
152 env,
153 platform: "win32",
154 readWindowsEnvironment,
155 isWindowsCommandAvailable,
156 });
157
158 expect(env.PATH).toBe(
159 [
160 "C:\\Users\\testuser\\AppData\\Roaming\\npm",
161 "C:\\Users\\testuser\\.bun\\bin",
162 "C:\\Users\\testuser\\scoop\\shims",
163 "C:\\Custom\\Bin",
164 "C:\\Windows\\System32",
165 ].join(";"),
166 );
167 });
168
169 it("does nothing on unsupported platforms", () => {
170 const env: NodeJS.ProcessEnv = {
171 SHELL: "C:/Program Files/Git/bin/bash.exe",
172 PATH: "C:\\Windows\\System32",
173 };
174 const readPath = vi.fn(() => "/usr/local/bin:/usr/bin");
175
176 fixPath({
177 env,
178 platform: "freebsd",
179 readPath,
180 });
181
182 expect(readPath).not.toHaveBeenCalled();
183 expect(env.PATH).toBe("C:\\Windows\\System32");
184 });
185});
apps/server/src/os-jank.ts+9-6
......@@ -1,5 +1,6 @@
1import * as OS from "node:os";
2import { Effect, Path } from "effect";
1import * as NodeOS from "node:os";
2import * as Effect from "effect/Effect";
3import * as Path from "effect/Path";
34import {
45 readPathFromLoginShell,
56 readEnvironmentFromWindowsShell,
......@@ -17,7 +18,9 @@ type WindowsCommandAvailabilityChecker = (
1718) => boolean;
1819
1920function logPathHydrationWarning(message: string, error?: unknown): void {
20 console.warn(`[server] ${message}`, error instanceof Error ? error.message : (error ?? ""));
21 process.stderr.write(
22 `[server] ${message} ${error instanceof Error ? error.message : (error ?? "")}\n`,
23 );
2124}
2225
2326export function fixPath(
......@@ -84,10 +87,10 @@ export function fixPath(
8487export const expandHomePath = Effect.fn(function* (input: string) {
8588 const { join } = yield* Path.Path;
8689 if (input === "~") {
87 return OS.homedir();
90 return NodeOS.homedir();
8891 }
8992 if (input.startsWith("~/") || input.startsWith("~\\")) {
90 return join(OS.homedir(), input.slice(2));
93 return join(NodeOS.homedir(), input.slice(2));
9194 }
9295 return input;
9396});
......@@ -95,7 +98,7 @@ export const expandHomePath = Effect.fn(function* (input: string) {
9598export const resolveBaseDir = Effect.fn(function* (raw: string | undefined) {
9699 const { join, resolve } = yield* Path.Path;
97100 if (!raw || raw.trim().length === 0) {
98 return join(OS.homedir(), ".t3");
101 return join(NodeOS.homedir(), ".t3");
99102 }
100103 return resolve(yield* expandHomePath(raw.trim()));
101104});
apps/server/src/pathExpansion.test.ts+1
......@@ -1,3 +1,4 @@
1// @effect-diagnostics nodeBuiltinImport:off
12import { homedir } from "node:os";
23import { join } from "node:path";
34import { describe, expect, it } from "vitest";
apps/server/src/pathExpansion.ts+1
......@@ -1,3 +1,4 @@
1// @effect-diagnostics nodeBuiltinImport:off
12import { homedir } from "node:os";
23import { join } from "node:path";
34
apps/server/src/persistence/Errors.ts+2-1
......@@ -1,4 +1,5 @@
1import { Schema, SchemaIssue } from "effect";
1import * as Schema from "effect/Schema";
2import * as SchemaIssue from "effect/SchemaIssue";
23
34// ===============================
45// Core Persistence Errors
apps/server/src/persistence/Layers/AuthPairingLinks.ts+3-1
......@@ -1,6 +1,8 @@
11import * as SqlClient from "effect/unstable/sql/SqlClient";
22import * as SqlSchema from "effect/unstable/sql/SqlSchema";
3import { Effect, Layer, Schema } from "effect";
3import * as Effect from "effect/Effect";
4import * as Layer from "effect/Layer";
5import * as Schema from "effect/Schema";
46
57import {
68 toPersistenceDecodeError,
apps/server/src/persistence/Layers/AuthSessions.ts+4-1
......@@ -1,7 +1,10 @@
11import { AuthSessionId } from "@t3tools/contracts";
22import * as SqlClient from "effect/unstable/sql/SqlClient";
33import * as SqlSchema from "effect/unstable/sql/SqlSchema";
4import { Effect, Layer, Option, Schema } from "effect";
4import * as Effect from "effect/Effect";
5import * as Layer from "effect/Layer";
6import * as Option from "effect/Option";
7import * as Schema from "effect/Schema";
58
69import {
710 toPersistenceDecodeError,
apps/server/src/persistence/Layers/OrchestrationCommandReceipts.ts+2-1
......@@ -1,6 +1,7 @@
11import * as SqlClient from "effect/unstable/sql/SqlClient";
22import * as SqlSchema from "effect/unstable/sql/SqlSchema";
3import { Effect, Layer } from "effect";
3import * as Effect from "effect/Effect";
4import * as Layer from "effect/Layer";
45
56import { toPersistenceSqlError } from "../Errors.ts";
67
apps/server/src/persistence/Layers/OrchestrationEventStore.test.ts+6-3
......@@ -1,6 +1,9 @@
11import { CommandId, EventId, ProjectId } from "@t3tools/contracts";
22import { assert, it } from "@effect/vitest";
3import { Effect, Layer, Schema, Stream } from "effect";
3import * as Effect from "effect/Effect";
4import * as Layer from "effect/Layer";
5import * as Schema from "effect/Schema";
6import * as Stream from "effect/Stream";
47import * as SqlClient from "effect/unstable/sql/SqlClient";
58
69import { PersistenceDecodeError } from "../Errors.ts";
......@@ -17,7 +20,7 @@ layer("OrchestrationEventStore", (it) => {
1720 Effect.gen(function* () {
1821 const eventStore = yield* OrchestrationEventStore;
1922 const sql = yield* SqlClient.SqlClient;
20 const now = new Date().toISOString();
23 const now = "2026-01-01T00:00:00.000Z";
2124
2225 const appended = yield* eventStore.append({
2326 type: "project.created",
......@@ -69,7 +72,7 @@ layer("OrchestrationEventStore", (it) => {
6972 Effect.gen(function* () {
7073 const eventStore = yield* OrchestrationEventStore;
7174 const sql = yield* SqlClient.SqlClient;
72 const now = new Date().toISOString();
75 const now = "2026-01-01T00:00:00.000Z";
7376
7477 yield* sql`
7578 INSERT INTO orchestration_events (
apps/server/src/persistence/Layers/OrchestrationEventStore.ts+4-1
......@@ -13,7 +13,10 @@ import {
1313} from "@t3tools/contracts";
1414import * as SqlClient from "effect/unstable/sql/SqlClient";
1515import * as SqlSchema from "effect/unstable/sql/SqlSchema";
16import { Effect, Layer, Schema, Stream } from "effect";
16import * as Effect from "effect/Effect";
17import * as Layer from "effect/Layer";
18import * as Schema from "effect/Schema";
19import * as Stream from "effect/Stream";
1720
1821import {
1922 toPersistenceDecodeError,
apps/server/src/persistence/Layers/ProjectionCheckpoints.ts+5-1
......@@ -1,7 +1,11 @@
11import { OrchestrationCheckpointFile } from "@t3tools/contracts";
22import * as SqlClient from "effect/unstable/sql/SqlClient";
33import * as SqlSchema from "effect/unstable/sql/SqlSchema";
4import { Effect, Layer, Option, Schema, Struct } from "effect";
4import * as Effect from "effect/Effect";
5import * as Layer from "effect/Layer";
6import * as Option from "effect/Option";
7import * as Schema from "effect/Schema";
8import * as Struct from "effect/Struct";
59
610import { toPersistenceDecodeError, toPersistenceSqlError } from "../Errors.ts";
711import {
apps/server/src/persistence/Layers/ProjectionPendingApprovals.ts+2-1
......@@ -1,6 +1,7 @@
11import * as SqlClient from "effect/unstable/sql/SqlClient";
22import * as SqlSchema from "effect/unstable/sql/SqlSchema";
3import { Effect, Layer } from "effect";
3import * as Effect from "effect/Effect";
4import * as Layer from "effect/Layer";
45
56import { toPersistenceSqlError } from "../Errors.ts";
67import {
apps/server/src/persistence/Layers/ProjectionProjects.ts+4-1
......@@ -1,6 +1,9 @@
11import * as SqlClient from "effect/unstable/sql/SqlClient";
22import * as SqlSchema from "effect/unstable/sql/SqlSchema";
3import { Effect, Layer, Schema, Struct } from "effect";
3import * as Effect from "effect/Effect";
4import * as Layer from "effect/Layer";
5import * as Schema from "effect/Schema";
6import * as Struct from "effect/Struct";
47
58import { ModelSelection, ProjectScript } from "@t3tools/contracts";
69import { toPersistenceSqlError } from "../Errors.ts";
apps/server/src/persistence/Layers/ProjectionRepositories.test.ts+7-3
......@@ -1,6 +1,8 @@
11import { ProjectId, ThreadId, ProviderInstanceId } from "@t3tools/contracts";
22import { assert, it } from "@effect/vitest";
3import { Effect, Layer, Option } from "effect";
3import * as Effect from "effect/Effect";
4import * as Layer from "effect/Layer";
5import * as Option from "effect/Option";
46import * as SqlClient from "effect/unstable/sql/SqlClient";
57
68import { SqlitePersistenceMemory } from "./Sqlite.ts";
......@@ -46,11 +48,12 @@ projectionRepositoriesLayer("Projection repositories", (it) => {
4648 `;
4749 const row = rows[0];
4850 if (!row) {
49 return yield* Effect.fail(new Error("Expected projection_projects row to exist."));
51 return yield* Effect.die("Expected projection_projects row to exist.");
5052 }
5153
5254 assert.strictEqual(
5355 row.defaultModelSelection,
56 // @effect-diagnostics-next-line preferSchemaOverJson:off
5457 JSON.stringify({
5558 instanceId: ProviderInstanceId.make("codex"),
5659 model: "gpt-5.4",
......@@ -104,11 +107,12 @@ projectionRepositoriesLayer("Projection repositories", (it) => {
104107 `;
105108 const row = rows[0];
106109 if (!row) {
107 return yield* Effect.fail(new Error("Expected projection_threads row to exist."));
110 return yield* Effect.die("Expected projection_threads row to exist.");
108111 }
109112
110113 assert.strictEqual(
111114 row.modelSelection,
115 // @effect-diagnostics-next-line preferSchemaOverJson:off
112116 JSON.stringify({
113117 instanceId: ProviderInstanceId.make("claudeAgent"),
114118 model: "claude-opus-4-6",
apps/server/src/persistence/Layers/ProjectionState.ts+3-1
......@@ -1,7 +1,9 @@
11import { NonNegativeInt } from "@t3tools/contracts";
22import * as SqlClient from "effect/unstable/sql/SqlClient";
33import * as SqlSchema from "effect/unstable/sql/SqlSchema";
4import { Effect, Layer, Schema } from "effect";
4import * as Effect from "effect/Effect";
5import * as Layer from "effect/Layer";
6import * as Schema from "effect/Schema";
57
68import { toPersistenceSqlError } from "../Errors.ts";
79
apps/server/src/persistence/Layers/ProjectionThreadActivities.ts+4-1
......@@ -1,7 +1,10 @@
11import * as SqlClient from "effect/unstable/sql/SqlClient";
22import * as SqlSchema from "effect/unstable/sql/SqlSchema";
33import { NonNegativeInt } from "@t3tools/contracts";
4import { Effect, Layer, Schema, Struct } from "effect";
4import * as Effect from "effect/Effect";
5import * as Layer from "effect/Layer";
6import * as Schema from "effect/Schema";
7import * as Struct from "effect/Struct";
58
69import { toPersistenceDecodeError, toPersistenceSqlError } from "../Errors.ts";
710
apps/server/src/persistence/Layers/ProjectionThreadMessages.test.ts+2-1
......@@ -1,6 +1,7 @@
11import { MessageId, ThreadId } from "@t3tools/contracts";
22import { assert, it } from "@effect/vitest";
3import { Effect, Layer } from "effect";
3import * as Effect from "effect/Effect";
4import * as Layer from "effect/Layer";
45
56import { ProjectionThreadMessageRepository } from "../Services/ProjectionThreadMessages.ts";
67import { ProjectionThreadMessageRepositoryLive } from "./ProjectionThreadMessages.ts";
apps/server/src/persistence/Layers/ProjectionThreadMessages.ts+5-1
......@@ -1,6 +1,10 @@
11import * as SqlClient from "effect/unstable/sql/SqlClient";
22import * as SqlSchema from "effect/unstable/sql/SqlSchema";
3import { Effect, Layer, Option, Schema, Struct } from "effect";
3import * as Effect from "effect/Effect";
4import * as Layer from "effect/Layer";
5import * as Option from "effect/Option";
6import * as Schema from "effect/Schema";
7import * as Struct from "effect/Struct";
48import { ChatAttachment } from "@t3tools/contracts";
59
610import { toPersistenceSqlError } from "../Errors.ts";
apps/server/src/persistence/Layers/ProjectionThreadProposedPlans.ts+2-1
......@@ -1,4 +1,5 @@
1import { Effect, Layer } from "effect";
1import * as Effect from "effect/Effect";
2import * as Layer from "effect/Layer";
23import * as SqlClient from "effect/unstable/sql/SqlClient";
34import * as SqlSchema from "effect/unstable/sql/SqlSchema";
45
apps/server/src/persistence/Layers/ProjectionThreadSessions.ts+2-1
......@@ -1,6 +1,7 @@
11import * as SqlClient from "effect/unstable/sql/SqlClient";
22import * as SqlSchema from "effect/unstable/sql/SqlSchema";
3import { Effect, Layer } from "effect";
3import * as Effect from "effect/Effect";
4import * as Layer from "effect/Layer";
45
56import { toPersistenceSqlError } from "../Errors.ts";
67
apps/server/src/persistence/Layers/ProjectionThreads.ts+4-1
......@@ -1,6 +1,9 @@
11import * as SqlClient from "effect/unstable/sql/SqlClient";
22import * as SqlSchema from "effect/unstable/sql/SqlSchema";
3import { Effect, Layer, Schema, Struct } from "effect";
3import * as Effect from "effect/Effect";
4import * as Layer from "effect/Layer";
5import * as Schema from "effect/Schema";
6import * as Struct from "effect/Struct";
47
58import { toPersistenceSqlError } from "../Errors.ts";
69import {
apps/server/src/persistence/Layers/ProjectionTurns.ts+5-1
......@@ -1,7 +1,11 @@
11import { OrchestrationCheckpointFile } from "@t3tools/contracts";
22import * as SqlClient from "effect/unstable/sql/SqlClient";
33import * as SqlSchema from "effect/unstable/sql/SqlSchema";
4import { Effect, Layer, Option, Schema, Struct } from "effect";
4import * as Effect from "effect/Effect";
5import * as Layer from "effect/Layer";
6import * as Option from "effect/Option";
7import * as Schema from "effect/Schema";
8import * as Struct from "effect/Struct";
59
610import { toPersistenceDecodeError, toPersistenceSqlError } from "../Errors.ts";
711import {
apps/server/src/persistence/Layers/ProviderSessionRuntime.ts+5-1
......@@ -1,7 +1,11 @@
11import { ThreadId } from "@t3tools/contracts";
22import * as SqlClient from "effect/unstable/sql/SqlClient";
33import * as SqlSchema from "effect/unstable/sql/SqlSchema";
4import { Effect, Layer, Option, Schema, Struct } from "effect";
4import * as Effect from "effect/Effect";
5import * as Layer from "effect/Layer";
6import * as Option from "effect/Option";
7import * as Schema from "effect/Schema";
8import * as Struct from "effect/Struct";
59
610import {
711 toPersistenceDecodeError,
apps/server/src/persistence/Layers/Sqlite.ts+4-1
......@@ -1,4 +1,7 @@
1import { Effect, Layer, FileSystem, Path } from "effect";
1import * as Effect from "effect/Effect";
2import * as Layer from "effect/Layer";
3import * as FileSystem from "effect/FileSystem";
4import * as Path from "effect/Path";
25import * as SqlClient from "effect/unstable/sql/SqlClient";
36
47import { runMigrations } from "../Migrations.ts";
apps/server/src/persistence/Migrations/016_CanonicalizeModelSelections.test.ts+9-8
......@@ -1,5 +1,6 @@
11import { assert, it } from "@effect/vitest";
2import { Effect, Layer } from "effect";
2import * as Effect from "effect/Effect";
3import * as Layer from "effect/Layer";
34import * as SqlClient from "effect/unstable/sql/SqlClient";
45
56import { runMigrations } from "../Migrations.ts";
......@@ -264,7 +265,7 @@ layer("016_CanonicalizeModelSelections", (it) => {
264265 FROM orchestration_events
265266 ORDER BY rowid ASC
266267 `;
267
268 // @effect-diagnostics-next-line preferSchemaOverJson:off
268269 assert.deepStrictEqual(JSON.parse(eventRows[0]!.payloadJson), {
269270 projectId: "project-1",
270271 title: "Project",
......@@ -280,7 +281,7 @@ layer("016_CanonicalizeModelSelections", (it) => {
280281 createdAt: "2026-01-01T00:00:00.000Z",
281282 updatedAt: "2026-01-01T00:00:00.000Z",
282283 });
283
284 // @effect-diagnostics-next-line preferSchemaOverJson:off
284285 assert.deepStrictEqual(JSON.parse(eventRows[1]!.payloadJson), {
285286 projectId: "project-2",
286287 title: "Fallback Project",
......@@ -296,7 +297,7 @@ layer("016_CanonicalizeModelSelections", (it) => {
296297 createdAt: "2026-01-01T00:00:00.000Z",
297298 updatedAt: "2026-01-01T00:00:00.000Z",
298299 });
299
300 // @effect-diagnostics-next-line preferSchemaOverJson:off
300301 assert.deepStrictEqual(JSON.parse(eventRows[2]!.payloadJson), {
301302 projectId: "project-3",
302303 title: "Null Model Project",
......@@ -306,7 +307,7 @@ layer("016_CanonicalizeModelSelections", (it) => {
306307 createdAt: "2026-01-01T00:00:00.000Z",
307308 updatedAt: "2026-01-01T00:00:00.000Z",
308309 });
309
310 // @effect-diagnostics-next-line preferSchemaOverJson:off
310311 assert.deepStrictEqual(JSON.parse(eventRows[3]!.payloadJson), {
311312 threadId: "thread-1",
312313 projectId: "project-1",
......@@ -326,7 +327,7 @@ layer("016_CanonicalizeModelSelections", (it) => {
326327 createdAt: "2026-01-01T00:00:00.000Z",
327328 updatedAt: "2026-01-01T00:00:00.000Z",
328329 });
329
330 // @effect-diagnostics-next-line preferSchemaOverJson:off
330331 assert.deepStrictEqual(JSON.parse(eventRows[4]!.payloadJson), {
331332 threadId: "thread-2",
332333 projectId: "project-1",
......@@ -345,7 +346,7 @@ layer("016_CanonicalizeModelSelections", (it) => {
345346 createdAt: "2026-01-01T00:00:00.000Z",
346347 updatedAt: "2026-01-01T00:00:00.000Z",
347348 });
348
349 // @effect-diagnostics-next-line preferSchemaOverJson:off
349350 assert.deepStrictEqual(JSON.parse(eventRows[5]!.payloadJson), {
350351 threadId: "thread-1",
351352 turnId: "turn-1",
......@@ -359,7 +360,7 @@ layer("016_CanonicalizeModelSelections", (it) => {
359360 },
360361 deliveryMode: "buffered",
361362 });
362
363 // @effect-diagnostics-next-line preferSchemaOverJson:off
363364 assert.deepStrictEqual(JSON.parse(eventRows[6]!.payloadJson), {
364365 threadId: "thread-3",
365366 projectId: "project-1",
apps/server/src/persistence/Migrations/019_ProjectionSnapshotLookupIndexes.test.ts+2-1
......@@ -1,5 +1,6 @@
11import { assert, it } from "@effect/vitest";
2import { Effect, Layer } from "effect";
2import * as Effect from "effect/Effect";
3import * as Layer from "effect/Layer";
34import * as SqlClient from "effect/unstable/sql/SqlClient";
45
56import { runMigrations } from "../Migrations.ts";
apps/server/src/persistence/Migrations/024_BackfillProjectionThreadShellSummary.test.ts+2-1
......@@ -1,5 +1,6 @@
11import { assert, it } from "@effect/vitest";
2import { Effect, Layer } from "effect";
2import * as Effect from "effect/Effect";
3import * as Layer from "effect/Layer";
34import * as SqlClient from "effect/unstable/sql/SqlClient";
45
56import { runMigrations } from "../Migrations.ts";
apps/server/src/persistence/Migrations/025_CleanupInvalidProjectionPendingApprovals.test.ts+2-1
......@@ -1,5 +1,6 @@
11import { assert, it } from "@effect/vitest";
2import { Effect, Layer } from "effect";
2import * as Effect from "effect/Effect";
3import * as Layer from "effect/Layer";
34import * as SqlClient from "effect/unstable/sql/SqlClient";
45
56import { runMigrations } from "../Migrations.ts";
apps/server/src/persistence/Migrations/026_CanonicalizeModelSelectionOptions.test.ts+2-1
......@@ -1,5 +1,6 @@
11import { assert, it } from "@effect/vitest";
2import { Effect, Layer } from "effect";
2import * as Effect from "effect/Effect";
3import * as Layer from "effect/Layer";
34import * as SqlClient from "effect/unstable/sql/SqlClient";
45
56import { runMigrations } from "../Migrations.ts";
apps/server/src/persistence/Migrations/027_028_ProviderInstanceIdColumns.test.ts+2-1
......@@ -1,5 +1,6 @@
11import { assert, it } from "@effect/vitest";
2import { Effect, Layer } from "effect";
2import * as Effect from "effect/Effect";
3import * as Layer from "effect/Layer";
34import * as SqlClient from "effect/unstable/sql/SqlClient";
45
56import { runMigrations } from "../Migrations.ts";
apps/server/src/persistence/Migrations/029_ProjectionThreadDetailOrderingIndexes.test.ts+2-1
......@@ -1,5 +1,6 @@
11import { assert, it } from "@effect/vitest";
2import { Effect, Layer } from "effect";
2import * as Effect from "effect/Effect";
3import * as Layer from "effect/Layer";
34import * as SqlClient from "effect/unstable/sql/SqlClient";
45
56import { runMigrations } from "../Migrations.ts";
apps/server/src/persistence/NodeSqliteClient.test.ts+1-1
......@@ -1,5 +1,5 @@
11import { assert, it } from "@effect/vitest";
2import { Effect } from "effect";
2import * as Effect from "effect/Effect";
33import * as SqlClient from "effect/unstable/sql/SqlClient";
44
55import * as SqliteClient from "./NodeSqliteClient.ts";
apps/server/src/persistence/Services/AuthPairingLinks.ts+4-2
......@@ -1,5 +1,7 @@
1import { Option, Schema, Context } from "effect";
2import type { Effect } from "effect";
1import * as Option from "effect/Option";
2import * as Schema from "effect/Schema";
3import * as Context from "effect/Context";
4import type * as Effect from "effect/Effect";
35
46import type { AuthPairingLinkRepositoryError } from "../Errors.ts";
57
apps/server/src/persistence/Services/AuthSessions.ts+4-2
......@@ -1,6 +1,8 @@
11import { AuthClientMetadataDeviceType, AuthSessionId } from "@t3tools/contracts";
2import { Option, Schema, Context } from "effect";
3import type { Effect } from "effect";
2import * as Option from "effect/Option";
3import * as Schema from "effect/Schema";
4import * as Context from "effect/Context";
5import type * as Effect from "effect/Effect";
46
57import type { AuthSessionRepositoryError } from "../Errors.ts";
68
apps/server/src/persistence/Services/OrchestrationCommandReceipts.ts+4-2
......@@ -15,8 +15,10 @@ import {
1515 ProjectId,
1616 ThreadId,
1717} from "@t3tools/contracts";
18import { Option, Schema, Context } from "effect";
19import type { Effect } from "effect";
18import * as Option from "effect/Option";
19import * as Schema from "effect/Schema";
20import * as Context from "effect/Context";
21import type * as Effect from "effect/Effect";
2022
2123import type { OrchestrationCommandReceiptRepositoryError } from "../Errors.ts";
2224
apps/server/src/persistence/Services/OrchestrationEventStore.ts+3-2
......@@ -10,8 +10,9 @@
1010 * @module OrchestrationEventStore
1111 */
1212import { OrchestrationEvent } from "@t3tools/contracts";
13import { Context } from "effect";
14import type { Effect, Stream } from "effect";
13import * as Context from "effect/Context";
14import type * as Effect from "effect/Effect";
15import type * as Stream from "effect/Stream";
1516
1617import type { OrchestrationEventStoreError } from "../Errors.ts";
1718
apps/server/src/persistence/Services/ProjectionCheckpoints.ts+4-2
......@@ -16,8 +16,10 @@ import {
1616 ThreadId,
1717 TurnId,
1818} from "@t3tools/contracts";
19import { Option, Context, Schema } from "effect";
20import type { Effect } from "effect";
19import * as Option from "effect/Option";
20import * as Context from "effect/Context";
21import * as Schema from "effect/Schema";
22import type * as Effect from "effect/Effect";
2123
2224import type { ProjectionRepositoryError } from "../Errors.ts";
2325
apps/server/src/persistence/Services/ProjectionPendingApprovals.ts+4-2
......@@ -14,8 +14,10 @@ import {
1414 ThreadId,
1515 TurnId,
1616} from "@t3tools/contracts";
17import { Option, Schema, Context } from "effect";
18import type { Effect } from "effect";
17import * as Option from "effect/Option";
18import * as Schema from "effect/Schema";
19import * as Context from "effect/Context";
20import type * as Effect from "effect/Effect";
1921
2022import type { ProjectionRepositoryError } from "../Errors.ts";
2123
apps/server/src/persistence/Services/ProjectionProjects.ts+4-2
......@@ -7,8 +7,10 @@
77 * @module ProjectionProjectRepository
88 */
99import { IsoDateTime, ModelSelection, ProjectId, ProjectScript } from "@t3tools/contracts";
10import { Option, Schema, Context } from "effect";
11import type { Effect } from "effect";
10import * as Option from "effect/Option";
11import * as Schema from "effect/Schema";
12import * as Context from "effect/Context";
13import type * as Effect from "effect/Effect";
1214
1315import type { ProjectionRepositoryError } from "../Errors.ts";
1416
apps/server/src/persistence/Services/ProjectionState.ts+4-2
......@@ -7,8 +7,10 @@
77 * @module ProjectionStateRepository
88 */
99import { IsoDateTime, NonNegativeInt } from "@t3tools/contracts";
10import { Option, Schema, Context } from "effect";
11import type { Effect } from "effect";
10import * as Option from "effect/Option";
11import * as Schema from "effect/Schema";
12import * as Context from "effect/Context";
13import type * as Effect from "effect/Effect";
1214
1315import type { ProjectionRepositoryError } from "../Errors.ts";
1416
apps/server/src/persistence/Services/ProjectionThreadActivities.ts+3-2
......@@ -14,8 +14,9 @@ import {
1414 ThreadId,
1515 TurnId,
1616} from "@t3tools/contracts";
17import { Schema, Context } from "effect";
18import type { Effect } from "effect";
17import * as Schema from "effect/Schema";
18import * as Context from "effect/Context";
19import type * as Effect from "effect/Effect";
1920
2021import type { ProjectionRepositoryError } from "../Errors.ts";
2122
apps/server/src/persistence/Services/ProjectionThreadMessages.ts+4-3
......@@ -14,9 +14,10 @@ import {
1414 TurnId,
1515 IsoDateTime,
1616} from "@t3tools/contracts";
17import { Schema, Context } from "effect";
18import type { Option } from "effect";
19import type { Effect } from "effect";
17import * as Schema from "effect/Schema";
18import * as Context from "effect/Context";
19import type * as Option from "effect/Option";
20import type * as Effect from "effect/Effect";
2021
2122import type { ProjectionRepositoryError } from "../Errors.ts";
2223
apps/server/src/persistence/Services/ProjectionThreadProposedPlans.ts+3-2
......@@ -5,8 +5,9 @@ import {
55 TrimmedNonEmptyString,
66 TurnId,
77} from "@t3tools/contracts";
8import { Schema, Context } from "effect";
9import type { Effect } from "effect";
8import * as Schema from "effect/Schema";
9import * as Context from "effect/Context";
10import type * as Effect from "effect/Effect";
1011
1112import type { ProjectionRepositoryError } from "../Errors.ts";
1213
apps/server/src/persistence/Services/ProjectionThreadSessions.ts+4-2
......@@ -14,8 +14,10 @@ import {
1414 ThreadId,
1515 TurnId,
1616} from "@t3tools/contracts";
17import { Option, Schema, Context } from "effect";
18import type { Effect } from "effect";
17import * as Option from "effect/Option";
18import * as Schema from "effect/Schema";
19import * as Context from "effect/Context";
20import type * as Effect from "effect/Effect";
1921
2022import type { ProjectionRepositoryError } from "../Errors.ts";
2123
apps/server/src/persistence/Services/ProjectionThreads.ts+4-2
......@@ -16,8 +16,10 @@ import {
1616 ThreadId,
1717 TurnId,
1818} from "@t3tools/contracts";
19import { Option, Schema, Context } from "effect";
20import type { Effect } from "effect";
19import * as Option from "effect/Option";
20import * as Schema from "effect/Schema";
21import * as Context from "effect/Context";
22import type * as Effect from "effect/Effect";
2123
2224import type { ProjectionRepositoryError } from "../Errors.ts";
2325
apps/server/src/persistence/Services/ProjectionTurns.ts+4-2
......@@ -17,8 +17,10 @@ import {
1717 ThreadId,
1818 TurnId,
1919} from "@t3tools/contracts";
20import { Option, Schema, Context } from "effect";
21import type { Effect } from "effect";
20import * as Option from "effect/Option";
21import * as Schema from "effect/Schema";
22import * as Context from "effect/Context";
23import type * as Effect from "effect/Effect";
2224
2325import type { ProjectionRepositoryError } from "../Errors.ts";
2426
apps/server/src/persistence/Services/ProviderSessionRuntime.ts+4-2
......@@ -12,8 +12,10 @@ import {
1212 RuntimeMode,
1313 ThreadId,
1414} from "@t3tools/contracts";
15import { Option, Schema, Context } from "effect";
16import type { Effect } from "effect";
15import * as Option from "effect/Option";
16import * as Schema from "effect/Schema";
17import * as Context from "effect/Context";
18import type * as Effect from "effect/Effect";
1719
1820import type { ProviderSessionRuntimeRepositoryError } from "../Errors.ts";
1921
apps/server/src/processRunner.ts+9-6
......@@ -1,5 +1,6 @@
1// @effect-diagnostics nodeBuiltinImport:off
12import { type ChildProcess as ChildProcessHandle, spawn, spawnSync } from "node:child_process";
2
3import * as NodeTimers from "node:timers";
34export interface ProcessRunOptions {
45 cwd?: string | undefined;
56 timeoutMs?: number | undefined;
......@@ -163,12 +164,14 @@ export async function runProcess(
163164 let stderrTruncated = false;
164165 let timedOut = false;
165166 let settled = false;
166 let forceKillTimer: ReturnType<typeof setTimeout> | null = null;
167 let forceKillTimer: ReturnType<typeof NodeTimers.setTimeout> | null = null;
167168
168 const timeoutTimer = setTimeout(() => {
169 // @effect-diagnostics-next-line globalTimers:off - Promise/child_process boundary; moving this runner to Effect timers is a separate refactor.
170 const timeoutTimer = NodeTimers.setTimeout(() => {
169171 timedOut = true;
170172 killChild(child, "SIGTERM");
171 forceKillTimer = setTimeout(() => {
173 // @effect-diagnostics-next-line globalTimers:off - Promise/child_process boundary; see timeout timer above.
174 forceKillTimer = NodeTimers.setTimeout(() => {
172175 killChild(child, "SIGKILL");
173176 }, 1_000);
174177 }, timeoutMs);
......@@ -176,9 +179,9 @@ export async function runProcess(
176179 const finalize = (callback: () => void): void => {
177180 if (settled) return;
178181 settled = true;
179 clearTimeout(timeoutTimer);
182 NodeTimers.clearTimeout(timeoutTimer);
180183 if (forceKillTimer) {
181 clearTimeout(forceKillTimer);
184 NodeTimers.clearTimeout(forceKillTimer);
182185 }
183186 callback();
184187 };
apps/server/src/project/Layers/ProjectFaviconResolver.test.ts+4-1
......@@ -1,6 +1,9 @@
11import * as NodeServices from "@effect/platform-node/NodeServices";
22import { it, describe, expect } from "@effect/vitest";
3import { Effect, FileSystem, Layer, Path } from "effect";
3import * as Effect from "effect/Effect";
4import * as FileSystem from "effect/FileSystem";
5import * as Layer from "effect/Layer";
6import * as Path from "effect/Path";
47
58import { ProjectFaviconResolver } from "../Services/ProjectFaviconResolver.ts";
69import { ProjectFaviconResolverLive } from "./ProjectFaviconResolver.ts";
apps/server/src/project/Layers/ProjectFaviconResolver.ts+4-1
......@@ -1,4 +1,7 @@
1import { Effect, FileSystem, Layer, Path } from "effect";
1import * as Effect from "effect/Effect";
2import * as FileSystem from "effect/FileSystem";
3import * as Layer from "effect/Layer";
4import * as Path from "effect/Path";
25
36import {
47 ProjectFaviconResolver,
apps/server/src/project/Layers/ProjectSetupScriptRunner.test.ts+3-1
......@@ -1,5 +1,7 @@
11import { ProjectId, type OrchestrationProject } from "@t3tools/contracts";
2import { Effect, Layer, Option } from "effect";
2import * as Effect from "effect/Effect";
3import * as Layer from "effect/Layer";
4import * as Option from "effect/Option";
35import { describe, expect, it, vi } from "vitest";
46
57import { ProjectionSnapshotQuery } from "../../orchestration/Services/ProjectionSnapshotQuery.ts";
apps/server/src/project/Layers/ProjectSetupScriptRunner.ts+27-3
......@@ -1,12 +1,15 @@
11import { ProjectId } from "@t3tools/contracts";
22import { projectScriptRuntimeEnv, setupProjectScript } from "@t3tools/shared/projectScripts";
3import { Effect, Layer, Option } from "effect";
3import * as Effect from "effect/Effect";
4import * as Layer from "effect/Layer";
5import * as Option from "effect/Option";
46
57import { ProjectionSnapshotQuery } from "../../orchestration/Services/ProjectionSnapshotQuery.ts";
68import { TerminalManager } from "../../terminal/Services/Manager.ts";
79import {
810 type ProjectSetupScriptRunnerShape,
911 ProjectSetupScriptRunner,
12 ProjectSetupScriptRunnerError,
1013} from "../Services/ProjectSetupScriptRunner.ts";
1114
1215const makeProjectSetupScriptRunner = Effect.gen(function* () {
......@@ -29,7 +32,9 @@ const makeProjectSetupScriptRunner = Effect.gen(function* () {
2932 null;
3033
3134 if (!project) {
32 return yield* Effect.fail(new Error("Project was not found for setup script execution."));
35 return yield* new ProjectSetupScriptRunnerError({
36 message: "Project was not found for setup script execution.",
37 });
3338 }
3439
3540 const script = setupProjectScript(project.scripts);
......@@ -66,7 +71,26 @@ const makeProjectSetupScriptRunner = Effect.gen(function* () {
6671 terminalId,
6772 cwd,
6873 } as const;
69 });
74 }).pipe(
75 Effect.mapError((cause) => {
76 if (
77 typeof cause === "object" &&
78 cause !== null &&
79 "_tag" in cause &&
80 cause._tag === "ProjectSetupScriptRunnerError"
81 ) {
82 return cause as ProjectSetupScriptRunnerError;
83 }
84 const message =
85 typeof cause === "object" &&
86 cause !== null &&
87 "message" in cause &&
88 typeof cause.message === "string"
89 ? cause.message
90 : String(cause);
91 return new ProjectSetupScriptRunnerError({ message });
92 }),
93 );
7094
7195 return {
7296 runForThread,
apps/server/src/project/Layers/RepositoryIdentityResolver.test.ts+6-2
......@@ -1,9 +1,13 @@
1// @effect-diagnostics nodeBuiltinImport:off
12import { realpathSync } from "node:fs";
23
34import * as NodeServices from "@effect/platform-node/NodeServices";
45import { expect, it } from "@effect/vitest";
5import { Duration, Effect, FileSystem, Layer } from "effect";
6import { TestClock } from "effect/testing";
6import * as Duration from "effect/Duration";
7import * as Effect from "effect/Effect";
8import * as FileSystem from "effect/FileSystem";
9import * as Layer from "effect/Layer";
10import * as TestClock from "effect/testing/TestClock";
711
812import { runProcess } from "../../processRunner.ts";
913import { RepositoryIdentityResolver } from "../Services/RepositoryIdentityResolver.ts";
apps/server/src/project/Layers/RepositoryIdentityResolver.ts+5-1
......@@ -1,5 +1,9 @@
11import type { RepositoryIdentity } from "@t3tools/contracts";
2import { Cache, Duration, Effect, Exit, Layer } from "effect";
2import * as Cache from "effect/Cache";
3import * as Duration from "effect/Duration";
4import * as Effect from "effect/Effect";
5import * as Exit from "effect/Exit";
6import * as Layer from "effect/Layer";
37import {
48 detectSourceControlProviderFromGitRemoteUrl,
59 normalizeGitRemoteUrl,
apps/server/src/project/Services/ProjectFaviconResolver.ts+2-2
......@@ -6,8 +6,8 @@
66 *
77 * @module ProjectFaviconResolver
88 */
9import { Context } from "effect";
10import type { Effect } from "effect";
9import * as Context from "effect/Context";
10import type * as Effect from "effect/Effect";
1111
1212/**
1313 * ProjectFaviconResolverShape - Service API for project favicon lookup.
apps/server/src/project/Services/ProjectSetupScriptRunner.ts+10-3
......@@ -1,5 +1,6 @@
1import { Context } from "effect";
2import type { Effect } from "effect";
1import * as Context from "effect/Context";
2import * as Data from "effect/Data";
3import type * as Effect from "effect/Effect";
34
45export interface ProjectSetupScriptRunnerResultNoScript {
56 readonly status: "no-script";
......@@ -25,10 +26,16 @@ export interface ProjectSetupScriptRunnerInput {
2526 readonly preferredTerminalId?: string;
2627}
2728
29export class ProjectSetupScriptRunnerError extends Data.TaggedError(
30 "ProjectSetupScriptRunnerError",
31)<{
32 readonly message: string;
33}> {}
34
2835export interface ProjectSetupScriptRunnerShape {
2936 readonly runForThread: (
3037 input: ProjectSetupScriptRunnerInput,
31 ) => Effect.Effect<ProjectSetupScriptRunnerResult, Error>;
38 ) => Effect.Effect<ProjectSetupScriptRunnerResult, ProjectSetupScriptRunnerError>;
3239}
3340
3441export class ProjectSetupScriptRunner extends Context.Service<
apps/server/src/project/Services/RepositoryIdentityResolver.ts+2-2
......@@ -1,6 +1,6 @@
11import type { RepositoryIdentity } from "@t3tools/contracts";
2import { Context } from "effect";
3import type { Effect } from "effect";
2import * as Context from "effect/Context";
3import type * as Effect from "effect/Effect";
44
55export interface RepositoryIdentityResolverShape {
66 readonly resolve: (cwd: string) => Effect.Effect<RepositoryIdentity | null>;
apps/server/src/provider/Drivers/ClaudeDriver.ts+9-2
......@@ -13,7 +13,13 @@
1313 * @module provider/Drivers/ClaudeDriver
1414 */
1515import { ClaudeSettings, ProviderDriverKind, type ServerProvider } from "@t3tools/contracts";
16import { Cache, Duration, Effect, FileSystem, Path, Schema, Stream } from "effect";
16import * as Cache from "effect/Cache";
17import * as Duration from "effect/Duration";
18import * as Effect from "effect/Effect";
19import * as FileSystem from "effect/FileSystem";
20import * as Path from "effect/Path";
21import * as Schema from "effect/Schema";
22import * as Stream from "effect/Stream";
1723import { HttpClient } from "effect/unstable/http";
1824import { ChildProcessSpawner } from "effect/unstable/process";
1925
......@@ -159,7 +165,8 @@ export const ClaudeDriver: ProviderDriver<ClaudeSettings, ClaudeDriverEnv> = {
159165 getSettings: Effect.succeed(effectiveConfig),
160166 streamSettings: Stream.never,
161167 haveSettingsChanged: () => false,
162 initialSnapshot: (settings) => stampIdentity(makePendingClaudeProvider(settings)),
168 initialSnapshot: (settings) =>
169 makePendingClaudeProvider(settings).pipe(Effect.map(stampIdentity)),
163170 checkProvider,
164171 enrichSnapshot: ({ snapshot, publishSnapshot }) =>
165172 enrichProviderSnapshotWithVersionAdvisory(snapshot, maintenanceCapabilities).pipe(
apps/server/src/provider/Drivers/ClaudeHome.test.ts+2-1
......@@ -2,7 +2,8 @@ import * as NodeOS from "node:os";
22
33import * as NodeServices from "@effect/platform-node/NodeServices";
44import { describe, expect, it } from "@effect/vitest";
5import { Effect, Path } from "effect";
5import * as Effect from "effect/Effect";
6import * as Path from "effect/Path";
67
78import {
89 makeClaudeCapabilitiesCacheKey,
apps/server/src/provider/Drivers/ClaudeHome.ts+2-1
......@@ -1,7 +1,8 @@
11import * as NodeOS from "node:os";
22
33import type { ClaudeSettings } from "@t3tools/contracts";
4import { Effect, Path } from "effect";
4import * as Effect from "effect/Effect";
5import * as Path from "effect/Path";
56
67import { expandHomePath } from "../../pathExpansion.ts";
78
apps/server/src/provider/Drivers/CodexDriver.ts+8-2
......@@ -22,7 +22,12 @@
2222 * @module provider/Drivers/CodexDriver
2323 */
2424import { CodexSettings, ProviderDriverKind, type ServerProvider } from "@t3tools/contracts";
25import { Duration, Effect, FileSystem, Path, Schema, Stream } from "effect";
25import * as Duration from "effect/Duration";
26import * as Effect from "effect/Effect";
27import * as FileSystem from "effect/FileSystem";
28import * as Path from "effect/Path";
29import * as Schema from "effect/Schema";
30import * as Stream from "effect/Stream";
2631import { HttpClient } from "effect/unstable/http";
2732import { ChildProcessSpawner } from "effect/unstable/process";
2833
......@@ -160,7 +165,8 @@ export const CodexDriver: ProviderDriver<CodexSettings, CodexDriverEnv> = {
160165 getSettings: Effect.succeed(effectiveConfig),
161166 streamSettings: Stream.never,
162167 haveSettingsChanged: () => false,
163 initialSnapshot: (settings) => stampIdentity(makePendingCodexProvider(settings)),
168 initialSnapshot: (settings) =>
169 makePendingCodexProvider(settings).pipe(Effect.map(stampIdentity)),
164170 checkProvider,
165171 enrichSnapshot: ({ snapshot, publishSnapshot }) =>
166172 enrichProviderSnapshotWithVersionAdvisory(snapshot, maintenanceCapabilities).pipe(
apps/server/src/provider/Drivers/CodexHomeLayout.test.ts+4-1
......@@ -1,6 +1,9 @@
11import * as NodeServices from "@effect/platform-node/NodeServices";
22import { describe, expect, it } from "@effect/vitest";
3import { Effect, FileSystem, Path, Schema } from "effect";
3import * as Effect from "effect/Effect";
4import * as FileSystem from "effect/FileSystem";
5import * as Path from "effect/Path";
6import * as Schema from "effect/Schema";
47
58import { CodexSettings } from "@t3tools/contracts";
69import {
apps/server/src/provider/Drivers/CodexHomeLayout.ts+4-1
......@@ -1,7 +1,10 @@
11import * as NodeOS from "node:os";
22
33import { ProviderDriverKind, type CodexSettings } from "@t3tools/contracts";
4import { Effect, FileSystem, Path, Schema } from "effect";
4import * as Effect from "effect/Effect";
5import * as FileSystem from "effect/FileSystem";
6import * as Path from "effect/Path";
7import * as Schema from "effect/Schema";
58import * as PlatformError from "effect/PlatformError";
69
710import { expandHomePath } from "../../pathExpansion.ts";
apps/server/src/provider/Drivers/CursorDriver.ts+8-2
......@@ -13,7 +13,12 @@
1313 * @module provider/Drivers/CursorDriver
1414 */
1515import { CursorSettings, ProviderDriverKind, type ServerProvider } from "@t3tools/contracts";
16import { Duration, Effect, FileSystem, Path, Schema, Stream } from "effect";
16import * as Duration from "effect/Duration";
17import * as Effect from "effect/Effect";
18import * as FileSystem from "effect/FileSystem";
19import * as Path from "effect/Path";
20import * as Schema from "effect/Schema";
21import * as Stream from "effect/Stream";
1722import { HttpClient } from "effect/unstable/http";
1823import { ChildProcessSpawner } from "effect/unstable/process";
1924
......@@ -128,7 +133,8 @@ export const CursorDriver: ProviderDriver<CursorSettings, CursorDriverEnv> = {
128133 getSettings: Effect.succeed(effectiveConfig),
129134 streamSettings: Stream.never,
130135 haveSettingsChanged: () => false,
131 initialSnapshot: (settings) => stampIdentity(buildInitialCursorProviderSnapshot(settings)),
136 initialSnapshot: (settings) =>
137 buildInitialCursorProviderSnapshot(settings).pipe(Effect.map(stampIdentity)),
132138 checkProvider,
133139 // Preserve the background ACP model-capability probe that used to
134140 // live on `CursorProviderLive`. Only fires when the snapshot reports
apps/server/src/provider/Drivers/OpenCodeDriver.ts+8-2
......@@ -13,7 +13,12 @@
1313 * @module provider/Drivers/OpenCodeDriver
1414 */
1515import { OpenCodeSettings, ProviderDriverKind, type ServerProvider } from "@t3tools/contracts";
16import { Duration, Effect, FileSystem, Path, Schema, Stream } from "effect";
16import * as Duration from "effect/Duration";
17import * as Effect from "effect/Effect";
18import * as FileSystem from "effect/FileSystem";
19import * as Path from "effect/Path";
20import * as Schema from "effect/Schema";
21import * as Stream from "effect/Stream";
1722import { HttpClient } from "effect/unstable/http";
1823import { ChildProcessSpawner } from "effect/unstable/process";
1924
......@@ -139,7 +144,8 @@ export const OpenCodeDriver: ProviderDriver<OpenCodeSettings, OpenCodeDriverEnv>
139144 getSettings: Effect.succeed(effectiveConfig),
140145 streamSettings: Stream.never,
141146 haveSettingsChanged: () => false,
142 initialSnapshot: (settings) => stampIdentity(makePendingOpenCodeProvider(settings)),
147 initialSnapshot: (settings) =>
148 makePendingOpenCodeProvider(settings).pipe(Effect.map(stampIdentity)),
143149 checkProvider,
144150 enrichSnapshot: ({ snapshot, publishSnapshot }) =>
145151 enrichProviderSnapshotWithVersionAdvisory(snapshot, maintenanceCapabilities).pipe(
apps/server/src/provider/Errors.ts+1-1
......@@ -1,4 +1,4 @@
1import { Schema } from "effect";
1import * as Schema from "effect/Schema";
22
33import type { CheckpointServiceError } from "../checkpointing/Errors.ts";
44
apps/server/src/provider/Layers/ClaudeAdapter.test.ts+11-2
......@@ -1,3 +1,4 @@
1// @effect-diagnostics nodeBuiltinImport:off
12import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
23import os from "node:os";
34import path from "node:path";
......@@ -22,7 +23,14 @@ import {
2223} from "@t3tools/contracts";
2324import { createModelSelection } from "@t3tools/shared/model";
2425import { assert, describe, it } from "@effect/vitest";
25import { Context, Effect, Fiber, Layer, Random, Schema, Stream } from "effect";
26import * as Context from "effect/Context";
27import * as Effect from "effect/Effect";
28import * as Fiber from "effect/Fiber";
29import * as Layer from "effect/Layer";
30import * as Random from "effect/Random";
31import * as Schema from "effect/Schema";
32import * as Stream from "effect/Stream";
33import * as TestClock from "effect/testing/TestClock";
2634
2735import { attachmentRelativePath } from "../../attachmentStore.ts";
2836import { ServerConfig } from "../../config.ts";
......@@ -1463,7 +1471,8 @@ describe("ClaudeAdapterLive", () => {
14631471 yield* Effect.yieldNow;
14641472 yield* Effect.yieldNow;
14651473 yield* Effect.yieldNow;
1466 yield* Effect.promise(() => new Promise((resolve) => setTimeout(resolve, 50)));
1474 yield* TestClock.adjust("50 millis");
1475 yield* Effect.yieldNow;
14671476
14681477 runtimeEventsFiber.interruptUnsafe();
14691478
apps/server/src/provider/Layers/ClaudeAdapter.ts+65-36
......@@ -51,20 +51,19 @@ import {
5151 getProviderOptionDescriptors,
5252 resolvePromptInjectedEffort,
5353} from "@t3tools/shared/model";
54import {
55 Cause,
56 DateTime,
57 Deferred,
58 Effect,
59 Exit,
60 FileSystem,
61 Fiber,
62 Path,
63 Queue,
64 Random,
65 Ref,
66 Stream,
67} from "effect";
54import * as Cause from "effect/Cause";
55import * as DateTime from "effect/DateTime";
56import * as Deferred from "effect/Deferred";
57import * as Effect from "effect/Effect";
58import * as Exit from "effect/Exit";
59import * as FileSystem from "effect/FileSystem";
60import * as Fiber from "effect/Fiber";
61import * as Path from "effect/Path";
62import * as Queue from "effect/Queue";
63import * as Random from "effect/Random";
64import * as Ref from "effect/Ref";
65import * as Schema from "effect/Schema";
66import * as Stream from "effect/Stream";
6867
6968import { resolveAttachmentPath } from "../../attachmentStore.ts";
7069import { ServerConfig } from "../../config.ts";
......@@ -222,11 +221,22 @@ function toMessage(cause: unknown, fallback: string): string {
222221 return fallback;
223222}
224223
225function toError(cause: unknown, fallback: string): Error {
226 return cause instanceof Error ? cause : new Error(toMessage(cause, fallback));
224function toProcessError(
225 cause: unknown,
226 fallback: string,
227 threadId: ThreadId,
228): ProviderAdapterProcessError {
229 return new ProviderAdapterProcessError({
230 provider: PROVIDER,
231 threadId,
232 detail: toMessage(cause, fallback),
233 cause,
234 });
227235}
228236
229function normalizeClaudeStreamMessages(cause: Cause.Cause<Error>): ReadonlyArray<string> {
237function normalizeClaudeStreamMessages(
238 cause: Cause.Cause<{ readonly message: string }>,
239): ReadonlyArray<string> {
230240 const errors = Cause.prettyErrors(cause)
231241 .map((error) => error.message.trim())
232242 .filter((message) => message.length > 0);
......@@ -252,18 +262,23 @@ function isClaudeInterruptedMessage(message: string): boolean {
252262 );
253263}
254264
255function isClaudeInterruptedCause(cause: Cause.Cause<Error>): boolean {
265function isClaudeInterruptedCause(cause: Cause.Cause<{ readonly message: string }>): boolean {
256266 return (
257267 Cause.hasInterruptsOnly(cause) ||
258268 normalizeClaudeStreamMessages(cause).some(isClaudeInterruptedMessage)
259269 );
260270}
261271
262function messageFromClaudeStreamCause(cause: Cause.Cause<Error>, fallback: string): string {
272function messageFromClaudeStreamCause(
273 cause: Cause.Cause<{ readonly message: string }>,
274 fallback: string,
275): string {
263276 return normalizeClaudeStreamMessages(cause)[0] ?? fallback;
264277}
265278
266function interruptionMessageFromClaudeCause(cause: Cause.Cause<Error>): string {
279function interruptionMessageFromClaudeCause(
280 cause: Cause.Cause<{ readonly message: string }>,
281): string {
267282 const message = messageFromClaudeStreamCause(cause, "Claude runtime interrupted.");
268283 return isClaudeInterruptedMessage(message) ? "Claude runtime interrupted." : message;
269284}
......@@ -530,7 +545,7 @@ function summarizeToolRequest(toolName: string, input: Record<string, unknown>):
530545 }
531546 }
532547
533 const serialized = JSON.stringify(input);
548 const serialized = Schema.encodeUnknownSync(Schema.UnknownFromJsonString)(input);
534549 if (serialized.length <= 400) {
535550 return `${toolName}: ${serialized}`;
536551 }
......@@ -796,7 +811,7 @@ function exitPlanCaptureKey(input: {
796811
797812function tryParseJsonRecord(value: string): Record<string, unknown> | undefined {
798813 try {
799 const parsed = JSON.parse(value);
814 const parsed = Schema.decodeUnknownSync(Schema.UnknownFromJsonString)(value);
800815 return parsed && typeof parsed === "object" && !Array.isArray(parsed)
801816 ? (parsed as Record<string, unknown>)
802817 : undefined;
......@@ -807,7 +822,7 @@ function tryParseJsonRecord(value: string): Record<string, unknown> | undefined
807822
808823function toolInputFingerprint(input: Record<string, unknown>): string | undefined {
809824 try {
810 return JSON.stringify(input);
825 return Schema.encodeUnknownSync(Schema.UnknownFromJsonString)(input);
811826 } catch {
812827 return undefined;
813828 }
......@@ -1020,7 +1035,7 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (
10201035 return;
10211036 }
10221037
1023 const observedAt = new Date().toISOString();
1038 const observedAt = yield* nowIso;
10241039 const itemId = sdkNativeItemId(message);
10251040
10261041 yield* nativeEventLogger.write(
......@@ -1030,7 +1045,7 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (
10301045 id:
10311046 "uuid" in message && typeof message.uuid === "string"
10321047 ? message.uuid
1033 : crypto.randomUUID(),
1048 : yield* Random.nextUUIDv4,
10341049 kind: "notification",
10351050 provider: PROVIDER,
10361051 createdAt: observedAt,
......@@ -2353,9 +2368,11 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (
23532368 }
23542369 });
23552370
2356 const runSdkStream = (context: ClaudeSessionContext): Effect.Effect<void, Error> =>
2371 const runSdkStream = (
2372 context: ClaudeSessionContext,
2373 ): Effect.Effect<void, ProviderAdapterProcessError> =>
23572374 Stream.fromAsyncIterable(context.query, (cause) =>
2358 toError(cause, "Claude runtime stream failed."),
2375 toProcessError(cause, "Claude runtime stream failed.", context.session.threadId),
23592376 ).pipe(
23602377 Stream.takeWhile(() => !context.stopped),
23612378 Stream.runForEach((message) => handleSdkMessage(context, message)),
......@@ -2363,7 +2380,7 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (
23632380
23642381 const handleStreamExit = Effect.fn("handleStreamExit")(function* (
23652382 context: ClaudeSessionContext,
2366 exit: Exit.Exit<void, Error>,
2383 exit: Exit.Exit<void, ProviderAdapterProcessError>,
23672384 ) {
23682385 if (context.stopped) {
23692386 return;
......@@ -2432,12 +2449,20 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (
24322449 yield* Fiber.interrupt(streamFiber);
24332450 }
24342451
2435 // @effect-diagnostics-next-line tryCatchInEffectGen:off
2436 try {
2437 context.query.close();
2438 } catch (cause) {
2439 yield* emitRuntimeError(context, "Failed to close Claude runtime query.", cause);
2440 }
2452 yield* Effect.try({
2453 try: () => context.query.close(),
2454 catch: (cause) =>
2455 new ProviderAdapterProcessError({
2456 provider: PROVIDER,
2457 threadId: context.session.threadId,
2458 detail: toMessage(cause, "Failed to close Claude runtime query."),
2459 cause,
2460 }),
2461 }).pipe(
2462 Effect.catch((cause) =>
2463 emitRuntimeError(context, "Failed to close Claude runtime query.", cause),
2464 ),
2465 );
24412466
24422467 const updatedAt = yield* nowIso;
24432468 context.session = {
......@@ -2916,8 +2941,12 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (
29162941 "claude.query.include_partial_messages": true,
29172942 "claude.query.additional_directories": input.cwd ? [input.cwd] : [],
29182943 "claude.query.setting_sources": [...CLAUDE_SETTING_SOURCES],
2919 "claude.query.settings_json": JSON.stringify(settings),
2920 "claude.query.extra_args_json": JSON.stringify(extraArgs),
2944 "claude.query.settings_json": Schema.encodeUnknownSync(Schema.UnknownFromJsonString)(
2945 settings,
2946 ),
2947 "claude.query.extra_args_json": Schema.encodeUnknownSync(Schema.UnknownFromJsonString)(
2948 extraArgs,
2949 ),
29212950 "claude.query.path_to_executable": claudeBinaryPath,
29222951 });
29232952
apps/server/src/provider/Layers/ClaudeProvider.ts+37-28
......@@ -6,7 +6,11 @@ import {
66 type ServerProviderModel,
77 type ServerProviderSlashCommand,
88} from "@t3tools/contracts";
9import { Effect, Option, Path, Result } from "effect";
9import * as DateTime from "effect/DateTime";
10import * as Effect from "effect/Effect";
11import * as Option from "effect/Option";
12import * as Path from "effect/Path";
13import * as Result from "effect/Result";
1014import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
1115import {
1216 createModelCapabilities,
......@@ -521,7 +525,7 @@ export const checkClaudeProviderStatus = Effect.fn("checkClaudeProviderStatus")(
521525 never,
522526 ChildProcessSpawner.ChildProcessSpawner | Path.Path
523527> {
524 const checkedAt = new Date().toISOString();
528 const checkedAt = DateTime.formatIso(yield* DateTime.now);
525529 const allModels = providerModelsFromSettings(
526530 BUILT_IN_MODELS,
527531 PROVIDER,
......@@ -664,19 +668,39 @@ export const checkClaudeProviderStatus = Effect.fn("checkClaudeProviderStatus")(
664668 });
665669});
666670
667export const makePendingClaudeProvider = (claudeSettings: ClaudeSettings): ServerProviderDraft => {
668 const checkedAt = new Date().toISOString();
669 const models = providerModelsFromSettings(
670 BUILT_IN_MODELS,
671 PROVIDER,
672 claudeSettings.customModels,
673 DEFAULT_CLAUDE_MODEL_CAPABILITIES,
674 );
671const nowIso = Effect.map(DateTime.now, DateTime.formatIso);
672
673export const makePendingClaudeProvider = (
674 claudeSettings: ClaudeSettings,
675): Effect.Effect<ServerProviderDraft> =>
676 Effect.gen(function* () {
677 const checkedAt = yield* nowIso;
678 const models = providerModelsFromSettings(
679 BUILT_IN_MODELS,
680 PROVIDER,
681 claudeSettings.customModels,
682 DEFAULT_CLAUDE_MODEL_CAPABILITIES,
683 );
684
685 if (!claudeSettings.enabled) {
686 return buildServerProvider({
687 presentation: CLAUDE_PRESENTATION,
688 enabled: false,
689 checkedAt,
690 models,
691 probe: {
692 installed: false,
693 version: null,
694 status: "warning",
695 auth: { status: "unknown" },
696 message: "Claude is disabled in T3 Code settings.",
697 },
698 });
699 }
675700
676 if (!claudeSettings.enabled) {
677701 return buildServerProvider({
678702 presentation: CLAUDE_PRESENTATION,
679 enabled: false,
703 enabled: true,
680704 checkedAt,
681705 models,
682706 probe: {
......@@ -684,24 +708,9 @@ export const makePendingClaudeProvider = (claudeSettings: ClaudeSettings): Serve
684708 version: null,
685709 status: "warning",
686710 auth: { status: "unknown" },
687 message: "Claude is disabled in T3 Code settings.",
711 message: "Claude provider status has not been checked in this session yet.",
688712 },
689713 });
690 }
691
692 return buildServerProvider({
693 presentation: CLAUDE_PRESENTATION,
694 enabled: true,
695 checkedAt,
696 models,
697 probe: {
698 installed: false,
699 version: null,
700 status: "warning",
701 auth: { status: "unknown" },
702 message: "Claude provider status has not been checked in this session yet.",
703 },
704714 });
705};
706715
707716export { probeClaudeCapabilities };
apps/server/src/provider/Layers/CodexAdapter.test.ts+28-18
......@@ -1,3 +1,4 @@
1// @effect-diagnostics nodeBuiltinImport:off
12import assert from "node:assert/strict";
23import fs from "node:fs";
34import os from "node:os";
......@@ -21,7 +22,16 @@ import { createModelSelection } from "@t3tools/shared/model";
2122import * as NodeServices from "@effect/platform-node/NodeServices";
2223import { it, vi } from "@effect/vitest";
2324
24import { Context, Effect, Exit, Fiber, Layer, Option, Queue, Schema, Scope, Stream } from "effect";
25import * as Context from "effect/Context";
26import * as Effect from "effect/Effect";
27import * as Exit from "effect/Exit";
28import * as Fiber from "effect/Fiber";
29import * as Layer from "effect/Layer";
30import * as Option from "effect/Option";
31import * as Queue from "effect/Queue";
32import * as Schema from "effect/Schema";
33import * as Scope from "effect/Scope";
34import * as Stream from "effect/Stream";
2535import * as CodexErrors from "effect-codex-app-server/errors";
2636
2737import { ServerConfig } from "../../config.ts";
......@@ -49,7 +59,7 @@ const asItemId = (value: string): ProviderItemId => ProviderItemId.make(value);
4959
5060class FakeCodexRuntime implements CodexSessionRuntimeShape {
5161 private readonly eventQueue = Effect.runSync(Queue.unbounded<ProviderEvent>());
52 private readonly now = new Date().toISOString();
62 private readonly now = "2026-01-01T00:00:00.000Z";
5363
5464 public readonly startImpl = vi.fn(() =>
5565 Promise.resolve({
......@@ -446,7 +456,7 @@ lifecycleLayer("CodexAdapterLive lifecycle", (it) => {
446456 id: asEventId("evt-msg-complete"),
447457 kind: "notification",
448458 provider: ProviderDriverKind.make("codex"),
449 createdAt: new Date().toISOString(),
459 createdAt: "2026-01-01T00:00:00.000Z",
450460 method: "item/completed",
451461 threadId: asThreadId("thread-1"),
452462 turnId: asTurnId("turn-1"),
......@@ -489,7 +499,7 @@ lifecycleLayer("CodexAdapterLive lifecycle", (it) => {
489499 id: asEventId("evt-plan-complete"),
490500 kind: "notification",
491501 provider: ProviderDriverKind.make("codex"),
492 createdAt: new Date().toISOString(),
502 createdAt: "2026-01-01T00:00:00.000Z",
493503 method: "item/completed",
494504 threadId: asThreadId("thread-1"),
495505 turnId: asTurnId("turn-1"),
......@@ -531,7 +541,7 @@ lifecycleLayer("CodexAdapterLive lifecycle", (it) => {
531541 id: asEventId("evt-plan-delta"),
532542 kind: "notification",
533543 provider: ProviderDriverKind.make("codex"),
534 createdAt: new Date().toISOString(),
544 createdAt: "2026-01-01T00:00:00.000Z",
535545 method: "item/plan/delta",
536546 threadId: asThreadId("thread-1"),
537547 turnId: asTurnId("turn-1"),
......@@ -569,7 +579,7 @@ lifecycleLayer("CodexAdapterLive lifecycle", (it) => {
569579 kind: "session",
570580 provider: ProviderDriverKind.make("codex"),
571581 threadId: asThreadId("thread-1"),
572 createdAt: new Date().toISOString(),
582 createdAt: "2026-01-01T00:00:00.000Z",
573583 method: "session/closed",
574584 message: "Session stopped",
575585 };
......@@ -600,7 +610,7 @@ lifecycleLayer("CodexAdapterLive lifecycle", (it) => {
600610 kind: "notification",
601611 provider: ProviderDriverKind.make("codex"),
602612 threadId: asThreadId("thread-1"),
603 createdAt: new Date().toISOString(),
613 createdAt: "2026-01-01T00:00:00.000Z",
604614 method: "error",
605615 turnId: asTurnId("turn-1"),
606616 payload: {
......@@ -638,7 +648,7 @@ lifecycleLayer("CodexAdapterLive lifecycle", (it) => {
638648 kind: "notification",
639649 provider: ProviderDriverKind.make("codex"),
640650 threadId: asThreadId("thread-1"),
641 createdAt: new Date().toISOString(),
651 createdAt: "2026-01-01T00:00:00.000Z",
642652 method: "process/stderr",
643653 turnId: asTurnId("turn-1"),
644654 message: "The filename or extension is too long. (os error 206)",
......@@ -672,7 +682,7 @@ lifecycleLayer("CodexAdapterLive lifecycle", (it) => {
672682 kind: "notification",
673683 provider: ProviderDriverKind.make("codex"),
674684 threadId: asThreadId("thread-1"),
675 createdAt: new Date().toISOString(),
685 createdAt: "2026-01-01T00:00:00.000Z",
676686 method: "thread/realtime/started",
677687 payload: {
678688 threadId: "thread-1",
......@@ -706,7 +716,7 @@ lifecycleLayer("CodexAdapterLive lifecycle", (it) => {
706716 kind: "notification",
707717 provider: ProviderDriverKind.make("codex"),
708718 threadId: asThreadId("thread-1"),
709 createdAt: new Date().toISOString(),
719 createdAt: "2026-01-01T00:00:00.000Z",
710720 method: "process/stderr",
711721 turnId: asTurnId("turn-1"),
712722 message:
......@@ -742,7 +752,7 @@ lifecycleLayer("CodexAdapterLive lifecycle", (it) => {
742752 kind: "notification",
743753 provider: ProviderDriverKind.make("codex"),
744754 threadId: asThreadId("thread-1"),
745 createdAt: new Date().toISOString(),
755 createdAt: "2026-01-01T00:00:00.000Z",
746756 method: "serverRequest/resolved",
747757 requestKind: "command",
748758 requestId: ApprovalRequestId.make("req-1"),
......@@ -777,7 +787,7 @@ lifecycleLayer("CodexAdapterLive lifecycle", (it) => {
777787 kind: "notification",
778788 provider: ProviderDriverKind.make("codex"),
779789 threadId: asThreadId("thread-1"),
780 createdAt: new Date().toISOString(),
790 createdAt: "2026-01-01T00:00:00.000Z",
781791 method: "serverRequest/resolved",
782792 requestKind: "file-read",
783793 requestId: ApprovalRequestId.make("req-file-read-1"),
......@@ -812,7 +822,7 @@ lifecycleLayer("CodexAdapterLive lifecycle", (it) => {
812822 kind: "notification",
813823 provider: ProviderDriverKind.make("codex"),
814824 threadId: asThreadId("thread-1"),
815 createdAt: new Date().toISOString(),
825 createdAt: "2026-01-01T00:00:00.000Z",
816826 method: "item/tool/requestUserInput/answered",
817827 payload: {
818828 answers: {
......@@ -852,7 +862,7 @@ lifecycleLayer("CodexAdapterLive lifecycle", (it) => {
852862 kind: "notification",
853863 provider: ProviderDriverKind.make("codex"),
854864 threadId: asThreadId("thread-1"),
855 createdAt: new Date().toISOString(),
865 createdAt: "2026-01-01T00:00:00.000Z",
856866 method: "windowsSandbox/setupCompleted",
857867 message: "Sandbox setup failed",
858868 payload: {
......@@ -897,7 +907,7 @@ lifecycleLayer("CodexAdapterLive lifecycle", (it) => {
897907 kind: "request",
898908 provider: ProviderDriverKind.make("codex"),
899909 threadId: asThreadId("thread-1"),
900 createdAt: new Date().toISOString(),
910 createdAt: "2026-01-01T00:00:00.000Z",
901911 method: "item/tool/requestUserInput",
902912 requestId: ApprovalRequestId.make("req-user-input-1"),
903913 payload: {
......@@ -924,7 +934,7 @@ lifecycleLayer("CodexAdapterLive lifecycle", (it) => {
924934 kind: "notification",
925935 provider: ProviderDriverKind.make("codex"),
926936 threadId: asThreadId("thread-1"),
927 createdAt: new Date().toISOString(),
937 createdAt: "2026-01-01T00:00:00.000Z",
928938 method: "item/tool/requestUserInput/answered",
929939 requestId: ApprovalRequestId.make("req-user-input-1"),
930940 payload: {
......@@ -965,7 +975,7 @@ lifecycleLayer("CodexAdapterLive lifecycle", (it) => {
965975 provider: ProviderDriverKind.make("codex"),
966976 threadId: asThreadId("thread-1"),
967977 turnId: asTurnId("turn-1"),
968 createdAt: new Date().toISOString(),
978 createdAt: "2026-01-01T00:00:00.000Z",
969979 method: "thread/tokenUsage/updated",
970980 payload: {
971981 threadId: "thread-1",
......@@ -1147,7 +1157,7 @@ it.effect("flushes managed native logs when the adapter layer shuts down", () =>
11471157 kind: "notification",
11481158 provider: ProviderDriverKind.make("codex"),
11491159 threadId: asThreadId("thread-logger"),
1150 createdAt: new Date().toISOString(),
1160 createdAt: "2026-01-01T00:00:00.000Z",
11511161 method: "process/stderr",
11521162 message: "native flush test",
11531163 } satisfies ProviderEvent);
apps/server/src/provider/Layers/CodexAdapter.ts+8-1
......@@ -24,7 +24,14 @@ import {
2424 ThreadId,
2525 ProviderSendTurnInput,
2626} from "@t3tools/contracts";
27import { Effect, Exit, Fiber, FileSystem, Queue, Schema, Scope, Stream } from "effect";
27import * as Effect from "effect/Effect";
28import * as Exit from "effect/Exit";
29import * as Fiber from "effect/Fiber";
30import * as FileSystem from "effect/FileSystem";
31import * as Queue from "effect/Queue";
32import * as Schema from "effect/Schema";
33import * as Scope from "effect/Scope";
34import * as Stream from "effect/Stream";
2835import { ChildProcessSpawner } from "effect/unstable/process";
2936import * as CodexErrors from "effect-codex-app-server/errors";
3037import * as EffectCodexSchema from "effect-codex-app-server/schema";
apps/server/src/provider/Layers/CodexProvider.ts+33-23
......@@ -1,4 +1,11 @@
1import { DateTime, Duration, Effect, Layer, Option, Result, Schema, Types } from "effect";
1import * as DateTime from "effect/DateTime";
2import * as Duration from "effect/Duration";
3import * as Effect from "effect/Effect";
4import * as Layer from "effect/Layer";
5import * as Option from "effect/Option";
6import * as Result from "effect/Result";
7import * as Schema from "effect/Schema";
8import * as Types from "effect/Types";
29import { ChildProcessSpawner } from "effect/unstable/process";
310import * as CodexClient from "effect-codex-app-server/client";
411import * as CodexSchema from "effect-codex-app-server/schema";
......@@ -325,14 +332,33 @@ const emptyCodexModelsFromSettings = (codexSettings: CodexSettings): ServerProvi
325332 capabilities: null,
326333 }));
327334
328const makePendingCodexProvider = (codexSettings: CodexSettings): ServerProviderDraft => {
329 const checkedAt = new Date().toISOString();
330 const models = emptyCodexModelsFromSettings(codexSettings);
335const makePendingCodexProvider = (
336 codexSettings: CodexSettings,
337): Effect.Effect<ServerProviderDraft> =>
338 Effect.gen(function* () {
339 const checkedAt = yield* Effect.map(DateTime.now, DateTime.formatIso);
340 const models = emptyCodexModelsFromSettings(codexSettings);
341
342 if (!codexSettings.enabled) {
343 return buildServerProvider({
344 presentation: CODEX_PRESENTATION,
345 enabled: false,
346 checkedAt,
347 models,
348 skills: [],
349 probe: {
350 installed: false,
351 version: null,
352 status: "warning",
353 auth: { status: "unknown" },
354 message: "Codex is disabled in T3 Code settings.",
355 },
356 });
357 }
331358
332 if (!codexSettings.enabled) {
333359 return buildServerProvider({
334360 presentation: CODEX_PRESENTATION,
335 enabled: false,
361 enabled: true,
336362 checkedAt,
337363 models,
338364 skills: [],
......@@ -341,26 +367,10 @@ const makePendingCodexProvider = (codexSettings: CodexSettings): ServerProviderD
341367 version: null,
342368 status: "warning",
343369 auth: { status: "unknown" },
344 message: "Codex is disabled in T3 Code settings.",
370 message: "Codex provider status has not been checked in this session yet.",
345371 },
346372 });
347 }
348
349 return buildServerProvider({
350 presentation: CODEX_PRESENTATION,
351 enabled: true,
352 checkedAt,
353 models,
354 skills: [],
355 probe: {
356 installed: false,
357 version: null,
358 status: "warning",
359 auth: { status: "unknown" },
360 message: "Codex provider status has not been checked in this session yet.",
361 },
362373 });
363};
364374
365375function accountProbeStatus(account: CodexAppServerProviderSnapshot["account"]): {
366376 readonly status: Exclude<ServerProviderState, "disabled">;
apps/server/src/provider/Layers/CodexSessionRuntime.test.ts+2-1
......@@ -1,6 +1,7 @@
11import assert from "node:assert/strict";
22
3import { Effect, Schema } from "effect";
3import * as Effect from "effect/Effect";
4import * as Schema from "effect/Schema";
45import { describe, it } from "vitest";
56import { ThreadId } from "@t3tools/contracts";
67import * as CodexErrors from "effect-codex-app-server/errors";
apps/server/src/provider/Layers/CodexSessionRuntime.ts+30-14
......@@ -17,7 +17,17 @@ import {
1717 TurnId,
1818} from "@t3tools/contracts";
1919import { normalizeModelSlug } from "@t3tools/shared/model";
20import { Deferred, Effect, Exit, Layer, Queue, Ref, Scope, Random, Schema, Stream } from "effect";
20import * as DateTime from "effect/DateTime";
21import * as Deferred from "effect/Deferred";
22import * as Effect from "effect/Effect";
23import * as Exit from "effect/Exit";
24import * as Layer from "effect/Layer";
25import * as Queue from "effect/Queue";
26import * as Ref from "effect/Ref";
27import * as Scope from "effect/Scope";
28import * as Random from "effect/Random";
29import * as Schema from "effect/Schema";
30import * as Stream from "effect/Stream";
2131import * as SchemaIssue from "effect/SchemaIssue";
2232import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
2333import * as CodexClient from "effect-codex-app-server/client";
......@@ -653,11 +663,14 @@ function updateSession(
653663 sessionRef: Ref.Ref<ProviderSession>,
654664 updates: Partial<ProviderSession>,
655665): Effect.Effect<void> {
656 return Ref.update(sessionRef, (session) => ({
657 ...session,
658 ...updates,
659 updatedAt: new Date().toISOString(),
660 }));
666 return Effect.gen(function* () {
667 const updatedAt = DateTime.formatIso(yield* DateTime.now);
668 yield* Ref.update(sessionRef, (session) => ({
669 ...session,
670 ...updates,
671 updatedAt,
672 }));
673 });
661674}
662675
663676function parseThreadSnapshot(
......@@ -724,7 +737,9 @@ export const makeCodexSessionRuntime = (
724737 Effect.provide(clientContext),
725738 );
726739 const serverNotifications = yield* Queue.unbounded<CodexServerNotification>();
740 const nowIso = Effect.map(DateTime.now, DateTime.formatIso);
727741
742 const sessionCreatedAt = yield* nowIso;
728743 const initialSession = {
729744 provider: PROVIDER,
730745 ...(options.providerInstanceId ? { providerInstanceId: options.providerInstanceId } : {}),
......@@ -734,22 +749,23 @@ export const makeCodexSessionRuntime = (
734749 ...(options.model ? { model: options.model } : {}),
735750 threadId: options.threadId,
736751 ...(options.resumeCursor !== undefined ? { resumeCursor: options.resumeCursor } : {}),
737 createdAt: new Date().toISOString(),
738 updatedAt: new Date().toISOString(),
752 createdAt: sessionCreatedAt,
753 updatedAt: sessionCreatedAt,
739754 } satisfies ProviderSession;
740755 const sessionRef = yield* Ref.make<ProviderSession>(initialSession);
741756 const offerEvent = (event: ProviderEvent) => Queue.offer(events, event).pipe(Effect.asVoid);
742757
743758 const emitEvent = (event: Omit<ProviderEvent, "id" | "provider" | "createdAt">) =>
744 Effect.flatMap(Random.nextUUIDv4, (id) =>
745 offerEvent({
759 Effect.gen(function* () {
760 const id = yield* Random.nextUUIDv4;
761 return yield* offerEvent({
746762 id: EventId.make(id),
747763 provider: PROVIDER,
748764 ...(options.providerInstanceId ? { providerInstanceId: options.providerInstanceId } : {}),
749 createdAt: new Date().toISOString(),
765 createdAt: yield* nowIso,
750766 ...event,
751 }),
752 );
767 });
768 });
753769 const emitSessionEvent = (method: string, message: string) =>
754770 emitEvent({
755771 kind: "session",
......@@ -1177,7 +1193,7 @@ export const makeCodexSessionRuntime = (
11771193 cwd: opened.cwd,
11781194 model: opened.model,
11791195 resumeCursor: { threadId: providerThreadId },
1180 updatedAt: new Date().toISOString(),
1196 updatedAt: yield* nowIso,
11811197 } satisfies ProviderSession;
11821198 yield* Ref.set(sessionRef, session);
11831199 yield* emitSessionEvent("session/ready", "Codex App Server session ready.");
apps/server/src/provider/Layers/CursorAdapter.test.ts+14-6
......@@ -1,3 +1,4 @@
1// @effect-diagnostics nodeBuiltinImport:off
12import * as path from "node:path";
23import * as os from "node:os";
34import { chmod, mkdtemp, readFile, writeFile } from "node:fs/promises";
......@@ -5,7 +6,13 @@ import { fileURLToPath } from "node:url";
56
67import * as NodeServices from "@effect/platform-node/NodeServices";
78import { assert, it } from "@effect/vitest";
8import { Context, Deferred, Effect, Fiber, Layer, Schema, Stream } from "effect";
9import * as Context from "effect/Context";
10import * as Deferred from "effect/Deferred";
11import * as Effect from "effect/Effect";
12import * as Fiber from "effect/Fiber";
13import * as Layer from "effect/Layer";
14import * as Schema from "effect/Schema";
15import * as Stream from "effect/Stream";
916import { createModelSelection } from "@t3tools/shared/model";
1017
1118import {
......@@ -98,7 +105,7 @@ async function waitForFileContent(filePath: string, attempts = 40) {
98105 return raw;
99106 }
100107 } catch {}
101 await new Promise((resolve) => setTimeout(resolve, 50));
108 await Effect.runPromise(Effect.yieldNow);
102109 }
103110 throw new Error(`Timed out waiting for file content at ${filePath}`);
104111}
......@@ -113,10 +120,11 @@ async function waitForFileContent(filePath: string, attempts = 40) {
113120// tests assumed.
114121const makeResolveCursorSettings = Effect.gen(function* () {
115122 const serverSettings = yield* ServerSettingsService;
116 // @effect-diagnostics effect/returnEffectInGen:off
117 return serverSettings.getSettings.pipe(
118 Effect.map((snapshot) => snapshot.providers.cursor),
119 Effect.orDie,
123 return yield* Effect.succeed(
124 serverSettings.getSettings.pipe(
125 Effect.map((snapshot) => snapshot.providers.cursor),
126 Effect.orDie,
127 ),
120128 );
121129});
122130
apps/server/src/provider/Layers/CursorAdapter.ts+23-21
......@@ -3,7 +3,6 @@
33 *
44 * @module CursorAdapterLive
55 */
6import * as nodePath from "node:path";
76
87import {
98 ApprovalRequestId,
......@@ -22,21 +21,21 @@ import {
2221 type ThreadId,
2322 TurnId,
2423} from "@t3tools/contracts";
25import {
26 DateTime,
27 Deferred,
28 Effect,
29 Exit,
30 Fiber,
31 FileSystem,
32 Option,
33 PubSub,
34 Random,
35 Scope,
36 Semaphore,
37 Stream,
38 SynchronizedRef,
39} from "effect";
24import * as DateTime from "effect/DateTime";
25import * as Deferred from "effect/Deferred";
26import * as Effect from "effect/Effect";
27import * as Exit from "effect/Exit";
28import * as Fiber from "effect/Fiber";
29import * as FileSystem from "effect/FileSystem";
30import * as Option from "effect/Option";
31import * as Path from "effect/Path";
32import * as PubSub from "effect/PubSub";
33import * as Random from "effect/Random";
34import * as Schema from "effect/Schema";
35import * as Scope from "effect/Scope";
36import * as Semaphore from "effect/Semaphore";
37import * as Stream from "effect/Stream";
38import * as SynchronizedRef from "effect/SynchronizedRef";
4039import { ChildProcessSpawner } from "effect/unstable/process";
4140import type * as EffectAcpSchema from "effect-acp/schema";
4241
......@@ -306,6 +305,7 @@ export function makeCursorAdapter(
306305 return Effect.gen(function* () {
307306 const boundInstanceId = options?.instanceId ?? ProviderInstanceId.make("cursor");
308307 const fileSystem = yield* FileSystem.FileSystem;
308 const path = yield* Path.Path;
309309 const childProcessSpawner = yield* ChildProcessSpawner.ChildProcessSpawner;
310310 const serverConfig = yield* Effect.service(ServerConfig);
311311 const nativeEventLogger =
......@@ -358,12 +358,12 @@ export function makeCursorAdapter(
358358 ) =>
359359 Effect.gen(function* () {
360360 if (!nativeEventLogger) return;
361 const observedAt = new Date().toISOString();
361 const observedAt = yield* nowIso;
362362 yield* nativeEventLogger.write(
363363 {
364364 observedAt,
365365 event: {
366 id: crypto.randomUUID(),
366 id: yield* Random.nextUUIDv4,
367367 kind: "notification",
368368 provider: PROVIDER,
369369 createdAt: observedAt,
......@@ -390,7 +390,7 @@ export function makeCursorAdapter(
390390 method: string,
391391 ) =>
392392 Effect.gen(function* () {
393 const fingerprint = `${ctx.activeTurnId ?? "no-turn"}:${JSON.stringify(payload)}`;
393 const fingerprint = `${ctx.activeTurnId ?? "no-turn"}:${Schema.encodeUnknownSync(Schema.UnknownFromJsonString)(payload)}`;
394394 if (ctx.lastPlanFingerprint === fingerprint) {
395395 return;
396396 }
......@@ -460,7 +460,7 @@ export function makeCursorAdapter(
460460 });
461461 }
462462
463 const cwd = nodePath.resolve(input.cwd.trim());
463 const cwd = path.resolve(input.cwd.trim());
464464 const cursorModelSelection =
465465 input.modelSelection?.instanceId === boundInstanceId ? input.modelSelection : undefined;
466466 const existing = sessions.get(input.threadId);
......@@ -638,7 +638,9 @@ export function makeCursorAdapter(
638638 turnId: ctx?.activeTurnId,
639639 requestId: runtimeRequestId,
640640 permissionRequest,
641 detail: permissionRequest.detail ?? JSON.stringify(params).slice(0, 2000),
641 detail:
642 permissionRequest.detail ??
643 Schema.encodeUnknownSync(Schema.UnknownFromJsonString)(params).slice(0, 2000),
642644 args: params,
643645 source: "acp.jsonrpc",
644646 method: "session/request_permission",
apps/server/src/provider/Layers/CursorProvider.test.ts+14-4
......@@ -1,7 +1,9 @@
11import * as NodeOS from "node:os";
22
33import * as NodeServices from "@effect/platform-node/NodeServices";
4import { Effect, FileSystem, Path } from "effect";
4import * as Effect from "effect/Effect";
5import * as FileSystem from "effect/FileSystem";
6import * as Path from "effect/Path";
57import { describe, expect, it } from "vitest";
68import type * as EffectAcpSchema from "effect-acp/schema";
79import type { CursorSettings, ServerProviderModel } from "@t3tools/contracts";
......@@ -68,12 +70,16 @@ const makeMockAgentWrapper = Effect.fn("makeMockAgentWrapper")(function* (
6870 prefix: "cursor-provider-mock-",
6971 });
7072 const wrapperPath = path.join(dir, "fake-agent.sh");
73 // @effect-diagnostics-next-line preferSchemaOverJson:off
74 const bunCommand = JSON.stringify("bun");
75 // @effect-diagnostics-next-line preferSchemaOverJson:off
76 const mockAgentPathJson = JSON.stringify(mockAgentPath);
7177 const envExports = Object.entries(extraEnv ?? {})
7278 .map(([key, value]) => `export ${key}=${JSON.stringify(value)}`)
7379 .join("\n");
7480 const script = `#!/bin/sh
7581${envExports}
76exec ${JSON.stringify("bun")} ${JSON.stringify(mockAgentPath)} "$@"
82exec ${bunCommand} ${mockAgentPathJson} "$@"
7783`;
7884 yield* fileSystem.writeFileString(wrapperPath, script);
7985 yield* fileSystem.chmod(wrapperPath, 0o755);
......@@ -89,13 +95,17 @@ const makeMockAgentWithAboutWrapper = Effect.fn("makeMockAgentWithAboutWrapper")
8995 prefix: "cursor-provider-about-mock-",
9096 });
9197 const wrapperPath = path.join(dir, "fake-agent.sh");
98 // @effect-diagnostics-next-line preferSchemaOverJson:off
99 const bunCommand = JSON.stringify("bun");
100 // @effect-diagnostics-next-line preferSchemaOverJson:off
101 const mockAgentPathJson = JSON.stringify(mockAgentPath);
92102 const script = `#!/bin/sh
93103if [ "$1" = "about" ]; then
94104 printf 'CLI Version 2026.04.09-f2b0fcd\\n'
95105 printf 'User Email cursor@example.com\\n'
96106 exit 0
97107fi
98exec ${JSON.stringify("bun")} ${JSON.stringify(mockAgentPath)} "$@"
108exec ${bunCommand} ${mockAgentPathJson} "$@"
99109`;
100110 yield* fileSystem.writeFileString(wrapperPath, script);
101111 yield* fileSystem.chmod(wrapperPath, 0o755);
......@@ -118,7 +128,7 @@ const waitForFileContent = Effect.fn("waitForFileContent")(function* (
118128 }
119129 yield* Effect.sleep("50 millis");
120130 }
121 return yield* Effect.fail(new Error(`Timed out waiting for file content at ${filePath}`));
131 return yield* Effect.die(`Timed out waiting for file content at ${filePath}`);
122132});
123133
124134const makeProviderStatusEnvFixture = Effect.fn("makeProviderStatusEnvFixture")(function* () {
apps/server/src/provider/Layers/CursorProvider.ts+35-26
......@@ -1,5 +1,4 @@
1import * as nodeOs from "node:os";
2
1import * as NodeOs from "node:os";
32import type {
43 CursorSettings,
54 ModelCapabilities,
......@@ -11,7 +10,15 @@ import type {
1110} from "@t3tools/contracts";
1211import { ProviderDriverKind } from "@t3tools/contracts";
1312import type * as EffectAcpSchema from "effect-acp/schema";
14import { Cause, Effect, Exit, FileSystem, Layer, Option, Path, Result } from "effect";
13import * as Cause from "effect/Cause";
14import * as DateTime from "effect/DateTime";
15import * as Effect from "effect/Effect";
16import * as Exit from "effect/Exit";
17import * as FileSystem from "effect/FileSystem";
18import * as Layer from "effect/Layer";
19import * as Option from "effect/Option";
20import * as Path from "effect/Path";
21import * as Result from "effect/Result";
1522import { HttpClient } from "effect/unstable/http";
1623import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
1724import {
......@@ -58,38 +65,40 @@ export const CURSOR_PARAMETERIZED_MODEL_PICKER_CAPABILITIES = {
5865
5966export function buildInitialCursorProviderSnapshot(
6067 cursorSettings: CursorSettings,
61): ServerProviderDraft {
62 const checkedAt = new Date().toISOString();
63 const models = getCursorFallbackModels(cursorSettings);
68): Effect.Effect<ServerProviderDraft> {
69 return Effect.gen(function* () {
70 const checkedAt = yield* Effect.map(DateTime.now, DateTime.formatIso);
71 const models = getCursorFallbackModels(cursorSettings);
72
73 if (!cursorSettings.enabled) {
74 return buildServerProvider({
75 presentation: CURSOR_PRESENTATION,
76 enabled: false,
77 checkedAt,
78 models,
79 probe: {
80 installed: false,
81 version: null,
82 status: "warning",
83 auth: { status: "unknown" },
84 message: "Cursor is disabled in T3 Code settings.",
85 },
86 });
87 }
6488
65 if (!cursorSettings.enabled) {
6689 return buildServerProvider({
6790 presentation: CURSOR_PRESENTATION,
68 enabled: false,
91 enabled: true,
6992 checkedAt,
7093 models,
7194 probe: {
72 installed: false,
95 installed: true,
7396 version: null,
7497 status: "warning",
7598 auth: { status: "unknown" },
76 message: "Cursor is disabled in T3 Code settings.",
99 message: "Checking Cursor Agent availability...",
77100 },
78101 });
79 }
80
81 return buildServerProvider({
82 presentation: CURSOR_PRESENTATION,
83 enabled: true,
84 checkedAt,
85 models,
86 probe: {
87 installed: true,
88 version: null,
89 status: "warning",
90 auth: { status: "unknown" },
91 message: "Checking Cursor Agent availability...",
92 },
93102 });
94103}
95104
......@@ -854,7 +863,7 @@ function isCursorAboutJsonFormatUnsupported(result: CommandResult): boolean {
854863const readCursorCliConfigChannel = Effect.fn("readCursorCliConfigChannel")(function* () {
855864 const fileSystem = yield* FileSystem.FileSystem;
856865 const path = yield* Path.Path;
857 const configPath = path.join(nodeOs.homedir(), ".cursor", "cli-config.json");
866 const configPath = path.join(NodeOs.homedir(), ".cursor", "cli-config.json");
858867 const raw = yield* fileSystem.readFileString(configPath).pipe(Effect.orElseSucceed(() => ""));
859868 return parseCursorCliConfigChannel(raw);
860869});
......@@ -1085,7 +1094,7 @@ export const checkCursorProviderStatus = Effect.fn("checkCursorProviderStatus")(
10851094 never,
10861095 ChildProcessSpawner.ChildProcessSpawner | FileSystem.FileSystem | Path.Path
10871096> {
1088 const checkedAt = new Date().toISOString();
1097 const checkedAt = DateTime.formatIso(yield* DateTime.now);
10891098 const fallbackModels = getCursorFallbackModels(cursorSettings);
10901099
10911100 if (!cursorSettings.enabled) {
apps/server/src/provider/Layers/EventNdjsonLogger.test.ts+2-1
......@@ -1,10 +1,11 @@
1// @effect-diagnostics nodeBuiltinImport:off
12import fs from "node:fs";
23import os from "node:os";
34import path from "node:path";
45
56import { ThreadId } from "@t3tools/contracts";
67import { assert, describe, it } from "@effect/vitest";
7import { Effect } from "effect";
8import * as Effect from "effect/Effect";
89
910import { makeEventNdjsonLogger } from "./EventNdjsonLogger.ts";
1011
apps/server/src/provider/Layers/EventNdjsonLogger.ts+6-1
......@@ -1,3 +1,4 @@
1// @effect-diagnostics nodeBuiltinImport:off
12/**
23 * Provider event logger helper.
34 *
......@@ -10,7 +11,11 @@ import path from "node:path";
1011
1112import type { ThreadId } from "@t3tools/contracts";
1213import { RotatingFileSink } from "@t3tools/shared/logging";
13import { Effect, Exit, Logger, Scope, SynchronizedRef } from "effect";
14import * as Effect from "effect/Effect";
15import * as Exit from "effect/Exit";
16import * as Logger from "effect/Logger";
17import * as Scope from "effect/Scope";
18import * as SynchronizedRef from "effect/SynchronizedRef";
1419
1520import { toSafeThreadAttachmentSegment } from "../../attachmentStore.ts";
1621
apps/server/src/provider/Layers/OpenCodeAdapter.test.ts+14-6
......@@ -1,8 +1,16 @@
11import assert from "node:assert/strict";
2
32import * as NodeServices from "@effect/platform-node/NodeServices";
43import { it } from "@effect/vitest";
5import { Context, Effect, Exit, Fiber, Layer, Option, Schema, Scope, Stream } from "effect";
4import * as Context from "effect/Context";
5import * as Effect from "effect/Effect";
6import * as Exit from "effect/Exit";
7import * as Fiber from "effect/Fiber";
8import * as Layer from "effect/Layer";
9import * as Option from "effect/Option";
10import * as Schema from "effect/Schema";
11import * as Scope from "effect/Scope";
12import * as Stream from "effect/Stream";
13import * as TestClock from "effect/testing/TestClock";
614import { beforeEach } from "vitest";
715
816import {
......@@ -216,8 +224,8 @@ beforeEach(() => {
216224 runtimeMock.reset();
217225});
218226
219const sleep = (ms: number) =>
220 Effect.promise(() => new Promise<void>((resolve) => setTimeout(resolve, ms)));
227const advanceTestClock = (ms: number) =>
228 TestClock.adjust(`${ms} millis`).pipe(Effect.andThen(Effect.yieldNow));
221229
222230it.layer(OpenCodeAdapterTestLayer)("OpenCodeAdapterLive", (it) => {
223231 it.effect("reuses a configured OpenCode server URL instead of spawning a local server", () =>
......@@ -648,7 +656,7 @@ it.layer(OpenCodeAdapterTestLayer)("OpenCodeAdapterLive", (it) => {
648656 threadId: asThreadId("thread-native-log"),
649657 runtimeMode: "full-access",
650658 });
651 yield* sleep(10);
659 yield* advanceTestClock(10);
652660 return started;
653661 }).pipe(Effect.provide(adapterLayer));
654662
......@@ -736,7 +744,7 @@ it.layer(OpenCodeAdapterTestLayer)("OpenCodeAdapterLive", (it) => {
736744 threadId: asThreadId("thread-native-log-failure"),
737745 runtimeMode: "full-access",
738746 });
739 yield* sleep(10);
747 yield* advanceTestClock(10);
740748 return {
741749 sessions: yield* adapter.listSessions(),
742750 closeCallsDuringRun: [...runtimeMock.state.closeCalls],
apps/server/src/provider/Layers/OpenCodeAdapter.ts+52-39
......@@ -12,7 +12,15 @@ import {
1212 TurnId,
1313 type UserInputQuestion,
1414} from "@t3tools/contracts";
15import { Cause, Effect, Exit, Queue, Random, Ref, Scope, Stream } from "effect";
15import * as Cause from "effect/Cause";
16import * as DateTime from "effect/DateTime";
17import * as Effect from "effect/Effect";
18import * as Exit from "effect/Exit";
19import * as Queue from "effect/Queue";
20import * as Random from "effect/Random";
21import * as Ref from "effect/Ref";
22import * as Scope from "effect/Scope";
23import * as Stream from "effect/Stream";
1624import type { OpencodeClient, Part, PermissionRequest, QuestionRequest } from "@opencode-ai/sdk/v2";
1725import { getModelSelectionStringOptionValue } from "@t3tools/shared/model";
1826
......@@ -40,6 +48,7 @@ import {
4048 toOpenCodeQuestionAnswers,
4149 type OpenCodeServerConnection,
4250} from "../opencodeRuntime.ts";
51import * as Option from "effect/Option";
4352
4453const PROVIDER = ProviderDriverKind.make("opencode");
4554
......@@ -95,9 +104,7 @@ export interface OpenCodeAdapterLiveOptions {
95104 readonly nativeEventLogger?: EventNdjsonLogger;
96105}
97106
98function nowIso(): string {
99 return new Date().toISOString();
100}
107const nowIso = Effect.map(DateTime.now, DateTime.formatIso);
101108
102109/**
103110 * Map a tagged OpenCodeRuntimeError produced by {@link runOpenCodeSdk} into
......@@ -140,12 +147,14 @@ const buildEventBase = (input: {
140147 "eventId" | "provider" | "threadId" | "createdAt" | "turnId" | "itemId" | "requestId" | "raw"
141148 >
142149> =>
143 Random.nextUUIDv4.pipe(
144 Effect.map((uuid) => ({
150 Effect.gen(function* () {
151 const uuid = yield* Random.nextUUIDv4;
152 const createdAt = input.createdAt ?? (yield* nowIso);
153 return {
145154 eventId: EventId.make(uuid),
146155 provider: PROVIDER,
147156 threadId: input.threadId,
148 createdAt: input.createdAt ?? nowIso(),
157 createdAt,
149158 ...(input.turnId ? { turnId: input.turnId } : {}),
150159 ...(input.itemId ? { itemId: RuntimeItemId.make(input.itemId) } : {}),
151160 ...(input.requestId ? { requestId: RuntimeRequestId.make(input.requestId) } : {}),
......@@ -157,8 +166,8 @@ const buildEventBase = (input: {
157166 },
158167 }
159168 : {}),
160 })),
161 );
169 };
170 });
162171
163172function toToolLifecycleItemType(toolName: string): ToolLifecycleItemType {
164173 const normalized = toolName.toLowerCase();
......@@ -347,12 +356,13 @@ export function appendOpenCodeAssistantTextDelta(
347356 };
348357}
349358
350function isoFromEpochMs(value: number | undefined): string | undefined {
351 if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
352 return undefined;
353 }
354 return new Date(value).toISOString();
355}
359const isoFromEpochMs = (value: number) =>
360 DateTime.make(value).pipe(
361 Option.match({
362 onNone: () => undefined,
363 onSome: DateTime.formatIso,
364 }),
365 );
356366
357367function messageRoleForPart(
358368 context: OpenCodeSessionContext,
......@@ -408,21 +418,24 @@ function updateProviderSession(
408418 readonly clearActiveTurnId?: boolean;
409419 readonly clearLastError?: boolean;
410420 },
411): ProviderSession {
412 const nextSession = {
413 ...context.session,
414 ...patch,
415 updatedAt: nowIso(),
416 } as ProviderSession & Record<string, unknown>;
417 const mutableSession = nextSession as Record<string, unknown>;
418 if (options?.clearActiveTurnId) {
419 delete mutableSession.activeTurnId;
420 }
421 if (options?.clearLastError) {
422 delete mutableSession.lastError;
423 }
424 context.session = nextSession;
425 return nextSession;
421): Effect.Effect<ProviderSession> {
422 return Effect.gen(function* () {
423 const updatedAt = yield* nowIso;
424 const nextSession = {
425 ...context.session,
426 ...patch,
427 updatedAt,
428 } as ProviderSession & Record<string, unknown>;
429 const mutableSession = nextSession as Record<string, unknown>;
430 if (options?.clearActiveTurnId) {
431 delete mutableSession.activeTurnId;
432 }
433 if (options?.clearLastError) {
434 delete mutableSession.lastError;
435 }
436 context.session = nextSession;
437 return nextSession;
438 });
426439}
427440
428441const stopOpenCodeContext = Effect.fn("stopOpenCodeContext")(function* (
......@@ -592,8 +605,8 @@ export function makeOpenCodeAdapter(
592605 turnId,
593606 itemId: part.id,
594607 createdAt:
595 part.type === "text" || part.type === "reasoning"
596 ? isoFromEpochMs(part.time?.start)
608 (part.type === "text" || part.type === "reasoning") && part.time !== undefined
609 ? isoFromEpochMs(part.time.start)
597610 : undefined,
598611 raw,
599612 })),
......@@ -642,7 +655,7 @@ export function makeOpenCodeAdapter(
642655
643656 const turnId = context.activeTurnId;
644657 yield* writeNativeEventBestEffort(context.session.threadId, {
645 observedAt: nowIso(),
658 observedAt: yield* nowIso,
646659 event: {
647660 provider: PROVIDER,
648661 threadId: context.session.threadId,
......@@ -863,7 +876,7 @@ export function makeOpenCodeAdapter(
863876
864877 case "session.status": {
865878 if (event.properties.status.type === "busy") {
866 updateProviderSession(context, {
879 yield* updateProviderSession(context, {
867880 status: "running",
868881 activeTurnId: turnId,
869882 });
......@@ -887,7 +900,7 @@ export function makeOpenCodeAdapter(
887900
888901 if (event.properties.status.type === "idle" && turnId) {
889902 context.activeTurnId = undefined;
890 updateProviderSession(context, { status: "ready" }, { clearActiveTurnId: true });
903 yield* updateProviderSession(context, { status: "ready" }, { clearActiveTurnId: true });
891904 yield* emit({
892905 ...(yield* buildEventBase({
893906 threadId: context.session.threadId,
......@@ -907,7 +920,7 @@ export function makeOpenCodeAdapter(
907920 const message = sessionErrorMessage(event.properties.error);
908921 const activeTurnId = context.activeTurnId;
909922 context.activeTurnId = undefined;
910 updateProviderSession(
923 yield* updateProviderSession(
911924 context,
912925 {
913926 status: "error",
......@@ -1084,7 +1097,7 @@ export function makeOpenCodeAdapter(
10841097 return raceWinner.session;
10851098 }
10861099
1087 const createdAt = nowIso();
1100 const createdAt = yield* nowIso;
10881101 const session: ProviderSession = {
10891102 provider: PROVIDER,
10901103 providerInstanceId: boundInstanceId,
......@@ -1185,7 +1198,7 @@ export function makeOpenCodeAdapter(
11851198 context.activeTurnId = turnId;
11861199 context.activeAgent = agent ?? (input.interactionMode === "plan" ? "plan" : undefined);
11871200 context.activeVariant = variant;
1188 updateProviderSession(
1201 yield* updateProviderSession(
11891202 context,
11901203 {
11911204 status: "running",
......@@ -1223,7 +1236,7 @@ export function makeOpenCodeAdapter(
12231236 context.activeTurnId = undefined;
12241237 context.activeAgent = undefined;
12251238 context.activeVariant = undefined;
1226 updateProviderSession(
1239 yield* updateProviderSession(
12271240 context,
12281241 {
12291242 status: "ready",
apps/server/src/provider/Layers/OpenCodeProvider.test.ts+3-1
......@@ -2,7 +2,9 @@ import assert from "node:assert/strict";
22
33import * as NodeServices from "@effect/platform-node/NodeServices";
44import { it } from "@effect/vitest";
5import { Effect, Layer, Schema } from "effect";
5import * as Effect from "effect/Effect";
6import * as Layer from "effect/Layer";
7import * as Schema from "effect/Schema";
68import { beforeEach } from "vitest";
79
810import { OpenCodeSettings } from "@t3tools/contracts";
apps/server/src/provider/Layers/OpenCodeProvider.ts+35-31
......@@ -4,7 +4,10 @@ import {
44 type OpenCodeSettings,
55 type ServerProviderModel,
66} from "@t3tools/contracts";
7import { Cause, Data, Effect } from "effect";
7import * as Cause from "effect/Cause";
8import * as Data from "effect/Data";
9import * as DateTime from "effect/DateTime";
10import * as Effect from "effect/Effect";
811
912import { createModelCapabilities } from "@t3tools/shared/model";
1013import {
......@@ -249,19 +252,38 @@ function flattenOpenCodeModels(input: OpenCodeInventory): ReadonlyArray<ServerPr
249252
250253export const makePendingOpenCodeProvider = (
251254 openCodeSettings: OpenCodeSettings,
252): ServerProviderDraft => {
253 const checkedAt = new Date().toISOString();
254 const models = providerModelsFromSettings(
255 [],
256 PROVIDER,
257 openCodeSettings.customModels,
258 DEFAULT_OPENCODE_MODEL_CAPABILITIES,
259 );
255): Effect.Effect<ServerProviderDraft> =>
256 Effect.gen(function* () {
257 const checkedAt = yield* Effect.map(DateTime.now, DateTime.formatIso);
258 const models = providerModelsFromSettings(
259 [],
260 PROVIDER,
261 openCodeSettings.customModels,
262 DEFAULT_OPENCODE_MODEL_CAPABILITIES,
263 );
264
265 if (!openCodeSettings.enabled) {
266 return buildServerProvider({
267 presentation: OPENCODE_PRESENTATION,
268 enabled: false,
269 checkedAt,
270 models,
271 probe: {
272 installed: false,
273 version: null,
274 status: "warning",
275 auth: { status: "unknown" },
276 message:
277 openCodeSettings.serverUrl.trim().length > 0
278 ? "OpenCode is disabled in T3 Code settings. A server URL is configured."
279 : "OpenCode is disabled in T3 Code settings.",
280 },
281 });
282 }
260283
261 if (!openCodeSettings.enabled) {
262284 return buildServerProvider({
263285 presentation: OPENCODE_PRESENTATION,
264 enabled: false,
286 enabled: true,
265287 checkedAt,
266288 models,
267289 probe: {
......@@ -269,28 +291,10 @@ export const makePendingOpenCodeProvider = (
269291 version: null,
270292 status: "warning",
271293 auth: { status: "unknown" },
272 message:
273 openCodeSettings.serverUrl.trim().length > 0
274 ? "OpenCode is disabled in T3 Code settings. A server URL is configured."
275 : "OpenCode is disabled in T3 Code settings.",
294 message: "OpenCode provider status has not been checked in this session yet.",
276295 },
277296 });
278 }
279
280 return buildServerProvider({
281 presentation: OPENCODE_PRESENTATION,
282 enabled: true,
283 checkedAt,
284 models,
285 probe: {
286 installed: false,
287 version: null,
288 status: "warning",
289 auth: { status: "unknown" },
290 message: "OpenCode provider status has not been checked in this session yet.",
291 },
292297 });
293};
294298
295299export const checkOpenCodeProviderStatus = Effect.fn("checkOpenCodeProviderStatus")(function* (
296300 openCodeSettings: OpenCodeSettings,
......@@ -298,7 +302,7 @@ export const checkOpenCodeProviderStatus = Effect.fn("checkOpenCodeProviderStatu
298302 environment: NodeJS.ProcessEnv = process.env,
299303): Effect.fn.Return<ServerProviderDraft, never, OpenCodeRuntime> {
300304 const openCodeRuntime = yield* OpenCodeRuntime;
301 const checkedAt = new Date().toISOString();
305 const checkedAt = DateTime.formatIso(yield* DateTime.now);
302306 const customModels = openCodeSettings.customModels;
303307 const isExternalServer = openCodeSettings.serverUrl.trim().length > 0;
304308
apps/server/src/provider/Layers/ProviderAdapterRegistry.test.ts+4-1
......@@ -5,7 +5,10 @@ import {
55} from "@t3tools/contracts";
66import { it, assert, vi } from "@effect/vitest";
77
8import { Effect, Layer, PubSub, Stream } from "effect";
8import * as Effect from "effect/Effect";
9import * as Layer from "effect/Layer";
10import * as PubSub from "effect/PubSub";
11import * as Stream from "effect/Stream";
912
1013import type { ClaudeAdapterShape } from "../Services/ClaudeAdapter.ts";
1114import type { CodexAdapterShape } from "../Services/CodexAdapter.ts";
apps/server/src/provider/Layers/ProviderAdapterRegistry.ts+2-1
......@@ -20,7 +20,8 @@ import {
2020 ProviderInstanceId,
2121 type ProviderDriverKind,
2222} from "@t3tools/contracts";
23import { Effect, Layer } from "effect";
23import * as Effect from "effect/Effect";
24import * as Layer from "effect/Layer";
2425
2526import { ProviderUnsupportedError } from "../Errors.ts";
2627import { ProviderInstanceRegistry } from "../Services/ProviderInstanceRegistry.ts";
apps/server/src/provider/Layers/ProviderEventLoggers.ts+3-1
......@@ -27,7 +27,9 @@
2727 *
2828 * @module provider/Layers/ProviderEventLoggers
2929 */
30import { Context, Effect, Layer } from "effect";
30import * as Context from "effect/Context";
31import * as Effect from "effect/Effect";
32import * as Layer from "effect/Layer";
3133
3234import { ServerConfig } from "../../config.ts";
3335import { type EventNdjsonLogger, makeEventNdjsonLogger } from "./EventNdjsonLogger.ts";
apps/server/src/provider/Layers/ProviderInstanceRegistryHydration.ts+3-1
......@@ -47,7 +47,9 @@ import {
4747 type ProviderInstanceConfigMap,
4848 ServerSettings,
4949} from "@t3tools/contracts";
50import { Effect, Layer, Stream } from "effect";
50import * as Effect from "effect/Effect";
51import * as Layer from "effect/Layer";
52import * as Stream from "effect/Stream";
5153
5254import { ServerSettingsService } from "../../serverSettings.ts";
5355import { BUILT_IN_DRIVERS, type BuiltInDriversEnv } from "../builtInDrivers.ts";
apps/server/src/provider/Layers/ProviderInstanceRegistryLive.test.ts+2-1
......@@ -33,7 +33,8 @@ import {
3333 type ProviderInstanceConfigMap,
3434 ProviderInstanceId,
3535} from "@t3tools/contracts";
36import { Effect, Layer } from "effect";
36import * as Effect from "effect/Effect";
37import * as Layer from "effect/Layer";
3738import { HttpClient, HttpClientResponse } from "effect/unstable/http";
3839
3940import { ServerConfig } from "../../config.ts";
apps/server/src/provider/Layers/ProviderInstanceRegistryLive.ts+13-4
......@@ -40,7 +40,16 @@ import {
4040 type ProviderDriverKind,
4141 type ServerProvider,
4242} from "@t3tools/contracts";
43import { Context, Effect, Equal, Exit, Layer, PubSub, Ref, Schema, Scope, Stream } from "effect";
43import * as Context from "effect/Context";
44import * as Effect from "effect/Effect";
45import * as Equal from "effect/Equal";
46import * as Exit from "effect/Exit";
47import * as Layer from "effect/Layer";
48import * as PubSub from "effect/PubSub";
49import * as Ref from "effect/Ref";
50import * as Schema from "effect/Schema";
51import * as Scope from "effect/Scope";
52import * as Stream from "effect/Stream";
4453
4554import { buildUnavailableProviderSnapshot } from "../unavailableProviderSnapshot.ts";
4655import {
......@@ -115,7 +124,7 @@ const buildEntry = <R>(input: {
115124 if (!driver) {
116125 return {
117126 kind: "unavailable" as const,
118 snapshot: buildUnavailableProviderSnapshot({
127 snapshot: yield* buildUnavailableProviderSnapshot({
119128 driverKind: entry.driver,
120129 instanceId,
121130 displayName: entry.displayName,
......@@ -137,7 +146,7 @@ const buildEntry = <R>(input: {
137146 });
138147 return {
139148 kind: "unavailable" as const,
140 snapshot: buildUnavailableProviderSnapshot({
149 snapshot: yield* buildUnavailableProviderSnapshot({
141150 driverKind: entry.driver,
142151 instanceId,
143152 displayName: entry.displayName,
......@@ -175,7 +184,7 @@ const buildEntry = <R>(input: {
175184 yield* Scope.close(childScope, Exit.void).pipe(Effect.ignore);
176185 return {
177186 kind: "unavailable" as const,
178 snapshot: buildUnavailableProviderSnapshot({
187 snapshot: yield* buildUnavailableProviderSnapshot({
179188 driverKind: entry.driver,
180189 instanceId,
181190 displayName: entry.displayName,
apps/server/src/provider/Layers/ProviderRegistry.test.ts+23-26
......@@ -1,6 +1,15 @@
11import * as NodeServices from "@effect/platform-node/NodeServices";
2import { describe, it, assert, live } from "@effect/vitest";
3import { Effect, Exit, Layer, PubSub, Ref, Schema, Scope, Sink, Stream } from "effect";
2import { describe, it, assert } from "@effect/vitest";
3import * as Effect from "effect/Effect";
4import * as Exit from "effect/Exit";
5import * as Layer from "effect/Layer";
6import * as PubSub from "effect/PubSub";
7import * as Ref from "effect/Ref";
8import * as Schema from "effect/Schema";
9import * as Scope from "effect/Scope";
10import * as Sink from "effect/Sink";
11import * as Stream from "effect/Stream";
12import * as TestClock from "effect/testing/TestClock";
413import * as CodexErrors from "effect-codex-app-server/errors";
514import {
615 ClaudeSettings,
......@@ -696,7 +705,8 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T
696705 attempt < 50 && cachedProvider?.checkedAt !== refreshedProvider.checkedAt;
697706 attempt += 1
698707 ) {
699 yield* Effect.sleep("10 millis");
708 yield* TestClock.adjust("10 millis");
709 yield* Effect.yieldNow;
700710 cachedProvider = yield* readProviderStatusCache(filePath);
701711 }
702712
......@@ -840,8 +850,7 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T
840850 const changes = yield* PubSub.unbounded<void>();
841851 const instancesRef = yield* Ref.make<ReadonlyArray<ProviderInstance>>([codexInstance]);
842852 const failNextList = yield* Ref.make(false);
843 const wait = (millis: number) =>
844 Effect.promise<void>(() => new Promise((resolve) => setTimeout(resolve, millis)));
853 const wait = () => Effect.yieldNow;
845854 const instanceRegistryLayer = Layer.succeed(ProviderInstanceRegistry, {
846855 getInstance: (instanceId) =>
847856 Ref.get(instancesRef).pipe(
......@@ -892,7 +901,7 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T
892901 !providers.some((provider) => provider.instanceId === claudeInstanceId);
893902 attempt += 1
894903 ) {
895 yield* wait(10);
904 yield* wait();
896905 providers = yield* registry.getProviders;
897906 }
898907
......@@ -917,7 +926,7 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T
917926 // assertions below fail.
918927 it.effect("propagates real Codex probe failures to the aggregator at boot", () =>
919928 Effect.gen(function* () {
920 const missingBinary = `t3code_codex_missing_${process.pid}_${Date.now()}`;
929 const missingBinary = `t3code_codex_missing_`;
921930 const serverSettings = yield* makeMutableServerSettingsService(
922931 Schema.decodeSync(ServerSettings)(
923932 deepMerge(DEFAULT_SERVER_SETTINGS, {
......@@ -1013,20 +1022,10 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T
10131022 // rebuilt instance's refresh (previous bug mode), the aggregator
10141023 // keeps the old snapshot and this test fails.
10151024 //
1016 // `live` (imported from `@effect/vitest`) is used instead of
1017 // `it.effect` so real timers coordinate the fibres that drive the
1018 // settings → reconcile → sync pipeline. Under `it.effect`'s
1019 // TestClock, `Effect.sleep` blocks until `TestClock.adjust`, which
1020 // would require this test to reach into the internals of the
1021 // reconcile pipeline to advance it step by step.
1022 //
1023 // The nested `it` handed to `it.layer(…, (it) => …)` is the
1024 // `MethodsNonLive` variant and therefore lacks `.live`; the
1025 // top-level `live` export from `@effect/vitest` is the equivalent.
1026 live("re-probes when settings change the codex binaryPath", () =>
1025 it.effect("re-probes when settings change the codex binaryPath", () =>
10271026 Effect.gen(function* () {
1028 const firstMissing = `t3code_codex_first_${process.pid}_${Date.now()}`;
1029 const secondMissing = `t3code_codex_second_${process.pid}_${Date.now()}`;
1027 const firstMissing = `t3code_codex_first_`;
1028 const secondMissing = `t3code_codex_second_`;
10301029 const serverSettings = yield* makeMutableServerSettingsService(
10311030 Schema.decodeSync(ServerSettings)(
10321031 deepMerge(DEFAULT_SERVER_SETTINGS, {
......@@ -1093,11 +1092,8 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T
10931092 },
10941093 });
10951094
1096 // Poll with real timers (via `it.live`) until `checkedAt`
1097 // advances or we hit a generous 3-second ceiling. Anything
1098 // slower than that is a regression — the real probe fails
1099 // fast on ENOENT, and the reconcile + sync pipeline is
1100 // purely in-process.
1095 // Poll with TestClock until `checkedAt` advances or we hit a
1096 // generous virtual 3-second ceiling.
11011097 const refreshed = yield* Effect.gen(function* () {
11021098 for (let attempts = 0; attempts < 60; attempts += 1) {
11031099 const providers = yield* registry.getProviders;
......@@ -1105,7 +1101,8 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T
11051101 if (codex !== undefined && codex.checkedAt !== initialCheckedAt) {
11061102 return providers;
11071103 }
1108 yield* Effect.sleep("50 millis");
1104 yield* TestClock.adjust("50 millis");
1105 yield* Effect.yieldNow;
11091106 }
11101107 return yield* registry.getProviders;
11111108 });
apps/server/src/provider/Layers/ProviderRegistry.ts+9-1
......@@ -29,7 +29,15 @@ import {
2929 type ServerProvider,
3030 type ServerProviderUpdateState,
3131} from "@t3tools/contracts";
32import { Cause, Effect, Equal, FileSystem, Layer, Path, PubSub, Ref, Stream } from "effect";
32import * as Cause from "effect/Cause";
33import * as Effect from "effect/Effect";
34import * as Equal from "effect/Equal";
35import * as FileSystem from "effect/FileSystem";
36import * as Layer from "effect/Layer";
37import * as Path from "effect/Path";
38import * as PubSub from "effect/PubSub";
39import * as Ref from "effect/Ref";
40import * as Stream from "effect/Stream";
3341import * as Semaphore from "effect/Semaphore";
3442
3543import { ServerConfig } from "../../config.ts";
apps/server/src/provider/Layers/ProviderService.test.ts+31-20
......@@ -1,3 +1,4 @@
1// @effect-diagnostics nodeBuiltinImport:off
12import fs from "node:fs";
23import os from "node:os";
34import path from "node:path";
......@@ -21,7 +22,17 @@ import {
2122import { createModelSelection } from "@t3tools/shared/model";
2223import { it, assert, vi } from "@effect/vitest";
2324
24import { Effect, Exit, Fiber, Layer, Metric, Option, PubSub, Ref, Scope, Stream } from "effect";
25import * as Effect from "effect/Effect";
26import * as Exit from "effect/Exit";
27import * as Fiber from "effect/Fiber";
28import * as Layer from "effect/Layer";
29import * as Metric from "effect/Metric";
30import * as Option from "effect/Option";
31import * as PubSub from "effect/PubSub";
32import * as Ref from "effect/Ref";
33import * as Scope from "effect/Scope";
34import * as Stream from "effect/Stream";
35import * as TestClock from "effect/testing/TestClock";
2536import * as SqlClient from "effect/unstable/sql/SqlClient";
2637
2738import {
......@@ -83,7 +94,7 @@ function makeFakeCodexAdapter(provider: ProviderDriverKind = CODEX_DRIVER) {
8394
8495 const startSession = vi.fn((input: ProviderSessionStartInput) =>
8596 Effect.sync(() => {
86 const now = new Date().toISOString();
97 const now = "2026-01-01T00:00:00.000Z";
8798 const session: ProviderSession = {
8899 provider,
89100 ...(input.providerInstanceId !== undefined
......@@ -246,8 +257,8 @@ function makeFakeCodexAdapter(provider: ProviderDriverKind = CODEX_DRIVER) {
246257 };
247258}
248259
249const sleep = (ms: number) =>
250 Effect.promise(() => new Promise<void>((resolve) => setTimeout(resolve, ms)));
260const advanceTestClock = (ms: number) =>
261 TestClock.adjust(`${ms} millis`).pipe(Effect.andThen(Effect.yieldNow));
251262
252263const hasMetricSnapshot = (
253264 snapshots: ReadonlyArray<Metric.Metric.Snapshot>,
......@@ -572,18 +583,18 @@ it.effect("ProviderServiceLive writes canonical events to the emitting thread se
572583
573584 yield* Effect.gen(function* () {
574585 yield* ProviderService;
575 yield* sleep(10);
586 yield* advanceTestClock(10);
576587 codex.emit({
577588 eventId: asEventId("evt-canonical-thread-segment"),
578589 provider: ProviderDriverKind.make("codex"),
579590 threadId: asThreadId("thread-canonical-thread-segment"),
580 createdAt: new Date().toISOString(),
591 createdAt: "2026-01-01T00:00:00.000Z",
581592 type: "turn.completed",
582593 payload: {
583594 state: "completed",
584595 },
585596 });
586 yield* sleep(20);
597 yield* advanceTestClock(20);
587598 }).pipe(Effect.provide(providerLayer));
588599
589600 assert.equal(canonicalEvents.length, 1);
......@@ -704,7 +715,7 @@ it.effect(
704715 ...existing,
705716 status: "ready",
706717 resumeCursor: updatedResumeCursor,
707 updatedAt: new Date(Date.now() + 1_000).toISOString(),
718 updatedAt: "2026-01-01T00:00:01.000Z",
708719 }));
709720 return session;
710721 }).pipe(Effect.provide(firstProviderLayer));
......@@ -1403,20 +1414,20 @@ fanout.layer("ProviderServiceLive fanout", (it) => {
14031414 const consumer = yield* Stream.runForEach(provider.streamEvents, (event) =>
14041415 Ref.update(eventsRef, (current) => [...current, event]),
14051416 ).pipe(Effect.forkChild);
1406 yield* sleep(50);
1417 yield* advanceTestClock(50);
14071418
14081419 const completedEvent: LegacyProviderRuntimeEvent = {
14091420 type: "turn.completed",
14101421 eventId: asEventId("evt-1"),
14111422 provider: ProviderDriverKind.make("codex"),
1412 createdAt: new Date().toISOString(),
1423 createdAt: "2026-01-01T00:00:00.000Z",
14131424 threadId: session.threadId,
14141425 turnId: asTurnId("turn-1"),
14151426 status: "completed",
14161427 };
14171428
14181429 fanout.codex.emit(completedEvent);
1419 yield* sleep(50);
1430 yield* advanceTestClock(50);
14201431
14211432 const events = yield* Ref.get(eventsRef);
14221433 yield* Fiber.interrupt(consumer);
......@@ -1450,13 +1461,13 @@ fanout.layer("ProviderServiceLive fanout", (it) => {
14501461 Stream.runForEach((event) => Ref.update(receivedRef, (current) => [...current, event])),
14511462 Effect.forkChild,
14521463 );
1453 yield* sleep(50);
1464 yield* advanceTestClock(50);
14541465
14551466 fanout.codex.emit({
14561467 type: "tool.started",
14571468 eventId: asEventId("evt-seq-1"),
14581469 provider: ProviderDriverKind.make("codex"),
1459 createdAt: new Date().toISOString(),
1470 createdAt: "2026-01-01T00:00:00.000Z",
14601471 threadId: session.threadId,
14611472 turnId: asTurnId("turn-1"),
14621473 toolKind: "command",
......@@ -1466,7 +1477,7 @@ fanout.layer("ProviderServiceLive fanout", (it) => {
14661477 type: "tool.completed",
14671478 eventId: asEventId("evt-seq-2"),
14681479 provider: ProviderDriverKind.make("codex"),
1469 createdAt: new Date().toISOString(),
1480 createdAt: "2026-01-01T00:00:00.000Z",
14701481 threadId: session.threadId,
14711482 turnId: asTurnId("turn-1"),
14721483 toolKind: "command",
......@@ -1476,7 +1487,7 @@ fanout.layer("ProviderServiceLive fanout", (it) => {
14761487 type: "turn.completed",
14771488 eventId: asEventId("evt-seq-3"),
14781489 provider: ProviderDriverKind.make("codex"),
1479 createdAt: new Date().toISOString(),
1490 createdAt: "2026-01-01T00:00:00.000Z",
14801491 threadId: session.threadId,
14811492 turnId: asTurnId("turn-1"),
14821493 status: "completed",
......@@ -1515,14 +1526,14 @@ fanout.layer("ProviderServiceLive fanout", (it) => {
15151526 Stream.runForEach(() => Effect.fail("listener crash")),
15161527 Effect.forkChild,
15171528 );
1518 yield* sleep(50);
1529 yield* advanceTestClock(50);
15191530
15201531 const events: ReadonlyArray<LegacyProviderRuntimeEvent> = [
15211532 {
15221533 type: "tool.completed",
15231534 eventId: asEventId("evt-ordered-1"),
15241535 provider: ProviderDriverKind.make("codex"),
1525 createdAt: new Date().toISOString(),
1536 createdAt: "2026-01-01T00:00:00.000Z",
15261537 threadId: session.threadId,
15271538 turnId: asTurnId("turn-1"),
15281539 toolKind: "command",
......@@ -1533,7 +1544,7 @@ fanout.layer("ProviderServiceLive fanout", (it) => {
15331544 type: "message.delta",
15341545 eventId: asEventId("evt-ordered-2"),
15351546 provider: ProviderDriverKind.make("codex"),
1536 createdAt: new Date().toISOString(),
1547 createdAt: "2026-01-01T00:00:00.000Z",
15371548 threadId: session.threadId,
15381549 turnId: asTurnId("turn-1"),
15391550 delta: "hello",
......@@ -1542,7 +1553,7 @@ fanout.layer("ProviderServiceLive fanout", (it) => {
15421553 type: "turn.completed",
15431554 eventId: asEventId("evt-ordered-3"),
15441555 provider: ProviderDriverKind.make("codex"),
1545 createdAt: new Date().toISOString(),
1556 createdAt: "2026-01-01T00:00:00.000Z",
15461557 threadId: session.threadId,
15471558 turnId: asTurnId("turn-1"),
15481559 status: "completed",
......@@ -1758,7 +1769,7 @@ validation.layer("ProviderServiceLive validation", (it) => {
17581769
17591770 validation.codex.startSession.mockImplementationOnce((input: ProviderSessionStartInput) =>
17601771 Effect.sync(() => {
1761 const now = new Date().toISOString();
1772 const now = "2026-01-01T00:00:00.000Z";
17621773 return {
17631774 provider: ProviderDriverKind.make("codex"),
17641775 status: "ready",
apps/server/src/provider/Layers/ProviderService.ts+37-20
......@@ -24,7 +24,16 @@ import {
2424 type ProviderRuntimeEvent,
2525 type ProviderSession,
2626} from "@t3tools/contracts";
27import { Cause, Effect, Layer, Option, PubSub, Ref, Schema, SchemaIssue, Stream } from "effect";
27import * as Cause from "effect/Cause";
28import * as DateTime from "effect/DateTime";
29import * as Effect from "effect/Effect";
30import * as Layer from "effect/Layer";
31import * as Option from "effect/Option";
32import * as PubSub from "effect/PubSub";
33import * as Ref from "effect/Ref";
34import * as Schema from "effect/Schema";
35import * as SchemaIssue from "effect/SchemaIssue";
36import * as Stream from "effect/Stream";
2837
2938import {
3039 increment,
......@@ -199,6 +208,7 @@ const makeProviderService = Effect.fn("makeProviderService")(function* (
199208 const registry = yield* ProviderAdapterRegistry;
200209 const directory = yield* ProviderSessionDirectory;
201210 const runtimeEventPubSub = yield* PubSub.unbounded<ProviderRuntimeEvent>();
211 const nowIso = Effect.map(DateTime.now, DateTime.formatIso);
202212
203213 const publishRuntimeEvent = (event: ProviderRuntimeEvent): Effect.Effect<void> =>
204214 Effect.succeed(event).pipe(
......@@ -655,7 +665,7 @@ const makeProviderService = Effect.fn("makeProviderService")(function* (
655665 ...(input.modelSelection !== undefined ? { modelSelection: input.modelSelection } : {}),
656666 activeTurnId: turn.turnId,
657667 lastRuntimeEvent: "provider.sendTurn",
658 lastRuntimeEventAt: new Date().toISOString(),
668 lastRuntimeEventAt: yield* nowIso,
659669 },
660670 });
661671 yield* analytics.record("provider.turn.sent", {
......@@ -977,27 +987,34 @@ const makeProviderService = Effect.fn("makeProviderService")(function* (
977987 ),
978988 ).pipe(Effect.map((sessionsByAdapter) => sessionsByAdapter.flatMap((sessions) => sessions)));
979989 yield* Effect.forEach(activeSessions, (session) =>
980 upsertSessionBinding(session, session.threadId, {
981 lastRuntimeEvent: "provider.stopAll",
982 lastRuntimeEventAt: new Date().toISOString(),
983 }),
990 Effect.flatMap(nowIso, (lastRuntimeEventAt) =>
991 upsertSessionBinding(session, session.threadId, {
992 lastRuntimeEvent: "provider.stopAll",
993 lastRuntimeEventAt,
994 }),
995 ),
984996 ).pipe(Effect.asVoid);
985997 yield* Effect.forEach(currentAdapters, ([, adapter]) => adapter.stopAll()).pipe(Effect.asVoid);
986998 const bindings = yield* directory.listBindings().pipe(Effect.orElseSucceed(() => []));
987 yield* Effect.forEach(bindings, (binding) => {
988 const providerInstanceId = dieOnMissingBindingInstanceId("ProviderService.stopAll", binding);
989 return directory.upsert({
990 threadId: binding.threadId,
991 provider: binding.provider,
992 providerInstanceId,
993 status: "stopped",
994 runtimePayload: {
995 activeTurnId: null,
996 lastRuntimeEvent: "provider.stopAll",
997 lastRuntimeEventAt: new Date().toISOString(),
998 },
999 });
1000 }).pipe(Effect.asVoid);
999 yield* Effect.forEach(bindings, (binding) =>
1000 Effect.gen(function* () {
1001 const providerInstanceId = dieOnMissingBindingInstanceId(
1002 "ProviderService.stopAll",
1003 binding,
1004 );
1005 return yield* directory.upsert({
1006 threadId: binding.threadId,
1007 provider: binding.provider,
1008 providerInstanceId,
1009 status: "stopped",
1010 runtimePayload: {
1011 activeTurnId: null,
1012 lastRuntimeEvent: "provider.stopAll",
1013 lastRuntimeEventAt: yield* nowIso,
1014 },
1015 });
1016 }),
1017 ).pipe(Effect.asVoid);
10011018 yield* analytics.record("provider.sessions.stopped_all", {
10021019 sessionCount: threadIds.length,
10031020 });
apps/server/src/provider/Layers/ProviderSessionDirectory.test.ts+5-2
......@@ -1,3 +1,4 @@
1// @effect-diagnostics nodeBuiltinImport:off
12import fs from "node:fs";
23import os from "node:os";
34import path from "node:path";
......@@ -6,7 +7,9 @@ import * as NodeServices from "@effect/platform-node/NodeServices";
67import { ProviderDriverKind, ThreadId } from "@t3tools/contracts";
78import { it, assert } from "@effect/vitest";
89import { assertSome } from "@effect/vitest/utils";
9import { Effect, Layer, Option } from "effect";
10import * as Effect from "effect/Effect";
11import * as Layer from "effect/Layer";
12import * as Option from "effect/Option";
1013import * as SqlClient from "effect/unstable/sql/SqlClient";
1114
1215import {
......@@ -209,7 +212,7 @@ it.layer(makeDirectoryLayer(SqlitePersistenceMemory))("ProviderSessionDirectoryL
209212 adapterKey: "claudeAgent",
210213 runtimeMode: "full-access",
211214 status: "running",
212 lastSeenAt: new Date().toISOString(),
215 lastSeenAt: "2026-01-01T00:00:00.000Z",
213216 resumeCursor: null,
214217 runtimePayload: null,
215218 });
apps/server/src/provider/Layers/ProviderSessionDirectory.ts+6-2
......@@ -1,5 +1,9 @@
11import { defaultInstanceIdForDriver, ProviderDriverKind, type ThreadId } from "@t3tools/contracts";
2import { Effect, Layer, Option, Schema } from "effect";
2import * as DateTime from "effect/DateTime";
3import * as Effect from "effect/Effect";
4import * as Layer from "effect/Layer";
5import * as Option from "effect/Option";
6import * as Schema from "effect/Schema";
37
48import type { ProviderSessionRuntime } from "../../persistence/Services/ProviderSessionRuntime.ts";
59import { ProviderSessionRuntimeRepository } from "../../persistence/Services/ProviderSessionRuntime.ts";
......@@ -110,7 +114,7 @@ const makeProviderSessionDirectory = Effect.gen(function* () {
110114 });
111115 }
112116
113 const now = new Date().toISOString();
117 const now = DateTime.formatIso(yield* DateTime.now);
114118 const providerChanged =
115119 existingRuntime !== undefined && existingRuntime.providerName !== binding.provider;
116120 const providerInstanceId =
apps/server/src/provider/Layers/ProviderSessionReaper.test.ts+26-14
......@@ -6,7 +6,15 @@ import {
66 ProviderDriverKind,
77 ProviderInstanceId,
88} from "@t3tools/contracts";
9import { Effect, Exit, Layer, ManagedRuntime, Option, Scope, Stream } from "effect";
9import * as Clock from "effect/Clock";
10import * as DateTime from "effect/DateTime";
11import * as Effect from "effect/Effect";
12import * as Exit from "effect/Exit";
13import * as Layer from "effect/Layer";
14import * as ManagedRuntime from "effect/ManagedRuntime";
15import * as Option from "effect/Option";
16import * as Scope from "effect/Scope";
17import * as Stream from "effect/Stream";
1018import { afterEach, describe, expect, it, vi } from "vitest";
1119
1220import { ProjectionSnapshotQuery } from "../../orchestration/Services/ProjectionSnapshotQuery.ts";
......@@ -28,21 +36,25 @@ async function waitFor(
2836 predicate: () => boolean | Promise<boolean>,
2937 timeoutMs = 2_000,
3038): Promise<void> {
31 const deadline = Date.now() + timeoutMs;
39 const deadline = (await Effect.runPromise(Clock.currentTimeMillis)) + timeoutMs;
3240 const poll = async (): Promise<void> => {
3341 if (await predicate()) {
3442 return;
3543 }
36 if (Date.now() >= deadline) {
44 if ((await Effect.runPromise(Clock.currentTimeMillis)) >= deadline) {
3745 throw new Error("Timed out waiting for expectation.");
3846 }
39 await new Promise((resolve) => setTimeout(resolve, 10));
47 await Effect.runPromise(Effect.yieldNow);
4048 return poll();
4149 };
4250
4351 return poll();
4452}
4553
54const drainFibers = Effect.forEach(Array.from({ length: 10 }), () => Effect.yieldNow, {
55 discard: true,
56});
57
4658const unsupported = () => Effect.die(new Error("Unsupported provider call in test")) as never;
4759
4860function makeReadModel(
......@@ -59,7 +71,7 @@ function makeReadModel(
5971 } | null;
6072 }>,
6173) {
62 const now = new Date().toISOString();
74 const now = "2026-01-01T00:00:00.000Z";
6375 const projectId = ProjectId.make("project-provider-session-reaper");
6476
6577 return {
......@@ -207,7 +219,7 @@ describe("ProviderSessionReaper", () => {
207219
208220 it("reaps stale persisted sessions without active turns", async () => {
209221 const threadId = ThreadId.make("thread-reaper-stale");
210 const now = new Date().toISOString();
222 const now = "2026-01-01T00:00:00.000Z";
211223 const harness = await createHarness({
212224 readModel: makeReadModel([
213225 {
......@@ -255,7 +267,7 @@ describe("ProviderSessionReaper", () => {
255267 it("skips stale sessions when the thread still has an active turn", async () => {
256268 const threadId = ThreadId.make("thread-reaper-active-turn");
257269 const turnId = TurnId.make("turn-reaper-active");
258 const now = new Date().toISOString();
270 const now = "2026-01-01T00:00:00.000Z";
259271 const harness = await createHarness({
260272 readModel: makeReadModel([
261273 {
......@@ -293,7 +305,7 @@ describe("ProviderSessionReaper", () => {
293305 const reaper = await runtime!.runPromise(Effect.service(ProviderSessionReaper));
294306 scope = await Effect.runPromise(Scope.make("sequential"));
295307 await Effect.runPromise(reaper.start().pipe(Scope.provide(scope)));
296 await new Promise((resolve) => setTimeout(resolve, 50));
308 await Effect.runPromise(drainFibers);
297309
298310 expect(harness.stopSession).not.toHaveBeenCalled();
299311 const remaining = await runtime!.runPromise(repository.getByThreadId({ threadId }));
......@@ -302,7 +314,7 @@ describe("ProviderSessionReaper", () => {
302314
303315 it("does not reap sessions that are still within the inactivity threshold", async () => {
304316 const threadId = ThreadId.make("thread-reaper-fresh");
305 const now = new Date().toISOString();
317 const now = DateTime.formatIso(await Effect.runPromise(DateTime.now));
306318 const harness = await createHarness({
307319 readModel: makeReadModel([
308320 {
......@@ -340,7 +352,7 @@ describe("ProviderSessionReaper", () => {
340352 const reaper = await runtime!.runPromise(Effect.service(ProviderSessionReaper));
341353 scope = await Effect.runPromise(Scope.make("sequential"));
342354 await Effect.runPromise(reaper.start().pipe(Scope.provide(scope)));
343 await new Promise((resolve) => setTimeout(resolve, 50));
355 await Effect.runPromise(drainFibers);
344356
345357 expect(harness.stopSession).not.toHaveBeenCalled();
346358 const remaining = await runtime!.runPromise(repository.getByThreadId({ threadId }));
......@@ -349,7 +361,7 @@ describe("ProviderSessionReaper", () => {
349361
350362 it("skips persisted sessions that are already marked stopped", async () => {
351363 const threadId = ThreadId.make("thread-reaper-stopped");
352 const now = new Date().toISOString();
364 const now = "2026-01-01T00:00:00.000Z";
353365 const harness = await createHarness({
354366 readModel: makeReadModel([
355367 {
......@@ -387,7 +399,7 @@ describe("ProviderSessionReaper", () => {
387399 const reaper = await runtime!.runPromise(Effect.service(ProviderSessionReaper));
388400 scope = await Effect.runPromise(Scope.make("sequential"));
389401 await Effect.runPromise(reaper.start().pipe(Scope.provide(scope)));
390 await new Promise((resolve) => setTimeout(resolve, 50));
402 await Effect.runPromise(drainFibers);
391403
392404 expect(harness.stopSession).not.toHaveBeenCalled();
393405 const remaining = await runtime!.runPromise(repository.getByThreadId({ threadId }));
......@@ -397,7 +409,7 @@ describe("ProviderSessionReaper", () => {
397409 it("continues reaping other sessions when one stop attempt fails", async () => {
398410 const failedThreadId = ThreadId.make("thread-reaper-stop-failure");
399411 const reapedThreadId = ThreadId.make("thread-reaper-stop-success");
400 const now = new Date().toISOString();
412 const now = "2026-01-01T00:00:00.000Z";
401413 const harness = await createHarness({
402414 readModel: makeReadModel([
403415 {
......@@ -483,7 +495,7 @@ describe("ProviderSessionReaper", () => {
483495 it("continues reaping other sessions when one stop attempt defects", async () => {
484496 const defectThreadId = ThreadId.make("thread-reaper-stop-defect");
485497 const reapedThreadId = ThreadId.make("thread-reaper-stop-after-defect");
486 const now = new Date().toISOString();
498 const now = "2026-01-01T00:00:00.000Z";
487499 const harness = await createHarness({
488500 readModel: makeReadModel([
489501 {
apps/server/src/provider/Layers/ProviderSessionReaper.ts+7-2
......@@ -1,4 +1,9 @@
1import { Duration, Effect, Layer, Option, Schedule } from "effect";
1import * as Clock from "effect/Clock";
2import * as Duration from "effect/Duration";
3import * as Effect from "effect/Effect";
4import * as Layer from "effect/Layer";
5import * as Option from "effect/Option";
6import * as Schedule from "effect/Schedule";
27
38import { ProjectionSnapshotQuery } from "../../orchestration/Services/ProjectionSnapshotQuery.ts";
49import { ProviderSessionDirectory } from "../Services/ProviderSessionDirectory.ts";
......@@ -30,7 +35,7 @@ const makeProviderSessionReaper = (options?: ProviderSessionReaperLiveOptions) =
3035
3136 const sweep = Effect.gen(function* () {
3237 const bindings = yield* directory.listBindings();
33 const now = Date.now();
38 const now = yield* Clock.currentTimeMillis;
3439 let reapedCount = 0;
3540
3641 for (const binding of bindings) {
apps/server/src/provider/Layers/scopedSafeTeardown.test.ts+3-1
......@@ -1,5 +1,7 @@
11import { it } from "@effect/vitest";
2import { Cause, Effect, Exit } from "effect";
2import * as Cause from "effect/Cause";
3import * as Effect from "effect/Effect";
4import * as Exit from "effect/Exit";
35import { describe, expect } from "vitest";
46
57import { scopedSafeTeardown } from "./scopedSafeTeardown.ts";
apps/server/src/provider/Layers/scopedSafeTeardown.ts+3-1
......@@ -34,7 +34,9 @@
3434 *
3535 * @module provider/Layers/scopedSafeTeardown
3636 */
37import { Effect, Exit, Scope } from "effect";
37import * as Effect from "effect/Effect";
38import * as Exit from "effect/Exit";
39import * as Scope from "effect/Scope";
3840
3941/**
4042 * Run `effect` with a freshly made `Scope.Scope`, guaranteeing that
apps/server/src/provider/ProviderDriver.ts+3-1
......@@ -26,7 +26,9 @@ import type {
2626 ProviderInstanceEnvironment,
2727 ProviderInstanceId,
2828} from "@t3tools/contracts";
29import type { Effect, Schema, Scope } from "effect";
29import type * as Effect from "effect/Effect";
30import type * as Schema from "effect/Schema";
31import type * as Scope from "effect/Scope";
3032
3133import type { TextGenerationShape } from "../textGeneration/TextGeneration.ts";
3234import type { ProviderAdapterError, ProviderDriverError } from "./Errors.ts";
apps/server/src/provider/Services/ProviderAdapter.ts+2-2
......@@ -20,8 +20,8 @@ import type {
2020 ProviderTurnStartResult,
2121 TurnId,
2222} from "@t3tools/contracts";
23import type { Effect } from "effect";
24import type { Stream } from "effect";
23import type * as Effect from "effect/Effect";
24import type * as Stream from "effect/Stream";
2525
2626export type ProviderSessionModelSwitchMode = "in-session" | "unsupported";
2727
apps/server/src/provider/Services/ProviderAdapterRegistry.ts+5-2
......@@ -20,8 +20,11 @@
2020 * @module ProviderAdapterRegistry
2121 */
2222import type { ProviderDriverKind, ProviderInstanceId } from "@t3tools/contracts";
23import { Context } from "effect";
24import type { Effect, PubSub, Scope, Stream } from "effect";
23import * as Context from "effect/Context";
24import type * as Effect from "effect/Effect";
25import type * as PubSub from "effect/PubSub";
26import type * as Scope from "effect/Scope";
27import type * as Stream from "effect/Stream";
2528
2629import type { ProviderAdapterError, ProviderUnsupportedError } from "../Errors.ts";
2730import type { ProviderAdapterShape } from "./ProviderAdapter.ts";
apps/server/src/provider/Services/ProviderInstanceRegistry.ts+5-2
......@@ -18,8 +18,11 @@
1818 * @module provider/Services/ProviderInstanceRegistry
1919 */
2020import type { ProviderInstanceId, ServerProvider } from "@t3tools/contracts";
21import { Context } from "effect";
22import type { Effect, PubSub, Scope, Stream } from "effect";
21import * as Context from "effect/Context";
22import type * as Effect from "effect/Effect";
23import type * as PubSub from "effect/PubSub";
24import type * as Scope from "effect/Scope";
25import type * as Stream from "effect/Stream";
2326
2427import type { ProviderInstance } from "../ProviderDriver.ts";
2528
apps/server/src/provider/Services/ProviderInstanceRegistryMutator.ts+2-2
......@@ -29,8 +29,8 @@
2929 * @module provider/Services/ProviderInstanceRegistryMutator
3030 */
3131import type { ProviderInstanceConfigMap } from "@t3tools/contracts";
32import { Context } from "effect";
33import type { Effect } from "effect";
32import * as Context from "effect/Context";
33import type * as Effect from "effect/Effect";
3434
3535export interface ProviderInstanceRegistryMutatorShape {
3636 /**
apps/server/src/provider/Services/ProviderRegistry.ts+3-2
......@@ -12,8 +12,9 @@ import type {
1212 ServerProvider,
1313 ServerProviderUpdateState,
1414} from "@t3tools/contracts";
15import { Context } from "effect";
16import type { Effect, Stream } from "effect";
15import * as Context from "effect/Context";
16import type * as Effect from "effect/Effect";
17import type * as Stream from "effect/Stream";
1718import type { ProviderMaintenanceCapabilities } from "../providerMaintenance.ts";
1819
1920export type ProviderMaintenanceActionKind = "update";
apps/server/src/provider/Services/ProviderService.ts+3-2
......@@ -24,8 +24,9 @@ import type {
2424 ThreadId,
2525 ProviderTurnStartResult,
2626} from "@t3tools/contracts";
27import { Context } from "effect";
28import type { Effect, Stream } from "effect";
27import * as Context from "effect/Context";
28import type * as Effect from "effect/Effect";
29import type * as Stream from "effect/Stream";
2930
3031import type { ProviderServiceError } from "../Errors.ts";
3132import type { ProviderAdapterCapabilities } from "./ProviderAdapter.ts";
apps/server/src/provider/Services/ProviderSessionDirectory.ts+3-2
......@@ -5,8 +5,9 @@ import type {
55 RuntimeMode,
66 ThreadId,
77} from "@t3tools/contracts";
8import { Option, Context } from "effect";
9import type { Effect } from "effect";
8import * as Option from "effect/Option";
9import * as Context from "effect/Context";
10import type * as Effect from "effect/Effect";
1011
1112import type {
1213 ProviderSessionDirectoryPersistenceError,
apps/server/src/provider/Services/ProviderSessionReaper.ts+3-2
......@@ -1,5 +1,6 @@
1import { Context } from "effect";
2import type { Effect, Scope } from "effect";
1import * as Context from "effect/Context";
2import type * as Effect from "effect/Effect";
3import type * as Scope from "effect/Scope";
34
45export interface ProviderSessionReaperShape {
56 /**
apps/server/src/provider/Services/ServerProvider.ts+2-1
......@@ -1,5 +1,6 @@
11import type { ServerProvider } from "@t3tools/contracts";
2import type { Effect, Stream } from "effect";
2import type * as Effect from "effect/Effect";
3import type * as Stream from "effect/Stream";
34import type { ProviderMaintenanceCapabilities } from "../providerMaintenance.ts";
45
56export interface ServerProviderShape {
apps/server/src/provider/acp/AcpAdapterSupport.ts+1-1
......@@ -3,7 +3,7 @@ import {
33 type ProviderDriverKind,
44 type ThreadId,
55} from "@t3tools/contracts";
6import { Schema } from "effect";
6import * as Schema from "effect/Schema";
77import * as EffectAcpErrors from "effect-acp/errors";
88
99import {
apps/server/src/provider/acp/AcpJsonRpcConnection.test.ts+3-1
......@@ -1,3 +1,4 @@
1// @effect-diagnostics nodeBuiltinImport:off
12import * as path from "node:path";
23import * as os from "node:os";
34import { fileURLToPath } from "node:url";
......@@ -5,7 +6,8 @@ import { mkdtempSync, readFileSync, rmSync } from "node:fs";
56
67import * as NodeServices from "@effect/platform-node/NodeServices";
78import { it } from "@effect/vitest";
8import { Effect, Stream } from "effect";
9import * as Effect from "effect/Effect";
10import * as Stream from "effect/Stream";
911import { describe, expect } from "vitest";
1012
1113import { AcpSessionRuntime, type AcpSessionRequestLogEvent } from "./AcpSessionRuntime.ts";
apps/server/src/provider/acp/AcpNativeLogging.ts+6-3
......@@ -1,5 +1,8 @@
11import type { ProviderDriverKind, ThreadId } from "@t3tools/contracts";
2import { Cause, Effect } from "effect";
2import * as Cause from "effect/Cause";
3import * as DateTime from "effect/DateTime";
4import * as Effect from "effect/Effect";
5import * as Random from "effect/Random";
36import type * as EffectAcpProtocol from "effect-acp/protocol";
47
58import type { EventNdjsonLogger } from "../Layers/EventNdjsonLogger.ts";
......@@ -14,12 +17,12 @@ function writeNativeAcpLog(input: {
1417}): Effect.Effect<void, never> {
1518 return Effect.gen(function* () {
1619 if (!input.nativeEventLogger) return;
17 const observedAt = new Date().toISOString();
20 const observedAt = DateTime.formatIso(yield* DateTime.now);
1821 yield* input.nativeEventLogger.write(
1922 {
2023 observedAt,
2124 event: {
22 id: crypto.randomUUID(),
25 id: yield* Random.nextUUIDv4,
2326 kind: input.kind,
2427 provider: input.provider,
2528 createdAt: observedAt,
apps/server/src/provider/acp/AcpSessionRuntime.ts+15-5
......@@ -1,4 +1,14 @@
1import { Cause, Deferred, Effect, Exit, Layer, Queue, Ref, Scope, Context, Stream } from "effect";
1import * as Cause from "effect/Cause";
2import * as Deferred from "effect/Deferred";
3import * as Effect from "effect/Effect";
4import * as Exit from "effect/Exit";
5import * as Layer from "effect/Layer";
6import * as Queue from "effect/Queue";
7import * as Ref from "effect/Ref";
8import * as Scope from "effect/Scope";
9import * as Context from "effect/Context";
10import * as Schema from "effect/Schema";
11import * as Stream from "effect/Stream";
212import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
313import * as EffectAcpClient from "effect-acp/client";
414import * as EffectAcpErrors from "effect-acp/errors";
......@@ -248,7 +258,7 @@ const makeAcpSessionRuntime = (
248258 }
249259 return yield* new EffectAcpErrors.AcpTransportError({
250260 detail: "ACP session runtime has not been started",
251 cause: new Error("ACP session runtime has not been started"),
261 cause: "ACP session runtime has not been started",
252262 });
253263 });
254264
......@@ -267,7 +277,7 @@ const makeAcpSessionRuntime = (
267277 }
268278 return yield* new EffectAcpErrors.AcpRequestError({
269279 code: -32602,
270 errorMessage: `Invalid value ${JSON.stringify(value)} for session config option "${configOption.id}": expected boolean`,
280 errorMessage: `Invalid value ${Schema.encodeUnknownSync(Schema.UnknownFromJsonString)(value)} for session config option "${configOption.id}": expected boolean`,
271281 data: {
272282 configId: configOption.id,
273283 expectedType: "boolean",
......@@ -278,7 +288,7 @@ const makeAcpSessionRuntime = (
278288 if (typeof value !== "string") {
279289 return yield* new EffectAcpErrors.AcpRequestError({
280290 code: -32602,
281 errorMessage: `Invalid value ${JSON.stringify(value)} for session config option "${configOption.id}": expected string`,
291 errorMessage: `Invalid value ${Schema.encodeUnknownSync(Schema.UnknownFromJsonString)(value)} for session config option "${configOption.id}": expected string`,
282292 data: {
283293 configId: configOption.id,
284294 expectedType: "string",
......@@ -292,7 +302,7 @@ const makeAcpSessionRuntime = (
292302 }
293303 return yield* new EffectAcpErrors.AcpRequestError({
294304 code: -32602,
295 errorMessage: `Invalid value ${JSON.stringify(value)} for session config option "${configOption.id}": expected one of ${allowedValues.join(", ")}`,
305 errorMessage: `Invalid value ${Schema.encodeUnknownSync(Schema.UnknownFromJsonString)(value)} for session config option "${configOption.id}": expected one of ${allowedValues.join(", ")}`,
296306 data: {
297307 configId: configOption.id,
298308 allowedValues,
apps/server/src/provider/acp/CursorAcpCliProbe.test.ts+12-7
......@@ -4,7 +4,8 @@
44 */
55import * as NodeServices from "@effect/platform-node/NodeServices";
66import { it } from "@effect/vitest";
7import { Effect } from "effect";
7import * as Console from "effect/Console";
8import * as Effect from "effect/Effect";
89import { describe, expect } from "vitest";
910import type * as EffectAcpSchema from "effect-acp/schema";
1011
......@@ -44,12 +45,14 @@ describe.runIf(process.env.T3_CURSOR_ACP_PROBE === "1")("Cursor ACP CLI probe",
4445 const runtime = yield* AcpSessionRuntime;
4546 const started = yield* runtime.start();
4647 const result = started.sessionSetupResult;
47 console.log("session/new result:", JSON.stringify(result, null, 2));
48 // @effect-diagnostics-next-line preferSchemaOverJson:off
49 yield* Console.log("session/new result:", JSON.stringify(result, null, 2));
4850
4951 expect(typeof started.sessionId).toBe("string");
5052
5153 const configOptions = result.configOptions;
52 console.log("session/new configOptions:", JSON.stringify(configOptions, null, 2));
54 // @effect-diagnostics-next-line preferSchemaOverJson:off
55 yield* Console.log("session/new configOptions:", JSON.stringify(configOptions, null, 2));
5356
5457 if (Array.isArray(configOptions)) {
5558 const modelConfig = configOptions.find((opt) => opt.category === "model");
......@@ -59,9 +62,11 @@ describe.runIf(process.env.T3_CURSOR_ACP_PROBE === "1")("Cursor ACP CLI probe",
5962 opt.category === "model_option" ||
6063 opt.category === "model_config",
6164 );
62 console.log("Model config option:", JSON.stringify(modelConfig, null, 2));
63 console.log(
65 // @effect-diagnostics-next-line preferSchemaOverJson:off
66 yield* Console.log("Model config option:", JSON.stringify(modelConfig, null, 2));
67 yield* Console.log(
6468 "Parameterized model config options:",
69 // @effect-diagnostics-next-line preferSchemaOverJson:off
6570 JSON.stringify(parameterizedOptions, null, 2),
6671 );
6772 expect(modelConfig).toBeDefined();
......@@ -107,8 +112,8 @@ describe.runIf(process.env.T3_CURSOR_ACP_PROBE === "1")("Cursor ACP CLI probe",
107112
108113 const setResult: EffectAcpSchema.SetSessionConfigOptionResponse =
109114 yield* runtime.setConfigOption(modelConfigId, "gpt-5.4");
110
111 console.log("session/set_config_option result:", JSON.stringify(setResult, null, 2));
115 // @effect-diagnostics-next-line preferSchemaOverJson:off
116 yield* Console.log("session/set_config_option result:", JSON.stringify(setResult, null, 2));
112117
113118 if (Array.isArray(setResult.configOptions)) {
114119 const modelConfig = setResult.configOptions.find((opt) => opt.category === "model");
apps/server/src/provider/acp/CursorAcpExtension.ts+1-1
......@@ -3,7 +3,7 @@
33 * Additional reference provided by the Cursor team: https://anysphere.enterprise.slack.com/files/U068SSJE141/F0APT1HSZRP/cursor-acp-extension-method-schemas.md
44 */
55import type { UserInputQuestion } from "@t3tools/contracts";
6import { Schema } from "effect";
6import * as Schema from "effect/Schema";
77
88const CursorAskQuestionOption = Schema.Struct({
99 id: Schema.String,
apps/server/src/provider/acp/CursorAcpSupport.test.ts+4-6
......@@ -1,4 +1,4 @@
1import { Effect } from "effect";
1import * as Effect from "effect/Effect";
22import type * as EffectAcpSchema from "effect-acp/schema";
33import { describe, expect, it } from "vitest";
44
......@@ -105,11 +105,9 @@ describe("applyCursorAcpModelSelection", () => {
105105 { id: "fastMode", value: true },
106106 ],
107107 mapError: ({ step, configId, cause }) =>
108 new Error(
109 step === "set-config-option"
110 ? `failed to set config option ${configId}: ${cause.message}`
111 : `failed to set model: ${cause.message}`,
112 ),
108 step === "set-config-option"
109 ? `failed to set config option ${configId}: ${cause.message}`
110 : `failed to set model: ${cause.message}`,
113111 }),
114112 );
115113
apps/server/src/provider/acp/CursorAcpSupport.ts+3-1
......@@ -1,5 +1,7 @@
11import { type CursorSettings, type ProviderOptionSelection } from "@t3tools/contracts";
2import { Effect, Layer, Scope } from "effect";
2import * as Effect from "effect/Effect";
3import * as Layer from "effect/Layer";
4import * as Scope from "effect/Scope";
35import { ChildProcessSpawner } from "effect/unstable/process";
46import type * as EffectAcpErrors from "effect-acp/errors";
57
apps/server/src/provider/builtInProviderCatalog.ts+1-1
......@@ -1,5 +1,5 @@
11import type { ProviderDriverKind, ProviderInstanceId, ServerProvider } from "@t3tools/contracts";
2import type { Stream } from "effect";
2import type * as Stream from "effect/Stream";
33import type { ServerProviderShape } from "./Services/ServerProvider.ts";
44
55export type ProviderSnapshotSource = {
apps/server/src/provider/makeManagedServerProvider.test.ts+10-5
......@@ -1,7 +1,12 @@
11import { describe, it, assert } from "@effect/vitest";
22import { ProviderDriverKind, ProviderInstanceId, type ServerProvider } from "@t3tools/contracts";
33import { createModelCapabilities } from "@t3tools/shared/model";
4import { Deferred, Effect, Fiber, PubSub, Ref, Stream } from "effect";
4import * as Deferred from "effect/Deferred";
5import * as Effect from "effect/Effect";
6import * as Fiber from "effect/Fiber";
7import * as PubSub from "effect/PubSub";
8import * as Ref from "effect/Ref";
9import * as Stream from "effect/Stream";
510
611import { makeManagedServerProvider } from "./makeManagedServerProvider.ts";
712
......@@ -108,7 +113,7 @@ describe("makeManagedServerProvider", () => {
108113 getSettings: Effect.succeed({ enabled: true }),
109114 streamSettings: Stream.empty,
110115 haveSettingsChanged: (previous, next) => previous.enabled !== next.enabled,
111 initialSnapshot: () => initialSnapshot,
116 initialSnapshot: () => Effect.succeed(initialSnapshot),
112117 checkProvider: Ref.update(checkCalls, (count) => count + 1).pipe(
113118 Effect.flatMap(() => Deferred.await(releaseCheck)),
114119 Effect.as(refreshedSnapshot),
......@@ -150,7 +155,7 @@ describe("makeManagedServerProvider", () => {
150155 getSettings: Ref.get(settingsRef),
151156 streamSettings: Stream.fromPubSub(settingsChanges),
152157 haveSettingsChanged: (previous, next) => previous.enabled !== next.enabled,
153 initialSnapshot: () => initialSnapshot,
158 initialSnapshot: () => Effect.succeed(initialSnapshot),
154159 checkProvider: Ref.updateAndGet(checkCalls, (count) => count + 1).pipe(
155160 Effect.flatMap((count) =>
156161 count === 1
......@@ -192,7 +197,7 @@ describe("makeManagedServerProvider", () => {
192197 getSettings: Effect.succeed({ enabled: true }),
193198 streamSettings: Stream.empty,
194199 haveSettingsChanged: (previous, next) => previous.enabled !== next.enabled,
195 initialSnapshot: () => initialSnapshot,
200 initialSnapshot: () => Effect.succeed(initialSnapshot),
196201 checkProvider: Deferred.await(releaseCheck).pipe(Effect.as(refreshedSnapshot)),
197202 enrichSnapshot: ({ publishSnapshot }) =>
198203 Deferred.await(releaseEnrichment).pipe(
......@@ -233,7 +238,7 @@ describe("makeManagedServerProvider", () => {
233238 getSettings: Effect.succeed({ enabled: true }),
234239 streamSettings: Stream.empty,
235240 haveSettingsChanged: (previous, next) => previous.enabled !== next.enabled,
236 initialSnapshot: () => initialSnapshot,
241 initialSnapshot: () => Effect.succeed(initialSnapshot),
237242 checkProvider: Ref.updateAndGet(refreshCount, (count) => count + 1).pipe(
238243 Effect.flatMap((count) =>
239244 count === 1
apps/server/src/provider/makeManagedServerProvider.ts+10-3
......@@ -1,5 +1,12 @@
11import type { ServerProvider } from "@t3tools/contracts";
2import { Duration, Effect, Equal, Fiber, PubSub, Ref, Scope, Stream } from "effect";
2import * as Duration from "effect/Duration";
3import * as Effect from "effect/Effect";
4import * as Equal from "effect/Equal";
5import * as Fiber from "effect/Fiber";
6import * as PubSub from "effect/PubSub";
7import * as Ref from "effect/Ref";
8import * as Scope from "effect/Scope";
9import * as Stream from "effect/Stream";
310import * as Semaphore from "effect/Semaphore";
411
512import type { ServerProviderShape } from "./Services/ServerProvider.ts";
......@@ -17,7 +24,7 @@ export const makeManagedServerProvider = Effect.fn("makeManagedServerProvider")(
1724 readonly getSettings: Effect.Effect<Settings>;
1825 readonly streamSettings: Stream.Stream<Settings>;
1926 readonly haveSettingsChanged: (previous: Settings, next: Settings) => boolean;
20 readonly initialSnapshot: (settings: Settings) => ServerProvider;
27 readonly initialSnapshot: (settings: Settings) => Effect.Effect<ServerProvider>;
2128 readonly checkProvider: Effect.Effect<ServerProvider, ServerSettingsError>;
2229 readonly enrichSnapshot?: (input: {
2330 readonly settings: Settings;
......@@ -33,7 +40,7 @@ export const makeManagedServerProvider = Effect.fn("makeManagedServerProvider")(
3340 PubSub.shutdown,
3441 );
3542 const initialSettings = yield* input.getSettings;
36 const initialSnapshot = input.initialSnapshot(initialSettings);
43 const initialSnapshot = yield* input.initialSnapshot(initialSettings);
3744 const snapshotStateRef = yield* Ref.make<ProviderSnapshotState>({
3845 snapshot: initialSnapshot,
3946 enrichmentGeneration: 0,
apps/server/src/provider/opencodeRuntime.ts+19-20
......@@ -11,27 +11,26 @@ import {
1111 type QuestionAnswer,
1212 type QuestionRequest,
1313} from "@opencode-ai/sdk/v2";
14import {
15 Cause,
16 Context,
17 Data,
18 Deferred,
19 Effect,
20 Exit,
21 Fiber,
22 Layer,
23 Option,
24 Predicate as P,
25 Ref,
26 Result,
27 Scope,
28 Stream,
29} from "effect";
14import * as Cause from "effect/Cause";
15import * as Context from "effect/Context";
16import * as Data from "effect/Data";
17import * as Deferred from "effect/Deferred";
18import * as Effect from "effect/Effect";
19import * as Exit from "effect/Exit";
20import * as Fiber from "effect/Fiber";
21import * as Layer from "effect/Layer";
22import * as Option from "effect/Option";
23import * as P from "effect/Predicate";
24import * as Ref from "effect/Ref";
25import * as Result from "effect/Result";
26import * as Scope from "effect/Scope";
27import * as Schema from "effect/Schema";
28import * as Stream from "effect/Stream";
3029import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
3130
3231import { isWindowsCommandNotFound } from "../processRunner.ts";
3332import { collectStreamAsString } from "./providerSnapshot.ts";
34import { NetService } from "@t3tools/shared/Net";
33import * as NetService from "@t3tools/shared/Net";
3534
3635const OPENCODE_SERVER_READY_PREFIX = "opencode server listening";
3736const DEFAULT_OPENCODE_SERVER_TIMEOUT_MS = 5_000;
......@@ -66,7 +65,7 @@ export function openCodeRuntimeErrorDetail(cause: unknown): string {
6665 const status = (anyCause.response as { status?: number } | undefined)?.status;
6766 const body = anyCause.error ?? anyCause.data ?? anyCause.body;
6867 try {
69 return `status=${status ?? "?"} body=${JSON.stringify(body ?? cause)}`;
68 return `status=${status ?? "?"} body=${Schema.encodeUnknownSync(Schema.UnknownFromJsonString)(body ?? cause)}`;
7069 } catch {
7170 /* fall through */
7271 }
......@@ -270,7 +269,7 @@ function ensureRuntimeError(
270269
271270const makeOpenCodeRuntime = Effect.gen(function* () {
272271 const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
273 const netService = yield* NetService;
272 const netService = yield* NetService.NetService;
274273
275274 const runOpenCodeCommand: OpenCodeRuntimeShape["runOpenCodeCommand"] = (input) =>
276275 Effect.gen(function* () {
......@@ -337,7 +336,7 @@ const makeOpenCodeRuntime = Effect.gen(function* () {
337336 shell: process.platform === "win32",
338337 env: {
339338 ...(input.environment ?? process.env),
340 OPENCODE_CONFIG_CONTENT: JSON.stringify({}),
339 OPENCODE_CONFIG_CONTENT: Schema.encodeUnknownSync(Schema.UnknownFromJsonString)({}),
341340 },
342341 }),
343342 )
apps/server/src/provider/providerMaintenance.test.ts+3-1
......@@ -1,10 +1,12 @@
1// @effect-diagnostics nodeBuiltinImport:off
12import { afterEach, describe, expect, it } from "@effect/vitest";
23import { chmodSync, mkdirSync, symlinkSync, writeFileSync } from "node:fs";
34import * as NodeServices from "@effect/platform-node/NodeServices";
45import os from "node:os";
56import path from "node:path";
67import { ProviderDriverKind } from "@t3tools/contracts";
7import { Effect, Random } from "effect";
8import * as Effect from "effect/Effect";
9import * as Random from "effect/Random";
810import {
911 clearLatestProviderVersionCacheForTests,
1012 createProviderVersionAdvisory,
apps/server/src/provider/providerMaintenance.ts+5-1
......@@ -4,7 +4,11 @@ import {
44 type ServerProviderVersionAdvisory,
55} from "@t3tools/contracts";
66import { resolveCommandPath } from "@t3tools/shared/shell";
7import { DateTime, Effect, FileSystem, Option, Schema } from "effect";
7import * as DateTime from "effect/DateTime";
8import * as Effect from "effect/Effect";
9import * as FileSystem from "effect/FileSystem";
10import * as Option from "effect/Option";
11import * as Schema from "effect/Schema";
812import { HttpClient, HttpClientRequest } from "effect/unstable/http";
913
1014import { compareCliVersions } from "./cliVersion.ts";
apps/server/src/provider/providerMaintenanceCommandCoordinator.ts+2-1
......@@ -1,4 +1,5 @@
1import { Effect, Ref } from "effect";
1import * as Effect from "effect/Effect";
2import * as Ref from "effect/Ref";
23import * as Semaphore from "effect/Semaphore";
34
45export interface ProviderMaintenanceCommandCoordinatorShape<E> {
apps/server/src/provider/providerMaintenanceRunner.test.ts+9-1
......@@ -6,7 +6,15 @@ import {
66 type ServerProviderUpdateState,
77} from "@t3tools/contracts";
88import { ServerProviderUpdateError } from "@t3tools/contracts";
9import { Cause, Effect, Exit, Fiber, Layer, Ref, Schema, Sink, Stream } from "effect";
9import * as Cause from "effect/Cause";
10import * as Effect from "effect/Effect";
11import * as Exit from "effect/Exit";
12import * as Fiber from "effect/Fiber";
13import * as Layer from "effect/Layer";
14import * as Ref from "effect/Ref";
15import * as Schema from "effect/Schema";
16import * as Sink from "effect/Sink";
17import * as Stream from "effect/Stream";
1018import { HttpClient, HttpClientResponse } from "effect/unstable/http";
1119import { ChildProcessSpawner } from "effect/unstable/process";
1220
apps/server/src/provider/providerMaintenanceRunner.ts+23-3
......@@ -7,7 +7,16 @@ import {
77 type ServerProviderUpdatedPayload,
88 type ServerProviderUpdateState,
99} from "@t3tools/contracts";
10import { Cause, Context, DateTime, Duration, Effect, Layer, Option, Ref, Schema } from "effect";
10import * as Cause from "effect/Cause";
11import * as Context from "effect/Context";
12import * as Data from "effect/Data";
13import * as DateTime from "effect/DateTime";
14import * as Duration from "effect/Duration";
15import * as Effect from "effect/Effect";
16import * as Layer from "effect/Layer";
17import * as Option from "effect/Option";
18import * as Ref from "effect/Ref";
19import * as Schema from "effect/Schema";
1120import { HttpClient } from "effect/unstable/http";
1221import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
1322
......@@ -45,6 +54,11 @@ export class ProviderMaintenanceRunner extends Context.Service<
4554 ProviderMaintenanceRunnerShape
4655>()("t3/provider/ProviderMaintenanceRunner") {}
4756
57class ProviderMaintenanceCommandError extends Data.TaggedError("ProviderMaintenanceCommandError")<{
58 readonly message: string;
59 readonly cause?: unknown;
60}> {}
61
4862interface VerifiedProviderRefresh {
4963 readonly providers: ReadonlyArray<ServerProvider>;
5064 readonly verifiedProviders: ReadonlyArray<ServerProvider>;
......@@ -65,7 +79,10 @@ const runProviderMaintenanceCommandWithSpawner = Effect.fn("ProviderMaintenanceR
6579 .pipe(
6680 Effect.mapError(
6781 (cause) =>
68 new Error(`Failed to run update command ${input.command}: ${cause.message}`),
82 new ProviderMaintenanceCommandError({
83 message: `Failed to run update command ${input.command}: ${cause.message}`,
84 cause,
85 }),
6986 ),
7087 );
7188 yield* Effect.addFinalizer(() => child.kill().pipe(Effect.ignore));
......@@ -86,7 +103,10 @@ const runProviderMaintenanceCommandWithSpawner = Effect.fn("ProviderMaintenanceR
86103 ).pipe(
87104 Effect.mapError(
88105 (cause) =>
89 new Error(cause instanceof Error ? cause.message : "Update command failed to run."),
106 new ProviderMaintenanceCommandError({
107 message: cause instanceof Error ? cause.message : "Update command failed to run.",
108 cause,
109 }),
90110 ),
91111 );
92112
apps/server/src/provider/providerSnapshot.ts+11-3
......@@ -8,7 +8,9 @@ import type {
88 ServerProviderModel,
99 ServerProviderState,
1010} from "@t3tools/contracts";
11import { Effect, Stream } from "effect";
11import * as Effect from "effect/Effect";
12import * as Data from "effect/Data";
13import * as Stream from "effect/Stream";
1214import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
1315import { normalizeModelSlug } from "@t3tools/shared/model";
1416import { isWindowsCommandNotFound } from "../processRunner.ts";
......@@ -25,6 +27,12 @@ export interface CommandResult {
2527 readonly code: number;
2628}
2729
30export class ProviderCommandExecutionError extends Data.TaggedError(
31 "ProviderCommandExecutionError",
32)<{
33 readonly message: string;
34}> {}
35
2836export interface ProviderProbeResult {
2937 readonly installed: boolean;
3038 readonly version: string | null;
......@@ -47,7 +55,7 @@ export function nonEmptyTrimmed(value: string | undefined): string | undefined {
4755 return trimmed.length > 0 ? trimmed : undefined;
4856}
4957
50export function isCommandMissingCause(error: Error): boolean {
58export function isCommandMissingCause(error: { readonly message: string }): boolean {
5159 const lower = error.message.toLowerCase();
5260 return lower.includes("enoent") || lower.includes("notfound");
5361}
......@@ -67,7 +75,7 @@ export const spawnAndCollect = (binaryPath: string, command: ChildProcess.Comman
6775
6876 const result: CommandResult = { stdout, stderr, code: exitCode };
6977 if (isWindowsCommandNotFound(exitCode, stderr)) {
70 return yield* Effect.fail(new Error(`spawn ${binaryPath} ENOENT`));
78 return yield* new ProviderCommandExecutionError({ message: `spawn ${binaryPath} ENOENT` });
7179 }
7280 return result;
7381 }).pipe(Effect.scoped);
apps/server/src/provider/providerStatusCache.test.ts+2-1
......@@ -7,7 +7,8 @@ import {
77} from "@t3tools/contracts";
88import { createModelCapabilities } from "@t3tools/shared/model";
99import { assert, it } from "@effect/vitest";
10import { Effect, FileSystem } from "effect";
10import * as Effect from "effect/Effect";
11import * as FileSystem from "effect/FileSystem";
1112
1213import {
1314 hydrateCachedProvider,
apps/server/src/provider/providerStatusCache.ts+5-1
......@@ -4,7 +4,11 @@ import {
44 type ServerProvider,
55 ServerProvider as ServerProviderSchema,
66} from "@t3tools/contracts";
7import { Cause, Effect, FileSystem, Path, Schema } from "effect";
7import * as Cause from "effect/Cause";
8import * as Effect from "effect/Effect";
9import * as FileSystem from "effect/FileSystem";
10import * as Path from "effect/Path";
11import * as Schema from "effect/Schema";
812
913import { writeFileStringAtomically } from "../atomicWrite.ts";
1014
apps/server/src/provider/testUtils/providerAdapterRegistryMock.ts+5-1
......@@ -18,7 +18,11 @@ import {
1818 ProviderDriverKind,
1919 type ProviderInstanceId,
2020} from "@t3tools/contracts";
21import { Effect, PubSub, Record, Result, Stream } from "effect";
21import * as Effect from "effect/Effect";
22import * as PubSub from "effect/PubSub";
23import * as Record from "effect/Record";
24import * as Result from "effect/Result";
25import * as Stream from "effect/Stream";
2226
2327import { ProviderUnsupportedError, type ProviderAdapterError } from "../Errors.ts";
2428import type { ProviderAdapterShape } from "../Services/ProviderAdapter.ts";
apps/server/src/provider/unavailableProviderSnapshot.ts+36-29
......@@ -15,6 +15,8 @@ import {
1515 type ProviderInstanceId,
1616 type ServerProvider,
1717} from "@t3tools/contracts";
18import * as DateTime from "effect/DateTime";
19import * as Effect from "effect/Effect";
1820
1921import { buildServerProvider } from "./providerSnapshot.ts";
2022
......@@ -25,12 +27,15 @@ export interface UnavailableProviderSnapshotInput {
2527 readonly accentColor?: string | undefined;
2628 readonly reason: string;
2729 /**
28 * Optional override for `checkedAt`. Defaulted to `new Date()` so callers
30 * Optional override for `checkedAt`. Defaulted to the current Effect
31 * `DateTime` so callers
2932 * (notably tests) don't have to pass it.
3033 */
3134 readonly checkedAt?: string;
3235}
3336
37const nowIso = Effect.map(DateTime.now, DateTime.formatIso);
38
3439/**
3540 * Produce a `ServerProvider` snapshot representing a configured instance
3641 * whose driver the running build does not implement. The result is safe
......@@ -39,34 +44,36 @@ export interface UnavailableProviderSnapshotInput {
3944 */
4045export function buildUnavailableProviderSnapshot(
4146 input: UnavailableProviderSnapshotInput,
42): ServerProvider {
43 const checkedAt = input.checkedAt ?? new Date().toISOString();
44 const displayName = input.displayName?.trim() || (input.driverKind as string);
47): Effect.Effect<ServerProvider> {
48 return Effect.gen(function* () {
49 const checkedAt = input.checkedAt ?? (yield* nowIso);
50 const displayName = input.displayName?.trim() || (input.driverKind as string);
4551
46 const base = buildServerProvider({
47 presentation: { displayName },
48 enabled: false,
49 checkedAt,
50 models: [],
51 skills: [],
52 probe: {
53 installed: false,
54 version: null,
55 status: "error",
56 auth: { status: "unknown" },
57 message: input.reason,
58 },
59 });
52 const base = buildServerProvider({
53 presentation: { displayName },
54 enabled: false,
55 checkedAt,
56 models: [],
57 skills: [],
58 probe: {
59 installed: false,
60 version: null,
61 status: "error",
62 auth: { status: "unknown" },
63 message: input.reason,
64 },
65 });
6066
61 return {
62 ...base,
63 instanceId: input.instanceId,
64 ...(input.accentColor ? { accentColor: input.accentColor } : {}),
65 driver:
66 typeof input.driverKind === "string"
67 ? ProviderDriverKind.make(input.driverKind)
68 : input.driverKind,
69 availability: "unavailable",
70 unavailableReason: input.reason,
71 };
67 return {
68 ...base,
69 instanceId: input.instanceId,
70 ...(input.accentColor ? { accentColor: input.accentColor } : {}),
71 driver:
72 typeof input.driverKind === "string"
73 ? ProviderDriverKind.make(input.driverKind)
74 : input.driverKind,
75 availability: "unavailable",
76 unavailableReason: input.reason,
77 };
78 });
7279}
apps/server/src/server.test.ts+59-51
......@@ -27,18 +27,17 @@ import {
2727} from "@t3tools/contracts";
2828import { assert, it } from "@effect/vitest";
2929import { assertFailure, assertInclude, assertTrue } from "@effect/vitest/utils";
30import {
31 Deferred,
32 DateTime,
33 Duration,
34 Effect,
35 FileSystem,
36 Layer,
37 ManagedRuntime,
38 Option,
39 Path,
40 Stream,
41} from "effect";
30import * as Clock from "effect/Clock";
31import * as Deferred from "effect/Deferred";
32import * as DateTime from "effect/DateTime";
33import * as Duration from "effect/Duration";
34import * as Effect from "effect/Effect";
35import * as FileSystem from "effect/FileSystem";
36import * as Layer from "effect/Layer";
37import * as ManagedRuntime from "effect/ManagedRuntime";
38import * as Option from "effect/Option";
39import * as Path from "effect/Path";
40import * as Stream from "effect/Stream";
4241import { ChildProcessSpawner } from "effect/unstable/process";
4342import {
4443 FetchHttpClient,
......@@ -92,6 +91,7 @@ import {
9291import { ProjectFaviconResolverLive } from "./project/Layers/ProjectFaviconResolver.ts";
9392import {
9493 ProjectSetupScriptRunner,
94 ProjectSetupScriptRunnerError,
9595 type ProjectSetupScriptRunnerShape,
9696} from "./project/Services/ProjectSetupScriptRunner.ts";
9797import {
......@@ -116,6 +116,7 @@ import { ServerSecretStoreLive } from "./auth/Layers/ServerSecretStore.ts";
116116import { ServerAuthLive } from "./auth/Layers/ServerAuth.ts";
117117import * as ProcessDiagnostics from "./diagnostics/ProcessDiagnostics.ts";
118118import * as TraceDiagnostics from "./diagnostics/TraceDiagnostics.ts";
119import * as Data from "effect/Data";
119120
120121const defaultProjectId = ProjectId.make("project-default");
121122const defaultThreadId = ThreadId.make("thread-default");
......@@ -137,7 +138,7 @@ const testEnvironmentDescriptor = {
137138 },
138139};
139140const makeDefaultOrchestrationReadModel = () => {
140 const now = new Date().toISOString();
141 const now = "2026-01-01T00:00:00.000Z";
141142 return {
142143 snapshotSequence: 0,
143144 updatedAt: now,
......@@ -181,7 +182,7 @@ const makeDefaultOrchestrationReadModel = () => {
181182const makeDefaultOrchestrationThreadShell = (
182183 overrides: Partial<OrchestrationThreadShell> = {},
183184): OrchestrationThreadShell => {
184 const now = new Date().toISOString();
185 const now = "2026-01-01T00:00:00.000Z";
185186 return {
186187 id: defaultThreadId,
187188 projectId: defaultProjectId,
......@@ -302,15 +303,13 @@ const makeBrowserOtlpPayload = (spanName: string) =>
302303 yield* Effect.promise(() => runtime.dispose());
303304 }
304305
305 const request = yield* Effect.promise(() =>
306 Promise.race([
307 collector.firstRequest,
308 new Promise<never>((_, reject) => {
309 setTimeout(() => reject(new Error("Timed out waiting for OTLP trace export")), 1_000);
310 }),
311 ]),
306 const request = yield* Effect.raceFirst(
307 Effect.promise(() => collector.firstRequest).pipe(Effect.orDie),
308 Effect.sleep(Duration.seconds(1)).pipe(
309 Effect.andThen(Effect.die(new Error("Timed out waiting for OTLP trace export"))),
310 ),
312311 );
313
312 // @effect-diagnostics-next-line preferSchemaOverJson:off
314313 return JSON.parse(request.body) as OtlpTracer.TraceData;
315314 });
316315
......@@ -629,7 +628,7 @@ const buildAppUnderTest = (options?: {
629628 snapshotSequence: 0,
630629 projects: [],
631630 threads: [],
632 updatedAt: new Date(0).toISOString(),
631 updatedAt: "1970-01-01T00:00:00.000Z",
633632 }),
634633 getSnapshotSequence: () => Effect.succeed({ snapshotSequence: 0 }),
635634 getProjectShellById: () => Effect.succeed(Option.none()),
......@@ -823,17 +822,23 @@ const bootstrapBearerSession = (credential = defaultDesktopBootstrapToken) =>
823822 };
824823 });
825824
825class AuthenticationGetterError extends Data.TaggedError("AuthenticationGetterError")<{
826 readonly message: string;
827}> {}
828
826829const getAuthenticatedSessionCookieHeader = (credential = defaultDesktopBootstrapToken) =>
827830 Effect.gen(function* () {
828831 const { response, cookie } = yield* bootstrapBrowserSession(credential);
829832 if (!response.ok) {
830 return yield* Effect.fail(
831 new Error(`Expected bootstrap session response to succeed, got ${response.status}`),
832 );
833 return yield* new AuthenticationGetterError({
834 message: `Expected bootstrap session response to succeed, got ${response.status}`,
835 });
833836 }
834837
835838 if (!cookie) {
836 return yield* Effect.fail(new Error("Expected bootstrap session response to set a cookie."));
839 return yield* new AuthenticationGetterError({
840 message: "Expected bootstrap session response to set a cookie.",
841 });
837842 }
838843
839844 return cookie.split(";")[0] ?? cookie;
......@@ -843,15 +848,15 @@ const getAuthenticatedBearerSessionToken = (credential = defaultDesktopBootstrap
843848 Effect.gen(function* () {
844849 const { response, body } = yield* bootstrapBearerSession(credential);
845850 if (!response.ok) {
846 return yield* Effect.fail(
847 new Error(`Expected bearer bootstrap response to succeed, got ${response.status}`),
848 );
851 return yield* new AuthenticationGetterError({
852 message: `Expected bearer bootstrap response to succeed, got ${response.status}`,
853 });
849854 }
850855
851856 if (!body.sessionToken) {
852 return yield* Effect.fail(
853 new Error("Expected bearer bootstrap response to include a session token."),
854 );
857 return yield* new AuthenticationGetterError({
858 message: "Expected bearer bootstrap response to include a session token.",
859 });
855860 }
856861
857862 return body.sessionToken;
......@@ -1758,6 +1763,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
17581763 "content-type": "application/json",
17591764 origin: "http://localhost:5733",
17601765 },
1766 // @effect-diagnostics-next-line preferSchemaOverJson:off
17611767 body: HttpBody.text(JSON.stringify(payload), "application/json"),
17621768 });
17631769
......@@ -1803,6 +1809,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
18031809 ]);
18041810 assert.deepEqual(upstreamRequests, [
18051811 {
1812 // @effect-diagnostics-next-line preferSchemaOverJson:off
18061813 body: JSON.stringify(payload),
18071814 contentType: "application/json",
18081815 },
......@@ -1875,6 +1882,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
18751882 cookie: yield* getAuthenticatedSessionCookieHeader(),
18761883 "content-type": "application/json",
18771884 },
1885 // @effect-diagnostics-next-line preferSchemaOverJson:off
18781886 body: HttpBody.text(JSON.stringify(payload), "application/json"),
18791887 });
18801888
......@@ -2182,7 +2190,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
21822190 version: 1 as const,
21832191 sequence: 2,
21842192 type: "ready" as const,
2185 payload: { at: new Date().toISOString(), environment: testEnvironmentDescriptor },
2193 payload: { at: "2026-01-01T00:00:00.000Z", environment: testEnvironmentDescriptor },
21862194 });
21872195
21882196 yield* buildAppUnderTest({
......@@ -2370,7 +2378,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
23702378 instanceId: ProviderInstanceId.make("codex"),
23712379 model: "gpt-5-codex",
23722380 },
2373 createdAt: new Date().toISOString(),
2381 createdAt: "2026-01-01T00:00:00.000Z",
23742382 }),
23752383 ),
23762384 );
......@@ -2913,11 +2921,11 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
29132921 });
29142922
29152923 const wsUrl = yield* getWsServerUrl("/ws");
2916 const startedAt = Date.now();
2924 const startedAt = yield* Clock.currentTimeMillis;
29172925 const result = yield* Effect.scoped(
29182926 withWsRpcClient(wsUrl, (client) => client[WS_METHODS.vcsPull]({ cwd: "/tmp/repo" })),
29192927 );
2920 const elapsedMs = Date.now() - startedAt;
2928 const elapsedMs = (yield* Clock.currentTimeMillis) - startedAt;
29212929
29222930 assert.equal(result.status, "pulled");
29232931 assertTrue(elapsedMs < 1_000);
......@@ -2982,7 +2990,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
29822990 });
29832991
29842992 const wsUrl = yield* getWsServerUrl("/ws");
2985 const startedAt = Date.now();
2993 const startedAt = yield* Clock.currentTimeMillis;
29862994 yield* Effect.scoped(
29872995 withWsRpcClient(wsUrl, (client) =>
29882996 client[WS_METHODS.gitRunStackedAction]({
......@@ -2992,7 +3000,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
29923000 }).pipe(Stream.runCollect),
29933001 ),
29943002 );
2995 const elapsedMs = Date.now() - startedAt;
3003 const elapsedMs = (yield* Clock.currentTimeMillis) - startedAt;
29963004
29973005 assertTrue(elapsedMs < 1_000);
29983006 }).pipe(Effect.provide(NodeHttpServer.layerTest)),
......@@ -3079,7 +3087,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
30793087
30803088 it.effect("routes websocket rpc orchestration methods", () =>
30813089 Effect.gen(function* () {
3082 const now = new Date().toISOString();
3090 const now = "2026-01-01T00:00:00.000Z";
30833091 const snapshot = {
30843092 snapshotSequence: 1,
30853093 updatedAt: now,
......@@ -3292,7 +3300,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
32923300 const threadId = ThreadId.make("thread-archive");
32933301 const effects: string[] = [];
32943302 const dispatchedCommands: Array<OrchestrationCommand> = [];
3295 const now = new Date().toISOString();
3303 const now = "2026-01-01T00:00:00.000Z";
32963304
32973305 yield* buildAppUnderTest({
32983306 layers: {
......@@ -3363,7 +3371,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
33633371 const threadId = ThreadId.make("thread-archive-precheck");
33643372 const effects: string[] = [];
33653373 const dispatchedCommands: Array<OrchestrationCommand> = [];
3366 const now = new Date().toISOString();
3374 const now = "2026-01-01T00:00:00.000Z";
33673375 let archived = false;
33683376
33693377 yield* buildAppUnderTest({
......@@ -3494,7 +3502,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
34943502 const threadId = ThreadId.make("thread-archive-stopped-session");
34953503 const effects: string[] = [];
34963504 const dispatchedCommands: Array<OrchestrationCommand> = [];
3497 const now = new Date().toISOString();
3505 const now = "2026-01-01T00:00:00.000Z";
34983506
34993507 yield* buildAppUnderTest({
35003508 layers: {
......@@ -3560,7 +3568,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
35603568 const threadId = ThreadId.make("thread-archive-stop-failure");
35613569 const effects: string[] = [];
35623570 const dispatchedCommands: Array<OrchestrationCommand> = [];
3563 const now = new Date().toISOString();
3571 const now = "2026-01-01T00:00:00.000Z";
35643572
35653573 yield* buildAppUnderTest({
35663574 layers: {
......@@ -3637,7 +3645,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
36373645 const threadId = ThreadId.make("thread-archive-stop-defect");
36383646 const effects: string[] = [];
36393647 const dispatchedCommands: Array<OrchestrationCommand> = [];
3640 const now = new Date().toISOString();
3648 const now = "2026-01-01T00:00:00.000Z";
36413649
36423650 yield* buildAppUnderTest({
36433651 layers: {
......@@ -3769,7 +3777,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
37693777 },
37703778 });
37713779
3772 const createdAt = new Date().toISOString();
3780 const createdAt = "2026-01-01T00:00:00.000Z";
37733781 const wsUrl = yield* getWsServerUrl("/ws");
37743782 const response = yield* Effect.scoped(
37753783 withWsRpcClient(wsUrl, (client) =>
......@@ -3864,7 +3872,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
38643872 );
38653873 const runForThread = vi.fn(
38663874 (_: Parameters<ProjectSetupScriptRunnerShape["runForThread"]>[0]) =>
3867 Effect.fail(new Error("pty unavailable")),
3875 Effect.fail(new ProjectSetupScriptRunnerError({ message: "pty unavailable" })),
38683876 );
38693877
38703878 yield* buildAppUnderTest({
......@@ -3886,7 +3894,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
38863894 },
38873895 });
38883896
3889 const createdAt = new Date().toISOString();
3897 const createdAt = "2026-01-01T00:00:00.000Z";
38903898 const wsUrl = yield* getWsServerUrl("/ws");
38913899 const response = yield* Effect.scoped(
38923900 withWsRpcClient(wsUrl, (client) =>
......@@ -4003,7 +4011,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
40034011 },
40044012 });
40054013
4006 const createdAt = new Date().toISOString();
4014 const createdAt = "2026-01-01T00:00:00.000Z";
40074015 const wsUrl = yield* getWsServerUrl("/ws");
40084016 const response = yield* Effect.scoped(
40094017 withWsRpcClient(wsUrl, (client) =>
......@@ -4087,7 +4095,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
40874095 },
40884096 });
40894097
4090 const createdAt = new Date().toISOString();
4098 const createdAt = "2026-01-01T00:00:00.000Z";
40914099 const wsUrl = yield* getWsServerUrl("/ws");
40924100 const result = yield* Effect.scoped(
40934101 withWsRpcClient(wsUrl, (client) =>
......@@ -4149,7 +4157,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
41494157 history: "",
41504158 exitCode: null,
41514159 exitSignal: null,
4152 updatedAt: new Date().toISOString(),
4160 updatedAt: "2026-01-01T00:00:00.000Z",
41534161 };
41544162
41554163 yield* buildAppUnderTest({
apps/server/src/server.ts+5-8
......@@ -1,4 +1,5 @@
1import { Effect, Layer } from "effect";
1import * as Effect from "effect/Effect";
2import * as Layer from "effect/Layer";
23import { FetchHttpClient, HttpRouter, HttpServer } from "effect/unstable/http";
34
45import { ServerConfig } from "./config.ts";
......@@ -86,7 +87,7 @@ import {
8687 orchestrationDispatchRouteLayer,
8788 orchestrationSnapshotRouteLayer,
8889} from "./orchestration/http.ts";
89import { NetService } from "@t3tools/shared/Net";
90import * as NetService from "@t3tools/shared/Net";
9091import { disableTailscaleServe, ensureTailscaleServe } from "@t3tools/tailscale";
9192
9293const PtyAdapterLive = Layer.unwrap(
......@@ -333,7 +334,7 @@ export const makeServerLayer = Layer.unwrap(
333334 return;
334335 }
335336
336 const state = makePersistedServerRuntimeState({
337 const state = yield* makePersistedServerRuntimeState({
337338 config,
338339 port: address.port,
339340 });
......@@ -418,8 +419,4 @@ export const makeServerLayer = Layer.unwrap(
418419);
419420
420421// Important: Only `ServerConfig` should be provided by the CLI layer!!! Don't let other requirements leak into the launch layer.
421export const runServer = Layer.launch(makeServerLayer) satisfies Effect.Effect<
422 never,
423 any,
424 ServerConfig
425>;
422export const runServer = Layer.launch(makeServerLayer);
apps/server/src/serverLifecycleEvents.test.ts+3-2
......@@ -1,7 +1,8 @@
11import { EnvironmentId } from "@t3tools/contracts";
22import { assert, it } from "@effect/vitest";
33import { assertTrue } from "@effect/vitest/utils";
4import { Effect, Option } from "effect";
4import * as Effect from "effect/Effect";
5import * as Option from "effect/Option";
56
67import { ServerLifecycleEvents, ServerLifecycleEventsLive } from "./serverLifecycleEvents.ts";
78
......@@ -37,7 +38,7 @@ it.effect(
3738 version: 1,
3839 type: "ready",
3940 payload: {
40 at: new Date().toISOString(),
41 at: "2026-01-01T00:00:00.000Z",
4142 environment,
4243 },
4344 })
apps/server/src/serverLifecycleEvents.ts+6-1
......@@ -1,5 +1,10 @@
11import type { ServerLifecycleStreamEvent } from "@t3tools/contracts";
2import { Effect, Layer, PubSub, Ref, Context, Stream } from "effect";
2import * as Effect from "effect/Effect";
3import * as Layer from "effect/Layer";
4import * as PubSub from "effect/PubSub";
5import * as Ref from "effect/Ref";
6import * as Context from "effect/Context";
7import * as Stream from "effect/Stream";
38
49type LifecycleEventInput =
510 | Omit<Extract<ServerLifecycleStreamEvent, { type: "welcome" }>, "sequence">
apps/server/src/serverLogger.ts+4-1
......@@ -1,4 +1,7 @@
1import { Effect, Logger, References, Layer } from "effect";
1import * as Effect from "effect/Effect";
2import * as Logger from "effect/Logger";
3import * as References from "effect/References";
4import * as Layer from "effect/Layer";
25
36import { ServerConfig } from "./config.ts";
47
apps/server/src/serverRuntimeStartup.test.ts+6-1
......@@ -1,7 +1,12 @@
11import * as NodeServices from "@effect/platform-node/NodeServices";
22import { DEFAULT_MODEL, ProjectId, ProviderInstanceId, ThreadId } from "@t3tools/contracts";
33import { assert, it } from "@effect/vitest";
4import { Deferred, Effect, Fiber, Option, Ref, Stream } from "effect";
4import * as Deferred from "effect/Deferred";
5import * as Effect from "effect/Effect";
6import * as Fiber from "effect/Fiber";
7import * as Option from "effect/Option";
8import * as Ref from "effect/Ref";
9import * as Stream from "effect/Stream";
510
611import { ServerConfig } from "./config.ts";
712import {
apps/server/src/serverRuntimeStartup.ts+16-17
......@@ -7,20 +7,19 @@ import {
77 ProviderInstanceId,
88 ThreadId,
99} from "@t3tools/contracts";
10import {
11 Data,
12 Deferred,
13 Effect,
14 Exit,
15 Layer,
16 Option,
17 Path,
18 Queue,
19 Ref,
20 Scope,
21 Context,
22 Console,
23} from "effect";
10import * as Data from "effect/Data";
11import * as Deferred from "effect/Deferred";
12import * as Effect from "effect/Effect";
13import * as Exit from "effect/Exit";
14import * as Layer from "effect/Layer";
15import * as Option from "effect/Option";
16import * as Path from "effect/Path";
17import * as Queue from "effect/Queue";
18import * as Ref from "effect/Ref";
19import * as Scope from "effect/Scope";
20import * as Context from "effect/Context";
21import * as Console from "effect/Console";
22import * as DateTime from "effect/DateTime";
2423
2524import { ServerConfig } from "./config.ts";
2625import { Keybindings } from "./keybindings.ts";
......@@ -188,7 +187,7 @@ export const resolveAutoBootstrapWelcomeTargets = Effect.gen(function* () {
188187 let nextProjectDefaultModelSelection: ModelSelection;
189188
190189 if (Option.isNone(existingProject)) {
191 const createdAt = new Date().toISOString();
190 const createdAt = DateTime.formatIso(yield* DateTime.now);
192191 nextProjectId = ProjectId.make(crypto.randomUUID());
193192 const bootstrapProjectTitle = path.basename(serverConfig.cwd) || "project";
194193 nextProjectDefaultModelSelection = getAutoBootstrapDefaultModelSelection();
......@@ -210,7 +209,7 @@ export const resolveAutoBootstrapWelcomeTargets = Effect.gen(function* () {
210209 const existingThreadId =
211210 yield* projectionReadModelQuery.getFirstActiveThreadIdByProjectId(nextProjectId);
212211 if (Option.isNone(existingThreadId)) {
213 const createdAt = new Date().toISOString();
212 const createdAt = DateTime.formatIso(yield* DateTime.now);
214213 const createdThreadId = ThreadId.make(crypto.randomUUID());
215214 yield* orchestrationEngine.dispatch({
216215 type: "thread.create",
......@@ -423,7 +422,7 @@ export const makeServerRuntimeStartup = Effect.gen(function* () {
423422 version: 1,
424423 type: "ready",
425424 payload: {
426 at: new Date().toISOString(),
425 at: DateTime.formatIso(yield* DateTime.now),
427426 environment: yield* serverEnvironment.getDescriptor,
428427 },
429428 }),
apps/server/src/serverRuntimeState.ts+14-9
......@@ -1,4 +1,8 @@
1import { Effect, FileSystem, Option, Schema } from "effect";
1import * as DateTime from "effect/DateTime";
2import * as Effect from "effect/Effect";
3import * as FileSystem from "effect/FileSystem";
4import * as Option from "effect/Option";
5import * as Schema from "effect/Schema";
26
37import { writeFileStringAtomically } from "./atomicWrite.ts";
48import { type ServerConfigShape } from "./config.ts";
......@@ -30,14 +34,15 @@ const runtimeOriginForConfig = (
3034export const makePersistedServerRuntimeState = (input: {
3135 readonly config: Pick<ServerConfigShape, "host">;
3236 readonly port: number;
33}): PersistedServerRuntimeState => ({
34 version: 1,
35 pid: process.pid,
36 ...(input.config.host ? { host: input.config.host } : {}),
37 port: input.port,
38 origin: runtimeOriginForConfig(input.config, input.port),
39 startedAt: new Date().toISOString(),
40});
37}): Effect.Effect<PersistedServerRuntimeState> =>
38 Effect.map(DateTime.now, (now) => ({
39 version: 1,
40 pid: process.pid,
41 ...(input.config.host ? { host: input.config.host } : {}),
42 port: input.port,
43 origin: runtimeOriginForConfig(input.config, input.port),
44 startedAt: DateTime.formatIso(now),
45 }));
4146
4247export const persistServerRuntimeState = (input: {
4348 readonly path: string;
apps/server/src/serverSettings.test.ts+6-1
......@@ -8,7 +8,10 @@ import {
88} from "@t3tools/contracts";
99import { createModelSelection } from "@t3tools/shared/model";
1010import { assert, it } from "@effect/vitest";
11import { Effect, FileSystem, Layer, Schema } from "effect";
11import * as Effect from "effect/Effect";
12import * as FileSystem from "effect/FileSystem";
13import * as Layer from "effect/Layer";
14import * as Schema from "effect/Schema";
1215import { ServerConfig } from "./config.ts";
1316import { ServerSettingsLive, ServerSettingsService } from "./serverSettings.ts";
1417
......@@ -439,6 +442,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {
439442 assert.equal(next.providers.codex.binaryPath, "/opt/homebrew/bin/codex");
440443
441444 const raw = yield* fileSystem.readFileString(serverConfig.settingsPath);
445 // @effect-diagnostics-next-line preferSchemaOverJson:off
442446 assert.deepEqual(JSON.parse(raw), {
443447 addProjectBaseDirectory: "~/Development",
444448 observability: {
......@@ -490,6 +494,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {
490494
491495 const raw = yield* fileSystem.readFileString(serverConfig.settingsPath);
492496 assert.notInclude(raw, "sk-or-secret");
497 // @effect-diagnostics-next-line preferSchemaOverJson:off
493498 assert.deepEqual(JSON.parse(raw).providerInstances.codex_personal.environment, [
494499 {
495500 name: "OPENROUTER_API_KEY",
apps/server/src/serverSettings.ts+17-19
......@@ -24,25 +24,23 @@ import {
2424 ServerSettingsError,
2525 type ServerSettingsPatch,
2626} from "@t3tools/contracts";
27import {
28 Cache,
29 Deferred,
30 Duration,
31 Effect,
32 Exit,
33 FileSystem,
34 Layer,
35 Path,
36 Equal,
37 PubSub,
38 Ref,
39 Schema,
40 SchemaIssue,
41 Scope,
42 Context,
43 Stream,
44 Cause,
45} from "effect";
27import * as Cache from "effect/Cache";
28import * as Deferred from "effect/Deferred";
29import * as Duration from "effect/Duration";
30import * as Effect from "effect/Effect";
31import * as Exit from "effect/Exit";
32import * as FileSystem from "effect/FileSystem";
33import * as Layer from "effect/Layer";
34import * as Path from "effect/Path";
35import * as Equal from "effect/Equal";
36import * as PubSub from "effect/PubSub";
37import * as Ref from "effect/Ref";
38import * as Schema from "effect/Schema";
39import * as SchemaIssue from "effect/SchemaIssue";
40import * as Scope from "effect/Scope";
41import * as Context from "effect/Context";
42import * as Stream from "effect/Stream";
43import * as Cause from "effect/Cause";
4644import * as Semaphore from "effect/Semaphore";
4745import { writeFileStringAtomically } from "./atomicWrite.ts";
4846import { ServerConfig } from "./config.ts";
apps/server/src/sourceControl/AzureDevOpsCli.test.ts+9-3
......@@ -1,8 +1,10 @@
11import * as NodeServices from "@effect/platform-node/NodeServices";
22import { assert, it, afterEach, describe, expect, vi } from "@effect/vitest";
3import { Effect, FileSystem, Layer, Option } from "effect";
3import * as Effect from "effect/Effect";
4import * as FileSystem from "effect/FileSystem";
5import * as Layer from "effect/Layer";
6import * as Option from "effect/Option";
47import { ChildProcessSpawner } from "effect/unstable/process";
5import type { VcsError } from "@t3tools/contracts";
68
79import * as VcsProcess from "../vcs/VcsProcess.ts";
810import * as AzureDevOpsCli from "./AzureDevOpsCli.ts";
......@@ -35,6 +37,7 @@ describe("AzureDevOpsCli.layer", () => {
3537 mockRun.mockReturnValueOnce(
3638 Effect.succeed(
3739 processOutput(
40 // @effect-diagnostics-next-line preferSchemaOverJson:off
3841 JSON.stringify({
3942 pullRequestId: 42,
4043 title: "Add Azure provider",
......@@ -91,6 +94,7 @@ describe("AzureDevOpsCli.layer", () => {
9194 mockRun.mockReturnValueOnce(
9295 Effect.succeed(
9396 processOutput(
97 // @effect-diagnostics-next-line preferSchemaOverJson:off
9498 JSON.stringify([
9599 {
96100 pullRequestId: 7,
......@@ -149,6 +153,7 @@ describe("AzureDevOpsCli.layer", () => {
149153 mockRun.mockReturnValueOnce(
150154 Effect.succeed(
151155 processOutput(
156 // @effect-diagnostics-next-line preferSchemaOverJson:off
152157 JSON.stringify({
153158 name: "repo",
154159 webUrl: "https://dev.azure.com/acme/project/_git/repo",
......@@ -181,6 +186,7 @@ describe("AzureDevOpsCli.layer", () => {
181186 mockRun.mockReturnValueOnce(
182187 Effect.succeed(
183188 processOutput(
189 // @effect-diagnostics-next-line preferSchemaOverJson:off
184190 JSON.stringify({
185191 name: "repo",
186192 webUrl: "https://dev.azure.com/acme/project/_git/repo",
......@@ -231,7 +237,7 @@ describe("AzureDevOpsCli.layer", () => {
231237 it.effect("creates pull requests using the body file as the Azure description", () =>
232238 Effect.gen(function* () {
233239 const fileSystem = yield* FileSystem.FileSystem;
234 const bodyFile = `/tmp/t3code-azure-devops-cli-${Date.now()}.md`;
240 const bodyFile = `/tmp/t3code-azure-devops-cli-.md`;
235241 yield* fileSystem.writeFileString(bodyFile, "Generated body");
236242 mockRun.mockReturnValueOnce(Effect.succeed(processOutput("{}")));
237243
apps/server/src/sourceControl/AzureDevOpsCli.ts+6-1
......@@ -1,4 +1,9 @@
1import { Context, Effect, Layer, Result, Schema, SchemaIssue } from "effect";
1import * as Context from "effect/Context";
2import * as Effect from "effect/Effect";
3import * as Layer from "effect/Layer";
4import * as Result from "effect/Result";
5import * as Schema from "effect/Schema";
6import * as SchemaIssue from "effect/SchemaIssue";
27import {
38 TrimmedNonEmptyString,
49 type SourceControlRepositoryVisibility,
apps/server/src/sourceControl/AzureDevOpsSourceControlProvider.test.ts+3-1
......@@ -1,5 +1,7 @@
11import { assert, it } from "@effect/vitest";
2import { Effect, Layer, Option } from "effect";
2import * as Effect from "effect/Effect";
3import * as Layer from "effect/Layer";
4import * as Option from "effect/Option";
35
46import * as AzureDevOpsCli from "./AzureDevOpsCli.ts";
57import * as AzureDevOpsSourceControlProvider from "./AzureDevOpsSourceControlProvider.ts";
apps/server/src/sourceControl/AzureDevOpsSourceControlProvider.ts+7-6
......@@ -1,4 +1,5 @@
1import { Effect, Layer } from "effect";
1import * as Effect from "effect/Effect";
2import * as Layer from "effect/Layer";
23import { SourceControlProviderError, type ChangeRequest } from "@t3tools/contracts";
34
45import * as AzureDevOpsCli from "./AzureDevOpsCli.ts";
......@@ -30,7 +31,7 @@ function parseAzureAuth(input: SourceControlProviderDiscovery.SourceControlAuthP
3031 });
3132 }
3233
33 if (account && account.length > 0) {
34 if (account !== undefined && account.length > 0) {
3435 return SourceControlProviderDiscovery.providerAuth({
3536 status: "authenticated",
3637 account,
......@@ -90,7 +91,7 @@ export const make = Effect.fn("makeAzureDevOpsSourceControlProvider")(function*
9091 .listPullRequests({
9192 cwd: input.cwd,
9293 headSelector: input.headSelector,
93 ...(source ? { source } : {}),
94 ...(source !== undefined ? { source } : {}),
9495 state: input.state,
9596 ...(input.limit !== undefined ? { limit: input.limit } : {}),
9697 })
......@@ -111,8 +112,8 @@ export const make = Effect.fn("makeAzureDevOpsSourceControlProvider")(function*
111112 cwd: input.cwd,
112113 baseBranch: input.baseRefName,
113114 headSelector: input.headSelector,
114 ...(source ? { source } : {}),
115 ...(input.target ? { target: input.target } : {}),
115 ...(source !== undefined ? { source } : {}),
116 ...(input.target !== undefined ? { target: input.target } : {}),
116117 title: input.title,
117118 bodyFile: input.bodyFile,
118119 })
......@@ -135,7 +136,7 @@ export const make = Effect.fn("makeAzureDevOpsSourceControlProvider")(function*
135136 .checkoutPullRequest({
136137 cwd: input.cwd,
137138 reference: input.reference,
138 ...(input.context ? { remoteName: input.context.remoteName } : {}),
139 ...(input.context !== undefined ? { remoteName: input.context.remoteName } : {}),
139140 })
140141 .pipe(Effect.mapError((error) => providerError("checkoutChangeRequest", error))),
141142 });
apps/server/src/sourceControl/BitbucketApi.test.ts+8-1
......@@ -1,6 +1,11 @@
11import { assert, it, vi } from "@effect/vitest";
22import * as NodeServices from "@effect/platform-node/NodeServices";
3import { ConfigProvider, DateTime, Effect, FileSystem, Layer, Option } from "effect";
3import * as ConfigProvider from "effect/ConfigProvider";
4import * as DateTime from "effect/DateTime";
5import * as Effect from "effect/Effect";
6import * as FileSystem from "effect/FileSystem";
7import * as Layer from "effect/Layer";
8import * as Option from "effect/Option";
49import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http";
510
611import * as BitbucketApi from "./BitbucketApi.ts";
......@@ -423,6 +428,7 @@ it.effect("creates repositories through the Bitbucket REST API", () => {
423428 assert.ok(request);
424429 const rawBody = (request.body as { readonly body?: Uint8Array }).body;
425430 assert.ok(rawBody);
431 // @effect-diagnostics-next-line preferSchemaOverJson:off
426432 assert.deepStrictEqual(JSON.parse(new TextDecoder().decode(rawBody)), {
427433 scm: "git",
428434 is_private: true,
......@@ -458,6 +464,7 @@ it.effect("creates pull requests using the official REST payload shape", () => {
458464 assert.ok(request);
459465 const rawBody = (request.body as { readonly body?: Uint8Array }).body;
460466 assert.ok(rawBody);
467 // @effect-diagnostics-next-line preferSchemaOverJson:off
461468 assert.deepStrictEqual(JSON.parse(new TextDecoder().decode(rawBody)), {
462469 title: "Provider PR",
463470 description: "PR body",
apps/server/src/sourceControl/BitbucketApi.ts+7-1
......@@ -1,4 +1,10 @@
1import { Config, Context, Effect, FileSystem, Layer, Option, Schema } from "effect";
1import * as Config from "effect/Config";
2import * as Context from "effect/Context";
3import * as Effect from "effect/Effect";
4import * as FileSystem from "effect/FileSystem";
5import * as Layer from "effect/Layer";
6import * as Option from "effect/Option";
7import * as Schema from "effect/Schema";
28import {
39 TrimmedNonEmptyString,
410 type SourceControlProviderAuth,
apps/server/src/sourceControl/BitbucketSourceControlProvider.test.ts+3-1
......@@ -1,5 +1,7 @@
11import { assert, it } from "@effect/vitest";
2import { Effect, Layer, Option } from "effect";
2import * as Effect from "effect/Effect";
3import * as Layer from "effect/Layer";
4import * as Option from "effect/Option";
35
46import * as BitbucketApi from "./BitbucketApi.ts";
57import * as BitbucketSourceControlProvider from "./BitbucketSourceControlProvider.ts";
apps/server/src/sourceControl/BitbucketSourceControlProvider.ts+3-1
......@@ -1,4 +1,6 @@
1import { Effect, Layer, Option } from "effect";
1import * as Effect from "effect/Effect";
2import * as Layer from "effect/Layer";
3import * as Option from "effect/Option";
24import { SourceControlProviderError, type ChangeRequest } from "@t3tools/contracts";
35
46import * as BitbucketApi from "./BitbucketApi.ts";
apps/server/src/sourceControl/GitHubCli.test.ts+7-2
......@@ -1,7 +1,8 @@
11import { assert, it, afterEach, describe, expect, vi } from "@effect/vitest";
2import { Effect, Layer } from "effect";
2import * as Effect from "effect/Effect";
3import * as Layer from "effect/Layer";
34import { ChildProcessSpawner } from "effect/unstable/process";
4import { VcsProcessExitError, type VcsError } from "@t3tools/contracts";
5import { VcsProcessExitError } from "@t3tools/contracts";
56
67import * as VcsProcess from "../vcs/VcsProcess.ts";
78import * as GitHubCli from "./GitHubCli.ts";
......@@ -34,6 +35,7 @@ describe("GitHubCli.layer", () => {
3435 mockRun.mockReturnValueOnce(
3536 Effect.succeed(
3637 processOutput(
38 // @effect-diagnostics-next-line preferSchemaOverJson:off
3739 JSON.stringify({
3840 number: 42,
3941 title: "Add PR thread creation",
......@@ -92,6 +94,7 @@ describe("GitHubCli.layer", () => {
9294 mockRun.mockReturnValueOnce(
9395 Effect.succeed(
9496 processOutput(
97 // @effect-diagnostics-next-line preferSchemaOverJson:off
9598 JSON.stringify({
9699 number: 42,
97100 title: " Add PR thread creation \n",
......@@ -137,6 +140,7 @@ describe("GitHubCli.layer", () => {
137140 mockRun.mockReturnValueOnce(
138141 Effect.succeed(
139142 processOutput(
143 // @effect-diagnostics-next-line preferSchemaOverJson:off
140144 JSON.stringify([
141145 {
142146 number: 0,
......@@ -187,6 +191,7 @@ describe("GitHubCli.layer", () => {
187191 mockRun.mockReturnValueOnce(
188192 Effect.succeed(
189193 processOutput(
194 // @effect-diagnostics-next-line preferSchemaOverJson:off
190195 JSON.stringify({
191196 nameWithOwner: "octocat/codething-mvp",
192197 url: "https://github.com/octocat/codething-mvp",
apps/server/src/sourceControl/GitHubCli.ts+6-1
......@@ -1,4 +1,9 @@
1import { Context, Effect, Layer, Result, Schema, SchemaIssue } from "effect";
1import * as Context from "effect/Context";
2import * as Effect from "effect/Effect";
3import * as Layer from "effect/Layer";
4import * as Result from "effect/Result";
5import * as Schema from "effect/Schema";
6import * as SchemaIssue from "effect/SchemaIssue";
27
38import {
49 TrimmedNonEmptyString,
apps/server/src/sourceControl/GitHubSourceControlProvider.test.ts+4-1
......@@ -1,5 +1,8 @@
11import { assert, it } from "@effect/vitest";
2import { DateTime, Effect, Layer, Option } from "effect";
2import * as DateTime from "effect/DateTime";
3import * as Effect from "effect/Effect";
4import * as Layer from "effect/Layer";
5import * as Option from "effect/Option";
36import { ChildProcessSpawner } from "effect/unstable/process";
47
58import * as VcsProcess from "../vcs/VcsProcess.ts";
apps/server/src/sourceControl/GitHubSourceControlProvider.ts+5-1
......@@ -1,4 +1,8 @@
1import { Effect, Layer, Option, Result, Schema } from "effect";
1import * as Effect from "effect/Effect";
2import * as Layer from "effect/Layer";
3import * as Option from "effect/Option";
4import * as Result from "effect/Result";
5import * as Schema from "effect/Schema";
26import {
37 SourceControlProviderError,
48 type ChangeRequest,
apps/server/src/sourceControl/GitLabCli.test.ts+15-2
......@@ -1,5 +1,6 @@
11import { assert, it, afterEach, expect, vi } from "@effect/vitest";
2import { Effect, Layer } from "effect";
2import * as Effect from "effect/Effect";
3import * as Layer from "effect/Layer";
34import { ChildProcessSpawner } from "effect/unstable/process";
45
56import { VcsProcessExitError } from "@t3tools/contracts";
......@@ -38,6 +39,7 @@ layer("GitLabCli.layer", (it) => {
3839 mockedRun.mockReturnValueOnce(
3940 Effect.succeed(
4041 processOutput(
42 // @effect-diagnostics-next-line preferSchemaOverJson:off
4143 JSON.stringify({
4244 iid: 42,
4345 title: "Add MR thread creation",
......@@ -89,6 +91,7 @@ layer("GitLabCli.layer", (it) => {
8991 mockedRun.mockReturnValueOnce(
9092 Effect.succeed(
9193 processOutput(
94 // @effect-diagnostics-next-line preferSchemaOverJson:off
9295 JSON.stringify([
9396 {
9497 iid: 0,
......@@ -154,6 +157,7 @@ layer("GitLabCli.layer", (it) => {
154157 mockedRun.mockReturnValueOnce(
155158 Effect.succeed(
156159 processOutput(
160 // @effect-diagnostics-next-line preferSchemaOverJson:off
157161 JSON.stringify({
158162 path_with_namespace: "octocat/t3code",
159163 web_url: "https://gitlab.com/octocat/t3code",
......@@ -219,10 +223,19 @@ layer("GitLabCli.layer", (it) => {
219223 it.effect("creates repositories under an explicit namespace", () =>
220224 Effect.gen(function* () {
221225 mockedRun
222 .mockReturnValueOnce(Effect.succeed(processOutput(JSON.stringify({ id: 1234 }))))
226
227 .mockReturnValueOnce(
228 Effect.succeed(
229 processOutput(
230 // @effect-diagnostics-next-line preferSchemaOverJson:off
231 JSON.stringify({ id: 1234 }),
232 ),
233 ),
234 )
223235 .mockReturnValueOnce(
224236 Effect.succeed(
225237 processOutput(
238 // @effect-diagnostics-next-line preferSchemaOverJson:off
226239 JSON.stringify({
227240 path_with_namespace: "octocat/t3code",
228241 web_url: "https://gitlab.com/octocat/t3code",
apps/server/src/sourceControl/GitLabCli.ts+8-1
......@@ -1,4 +1,11 @@
1import { Context, Effect, Layer, Option, Result, Schema, SchemaIssue, type DateTime } from "effect";
1import * as Context from "effect/Context";
2import * as Effect from "effect/Effect";
3import * as Layer from "effect/Layer";
4import * as Option from "effect/Option";
5import * as Result from "effect/Result";
6import * as Schema from "effect/Schema";
7import * as SchemaIssue from "effect/SchemaIssue";
8import type * as DateTime from "effect/DateTime";
29
310import { TrimmedNonEmptyString, type SourceControlRepositoryVisibility } from "@t3tools/contracts";
411
apps/server/src/sourceControl/GitLabSourceControlProvider.test.ts+3-1
......@@ -1,5 +1,7 @@
11import { assert, it } from "@effect/vitest";
2import { Effect, Layer, Option } from "effect";
2import * as Effect from "effect/Effect";
3import * as Layer from "effect/Layer";
4import * as Option from "effect/Option";
35
46import * as GitLabCli from "./GitLabCli.ts";
57import * as GitLabSourceControlProvider from "./GitLabSourceControlProvider.ts";
apps/server/src/sourceControl/GitLabSourceControlProvider.ts+3-1
......@@ -1,4 +1,6 @@
1import { Effect, Layer, Option } from "effect";
1import * as Effect from "effect/Effect";
2import * as Layer from "effect/Layer";
3import * as Option from "effect/Option";
24import { SourceControlProviderError, type ChangeRequest } from "@t3tools/contracts";
35
46import * as GitLabCli from "./GitLabCli.ts";
apps/server/src/sourceControl/SourceControlDiscovery.test.ts+3-1
......@@ -1,6 +1,8 @@
11import { assert, it } from "@effect/vitest";
22import * as NodeServices from "@effect/platform-node/NodeServices";
3import { Effect, Layer, Option } from "effect";
3import * as Effect from "effect/Effect";
4import * as Layer from "effect/Layer";
5import * as Option from "effect/Option";
46import { ChildProcessSpawner } from "effect/unstable/process";
57import { VcsProcessSpawnError } from "@t3tools/contracts";
68
apps/server/src/sourceControl/SourceControlDiscovery.ts+4-1
......@@ -3,7 +3,10 @@ import {
33 type VcsDiscoveryItem,
44 type VcsDriverKind,
55} from "@t3tools/contracts";
6import { Context, Effect, Layer, Option } from "effect";
6import * as Context from "effect/Context";
7import * as Effect from "effect/Effect";
8import * as Layer from "effect/Layer";
9import * as Option from "effect/Option";
710
811import { ServerConfig } from "../config.ts";
912import * as VcsProcess from "../vcs/VcsProcess.ts";
apps/server/src/sourceControl/SourceControlProvider.ts+2-1
......@@ -1,4 +1,5 @@
1import { Context, Effect } from "effect";
1import * as Context from "effect/Context";
2import * as Effect from "effect/Effect";
23import type {
34 ChangeRequest,
45 ChangeRequestState,
apps/server/src/sourceControl/SourceControlProviderDiscovery.ts+2-1
......@@ -3,7 +3,8 @@ import type {
33 SourceControlProviderDiscoveryItem,
44 SourceControlProviderKind,
55} from "@t3tools/contracts";
6import { Effect, Option } from "effect";
6import * as Effect from "effect/Effect";
7import * as Option from "effect/Option";
78
89import type * as VcsProcess from "../vcs/VcsProcess.ts";
910
apps/server/src/sourceControl/SourceControlProviderRegistry.test.ts+4-1
......@@ -1,6 +1,9 @@
11import { assert, it } from "@effect/vitest";
22import * as NodeServices from "@effect/platform-node/NodeServices";
3import { DateTime, Effect, Layer, Option } from "effect";
3import * as DateTime from "effect/DateTime";
4import * as Effect from "effect/Effect";
5import * as Layer from "effect/Layer";
6import * as Option from "effect/Option";
47
58import { ServerConfig } from "../config.ts";
69import type * as VcsDriver from "../vcs/VcsDriver.ts";
apps/server/src/sourceControl/SourceControlProviderRegistry.ts+6-1
......@@ -1,4 +1,9 @@
1import { Cache, Context, Duration, Effect, Exit, Layer } from "effect";
1import * as Cache from "effect/Cache";
2import * as Context from "effect/Context";
3import * as Duration from "effect/Duration";
4import * as Effect from "effect/Effect";
5import * as Exit from "effect/Exit";
6import * as Layer from "effect/Layer";
27import {
38 SourceControlProviderError,
49 type SourceControlProviderDiscoveryItem,
apps/server/src/sourceControl/SourceControlRepositoryService.test.ts+3-1
......@@ -1,6 +1,8 @@
11import * as NodeServices from "@effect/platform-node/NodeServices";
22import { assert, it } from "@effect/vitest";
3import { Effect, FileSystem, Layer } from "effect";
3import * as Effect from "effect/Effect";
4import * as FileSystem from "effect/FileSystem";
5import * as Layer from "effect/Layer";
46import { ChildProcessSpawner } from "effect/unstable/process";
57
68import { GitCommandError, type SourceControlProviderError } from "@t3tools/contracts";
apps/server/src/sourceControl/SourceControlRepositoryService.ts+6-1
......@@ -1,5 +1,10 @@
11import * as NodeOS from "node:os";
2import { Context, Effect, FileSystem, Layer, Path, Schema } from "effect";
2import * as Context from "effect/Context";
3import * as Effect from "effect/Effect";
4import * as FileSystem from "effect/FileSystem";
5import * as Layer from "effect/Layer";
6import * as Path from "effect/Path";
7import * as Schema from "effect/Schema";
38
49import {
510 SourceControlRepositoryError,
apps/server/src/sourceControl/azureDevOpsPullRequests.ts+6-1
......@@ -1,4 +1,9 @@
1import { Cause, DateTime, Exit, Option, Result, Schema } from "effect";
1import * as Cause from "effect/Cause";
2import * as DateTime from "effect/DateTime";
3import * as Exit from "effect/Exit";
4import * as Option from "effect/Option";
5import * as Result from "effect/Result";
6import * as Schema from "effect/Schema";
27import { PositiveInt, TrimmedNonEmptyString } from "@t3tools/contracts";
38import { decodeJsonResult, formatSchemaError } from "@t3tools/shared/schemaJson";
49
apps/server/src/sourceControl/bitbucketPullRequests.ts+3-1
......@@ -1,4 +1,6 @@
1import { DateTime, Option, Schema } from "effect";
1import * as DateTime from "effect/DateTime";
2import * as Option from "effect/Option";
3import * as Schema from "effect/Schema";
24import { PositiveInt, TrimmedNonEmptyString } from "@t3tools/contracts";
35
46export interface NormalizedBitbucketPullRequestRecord {
apps/server/src/sourceControl/gitHubPullRequests.ts+6-1
......@@ -1,4 +1,9 @@
1import { Cause, DateTime, Exit, Option, Result, Schema } from "effect";
1import * as Cause from "effect/Cause";
2import * as DateTime from "effect/DateTime";
3import * as Exit from "effect/Exit";
4import * as Option from "effect/Option";
5import * as Result from "effect/Result";
6import * as Schema from "effect/Schema";
27import { PositiveInt, TrimmedNonEmptyString } from "@t3tools/contracts";
38import { decodeJsonResult, formatSchemaError } from "@t3tools/shared/schemaJson";
49
apps/server/src/sourceControl/gitLabMergeRequests.ts+6-1
......@@ -1,4 +1,9 @@
1import { Cause, DateTime, Exit, Option, Result, Schema } from "effect";
1import * as Cause from "effect/Cause";
2import * as DateTime from "effect/DateTime";
3import * as Exit from "effect/Exit";
4import * as Option from "effect/Option";
5import * as Result from "effect/Result";
6import * as Schema from "effect/Schema";
27import { PositiveInt, TrimmedNonEmptyString } from "@t3tools/contracts";
38import { decodeJsonResult, formatSchemaError } from "@t3tools/shared/schemaJson";
49
apps/server/src/startupAccess.ts+1-1
......@@ -1,7 +1,7 @@
11import { networkInterfaces } from "node:os";
22
33import { QrCode } from "@t3tools/shared/qrCode";
4import { Effect } from "effect";
4import * as Effect from "effect/Effect";
55import { HttpServer } from "effect/unstable/http";
66
77import { ServerConfig } from "./config.ts";
apps/server/src/stream/collectUint8StreamText.test.ts+2-1
......@@ -1,5 +1,6 @@
11import { assert, describe, it } from "@effect/vitest";
2import { Effect, Stream } from "effect";
2import * as Effect from "effect/Effect";
3import * as Stream from "effect/Stream";
34
45import { collectUint8StreamText } from "./collectUint8StreamText.ts";
56
apps/server/src/stream/collectUint8StreamText.ts+2-1
......@@ -1,4 +1,5 @@
1import { Effect, Stream } from "effect";
1import * as Effect from "effect/Effect";
2import * as Stream from "effect/Stream";
23
34export interface CollectedUint8StreamText {
45 readonly text: string;
apps/server/src/telemetry/Identify.ts+5-1
......@@ -1,4 +1,8 @@
1import { Effect, FileSystem, Path, Random, Schema } from "effect";
1import * as Effect from "effect/Effect";
2import * as FileSystem from "effect/FileSystem";
3import * as Path from "effect/Path";
4import * as Random from "effect/Random";
5import * as Schema from "effect/Schema";
26import * as Crypto from "node:crypto";
37import { homedir } from "node:os";
48import { ServerConfig } from "../config.ts";
apps/server/src/telemetry/Layers/AnalyticsService.test.ts+3-1
......@@ -1,7 +1,9 @@
11import * as NodeHttpServer from "@effect/platform-node/NodeHttpServer";
22import * as NodeServices from "@effect/platform-node/NodeServices";
33import { assert, it } from "@effect/vitest";
4import { ConfigProvider, Effect, Layer } from "effect";
4import * as ConfigProvider from "effect/ConfigProvider";
5import * as Effect from "effect/Effect";
6import * as Layer from "effect/Layer";
57import * as HttpServer from "effect/unstable/http/HttpServer";
68import * as HttpServerRequest from "effect/unstable/http/HttpServerRequest";
79import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse";
apps/server/src/telemetry/Layers/AnalyticsService.ts+5-1
......@@ -7,7 +7,11 @@
77 * @module AnalyticsServiceLive
88 */
99
10import { Config, DateTime, Effect, Layer, Ref } from "effect";
10import * as Config from "effect/Config";
11import * as DateTime from "effect/DateTime";
12import * as Effect from "effect/Effect";
13import * as Layer from "effect/Layer";
14import * as Ref from "effect/Ref";
1115import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http";
1216
1317import { ServerConfig } from "../../config.ts";
apps/server/src/telemetry/Services/AnalyticsService.ts+3-1
......@@ -6,7 +6,9 @@
66 *
77 * @module AnalyticsService
88 */
9import { Effect, Layer, Context } from "effect";
9import * as Effect from "effect/Effect";
10import * as Layer from "effect/Layer";
11import * as Context from "effect/Context";
1012
1113export interface AnalyticsServiceShape {
1214 /**
apps/server/src/terminal/Layers/BunPTY.ts+2-1
......@@ -1,4 +1,5 @@
1import { Effect, Layer } from "effect";
1import * as Effect from "effect/Effect";
2import * as Layer from "effect/Layer";
23import { PtyAdapter } from "../Services/PTY.ts";
34import type { PtyAdapterShape, PtyExitEvent, PtyProcess } from "../Services/PTY.ts";
45
apps/server/src/terminal/Layers/Manager.test.ts+31-38
......@@ -1,5 +1,3 @@
1import path from "node:path";
2
31import * as NodeServices from "@effect/platform-node/NodeServices";
42import { assert, it } from "@effect/vitest";
53import {
......@@ -8,20 +6,19 @@ import {
86 type TerminalOpenInput,
97 type TerminalRestartInput,
108} from "@t3tools/contracts";
11import {
12 Duration,
13 Effect,
14 Encoding,
15 Exit,
16 Fiber,
17 FileSystem,
18 Option,
19 PlatformError,
20 Ref,
21 Schedule,
22 Scope,
23} from "effect";
24import { TestClock } from "effect/testing";
9import * as Cause from "effect/Cause";
10import * as Duration from "effect/Duration";
11import * as Effect from "effect/Effect";
12import * as Encoding from "effect/Encoding";
13import * as Exit from "effect/Exit";
14import * as Fiber from "effect/Fiber";
15import * as FileSystem from "effect/FileSystem";
16import * as Path from "effect/Path";
17import * as PlatformError from "effect/PlatformError";
18import * as Ref from "effect/Ref";
19import * as Schedule from "effect/Schedule";
20import * as Scope from "effect/Scope";
21import * as TestClock from "effect/testing/TestClock";
2522import { expect } from "vitest";
2623
2724import type { TerminalManagerShape } from "../Services/Manager.ts";
......@@ -130,20 +127,11 @@ class FakePtyAdapter implements PtyAdapterShape {
130127const waitFor = <E, R>(
131128 predicate: Effect.Effect<boolean, E, R>,
132129 timeout: Duration.Input = 800,
133): Effect.Effect<void, Error | E, R> =>
130): Effect.Effect<void, E | Cause.TimeoutError, R> =>
134131 predicate.pipe(
135 Effect.filterOrFail(
136 (done) => done,
137 () => new Error("Condition not met"),
138 ),
132 Effect.filter((done) => done),
139133 Effect.retry(Schedule.spaced("15 millis")),
140 Effect.timeoutOption(timeout),
141 Effect.flatMap((result) =>
142 Option.match(result, {
143 onNone: () => Effect.fail(new Error("Timed out waiting for condition")),
144 onSome: () => Effect.void,
145 }),
146 ),
134 Effect.timeout(timeout),
147135 );
148136
149137function openInput(overrides: Partial<TerminalOpenInput> = {}): TerminalOpenInput {
......@@ -181,7 +169,7 @@ function multiTerminalHistoryLogName(threadId: string, terminalId: string): stri
181169}
182170
183171function historyLogPath(logsDir: string, threadId = "thread-1"): string {
184 return path.join(logsDir, historyLogName(threadId));
172 return joinPath(logsDir, historyLogName(threadId));
185173}
186174
187175function multiTerminalHistoryLogPath(
......@@ -189,7 +177,11 @@ function multiTerminalHistoryLogPath(
189177 threadId = "thread-1",
190178 terminalId = "default",
191179): string {
192 return path.join(logsDir, multiTerminalHistoryLogName(threadId, terminalId));
180 return joinPath(logsDir, multiTerminalHistoryLogName(threadId, terminalId));
181}
182
183function joinPath(...segments: ReadonlyArray<string>): string {
184 return segments.join("/");
193185}
194186
195187interface CreateManagerOptions {
......@@ -217,12 +209,13 @@ const createManager = (
217209): Effect.Effect<
218210 ManagerFixture,
219211 PlatformError.PlatformError,
220 FileSystem.FileSystem | Scope.Scope
212 FileSystem.FileSystem | Path.Path | Scope.Scope
221213> =>
222214 Effect.flatMap(Effect.service(FileSystem.FileSystem), (fs) =>
223215 Effect.gen(function* () {
216 const pathService = yield* Path.Path;
224217 const baseDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3code-terminal-" });
225 const logsDir = path.join(baseDir, "userdata", "logs", "terminals");
218 const logsDir = pathService.join(baseDir, "userdata", "logs", "terminals");
226219 const ptyAdapter = options.ptyAdapter ?? new FakePtyAdapter();
227220
228221 const manager = yield* makeTerminalManagerWithOptions({
......@@ -302,8 +295,8 @@ it.layer(NodeServices.layer, { excludeTestServices: true })("TerminalManager", (
302295 if (process.platform === "win32") return;
303296
304297 const { manager, baseDir } = yield* createManager();
305 const blockedRoot = path.join(baseDir, "blocked-root");
306 const blockedCwd = path.join(blockedRoot, "cwd");
298 const blockedRoot = joinPath(baseDir, "blocked-root");
299 const blockedCwd = joinPath(blockedRoot, "cwd");
307300 yield* makeDirectory(blockedCwd);
308301 yield* chmod(blockedRoot, 0o000);
309302
......@@ -441,8 +434,8 @@ it.layer(NodeServices.layer, { excludeTestServices: true })("TerminalManager", (
441434 it.effect("propagates explicit worktree metadata through snapshots and lifecycle events", () =>
442435 Effect.gen(function* () {
443436 const { manager, getEvents, baseDir } = yield* createManager();
444 const firstWorktreePath = path.join(baseDir, "worktrees", "feature-a");
445 const secondWorktreePath = path.join(baseDir, "worktrees", "feature-b");
437 const firstWorktreePath = joinPath(baseDir, "worktrees", "feature-a");
438 const secondWorktreePath = joinPath(baseDir, "worktrees", "feature-b");
446439 yield* makeDirectory(firstWorktreePath);
447440 yield* makeDirectory(secondWorktreePath);
448441 const startedSnapshot = yield* manager.open(
......@@ -478,7 +471,7 @@ it.layer(NodeServices.layer, { excludeTestServices: true })("TerminalManager", (
478471 it.effect("preserves worktree metadata when reopening an exited session", () =>
479472 Effect.gen(function* () {
480473 const { manager, ptyAdapter, getEvents, baseDir } = yield* createManager();
481 const worktreePath = path.join(baseDir, "worktrees", "feature-a");
474 const worktreePath = joinPath(baseDir, "worktrees", "feature-a");
482475 yield* makeDirectory(worktreePath);
483476
484477 yield* manager.open(
......@@ -816,7 +809,7 @@ it.layer(NodeServices.layer, { excludeTestServices: true })("TerminalManager", (
816809 it.effect("migrates legacy transcript filenames to terminal-scoped history path on open", () =>
817810 Effect.gen(function* () {
818811 const { manager, logsDir } = yield* createManager();
819 const legacyPath = path.join(logsDir, "thread-1.log");
812 const legacyPath = joinPath(logsDir, "thread-1.log");
820813 const nextPath = historyLogPath(logsDir);
821814 yield* writeFileString(legacyPath, "legacy-line\n");
822815
apps/server/src/terminal/Layers/Manager.ts+61-40
......@@ -1,5 +1,3 @@
1import path from "node:path";
2
31import {
42 DEFAULT_TERMINAL_ID,
53 type TerminalEvent,
......@@ -7,20 +5,20 @@ import {
75 type TerminalSessionStatus,
86} from "@t3tools/contracts";
97import { makeKeyedCoalescingWorker } from "@t3tools/shared/KeyedCoalescingWorker";
10import {
11 Effect,
12 Encoding,
13 Equal,
14 Exit,
15 Fiber,
16 FileSystem,
17 Layer,
18 Option,
19 Schema,
20 Scope,
21 Semaphore,
22 SynchronizedRef,
23} from "effect";
8import * as Effect from "effect/Effect";
9import * as Encoding from "effect/Encoding";
10import * as Equal from "effect/Equal";
11import * as Exit from "effect/Exit";
12import * as Fiber from "effect/Fiber";
13import * as FileSystem from "effect/FileSystem";
14import * as DateTime from "effect/DateTime";
15import * as Layer from "effect/Layer";
16import * as Option from "effect/Option";
17import * as Path from "effect/Path";
18import * as Schema from "effect/Schema";
19import * as Scope from "effect/Scope";
20import * as Semaphore from "effect/Semaphore";
21import * as SynchronizedRef from "effect/SynchronizedRef";
2422
2523import { ServerConfig } from "../../config.ts";
2624import {
......@@ -213,15 +211,25 @@ function normalizeShellCommand(
213211 return firstToken.replace(/^['"]|['"]$/g, "");
214212}
215213
214function basename(command: string, platform: NodeJS.Platform): string {
215 const normalized = platform === "win32" ? command.replaceAll("/", "\\") : command;
216 const separator = platform === "win32" ? "\\" : "/";
217 return normalized.slice(normalized.lastIndexOf(separator) + 1);
218}
219
220function joinWindowsPath(...segments: ReadonlyArray<string>): string {
221 return segments
222 .filter((segment) => segment.length > 0)
223 .join("\\")
224 .replace(/\\+/g, "\\");
225}
226
216227function shellCandidateFromCommand(
217228 command: string | null,
218229 platform: NodeJS.Platform = process.platform,
219230): ShellCandidate | null {
220231 if (!command || command.length === 0) return null;
221 const shellName =
222 platform === "win32"
223 ? path.win32.basename(command).toLowerCase()
224 : path.basename(command).toLowerCase();
232 const shellName = basename(command, platform).toLowerCase();
225233 if (platform === "win32" && (shellName === "pwsh.exe" || shellName === "powershell.exe")) {
226234 return { shell: command, args: ["-NoLogo"] };
227235 }
......@@ -236,7 +244,7 @@ function windowsSystemRoot(env: NodeJS.ProcessEnv): string {
236244}
237245
238246function windowsPowerShellPath(env: NodeJS.ProcessEnv): string {
239 return path.win32.join(
247 return joinWindowsPath(
240248 windowsSystemRoot(env),
241249 "System32",
242250 "WindowsPowerShell",
......@@ -246,7 +254,7 @@ function windowsPowerShellPath(env: NodeJS.ProcessEnv): string {
246254}
247255
248256function windowsCmdPath(env: NodeJS.ProcessEnv): string {
249 return path.win32.join(windowsSystemRoot(env), "System32", "cmd.exe");
257 return joinWindowsPath(windowsSystemRoot(env), "System32", "cmd.exe");
250258}
251259
252260function formatShellCandidate(candidate: ShellCandidate): string {
......@@ -714,8 +722,10 @@ const makeTerminalManager = Effect.fn("makeTerminalManager")(function* () {
714722export const makeTerminalManagerWithOptions = Effect.fn("makeTerminalManagerWithOptions")(
715723 function* (options: TerminalManagerOptions) {
716724 const fileSystem = yield* FileSystem.FileSystem;
725 const path = yield* Path.Path;
717726 const context = yield* Effect.context<never>();
718727 const runFork = Effect.runForkWith(context);
728 const nowIso = Effect.map(DateTime.now, DateTime.formatIso);
719729
720730 const logsDir = options.logsDir;
721731 const historyLineLimit = options.historyLineLimit ?? DEFAULT_HISTORY_LINE_LIMIT;
......@@ -1160,6 +1170,7 @@ export const makeTerminalManagerWithOptions = Effect.fn("makeTerminalManagerWith
11601170 expectedPid: number,
11611171 ) {
11621172 while (true) {
1173 const updatedAt = yield* nowIso;
11631174 const action: DrainProcessEventAction = yield* Effect.sync(() => {
11641175 if (session.pid !== expectedPid || !session.process || session.status !== "running") {
11651176 session.pendingProcessEvents = [];
......@@ -1194,7 +1205,7 @@ export const makeTerminalManagerWithOptions = Effect.fn("makeTerminalManagerWith
11941205 historyLineLimit,
11951206 );
11961207 }
1197 session.updatedAt = new Date().toISOString();
1208 session.updatedAt = updatedAt;
11981209
11991210 return {
12001211 type: "output",
......@@ -1221,7 +1232,7 @@ export const makeTerminalManagerWithOptions = Effect.fn("makeTerminalManagerWith
12211232 session.exitSignal = Number.isInteger(nextEvent.event.signal)
12221233 ? nextEvent.event.signal
12231234 : null;
1224 session.updatedAt = new Date().toISOString();
1235 session.updatedAt = updatedAt;
12251236
12261237 return {
12271238 type: "exit",
......@@ -1242,22 +1253,24 @@ export const makeTerminalManagerWithOptions = Effect.fn("makeTerminalManagerWith
12421253 yield* queuePersist(action.threadId, action.terminalId, action.history);
12431254 }
12441255
1256 const createdAt = yield* nowIso;
12451257 yield* publishEvent({
12461258 type: "output",
12471259 threadId: action.threadId,
12481260 terminalId: action.terminalId,
1249 createdAt: new Date().toISOString(),
1261 createdAt,
12501262 data: action.data,
12511263 });
12521264 continue;
12531265 }
12541266
12551267 yield* clearKillFiber(action.process);
1268 const createdAt = yield* nowIso;
12561269 yield* publishEvent({
12571270 type: "exited",
12581271 threadId: action.threadId,
12591272 terminalId: action.terminalId,
1260 createdAt: new Date().toISOString(),
1273 createdAt,
12611274 exitCode: action.exitCode,
12621275 exitSignal: action.exitSignal,
12631276 });
......@@ -1272,6 +1285,7 @@ export const makeTerminalManagerWithOptions = Effect.fn("makeTerminalManagerWith
12721285 const process = session.process;
12731286 if (!process) return;
12741287
1288 const updatedAt = yield* nowIso;
12751289 yield* modifyManagerState((state) => {
12761290 cleanupProcessHandles(session);
12771291 session.process = null;
......@@ -1282,7 +1296,7 @@ export const makeTerminalManagerWithOptions = Effect.fn("makeTerminalManagerWith
12821296 session.pendingProcessEvents = [];
12831297 session.pendingProcessEventIndex = 0;
12841298 session.processEventDrainRunning = false;
1285 session.updatedAt = new Date().toISOString();
1299 session.updatedAt = updatedAt;
12861300 return [undefined, state] as const;
12871301 });
12881302
......@@ -1361,6 +1375,7 @@ export const makeTerminalManagerWithOptions = Effect.fn("makeTerminalManagerWith
13611375 "terminal.cwd": input.cwd,
13621376 });
13631377
1378 const startingAt = yield* nowIso;
13641379 yield* modifyManagerState((state) => {
13651380 session.status = "starting";
13661381 session.cwd = input.cwd;
......@@ -1373,7 +1388,7 @@ export const makeTerminalManagerWithOptions = Effect.fn("makeTerminalManagerWith
13731388 session.pendingProcessEvents = [];
13741389 session.pendingProcessEventIndex = 0;
13751390 session.processEventDrainRunning = false;
1376 session.updatedAt = new Date().toISOString();
1391 session.updatedAt = startingAt;
13771392 return [undefined, state] as const;
13781393 });
13791394
......@@ -1404,11 +1419,12 @@ export const makeTerminalManagerWithOptions = Effect.fn("makeTerminalManagerWith
14041419 runFork(drainProcessEvents(session, processPid));
14051420 });
14061421
1422 const startedAt = yield* nowIso;
14071423 yield* modifyManagerState((state) => {
14081424 session.process = ptyProcess;
14091425 session.pid = processPid;
14101426 session.status = "running";
1411 session.updatedAt = new Date().toISOString();
1427 session.updatedAt = startedAt;
14121428 session.unsubscribeData = unsubscribeData;
14131429 session.unsubscribeExit = unsubscribeExit;
14141430 return [undefined, state] as const;
......@@ -1418,7 +1434,7 @@ export const makeTerminalManagerWithOptions = Effect.fn("makeTerminalManagerWith
14181434 type: eventType,
14191435 threadId: session.threadId,
14201436 terminalId: session.terminalId,
1421 createdAt: new Date().toISOString(),
1437 createdAt: startedAt,
14221438 snapshot: snapshot(session),
14231439 });
14241440 }),
......@@ -1436,6 +1452,7 @@ export const makeTerminalManagerWithOptions = Effect.fn("makeTerminalManagerWith
14361452 yield* startKillEscalation(ptyProcess, session.threadId, session.terminalId);
14371453 }
14381454
1455 const failedAt = yield* nowIso;
14391456 yield* modifyManagerState((state) => {
14401457 session.status = "error";
14411458 session.pid = null;
......@@ -1446,7 +1463,7 @@ export const makeTerminalManagerWithOptions = Effect.fn("makeTerminalManagerWith
14461463 session.pendingProcessEvents = [];
14471464 session.pendingProcessEventIndex = 0;
14481465 session.processEventDrainRunning = false;
1449 session.updatedAt = new Date().toISOString();
1466 session.updatedAt = failedAt;
14501467 return [undefined, state] as const;
14511468 });
14521469
......@@ -1457,7 +1474,7 @@ export const makeTerminalManagerWithOptions = Effect.fn("makeTerminalManagerWith
14571474 type: "error",
14581475 threadId: session.threadId,
14591476 terminalId: session.terminalId,
1460 createdAt: new Date().toISOString(),
1477 createdAt: failedAt,
14611478 message,
14621479 });
14631480 yield* Effect.logError("failed to start terminal", {
......@@ -1529,6 +1546,7 @@ export const makeTerminalManagerWithOptions = Effect.fn("makeTerminalManagerWith
15291546 return;
15301547 }
15311548
1549 const updatedAt = yield* nowIso;
15321550 const event = yield* modifyManagerState((state) => {
15331551 const liveSession: Option.Option<TerminalSessionState> = Option.fromNullishOr(
15341552 state.sessions.get(toSessionKey(session.threadId, session.terminalId)),
......@@ -1543,14 +1561,14 @@ export const makeTerminalManagerWithOptions = Effect.fn("makeTerminalManagerWith
15431561 }
15441562
15451563 liveSession.value.hasRunningSubprocess = hasRunningSubprocess.value;
1546 liveSession.value.updatedAt = new Date().toISOString();
1564 liveSession.value.updatedAt = updatedAt;
15471565
15481566 return [
15491567 Option.some({
15501568 type: "activity" as const,
15511569 threadId: liveSession.value.threadId,
15521570 terminalId: liveSession.value.terminalId,
1553 createdAt: new Date().toISOString(),
1571 createdAt: updatedAt,
15541572 hasRunningSubprocess: hasRunningSubprocess.value,
15551573 }),
15561574 state,
......@@ -1629,6 +1647,7 @@ export const makeTerminalManagerWithOptions = Effect.fn("makeTerminalManagerWith
16291647 const history = yield* readHistory(input.threadId, terminalId);
16301648 const cols = input.cols ?? DEFAULT_OPEN_COLS;
16311649 const rows = input.rows ?? DEFAULT_OPEN_ROWS;
1650 const createdAt = yield* nowIso;
16321651 const session: TerminalSessionState = {
16331652 threadId: input.threadId,
16341653 terminalId,
......@@ -1643,7 +1662,7 @@ export const makeTerminalManagerWithOptions = Effect.fn("makeTerminalManagerWith
16431662 processEventDrainRunning: false,
16441663 exitCode: null,
16451664 exitSignal: null,
1646 updatedAt: new Date().toISOString(),
1665 updatedAt: createdAt,
16471666 cols,
16481667 rows,
16491668 process: null,
......@@ -1734,7 +1753,7 @@ export const makeTerminalManagerWithOptions = Effect.fn("makeTerminalManagerWith
17341753 if (liveSession.cols !== targetCols || liveSession.rows !== targetRows) {
17351754 liveSession.cols = targetCols;
17361755 liveSession.rows = targetRows;
1737 liveSession.updatedAt = new Date().toISOString();
1756 liveSession.updatedAt = yield* nowIso;
17381757 liveSession.process.resize(targetCols, targetRows);
17391758 }
17401759
......@@ -1768,7 +1787,7 @@ export const makeTerminalManagerWithOptions = Effect.fn("makeTerminalManagerWith
17681787 }
17691788 session.cols = input.cols;
17701789 session.rows = input.rows;
1771 session.updatedAt = new Date().toISOString();
1790 session.updatedAt = yield* nowIso;
17721791 yield* Effect.sync(() => process.resize(input.cols, input.rows));
17731792 });
17741793
......@@ -1783,13 +1802,14 @@ export const makeTerminalManagerWithOptions = Effect.fn("makeTerminalManagerWith
17831802 session.pendingProcessEvents = [];
17841803 session.pendingProcessEventIndex = 0;
17851804 session.processEventDrainRunning = false;
1786 session.updatedAt = new Date().toISOString();
1805 const clearedAt = yield* nowIso;
1806 session.updatedAt = clearedAt;
17871807 yield* persistHistory(input.threadId, terminalId, session.history);
17881808 yield* publishEvent({
17891809 type: "cleared",
17901810 threadId: input.threadId,
17911811 terminalId,
1792 createdAt: new Date().toISOString(),
1812 createdAt: clearedAt,
17931813 });
17941814 }),
17951815 );
......@@ -1808,6 +1828,7 @@ export const makeTerminalManagerWithOptions = Effect.fn("makeTerminalManagerWith
18081828 if (Option.isNone(existingSession)) {
18091829 const cols = input.cols ?? DEFAULT_OPEN_COLS;
18101830 const rows = input.rows ?? DEFAULT_OPEN_ROWS;
1831 const createdAt = yield* nowIso;
18111832 session = {
18121833 threadId: input.threadId,
18131834 terminalId,
......@@ -1822,7 +1843,7 @@ export const makeTerminalManagerWithOptions = Effect.fn("makeTerminalManagerWith
18221843 processEventDrainRunning: false,
18231844 exitCode: null,
18241845 exitSignal: null,
1825 updatedAt: new Date().toISOString(),
1846 updatedAt: createdAt,
18261847 cols,
18271848 rows,
18281849 process: null,
apps/server/src/terminal/Layers/NodePTY.test.ts+3-1
......@@ -1,4 +1,6 @@
1import { FileSystem, Path, Effect } from "effect";
1import * as FileSystem from "effect/FileSystem";
2import * as Path from "effect/Path";
3import * as Effect from "effect/Effect";
24import { assert, it } from "@effect/vitest";
35
46import { ensureNodePtySpawnHelperExecutable } from "./NodePTY.ts";
apps/server/src/terminal/Layers/NodePTY.ts+4-1
......@@ -1,6 +1,9 @@
11import { createRequire } from "node:module";
22
3import { Effect, FileSystem, Layer, Path } from "effect";
3import * as Effect from "effect/Effect";
4import * as FileSystem from "effect/FileSystem";
5import * as Layer from "effect/Layer";
6import * as Path from "effect/Path";
47import { PtyAdapter } from "../Services/PTY.ts";
58import {
69 PtySpawnError,
apps/server/src/terminal/Services/Manager.ts+2-1
......@@ -23,7 +23,8 @@ import {
2323 TerminalWriteInput,
2424} from "@t3tools/contracts";
2525import type { PtyProcess } from "./PTY.ts";
26import { Effect, Context } from "effect";
26import * as Effect from "effect/Effect";
27import * as Context from "effect/Context";
2728
2829export {
2930 TerminalCwdError,
apps/server/src/terminal/Services/PTY.ts+3-1
......@@ -6,7 +6,9 @@
66 *
77 * @module PtyAdapter
88 */
9import { Effect, Schema, Context } from "effect";
9import * as Effect from "effect/Effect";
10import * as Schema from "effect/Schema";
11import * as Context from "effect/Context";
1012
1113/**
1214 * PtyError - Error type for PTY adapter operations.
apps/server/src/textGeneration/ClaudeTextGeneration.test.ts+6-1
......@@ -1,7 +1,11 @@
11import { ClaudeSettings, ProviderInstanceId } from "@t3tools/contracts";
22import * as NodeServices from "@effect/platform-node/NodeServices";
33import { it } from "@effect/vitest";
4import { Effect, FileSystem, Layer, Path, Schema } from "effect";
4import * as Effect from "effect/Effect";
5import * as FileSystem from "effect/FileSystem";
6import * as Layer from "effect/Layer";
7import * as Path from "effect/Path";
8import * as Schema from "effect/Schema";
59import { createModelSelection } from "@t3tools/shared/model";
610import { expect } from "vitest";
711
......@@ -287,6 +291,7 @@ it.layer(ClaudeTextGenerationTestLayer)("ClaudeTextGeneration", (it) => {
287291 const claudeHome = path.join(process.cwd(), ".claude-work-test");
288292 return yield* withFakeClaudeEnv(
289293 {
294 // @effect-diagnostics-next-line preferSchemaOverJson:off
290295 output: JSON.stringify({
291296 structured_output: {
292297 title: "Use Claude home",
apps/server/src/textGeneration/ClaudeTextGeneration.ts+10-3
......@@ -7,7 +7,10 @@
77 *
88 * @module ClaudeTextGeneration
99 */
10import { Effect, Option, Schema, Stream } from "effect";
10import * as Effect from "effect/Effect";
11import * as Option from "effect/Option";
12import * as Schema from "effect/Schema";
13import * as Stream from "effect/Stream";
1114import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
1215
1316import { type ClaudeSettings, type ModelSelection } from "@t3tools/contracts";
......@@ -93,7 +96,9 @@ export const makeClaudeTextGeneration = Effect.fn("makeClaudeTextGeneration")(fu
9396 outputSchemaJson: S;
9497 modelSelection: ModelSelection;
9598 }): Effect.fn.Return<S["Type"], TextGenerationError, S["DecodingServices"]> {
96 const jsonSchemaStr = JSON.stringify(toJsonSchemaObject(outputSchemaJson));
99 const jsonSchemaStr = Schema.encodeUnknownSync(Schema.UnknownFromJsonString)(
100 toJsonSchemaObject(outputSchemaJson),
101 );
97102 const caps = getClaudeModelCapabilities(modelSelection.model);
98103 const descriptors = getProviderOptionDescriptors({
99104 caps,
......@@ -126,7 +131,9 @@ export const makeClaudeTextGeneration = Effect.fn("makeClaudeTextGeneration")(fu
126131 "--model",
127132 resolveClaudeApiModelId(modelSelection),
128133 ...(cliEffort ? ["--effort", cliEffort] : []),
129 ...(Object.keys(settings).length > 0 ? ["--settings", JSON.stringify(settings)] : []),
134 ...(Object.keys(settings).length > 0
135 ? ["--settings", Schema.encodeUnknownSync(Schema.UnknownFromJsonString)(settings)]
136 : []),
130137 "--dangerously-skip-permissions",
131138 ],
132139 {
apps/server/src/textGeneration/CodexTextGeneration.test.ts+15-5
......@@ -1,6 +1,11 @@
11import * as NodeServices from "@effect/platform-node/NodeServices";
22import { it } from "@effect/vitest";
3import { Effect, FileSystem, Layer, Path, Result, Schema } from "effect";
3import * as Effect from "effect/Effect";
4import * as FileSystem from "effect/FileSystem";
5import * as Layer from "effect/Layer";
6import * as Path from "effect/Path";
7import * as Result from "effect/Result";
8import * as Schema from "effect/Schema";
49import { createModelSelection } from "@t3tools/shared/model";
510import { expect } from "vitest";
611
......@@ -113,6 +118,7 @@ function makeFakeCodexBinary(
113118 : []),
114119 ...(input.stdinMustContain !== undefined
115120 ? [
121 // @effect-diagnostics-next-line preferSchemaOverJson:off
116122 `if ! printf "%s" "$stdin_content" | grep -F -- ${JSON.stringify(input.stdinMustContain)} >/dev/null; then`,
117123 ' printf "%s\\n" "stdin missing expected content" >&2',
118124 ` exit 3`,
......@@ -121,6 +127,7 @@ function makeFakeCodexBinary(
121127 : []),
122128 ...(input.stdinMustNotContain !== undefined
123129 ? [
130 // @effect-diagnostics-next-line preferSchemaOverJson:off
124131 `if printf "%s" "$stdin_content" | grep -F -- ${JSON.stringify(input.stdinMustNotContain)} >/dev/null; then`,
125132 ' printf "%s\\n" "stdin contained forbidden content" >&2',
126133 ` exit 4`,
......@@ -128,7 +135,10 @@ function makeFakeCodexBinary(
128135 ]
129136 : []),
130137 ...(input.stderr !== undefined
131 ? [`printf "%s\\n" ${JSON.stringify(input.stderr)} >&2`]
138 ? [
139 // @effect-diagnostics-next-line preferSchemaOverJson:off
140 `printf "%s\\n" ${JSON.stringify(input.stderr)} >&2`,
141 ]
132142 : []),
133143 'if [ -n "$output_path" ]; then',
134144 " cat > \"$output_path\" <<'__T3CODE_FAKE_CODEX_OUTPUT__'",
......@@ -415,7 +425,7 @@ it.layer(CodexTextGenerationTestLayer)("CodexTextGeneration", (it) => {
415425 const fs = yield* FileSystem.FileSystem;
416426 const path = yield* Path.Path;
417427 const { attachmentsDir } = yield* ServerConfig;
418 const attachmentId = `thread-branch-image-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
428 const attachmentId = "thread-branch-image-attachment";
419429 const attachmentPath = path.join(attachmentsDir, `${attachmentId}.png`);
420430 yield* fs.makeDirectory(attachmentsDir, { recursive: true });
421431 yield* fs.writeFile(attachmentPath, Buffer.from("hello"));
......@@ -453,7 +463,7 @@ it.layer(CodexTextGenerationTestLayer)("CodexTextGeneration", (it) => {
453463 const fs = yield* FileSystem.FileSystem;
454464 const path = yield* Path.Path;
455465 const { attachmentsDir } = yield* ServerConfig;
456 const attachmentId = `thread-1-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
466 const attachmentId = "thread-1-attachment";
457467 const imagePath = path.join(attachmentsDir, `${attachmentId}.png`);
458468 yield* fs.makeDirectory(attachmentsDir, { recursive: true });
459469 yield* fs.writeFile(imagePath, Buffer.from("hello"));
......@@ -502,7 +512,7 @@ it.layer(CodexTextGenerationTestLayer)("CodexTextGeneration", (it) => {
502512 const fs = yield* FileSystem.FileSystem;
503513 const path = yield* Path.Path;
504514 const { attachmentsDir } = yield* ServerConfig;
505 const missingAttachmentId = `thread-missing-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
515 const missingAttachmentId = "thread-missing-attachment";
506516 const missingPath = path.join(attachmentsDir, `${missingAttachmentId}.png`);
507517 yield* fs.remove(missingPath).pipe(Effect.catch(() => Effect.void));
508518
apps/server/src/textGeneration/CodexTextGeneration.ts+9-2
......@@ -1,4 +1,11 @@
1import { Effect, FileSystem, Option, Path, Random, Schema, Scope, Stream } from "effect";
1import * as Effect from "effect/Effect";
2import * as FileSystem from "effect/FileSystem";
3import * as Option from "effect/Option";
4import * as Path from "effect/Path";
5import * as Random from "effect/Random";
6import * as Schema from "effect/Schema";
7import * as Scope from "effect/Scope";
8import * as Stream from "effect/Stream";
29import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
310
411import { type CodexSettings, type ModelSelection } from "@t3tools/contracts";
......@@ -152,7 +159,7 @@ export const makeCodexTextGeneration = Effect.fn("makeCodexTextGeneration")(func
152159 const schemaPath = yield* writeTempFile(
153160 operation,
154161 "codex-schema",
155 JSON.stringify(toJsonSchemaObject(outputSchemaJson)),
162 Schema.encodeUnknownSync(Schema.UnknownFromJsonString)(toJsonSchemaObject(outputSchemaJson)),
156163 );
157164 const outputPath = yield* writeTempFile(operation, "codex-output", "");
158165
apps/server/src/textGeneration/CursorTextGeneration.test.ts+16-9
......@@ -1,3 +1,4 @@
1// @effect-diagnostics nodeBuiltinImport:off
12import * as path from "node:path";
23import * as os from "node:os";
34import { fileURLToPath } from "node:url";
......@@ -5,7 +6,11 @@ import { chmodSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync
56
67import * as NodeServices from "@effect/platform-node/NodeServices";
78import { it } from "@effect/vitest";
8import { Effect, Layer, Schema } from "effect";
9import * as Clock from "effect/Clock";
10import * as Effect from "effect/Effect";
11import * as Exit from "effect/Exit";
12import * as Layer from "effect/Layer";
13import * as Schema from "effect/Schema";
914import { createModelSelection } from "@t3tools/shared/model";
1015import { expect } from "vitest";
1116
......@@ -67,17 +72,19 @@ function withFakeAcpAgent<A, E, R>(
6772}
6873
6974function waitForFileContent(path: string): Effect.Effect<string> {
70 return Effect.promise(async () => {
71 const deadline = Date.now() + 5_000;
75 return Effect.gen(function* () {
76 const deadline = (yield* Clock.currentTimeMillis) + 5_000;
7277 for (;;) {
73 try {
74 return readFileSync(path, "utf8");
75 } catch (error) {
76 if (Date.now() >= deadline) {
77 throw error instanceof Error ? error : new Error(String(error));
78 const result = yield* Effect.exit(Effect.sync(() => readFileSync(path, "utf8")));
79 if (Exit.isSuccess(result)) {
80 return result.value;
81 }
82 {
83 if ((yield* Clock.currentTimeMillis) >= deadline) {
84 return yield* Effect.die(result.cause);
7885 }
7986 }
80 await new Promise((resolve) => setTimeout(resolve, 25));
87 yield* Effect.sleep(25);
8188 }
8289 });
8390}
apps/server/src/textGeneration/CursorTextGeneration.ts+4-1
......@@ -1,4 +1,7 @@
1import { Effect, Option, Ref, Schema } from "effect";
1import * as Effect from "effect/Effect";
2import * as Option from "effect/Option";
3import * as Ref from "effect/Ref";
4import * as Schema from "effect/Schema";
25import { ChildProcessSpawner } from "effect/unstable/process";
36
47import { type CursorSettings, type ModelSelection } from "@t3tools/contracts";
apps/server/src/textGeneration/OpenCodeTextGeneration.test.ts+6-3
......@@ -1,9 +1,12 @@
11import { OpenCodeSettings, ProviderInstanceId } from "@t3tools/contracts";
22import * as NodeServices from "@effect/platform-node/NodeServices";
33import { it } from "@effect/vitest";
4import { Duration, Effect, Layer, Schema } from "effect";
5import { TestClock } from "effect/testing";
6import { NetService } from "@t3tools/shared/Net";
4import * as Duration from "effect/Duration";
5import * as Effect from "effect/Effect";
6import * as Layer from "effect/Layer";
7import * as Schema from "effect/Schema";
8import * as TestClock from "effect/testing/TestClock";
9import * as NetService from "@t3tools/shared/Net";
710import { beforeEach, expect } from "vitest";
811
912import { ServerConfig } from "../config.ts";
apps/server/src/textGeneration/OpenCodeTextGeneration.ts+5-1
......@@ -1,4 +1,8 @@
1import { Effect, Exit, Fiber, Schema, Scope } from "effect";
1import * as Effect from "effect/Effect";
2import * as Exit from "effect/Exit";
3import * as Fiber from "effect/Fiber";
4import * as Schema from "effect/Schema";
5import * as Scope from "effect/Scope";
26import * as Semaphore from "effect/Semaphore";
37
48import {
apps/server/src/textGeneration/TextGeneration.test.ts+4-1
......@@ -1,5 +1,8 @@
11import { it } from "@effect/vitest";
2import { Effect, PubSub, Result, Stream } from "effect";
2import * as Effect from "effect/Effect";
3import * as PubSub from "effect/PubSub";
4import * as Result from "effect/Result";
5import * as Stream from "effect/Stream";
36import { describe, expect } from "vitest";
47
58import { ProviderInstanceId } from "@t3tools/contracts";
apps/server/src/textGeneration/TextGeneration.ts+3-1
......@@ -1,4 +1,6 @@
1import { Context, Effect, Layer } from "effect";
1import * as Context from "effect/Context";
2import * as Effect from "effect/Effect";
3import * as Layer from "effect/Layer";
24import type { ChatAttachment, ModelSelection, ProviderInstanceId } from "@t3tools/contracts";
35import { TextGenerationError } from "@t3tools/contracts";
46
apps/server/src/textGeneration/TextGenerationPolicy.ts+1-1
......@@ -1,4 +1,4 @@
1import { Schema } from "effect";
1import * as Schema from "effect/Schema";
22
33export const TextGenerationPolicyKind = Schema.Literals([
44 "default",
apps/server/src/textGeneration/TextGenerationPrompts.ts+1-1
......@@ -6,7 +6,7 @@
66 *
77 * @module textGenerationPrompts
88 */
9import { Schema } from "effect";
9import * as Schema from "effect/Schema";
1010import type { ChatAttachment } from "@t3tools/contracts";
1111
1212import { limitSection } from "./TextGenerationUtils.ts";
apps/server/src/textGeneration/TextGenerationUtils.ts+1-1
......@@ -1,4 +1,4 @@
1import { Schema } from "effect";
1import * as Schema from "effect/Schema";
22
33import { TextGenerationError } from "@t3tools/contracts";
44
apps/server/src/vcs/GitVcsDriver.test.ts+5-1
......@@ -1,5 +1,9 @@
11import * as NodeServices from "@effect/platform-node/NodeServices";
2import { Effect, FileSystem, Layer, Path, PlatformError } from "effect";
2import * as Effect from "effect/Effect";
3import * as FileSystem from "effect/FileSystem";
4import * as Layer from "effect/Layer";
5import * as Path from "effect/Path";
6import * as PlatformError from "effect/PlatformError";
37import { ChildProcessSpawner } from "effect/unstable/process";
48import { assert, it } from "@effect/vitest";
59
apps/server/src/vcs/GitVcsDriver.ts+5-1
......@@ -1,4 +1,8 @@
1import { Context, DateTime, Effect, Layer, Option } from "effect";
1import * as Context from "effect/Context";
2import * as DateTime from "effect/DateTime";
3import * as Effect from "effect/Effect";
4import * as Layer from "effect/Layer";
5import * as Option from "effect/Option";
26import { ChildProcessSpawner } from "effect/unstable/process";
37
48import {
apps/server/src/vcs/GitVcsDriverCore.test.ts+6-1
......@@ -1,6 +1,11 @@
11import * as NodeServices from "@effect/platform-node/NodeServices";
22import { assert, it, describe } from "@effect/vitest";
3import { Effect, FileSystem, Layer, Path, PlatformError, Scope } from "effect";
3import * as Effect from "effect/Effect";
4import * as FileSystem from "effect/FileSystem";
5import * as Layer from "effect/Layer";
6import * as Path from "effect/Path";
7import * as PlatformError from "effect/PlatformError";
8import * as Scope from "effect/Scope";
49
510import { GitCommandError } from "@t3tools/contracts";
611import { ServerConfig } from "../config.ts";
apps/server/src/vcs/GitVcsDriverCore.ts+16-18
......@@ -1,21 +1,19 @@
1import {
2 Cache,
3 Data,
4 DateTime,
5 Duration,
6 Effect,
7 Exit,
8 FileSystem,
9 Option,
10 Path,
11 PlatformError,
12 Ref,
13 Result,
14 Schema,
15 Scope,
16 Semaphore,
17 Stream,
18} from "effect";
1import * as Cache from "effect/Cache";
2import * as Data from "effect/Data";
3import * as DateTime from "effect/DateTime";
4import * as Duration from "effect/Duration";
5import * as Effect from "effect/Effect";
6import * as Exit from "effect/Exit";
7import * as FileSystem from "effect/FileSystem";
8import * as Option from "effect/Option";
9import * as Path from "effect/Path";
10import * as PlatformError from "effect/PlatformError";
11import * as Ref from "effect/Ref";
12import * as Result from "effect/Result";
13import * as Schema from "effect/Schema";
14import * as Scope from "effect/Scope";
15import * as Semaphore from "effect/Semaphore";
16import * as Stream from "effect/Stream";
1917import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
2018
2119import { GitCommandError, type VcsRef } from "@t3tools/contracts";
apps/server/src/vcs/VcsDriver.ts+2-1
......@@ -1,4 +1,5 @@
1import { Context, type Effect } from "effect";
1import * as Context from "effect/Context";
2import type * as Effect from "effect/Effect";
23
34import type {
45 VcsDriverCapabilities,
apps/server/src/vcs/VcsDriverRegistry.test.ts+2-1
......@@ -1,5 +1,6 @@
11import { assert, it, describe } from "@effect/vitest";
2import { Effect, Layer } from "effect";
2import * as Effect from "effect/Effect";
3import * as Layer from "effect/Layer";
34import { ChildProcessSpawner } from "effect/unstable/process";
45
56import * as VcsProcess from "./VcsProcess.ts";
apps/server/src/vcs/VcsDriverRegistry.ts+6-1
......@@ -1,4 +1,9 @@
1import { Cache, Context, Duration, Effect, Exit, Layer } from "effect";
1import * as Cache from "effect/Cache";
2import * as Context from "effect/Context";
3import * as Duration from "effect/Duration";
4import * as Effect from "effect/Effect";
5import * as Exit from "effect/Exit";
6import * as Layer from "effect/Layer";
27
38import type { VcsDriverKind, VcsError, VcsRepositoryIdentity } from "@t3tools/contracts";
49import { VcsUnsupportedOperationError } from "@t3tools/contracts";
apps/server/src/vcs/VcsProcess.ts+8-1
......@@ -1,4 +1,11 @@
1import { Duration, Context, Effect, Layer, Option, PlatformError, Sink, Stream } from "effect";
1import * as Duration from "effect/Duration";
2import * as Context from "effect/Context";
3import * as Effect from "effect/Effect";
4import * as Layer from "effect/Layer";
5import * as Option from "effect/Option";
6import * as PlatformError from "effect/PlatformError";
7import * as Sink from "effect/Sink";
8import * as Stream from "effect/Stream";
29import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
310
411import {
apps/server/src/vcs/VcsProjectConfig.test.ts+5-1
......@@ -1,6 +1,9 @@
11import { assert, it, describe } from "@effect/vitest";
22import * as NodeServices from "@effect/platform-node/NodeServices";
3import { Effect, FileSystem, Layer, Path } from "effect";
3import * as Effect from "effect/Effect";
4import * as FileSystem from "effect/FileSystem";
5import * as Layer from "effect/Layer";
6import * as Path from "effect/Path";
47
58import * as VcsProjectConfig from "./VcsProjectConfig.ts";
69
......@@ -38,6 +41,7 @@ describe("VcsProjectConfig", () => {
3841 yield* fileSystem.makeDirectory(nested, { recursive: true });
3942 yield* fileSystem.writeFileString(
4043 path.join(configDir, "vcs.json"),
44 // @effect-diagnostics-next-line preferSchemaOverJson:off
4145 JSON.stringify({ vcs: { kind: "jj" } }),
4246 );
4347
apps/server/src/vcs/VcsProjectConfig.ts+6-1
......@@ -1,4 +1,9 @@
1import { Context, Effect, FileSystem, Layer, Path, Schema } from "effect";
1import * as Context from "effect/Context";
2import * as Effect from "effect/Effect";
3import * as FileSystem from "effect/FileSystem";
4import * as Layer from "effect/Layer";
5import * as Path from "effect/Path";
6import * as Schema from "effect/Schema";
27
38import { VcsDriverKind, type VcsDriverKind as VcsDriverKindType } from "@t3tools/contracts";
49
apps/server/src/vcs/VcsProvisioningService.test.ts+4-1
......@@ -1,5 +1,8 @@
11import { assert, it } from "@effect/vitest";
2import { DateTime, Effect, Layer, Option } from "effect";
2import * as DateTime from "effect/DateTime";
3import * as Effect from "effect/Effect";
4import * as Layer from "effect/Layer";
5import * as Option from "effect/Option";
36import { ChildProcessSpawner } from "effect/unstable/process";
47
58import * as VcsDriver from "./VcsDriver.ts";
apps/server/src/vcs/VcsProvisioningService.ts+3-1
......@@ -1,4 +1,6 @@
1import { Context, Effect, Layer } from "effect";
1import * as Context from "effect/Context";
2import * as Effect from "effect/Effect";
3import * as Layer from "effect/Layer";
24
35import {
46 type VcsDriverKind,
apps/server/src/vcs/VcsStatusBroadcaster.test.ts+13-2
......@@ -1,6 +1,14 @@
11import { assert, it, describe } from "@effect/vitest";
22import * as NodeServices from "@effect/platform-node/NodeServices";
3import { Deferred, Effect, Exit, FileSystem, Layer, Option, Path, Scope, Stream } from "effect";
3import * as Deferred from "effect/Deferred";
4import * as Effect from "effect/Effect";
5import * as Exit from "effect/Exit";
6import * as FileSystem from "effect/FileSystem";
7import * as Layer from "effect/Layer";
8import * as Option from "effect/Option";
9import * as Path from "effect/Path";
10import * as Scope from "effect/Scope";
11import * as Stream from "effect/Stream";
412import type {
513 VcsStatusLocalResult,
614 VcsStatusRemoteResult,
......@@ -46,6 +54,7 @@ function makeTestLayer(state: {
4654 remoteInvalidationCalls: number;
4755}) {
4856 return VcsStatusBroadcaster.layer.pipe(
57 Layer.provideMerge(NodeServices.layer),
4958 Layer.provide(
5059 Layer.mock(GitWorkflowService.GitWorkflowService)({
5160 localStatus: () =>
......@@ -185,6 +194,7 @@ describe("VcsStatusBroadcaster", () => {
185194 remoteInvalidationCalls: 0,
186195 };
187196 const testLayer = VcsStatusBroadcaster.layer.pipe(
197 Layer.provideMerge(NodeServices.layer),
188198 Layer.provide(
189199 Layer.mock(GitWorkflowService.GitWorkflowService)({
190200 localStatus: (input) =>
......@@ -231,7 +241,7 @@ describe("VcsStatusBroadcaster", () => {
231241 assert.deepStrictEqual(seenCwds, [realPath, realPath]);
232242 assert.equal(state.localStatusCalls, 1);
233243 assert.equal(state.remoteStatusCalls, 1);
234 }).pipe(Effect.provide(Layer.mergeAll(testLayer, NodeServices.layer)));
244 }).pipe(Effect.provide(testLayer));
235245 });
236246
237247 it.effect("streams a local snapshot first and remote updates later", () => {
......@@ -286,6 +296,7 @@ describe("VcsStatusBroadcaster", () => {
286296 let remoteInterruptedDeferred: Deferred.Deferred<void, never> | null = null;
287297 let remoteStartedDeferred: Deferred.Deferred<void, never> | null = null;
288298 const testLayer = VcsStatusBroadcaster.layer.pipe(
299 Layer.provideMerge(NodeServices.layer),
289300 Layer.provide(
290301 Layer.mock(GitWorkflowService.GitWorkflowService)({
291302 localStatus: () =>
apps/server/src/vcs/VcsStatusBroadcaster.ts+24-26
......@@ -1,18 +1,15 @@
1import { realpathSync } from "node:fs";
2
3import {
4 Context,
5 Duration,
6 Effect,
7 Exit,
8 Fiber,
9 Layer,
10 PubSub,
11 Ref,
12 Scope,
13 Stream,
14 SynchronizedRef,
15} from "effect";
1import * as Context from "effect/Context";
2import * as Duration from "effect/Duration";
3import * as Effect from "effect/Effect";
4import * as Exit from "effect/Exit";
5import * as Fiber from "effect/Fiber";
6import * as FileSystem from "effect/FileSystem";
7import * as Layer from "effect/Layer";
8import * as PubSub from "effect/PubSub";
9import * as Ref from "effect/Ref";
10import * as Scope from "effect/Scope";
11import * as Stream from "effect/Stream";
12import * as SynchronizedRef from "effect/SynchronizedRef";
1613import type {
1714 GitManagerServiceError,
1815 VcsStatusInput,
......@@ -69,18 +66,17 @@ function fingerprintStatusPart(status: unknown): string {
6966 return JSON.stringify(status);
7067}
7168
72function normalizeCwd(cwd: string): string {
73 try {
74 return realpathSync.native(cwd);
75 } catch {
76 return cwd;
77 }
78}
69const normalizeCwd = (cwd: string) =>
70 Effect.service(FileSystem.FileSystem).pipe(
71 Effect.flatMap((fs) => fs.realPath(cwd)),
72 Effect.orElseSucceed(() => cwd),
73 );
7974
8075export const layer = Layer.effect(
8176 VcsStatusBroadcaster,
8277 Effect.gen(function* () {
8378 const workflow = yield* GitWorkflowService.GitWorkflowService;
79 const fs = yield* FileSystem.FileSystem;
8480 const changesPubSub = yield* Effect.acquireRelease(
8581 PubSub.unbounded<VcsStatusChange>(),
8682 (pubsub) => PubSub.shutdown(pubsub),
......@@ -195,10 +191,12 @@ export const layer = Layer.effect(
195191 },
196192 );
197193
194 const withFileSystem = Effect.provideService(FileSystem.FileSystem, fs);
195
198196 const getStatus: VcsStatusBroadcasterShape["getStatus"] = Effect.fn(
199197 "VcsStatusBroadcaster.getStatus",
200198 )(function* (input) {
201 const cwd = normalizeCwd(input.cwd);
199 const cwd = yield* withFileSystem(normalizeCwd(input.cwd));
202200 const [local, remote] = yield* Effect.all([
203201 getOrLoadLocalStatus(cwd),
204202 getOrLoadRemoteStatus(cwd),
......@@ -209,7 +207,7 @@ export const layer = Layer.effect(
209207 const refreshLocalStatus: VcsStatusBroadcasterShape["refreshLocalStatus"] = Effect.fn(
210208 "VcsStatusBroadcaster.refreshLocalStatus",
211209 )(function* (rawCwd) {
212 const cwd = normalizeCwd(rawCwd);
210 const cwd = yield* withFileSystem(normalizeCwd(rawCwd));
213211 yield* workflow.invalidateLocalStatus(cwd);
214212 const local = yield* workflow.localStatus({ cwd });
215213 return yield* updateCachedLocalStatus(cwd, local, { publish: true });
......@@ -226,7 +224,7 @@ export const layer = Layer.effect(
226224 const refreshStatus: VcsStatusBroadcasterShape["refreshStatus"] = Effect.fn(
227225 "VcsStatusBroadcaster.refreshStatus",
228226 )(function* (rawCwd) {
229 const cwd = normalizeCwd(rawCwd);
227 const cwd = yield* withFileSystem(normalizeCwd(rawCwd));
230228 const [local, remote] = yield* Effect.all([
231229 refreshLocalStatus(cwd),
232230 refreshRemoteStatus(cwd),
......@@ -312,7 +310,7 @@ export const layer = Layer.effect(
312310 const streamStatus: VcsStatusBroadcasterShape["streamStatus"] = (input) =>
313311 Stream.unwrap(
314312 Effect.gen(function* () {
315 const cwd = normalizeCwd(input.cwd);
313 const cwd = yield* withFileSystem(normalizeCwd(input.cwd));
316314 const subscription = yield* PubSub.subscribe(changesPubSub);
317315 const initialLocal = yield* getOrLoadLocalStatus(cwd);
318316 const initialRemote = (yield* getCachedStatus(cwd))?.remote?.value ?? null;
apps/server/src/vcs/testing/VcsDriverContractHarness.ts+8-10
......@@ -1,14 +1,12 @@
11import { assert, it, describe } from "@effect/vitest";
2import {
3 Effect,
4 FileSystem,
5 Layer,
6 Path,
7 type PlatformError,
8 type Scope,
9 DateTime,
10 Option,
11} from "effect";
2import * as Effect from "effect/Effect";
3import * as FileSystem from "effect/FileSystem";
4import * as Layer from "effect/Layer";
5import * as Path from "effect/Path";
6import type * as PlatformError from "effect/PlatformError";
7import type * as Scope from "effect/Scope";
8import * as DateTime from "effect/DateTime";
9import * as Option from "effect/Option";
1210
1311import type { VcsDriverKind } from "@t3tools/contracts";
1412import * as VcsDriver from "../VcsDriver.ts";
apps/server/src/workspace/Layers/WorkspaceEntries.test.ts+38-7
......@@ -1,8 +1,13 @@
1// @effect-diagnostics nodeBuiltinImport:off
12import fsPromises from "node:fs/promises";
2
33import * as NodeServices from "@effect/platform-node/NodeServices";
44import { it, afterEach, describe, expect, vi } from "@effect/vitest";
5import { Effect, FileSystem, Layer, Path, PlatformError } from "effect";
5import * as Effect from "effect/Effect";
6import * as Fiber from "effect/Fiber";
7import * as FileSystem from "effect/FileSystem";
8import * as Layer from "effect/Layer";
9import * as Path from "effect/Path";
10import * as PlatformError from "effect/PlatformError";
611
712import { ServerConfig } from "../../config.ts";
813import * as VcsDriverRegistry from "../../vcs/VcsDriverRegistry.ts";
......@@ -222,25 +227,37 @@ it.layer(TestLayer)("WorkspaceEntriesLive", (it) => {
222227 yield* writeTextFile(cwd, "src/components/Composer.tsx");
223228
224229 let rootReadCount = 0;
230 let releaseRootRead: (() => void) | undefined;
231 const rootReadGate = new Promise<void>((resolve) => {
232 releaseRootRead = resolve;
233 });
225234 const originalReaddir = fsPromises.readdir.bind(fsPromises);
226235 vi.spyOn(fsPromises, "readdir").mockImplementation((async (
227236 ...args: Parameters<typeof fsPromises.readdir>
228237 ) => {
229238 if (args[0] === cwd) {
230239 rootReadCount += 1;
231 await new Promise((resolve) => setTimeout(resolve, 20));
240 await rootReadGate;
232241 }
233242 return originalReaddir(...args);
234243 }) as typeof fsPromises.readdir);
235244
236 yield* Effect.all(
245 const searches = yield* Effect.all(
237246 [
238247 searchWorkspaceEntries({ cwd, query: "", limit: 100 }),
239248 searchWorkspaceEntries({ cwd, query: "comp", limit: 100 }),
240249 searchWorkspaceEntries({ cwd, query: "src", limit: 100 }),
241250 ],
242251 { concurrency: "unbounded" },
243 );
252 ).pipe(Effect.forkScoped);
253 for (let attempt = 0; attempt < 50; attempt += 1) {
254 if (rootReadCount > 0) {
255 break;
256 }
257 yield* Effect.yieldNow;
258 }
259 releaseRootRead?.();
260 yield* Fiber.join(searches);
244261
245262 expect(rootReadCount).toBe(1);
246263 }),
......@@ -257,6 +274,10 @@ it.layer(TestLayer)("WorkspaceEntriesLive", (it) => {
257274
258275 let activeReads = 0;
259276 let peakReads = 0;
277 let releaseReads: (() => void) | undefined;
278 const readsGate = new Promise<void>((resolve) => {
279 releaseReads = resolve;
280 });
260281 const originalReaddir = fsPromises.readdir.bind(fsPromises);
261282 vi.spyOn(fsPromises, "readdir").mockImplementation((async (
262283 ...args: Parameters<typeof fsPromises.readdir>
......@@ -265,7 +286,7 @@ it.layer(TestLayer)("WorkspaceEntriesLive", (it) => {
265286 if (typeof target === "string" && target.startsWith(cwd)) {
266287 activeReads += 1;
267288 peakReads = Math.max(peakReads, activeReads);
268 await new Promise((resolve) => setTimeout(resolve, 4));
289 await readsGate;
269290 try {
270291 return await originalReaddir(...args);
271292 } finally {
......@@ -275,7 +296,17 @@ it.layer(TestLayer)("WorkspaceEntriesLive", (it) => {
275296 return originalReaddir(...args);
276297 }) as typeof fsPromises.readdir);
277298
278 yield* searchWorkspaceEntries({ cwd, query: "", limit: 200 });
299 const search = yield* searchWorkspaceEntries({ cwd, query: "", limit: 200 }).pipe(
300 Effect.forkScoped,
301 );
302 for (let attempt = 0; attempt < 50; attempt += 1) {
303 if (activeReads > 0) {
304 break;
305 }
306 yield* Effect.yieldNow;
307 }
308 releaseReads?.();
309 yield* Fiber.join(search);
279310
280311 expect(peakReads).toBeLessThanOrEqual(32);
281312 }),
apps/server/src/workspace/Layers/WorkspaceEntries.ts+8-1
......@@ -1,8 +1,15 @@
1// @effect-diagnostics nodeBuiltinImport:off
12import * as OS from "node:os";
23import fsPromises from "node:fs/promises";
34import type { Dirent } from "node:fs";
45
5import { Cache, DateTime, Duration, Effect, Exit, Layer, Path } from "effect";
6import * as Cache from "effect/Cache";
7import * as DateTime from "effect/DateTime";
8import * as Duration from "effect/Duration";
9import * as Effect from "effect/Effect";
10import * as Exit from "effect/Exit";
11import * as Layer from "effect/Layer";
12import * as Path from "effect/Path";
613
714import { type FilesystemBrowseInput, type ProjectEntry } from "@t3tools/contracts";
815import { isExplicitRelativePath, isWindowsAbsolutePath } from "@t3tools/shared/path";
apps/server/src/workspace/Layers/WorkspaceFileSystem.test.ts+4-1
......@@ -1,6 +1,9 @@
11import * as NodeServices from "@effect/platform-node/NodeServices";
22import { it, describe, expect } from "@effect/vitest";
3import { Effect, FileSystem, Layer, Path } from "effect";
3import * as Effect from "effect/Effect";
4import * as FileSystem from "effect/FileSystem";
5import * as Layer from "effect/Layer";
6import * as Path from "effect/Path";
47
58import { ServerConfig } from "../../config.ts";
69import * as VcsDriverRegistry from "../../vcs/VcsDriverRegistry.ts";
apps/server/src/workspace/Layers/WorkspaceFileSystem.ts+4-1
......@@ -1,4 +1,7 @@
1import { Effect, FileSystem, Layer, Path } from "effect";
1import * as Effect from "effect/Effect";
2import * as FileSystem from "effect/FileSystem";
3import * as Layer from "effect/Layer";
4import * as Path from "effect/Path";
25
36import {
47 WorkspaceFileSystem,
apps/server/src/workspace/Layers/WorkspacePaths.test.ts+4-1
......@@ -1,6 +1,9 @@
11import * as NodeServices from "@effect/platform-node/NodeServices";
22import { it, describe, expect } from "@effect/vitest";
3import { Effect, FileSystem, Layer, Path } from "effect";
3import * as Effect from "effect/Effect";
4import * as FileSystem from "effect/FileSystem";
5import * as Layer from "effect/Layer";
6import * as Path from "effect/Path";
47
58import { WorkspacePaths } from "../Services/WorkspacePaths.ts";
69import { WorkspacePathsLive } from "./WorkspacePaths.ts";
apps/server/src/workspace/Layers/WorkspacePaths.ts+4-1
......@@ -1,5 +1,8 @@
11import * as OS from "node:os";
2import { Effect, FileSystem, Layer, Path } from "effect";
2import * as Effect from "effect/Effect";
3import * as FileSystem from "effect/FileSystem";
4import * as Layer from "effect/Layer";
5import * as Path from "effect/Path";
36
47import {
58 WorkspacePaths,
apps/server/src/workspace/Services/WorkspaceEntries.ts+3-2
......@@ -6,8 +6,9 @@
66 *
77 * @module WorkspaceEntries
88 */
9import { Schema, Context } from "effect";
10import type { Effect } from "effect";
9import * as Schema from "effect/Schema";
10import * as Context from "effect/Context";
11import type * as Effect from "effect/Effect";
1112
1213import type {
1314 FilesystemBrowseInput,
apps/server/src/workspace/Services/WorkspaceFileSystem.ts+3-2
......@@ -6,8 +6,9 @@
66 *
77 * @module WorkspaceFileSystem
88 */
9import { Schema, Context } from "effect";
10import type { Effect } from "effect";
9import * as Schema from "effect/Schema";
10import * as Context from "effect/Context";
11import type * as Effect from "effect/Effect";
1112
1213import type { ProjectWriteFileInput, ProjectWriteFileResult } from "@t3tools/contracts";
1314import { WorkspacePathOutsideRootError } from "./WorkspacePaths.ts";
apps/server/src/workspace/Services/WorkspacePaths.ts+3-2
......@@ -6,8 +6,9 @@
66 *
77 * @module WorkspacePaths
88 */
9import { Schema, Context } from "effect";
10import type { Effect } from "effect";
9import * as Schema from "effect/Schema";
10import * as Context from "effect/Context";
11import type * as Effect from "effect/Effect";
1112
1213export class WorkspaceRootNotExistsError extends Schema.TaggedErrorClass<WorkspaceRootNotExistsError>()(
1314 "WorkspaceRootNotExistsError",
apps/server/src/ws.ts+89-75
......@@ -1,4 +1,13 @@
1import { Cause, Duration, Effect, Layer, Option, Queue, Ref, Schema, Stream } from "effect";
1import * as Cause from "effect/Cause";
2import * as DateTime from "effect/DateTime";
3import * as Duration from "effect/Duration";
4import * as Effect from "effect/Effect";
5import * as Layer from "effect/Layer";
6import * as Option from "effect/Option";
7import * as Queue from "effect/Queue";
8import * as Ref from "effect/Ref";
9import * as Schema from "effect/Schema";
10import * as Stream from "effect/Stream";
211import {
312 type AuthAccessStreamEvent,
413 AuthSessionId,
......@@ -78,6 +87,8 @@ import {
7887} from "./auth/Services/SessionCredentialService.ts";
7988import { respondToAuthError } from "./auth/http.ts";
8089
90const nowIso = Effect.map(DateTime.now, DateTime.formatIso);
91
8192function isThreadDetailEvent(event: OrchestrationEvent): event is Extract<
8293 OrchestrationEvent,
8394 {
......@@ -378,83 +389,86 @@ const makeWsRpcLayer = (currentSessionId: AuthSessionId) =>
378389 readonly scriptId: string;
379390 readonly scriptName: string;
380391 readonly terminalId: string;
381 }) => {
382 const payload = {
383 scriptId: input.scriptId,
384 scriptName: input.scriptName,
385 terminalId: input.terminalId,
386 worktreePath: input.worktreePath,
387 };
388 return Effect.all([
389 appendSetupScriptActivity({
390 threadId: command.threadId,
391 kind: "setup-script.requested",
392 summary: "Starting setup script",
393 createdAt: input.requestedAt,
394 payload,
395 tone: "info",
396 }),
397 appendSetupScriptActivity({
398 threadId: command.threadId,
399 kind: "setup-script.started",
400 summary: "Setup script started",
401 createdAt: new Date().toISOString(),
402 payload,
403 tone: "info",
404 }),
405 ]).pipe(
406 Effect.asVoid,
407 Effect.catch((error) =>
408 Effect.logWarning(
409 "bootstrap turn start launched setup script but failed to record setup activity",
410 {
411 threadId: command.threadId,
412 worktreePath: input.worktreePath,
413 scriptId: input.scriptId,
414 terminalId: input.terminalId,
415 detail: error.message,
416 },
392 }) =>
393 Effect.gen(function* () {
394 const startedAt = yield* nowIso;
395 const payload = {
396 scriptId: input.scriptId,
397 scriptName: input.scriptName,
398 terminalId: input.terminalId,
399 worktreePath: input.worktreePath,
400 };
401 yield* Effect.all([
402 appendSetupScriptActivity({
403 threadId: command.threadId,
404 kind: "setup-script.requested",
405 summary: "Starting setup script",
406 createdAt: input.requestedAt,
407 payload,
408 tone: "info",
409 }),
410 appendSetupScriptActivity({
411 threadId: command.threadId,
412 kind: "setup-script.started",
413 summary: "Setup script started",
414 createdAt: startedAt,
415 payload,
416 tone: "info",
417 }),
418 ]).pipe(
419 Effect.asVoid,
420 Effect.catch((error) =>
421 Effect.logWarning(
422 "bootstrap turn start launched setup script but failed to record setup activity",
423 {
424 threadId: command.threadId,
425 worktreePath: input.worktreePath,
426 scriptId: input.scriptId,
427 terminalId: input.terminalId,
428 detail: error.message,
429 },
430 ),
417431 ),
418 ),
419 );
420 };
432 );
433 });
421434
422435 const runSetupProgram = () =>
423 bootstrap?.runSetupScript && targetWorktreePath
424 ? (() => {
425 const worktreePath = targetWorktreePath;
426 const requestedAt = new Date().toISOString();
427 return projectSetupScriptRunner
428 .runForThread({
429 threadId: command.threadId,
430 ...(targetProjectId ? { projectId: targetProjectId } : {}),
431 ...(targetProjectCwd ? { projectCwd: targetProjectCwd } : {}),
432 worktreePath,
433 })
434 .pipe(
435 Effect.matchEffect({
436 onFailure: (error) =>
437 recordSetupScriptLaunchFailure({
438 error,
439 requestedAt,
440 worktreePath,
441 }),
442 onSuccess: (setupResult) => {
443 if (setupResult.status !== "started") {
444 return Effect.void;
445 }
446 return recordSetupScriptStarted({
447 requestedAt,
448 worktreePath,
449 scriptId: setupResult.scriptId,
450 scriptName: setupResult.scriptName,
451 terminalId: setupResult.terminalId,
452 });
453 },
436 Effect.gen(function* () {
437 if (!bootstrap?.runSetupScript || !targetWorktreePath) {
438 return;
439 }
440 const worktreePath = targetWorktreePath;
441 const requestedAt = yield* nowIso;
442 yield* projectSetupScriptRunner
443 .runForThread({
444 threadId: command.threadId,
445 ...(targetProjectId ? { projectId: targetProjectId } : {}),
446 ...(targetProjectCwd ? { projectCwd: targetProjectCwd } : {}),
447 worktreePath,
448 })
449 .pipe(
450 Effect.matchEffect({
451 onFailure: (error) =>
452 recordSetupScriptLaunchFailure({
453 error,
454 requestedAt,
455 worktreePath,
454456 }),
455 );
456 })()
457 : Effect.void;
457 onSuccess: (setupResult) => {
458 if (setupResult.status !== "started") {
459 return Effect.void;
460 }
461 return recordSetupScriptStarted({
462 requestedAt,
463 worktreePath,
464 scriptId: setupResult.scriptId,
465 scriptName: setupResult.scriptName,
466 terminalId: setupResult.terminalId,
467 });
468 },
469 }),
470 );
471 });
458472
459473 const bootstrapProgram = Effect.gen(function* () {
460474 if (bootstrap?.createThread) {
......@@ -597,7 +611,7 @@ const makeWsRpcLayer = (currentSessionId: AuthSessionId) =>
597611 `session-stop-for-archive:${normalizedCommand.commandId}`,
598612 ),
599613 threadId: normalizedCommand.threadId,
600 createdAt: new Date().toISOString(),
614 createdAt: yield* nowIso,
601615 });
602616
603617 yield* dispatchNormalizedCommand(stopCommand);
apps/server/tsconfig.json+1-15
......@@ -3,21 +3,7 @@
33 "compilerOptions": {
44 "composite": true,
55 "types": ["node", "bun"],
6 "lib": ["ESNext", "esnext.disposable"],
7 "plugins": [
8 {
9 "name": "@effect/language-service",
10 "namespaceImportPackages": ["@effect/platform-node"],
11 "diagnosticSeverity": {
12 "importFromBarrel": "error",
13 "anyUnknownInErrorContext": "warning",
14 "instanceOfSchema": "warning",
15 "deterministicKeys": "warning",
16 "preferSchemaOverJson": "off",
17 "globalErrorInEffectFailure": "off"
18 }
19 }
20 ]
6 "lib": ["ESNext", "esnext.disposable"]
217 },
228 "include": ["src", "tsdown.config.ts", "scripts", "integration", "../../scripts/lib"]
239}
apps/web/src/components/BranchToolbar.logic.ts+1-1
......@@ -1,5 +1,5 @@
11import type { EnvironmentId, VcsRef, ProjectId } from "@t3tools/contracts";
2import { Schema } from "effect";
2import * as Schema from "effect/Schema";
33export {
44 dedupeRemoteBranchesWithLocalMatches,
55 deriveLocalBranchNameFromRemoteRef,
apps/web/src/components/ChatView.browser.tsx+1-1
......@@ -22,7 +22,7 @@ import {
2222import { scopedThreadKey, scopeThreadRef } from "@t3tools/client-runtime";
2323import { createModelCapabilities, createModelSelection } from "@t3tools/shared/model";
2424import { RouterProvider, createMemoryHistory } from "@tanstack/react-router";
25import { Option } from "effect";
25import * as Option from "effect/Option";
2626import { HttpResponse, http, ws } from "msw";
2727import { setupWorker } from "msw/browser";
2828import { page } from "vitest/browser";
apps/web/src/components/ChatView.logic.ts+1-1
......@@ -10,7 +10,7 @@ import {
1010} from "@t3tools/contracts";
1111import { type ChatMessage, type SessionPhase, type Thread, type ThreadSession } from "../types";
1212import { type ComposerImageAttachment, type DraftThreadState } from "../composerDraftStore";
13import { Schema } from "effect";
13import * as Schema from "effect/Schema";
1414import { selectThreadByRef, useStore } from "../store";
1515import {
1616 filterTerminalContextsWithText,
apps/web/src/components/CommandPalette.tsx+1-1
......@@ -13,7 +13,7 @@ import {
1313} from "@t3tools/contracts";
1414import { useQuery, useQueryClient } from "@tanstack/react-query";
1515import { useNavigate, useParams } from "@tanstack/react-router";
16import { Option } from "effect";
16import * as Option from "effect/Option";
1717import {
1818 ArrowDownIcon,
1919 ArrowLeftIcon,
apps/web/src/components/GitActionsControl.tsx+1-1
......@@ -12,7 +12,7 @@ import type {
1212} from "@t3tools/contracts";
1313import { useIsMutating, useMutation, useQueryClient } from "@tanstack/react-query";
1414import { useNavigate } from "@tanstack/react-router";
15import { Option } from "effect";
15import * as Option from "effect/Option";
1616import { useCallback, useEffect, useEffectEvent, useMemo, useRef, useState } from "react";
1717import { flushSync } from "react-dom";
1818import {
apps/web/src/components/settings/ConnectionsSettings.tsx+1-1
......@@ -17,7 +17,7 @@ import {
1717 type DesktopServerExposureState,
1818 type EnvironmentId,
1919} from "@t3tools/contracts";
20import { DateTime } from "effect";
20import * as DateTime from "effect/DateTime";
2121
2222import { useCopyToClipboard } from "../../hooks/useCopyToClipboard";
2323import { cn } from "../../lib/utils";
apps/web/src/components/settings/DiagnosticsSettings.tsx+2-1
......@@ -9,7 +9,8 @@ import {
99} from "lucide-react";
1010import { useCallback, useMemo, useState, type ReactNode } from "react";
1111import type { ServerProcessDiagnosticsEntry, ServerProcessSignal } from "@t3tools/contracts";
12import { DateTime, Option } from "effect";
12import * as DateTime from "effect/DateTime";
13import * as Option from "effect/Option";
1314
1415import { ensureLocalApi } from "../../localApi";
1516import { cn } from "../../lib/utils";
apps/web/src/components/settings/ProviderSettingsForm.tsx+1-1
......@@ -1,7 +1,7 @@
11"use client";
22
33import { useMemo, type ReactNode } from "react";
4import { Schema } from "effect";
4import * as Schema from "effect/Schema";
55import type {
66 ProviderSettingsFormAnnotation,
77 ProviderSettingsFormControl,
apps/web/src/components/settings/SettingsPanels.browser.tsx+3-2
......@@ -16,7 +16,8 @@ import {
1616 type ServerProvider,
1717 type SourceControlDiscoveryResult,
1818} from "@t3tools/contracts";
19import { DateTime, Option } from "effect";
19import * as DateTime from "effect/DateTime";
20import * as Option from "effect/Option";
2021import { page } from "vitest/browser";
2122import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2223import { render } from "vitest-browser-react";
......@@ -261,7 +262,7 @@ function createOutdatedProvider(driver: string): ServerProvider {
261262}
262263
263264function makeUtc(value: string) {
264 return DateTime.makeUnsafe(Date.parse(value));
265 return DateTime.makeUnsafe(value);
265266}
266267
267268function makePairingLink(input: {
apps/web/src/components/settings/SettingsPanels.tsx+1-1
......@@ -14,7 +14,7 @@ import {
1414import { scopeThreadRef } from "@t3tools/client-runtime";
1515import { DEFAULT_UNIFIED_SETTINGS } from "@t3tools/contracts/settings";
1616import { createModelSelection } from "@t3tools/shared/model";
17import { Equal } from "effect";
17import * as Equal from "effect/Equal";
1818import { APP_VERSION } from "../../branding";
1919import {
2020 canCheckForUpdate,
apps/web/src/components/settings/SourceControlSettings.tsx+1-1
......@@ -1,5 +1,5 @@
11import { GitPullRequestIcon, RefreshCwIcon } from "lucide-react";
2import { Option } from "effect";
2import * as Option from "effect/Option";
33import { type ReactNode } from "react";
44import type {
55 SourceControlProviderKind,
apps/web/src/components/settings/providerDriverMeta.ts+1-1
......@@ -5,7 +5,7 @@ import {
55 OpenCodeSettings,
66 ProviderDriverKind,
77} from "@t3tools/contracts";
8import type { Schema } from "effect";
8import type * as Schema from "effect/Schema";
99import { ClaudeAI, CursorIcon, type Icon, OpenAI, OpenCodeIcon } from "../Icons";
1010
1111type ProviderSettingsSchema = {
apps/web/src/components/ui/sidebar.tsx+1-1
......@@ -19,7 +19,7 @@ import { Skeleton } from "~/components/ui/skeleton";
1919import { Tooltip, TooltipPopup, TooltipTrigger } from "~/components/ui/tooltip";
2020import { useIsMobile } from "~/hooks/useMediaQuery";
2121import { getLocalStorageItem, setLocalStorageItem } from "~/hooks/useLocalStorage";
22import { Schema } from "effect";
22import * as Schema from "effect/Schema";
2323
2424const SIDEBAR_COOKIE_NAME = "sidebar_state";
2525const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
apps/web/src/environments/primary/auth.ts+2-1
......@@ -16,7 +16,8 @@ import {
1616} from "../../pairingUrl";
1717
1818import { resolvePrimaryEnvironmentHttpUrl } from "./target";
19import { Data, Predicate } from "effect";
19import * as Data from "effect/Data";
20import * as Predicate from "effect/Predicate";
2021
2122export class BootstrapHttpError extends Data.TaggedError("BootstrapHttpError")<{
2223 readonly message: string;
apps/web/src/hooks/useSettings.ts+1-1
......@@ -19,7 +19,7 @@ import {
1919 UnifiedSettings,
2020} from "@t3tools/contracts/settings";
2121import { ensureLocalApi } from "~/localApi";
22import { Struct } from "effect";
22import * as Struct from "effect/Struct";
2323import { applyServerSettingsPatch } from "@t3tools/shared/serverSettings";
2424import { applySettingsUpdated, getServerConfig, useServerSettings } from "~/rpc/serverState";
2525
apps/web/src/lib/gitStatusState.ts+1-1
......@@ -4,7 +4,7 @@ import {
44 type GitManagerServiceError,
55 type VcsStatusResult,
66} from "@t3tools/contracts";
7import { Cause } from "effect";
7import * as Cause from "effect/Cause";
88import { Atom } from "effect/unstable/reactivity";
99import { useEffect } from "react";
1010
apps/web/src/lib/processDiagnosticsState.ts+3-1
......@@ -1,6 +1,8 @@
11import { useAtomValue } from "@effect/atom-react";
22import type { ServerProcessDiagnosticsResult } from "@t3tools/contracts";
3import { Cause, Effect, Option } from "effect";
3import * as Cause from "effect/Cause";
4import * as Effect from "effect/Effect";
5import * as Option from "effect/Option";
46import { AsyncResult, Atom } from "effect/unstable/reactivity";
57import { useCallback } from "react";
68
apps/web/src/lib/projectScriptKeybindings.ts+1-1
......@@ -4,7 +4,7 @@ import {
44 type KeybindingRule,
55 type ResolvedKeybindingsConfig,
66} from "@t3tools/contracts";
7import { Schema } from "effect";
7import * as Schema from "effect/Schema";
88
99export const PROJECT_SCRIPT_KEYBINDING_INVALID_MESSAGE = "Invalid keybinding.";
1010
apps/web/src/lib/providerReactQuery.ts+2-1
......@@ -5,7 +5,8 @@ import {
55 ThreadId,
66} from "@t3tools/contracts";
77import { queryOptions } from "@tanstack/react-query";
8import { Option, Schema } from "effect";
8import * as Option from "effect/Option";
9import * as Schema from "effect/Schema";
910import { ensureEnvironmentApi } from "../environmentApi";
1011
1112interface CheckpointDiffQueryInput {
apps/web/src/lib/sourceControlDiscoveryState.ts+1-1
......@@ -7,7 +7,7 @@ import {
77 sourceControlDiscoveryStateAtom,
88} from "@t3tools/client-runtime";
99import { EnvironmentId, type SourceControlDiscoveryResult } from "@t3tools/contracts";
10import { Effect } from "effect";
10import * as Effect from "effect/Effect";
1111import { Atom } from "effect/unstable/reactivity";
1212
1313import { readPrimaryEnvironmentDescriptor } from "../environments/primary";
apps/web/src/lib/traceDiagnosticsState.ts+3-1
......@@ -1,6 +1,8 @@
11import { useAtomValue } from "@effect/atom-react";
22import type { ServerTraceDiagnosticsResult } from "@t3tools/contracts";
3import { Cause, Effect, Option } from "effect";
3import * as Cause from "effect/Cause";
4import * as Effect from "effect/Effect";
5import * as Option from "effect/Option";
46import { AsyncResult, Atom } from "effect/unstable/reactivity";
57import { useCallback } from "react";
68
apps/web/src/observability/clientTracing.ts+5-1
......@@ -1,4 +1,8 @@
1import { Exit, Layer, ManagedRuntime, Scope, Tracer } from "effect";
1import * as Exit from "effect/Exit";
2import * as Layer from "effect/Layer";
3import * as ManagedRuntime from "effect/ManagedRuntime";
4import * as Scope from "effect/Scope";
5import * as Tracer from "effect/Tracer";
26import { FetchHttpClient, HttpClient } from "effect/unstable/http";
37import { OtlpSerialization, OtlpTracer } from "effect/unstable/observability";
48
apps/web/src/projectScripts.ts+1-1
......@@ -4,7 +4,7 @@ import {
44 type KeybindingCommand,
55 type ProjectScript,
66} from "@t3tools/contracts";
7import { Schema } from "effect";
7import * as Schema from "effect/Schema";
88
99function normalizeScriptId(value: string): string {
1010 const cleaned = value
apps/web/src/rpc/protocol.ts+4-1
......@@ -1,5 +1,8 @@
11import { WsRpcGroup } from "@t3tools/contracts";
2import { Duration, Effect, Layer, Schedule } from "effect";
2import * as Duration from "effect/Duration";
3import * as Effect from "effect/Effect";
4import * as Layer from "effect/Layer";
5import * as Schedule from "effect/Schedule";
36import { RpcClient, RpcSerialization } from "effect/unstable/rpc";
47import * as Socket from "effect/unstable/socket/Socket";
58
apps/web/src/rpc/wsRpcClient.ts+2-1
......@@ -10,7 +10,8 @@ import {
1010 WS_METHODS,
1111} from "@t3tools/contracts";
1212import { applyGitStatusStreamEvent } from "@t3tools/shared/git";
13import { Effect, Stream } from "effect";
13import * as Effect from "effect/Effect";
14import * as Stream from "effect/Stream";
1415
1516import { type WsRpcProtocolClient } from "./protocol";
1617import { resetWsReconnectBackoff } from "./wsConnectionState";
apps/web/src/rpc/wsTransport.test.ts+1-1
......@@ -1,5 +1,5 @@
11import { DEFAULT_SERVER_SETTINGS, WS_METHODS } from "@t3tools/contracts";
2import { Stream } from "effect";
2import * as Stream from "effect/Stream";
33import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
44
55import {
apps/web/src/rpc/wsTransport.ts+9-11
......@@ -1,14 +1,12 @@
1import {
2 Cause,
3 Duration,
4 Effect,
5 Exit,
6 Layer,
7 ManagedRuntime,
8 Option,
9 Scope,
10 Stream,
11} from "effect";
1import * as Cause from "effect/Cause";
2import * as Duration from "effect/Duration";
3import * as Effect from "effect/Effect";
4import * as Exit from "effect/Exit";
5import * as Layer from "effect/Layer";
6import * as ManagedRuntime from "effect/ManagedRuntime";
7import * as Option from "effect/Option";
8import * as Scope from "effect/Scope";
9import * as Stream from "effect/Stream";
1210import { RpcClient } from "effect/unstable/rpc";
1311
1412import { ClientTracingLive } from "../observability/clientTracing";
apps/web/src/store.ts+1-1
......@@ -20,7 +20,7 @@ import type {
2020} from "@t3tools/contracts";
2121import { isProviderDriverKind, ProviderDriverKind } from "@t3tools/contracts";
2222import type { ThreadId, TurnId } from "@t3tools/contracts";
23import { Schema } from "effect";
23import * as Schema from "effect/Schema";
2424import { resolveModelSlugForProvider } from "@t3tools/shared/model";
2525import { create } from "zustand";
2626import {
apps/web/test/wsRpcHarness.ts+5-1
......@@ -1,5 +1,9 @@
1import { Effect, Exit, PubSub, Scope, Stream } from "effect";
21import { ORCHESTRATION_WS_METHODS, WS_METHODS, WsRpcGroup } from "@t3tools/contracts";
2import * as Effect from "effect/Effect";
3import * as Exit from "effect/Exit";
4import * as PubSub from "effect/PubSub";
5import * as Scope from "effect/Scope";
6import * as Stream from "effect/Stream";
37import { RpcMessage, RpcSerialization, RpcServer } from "effect/unstable/rpc";
48
59type RpcServerInstance = RpcServer.RpcServer<any>;
apps/web/tsconfig.json+13-3
......@@ -15,12 +15,22 @@
1515 "plugins": [
1616 {
1717 "name": "@effect/language-service",
18 "namespaceImportPackages": ["@effect/platform-node"],
18 "namespaceImportPackages": ["@effect/platform-node", "effect"],
1919 "diagnosticSeverity": {
2020 "importFromBarrel": "error",
2121 "anyUnknownInErrorContext": "error",
22 "instanceOfSchema": "warning",
23 "deterministicKeys": "warning"
22 "unsafeEffectTypeAssertion": "error",
23 "instanceOfSchema": "error",
24 "deterministicKeys": "error",
25 "strictEffectProvide": "off",
26 "missingEffectServiceDependency": "error",
27 "leakingRequirements": "error",
28 "globalErrorInEffectCatch": "error",
29 "globalErrorInEffectFailure": "error",
30 "unknownInEffectCatch": "error",
31 "strictBooleanExpressions": "off",
32 "preferSchemaOverJson": "error",
33 "schemaSyncInEffect": "error"
2434 }
2535 }
2636 ]
bun.lock+1
......@@ -201,6 +201,7 @@
201201 },
202202 "devDependencies": {
203203 "@effect/language-service": "catalog:",
204 "@effect/platform-node": "catalog:",
204205 "@effect/vitest": "catalog:",
205206 "@types/node": "catalog:",
206207 "typescript": "catalog:",
packages/client-runtime/tsconfig.json+1-14
......@@ -1,18 +1,5 @@
11{
22 "extends": "../../tsconfig.base.json",
3 "compilerOptions": {
4 "plugins": [
5 {
6 "name": "@effect/language-service",
7 "namespaceImportPackages": ["@effect/platform-node", "effect"],
8 "diagnosticSeverity": {
9 "importFromBarrel": "error",
10 "anyUnknownInErrorContext": "error",
11 "instanceOfSchema": "warning",
12 "deterministicKeys": "warning"
13 }
14 }
15 ]
16 },
3 "compilerOptions": {},
174 "include": ["src"]
185}
packages/contracts/src/model.ts+2-2
......@@ -99,10 +99,10 @@ function coerceLegacyOptionsObjectToArray(
9999 const entries: Array<ProviderOptionSelection> = [];
100100 for (const [rawKey, rawValue] of Object.entries(record)) {
101101 const id = typeof rawKey === "string" ? rawKey.trim() : "";
102 if (!id) continue;
102 if (id.length === 0) continue;
103103 if (typeof rawValue === "string") {
104104 const trimmed = rawValue.trim();
105 if (trimmed) entries.push({ id, value: trimmed });
105 if (trimmed.length > 0) entries.push({ id, value: trimmed });
106106 } else if (typeof rawValue === "boolean") {
107107 entries.push({ id, value: rawValue });
108108 }
packages/contracts/src/terminal.test.ts+10-6
......@@ -164,6 +164,8 @@ describe("TerminalCloseInput", () => {
164164});
165165
166166describe("TerminalSessionSnapshot", () => {
167 const isoTimestamp = "2026-01-01T00:00:00.000Z";
168
167169 it("accepts running snapshots", () => {
168170 expect(
169171 decodes(TerminalSessionSnapshot, {
......@@ -176,20 +178,22 @@ describe("TerminalSessionSnapshot", () => {
176178 history: "hello\n",
177179 exitCode: null,
178180 exitSignal: null,
179 updatedAt: new Date().toISOString(),
181 updatedAt: isoTimestamp,
180182 }),
181183 ).toBe(true);
182184 });
183185});
184186
185187describe("TerminalEvent", () => {
188 const isoTimestamp = "2026-01-01T00:00:00.000Z";
189
186190 it("accepts output events", () => {
187191 expect(
188192 decodes(TerminalEvent, {
189193 type: "output",
190194 threadId: "thread-1",
191195 terminalId: DEFAULT_TERMINAL_ID,
192 createdAt: new Date().toISOString(),
196 createdAt: isoTimestamp,
193197 data: "line\n",
194198 }),
195199 ).toBe(true);
......@@ -201,7 +205,7 @@ describe("TerminalEvent", () => {
201205 type: "exited",
202206 threadId: "thread-1",
203207 terminalId: DEFAULT_TERMINAL_ID,
204 createdAt: new Date().toISOString(),
208 createdAt: isoTimestamp,
205209 exitCode: 0,
206210 exitSignal: null,
207211 }),
......@@ -214,7 +218,7 @@ describe("TerminalEvent", () => {
214218 type: "activity",
215219 threadId: "thread-1",
216220 terminalId: DEFAULT_TERMINAL_ID,
217 createdAt: new Date().toISOString(),
221 createdAt: isoTimestamp,
218222 hasRunningSubprocess: true,
219223 }),
220224 ).toBe(true);
......@@ -226,7 +230,7 @@ describe("TerminalEvent", () => {
226230 type: "started",
227231 threadId: "thread-1",
228232 terminalId: DEFAULT_TERMINAL_ID,
229 createdAt: new Date().toISOString(),
233 createdAt: isoTimestamp,
230234 snapshot: {
231235 threadId: "thread-1",
232236 terminalId: DEFAULT_TERMINAL_ID,
......@@ -237,7 +241,7 @@ describe("TerminalEvent", () => {
237241 history: "",
238242 exitCode: null,
239243 exitSignal: null,
240 updatedAt: new Date().toISOString(),
244 updatedAt: isoTimestamp,
241245 },
242246 }),
243247 ).toBe(true);
packages/contracts/src/terminal.ts+5-2
......@@ -170,10 +170,13 @@ export class TerminalCwdError extends Schema.TaggedErrorClass<TerminalCwdError>(
170170 return `Terminal cwd does not exist: ${this.cwd}`;
171171 }
172172 const causeMessage =
173 this.cause && typeof this.cause === "object" && "message" in this.cause
173 this.cause !== undefined &&
174 this.cause !== null &&
175 typeof this.cause === "object" &&
176 "message" in this.cause
174177 ? this.cause.message
175178 : undefined;
176 return causeMessage
179 return typeof causeMessage === "string" && causeMessage.length > 0
177180 ? `Failed to access terminal cwd: ${this.cwd} (${causeMessage})`
178181 : `Failed to access terminal cwd: ${this.cwd}`;
179182 }
packages/contracts/tsconfig.json+1-14
......@@ -1,18 +1,5 @@
11{
22 "extends": "../../tsconfig.base.json",
3 "compilerOptions": {
4 "plugins": [
5 {
6 "name": "@effect/language-service",
7 "namespaceImportPackages": ["@effect/platform-node", "effect"],
8 "diagnosticSeverity": {
9 "importFromBarrel": "error",
10 "anyUnknownInErrorContext": "error",
11 "instanceOfSchema": "warning",
12 "deterministicKeys": "warning"
13 }
14 }
15 ]
16 },
3 "compilerOptions": {},
174 "include": ["src"]
185}
packages/effect-acp/test/examples/cursor-acp-client.example.ts+3-3
......@@ -48,7 +48,7 @@ const program = Effect.gen(function* () {
4848 version: "0.0.0",
4949 },
5050 });
51 yield* Console.log("initialized", JSON.stringify(initialized, null, 4));
51 yield* Console.log("initialized", initialized);
5252
5353 const session = yield* acp.agent.createSession({
5454 cwd: process.cwd(),
......@@ -61,7 +61,7 @@ const program = Effect.gen(function* () {
6161 value: "claude-opus-4-6",
6262 });
6363
64 yield* Console.log("config", JSON.stringify(config, null, 4));
64 yield* Console.log("config", config);
6565
6666 const result = yield* acp.agent.prompt({
6767 sessionId: session.sessionId,
......@@ -73,7 +73,7 @@ const program = Effect.gen(function* () {
7373 ],
7474 });
7575
76 yield* Console.log("prompt result", JSON.stringify(result));
76 yield* Console.log("prompt result", result);
7777 yield* acp.agent.cancel({ sessionId: session.sessionId });
7878 }).pipe(Effect.provide(acpLayer));
7979});
packages/effect-acp/tsconfig.json+1-14
......@@ -1,18 +1,5 @@
11{
22 "extends": "../../tsconfig.base.json",
3 "compilerOptions": {
4 "plugins": [
5 {
6 "name": "@effect/language-service",
7 "namespaceImportPackages": ["@effect/platform-node", "effect"],
8 "diagnosticSeverity": {
9 "importFromBarrel": "error",
10 "anyUnknownInErrorContext": "error",
11 "instanceOfSchema": "warning",
12 "deterministicKeys": "warning"
13 }
14 }
15 ]
16 },
3 "compilerOptions": {},
174 "include": ["src", "scripts", "test"]
185}
packages/effect-codex-app-server/src/client.ts+20-20
......@@ -17,6 +17,8 @@ import {
1717} from "./_internal/shared.ts";
1818import { makeChildStdio, makeTerminationError } from "./_internal/stdio.ts";
1919
20const DEFAULT_APP_SERVER_FORCE_KILL_AFTER = "2 seconds" as const;
21
2022export interface CodexAppServerClientOptions {
2123 readonly logIncoming?: boolean;
2224 readonly logOutgoing?: boolean;
......@@ -273,26 +275,24 @@ export const layerCommand = (
273275> =>
274276 Layer.effect(
275277 CodexAppServerClient,
276 Effect.acquireRelease(
277 Effect.gen(function* () {
278 const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
279 const command = ChildProcess.make(options.command, [...(options.args ?? [])], {
280 ...(options.cwd ? { cwd: options.cwd } : {}),
281 ...(options.env ? { env: { ...process.env, ...options.env } } : {}),
282 shell: process.platform === "win32",
283 });
284 return yield* spawner.spawn(command).pipe(
285 Effect.mapError(
286 (cause) =>
287 new CodexError.CodexAppServerSpawnError({
288 command: [options.command, ...(options.args ?? [])].join(" "),
289 cause,
290 }),
291 ),
292 );
293 }),
294 (handle) => handle.kill().pipe(Effect.orDie),
295 ).pipe(
278 Effect.gen(function* () {
279 const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
280 const command = ChildProcess.make(options.command, [...(options.args ?? [])], {
281 ...(options.cwd ? { cwd: options.cwd } : {}),
282 ...(options.env ? { env: { ...process.env, ...options.env } } : {}),
283 forceKillAfter: DEFAULT_APP_SERVER_FORCE_KILL_AFTER,
284 shell: process.platform === "win32",
285 });
286 return yield* spawner.spawn(command).pipe(
287 Effect.mapError(
288 (cause) =>
289 new CodexError.CodexAppServerSpawnError({
290 command: [options.command, ...(options.args ?? [])].join(" "),
291 cause,
292 }),
293 ),
294 );
295 }).pipe(
296296 Effect.flatMap((handle) =>
297297 make(makeChildStdio(handle), options, makeTerminationError(handle)),
298298 ),
packages/effect-codex-app-server/test/examples/codex-app-server-probe.ts+3-3
......@@ -45,17 +45,17 @@ const program = Effect.gen(function* () {
4545 optOutNotificationMethods: null,
4646 },
4747 });
48 yield* Console.log("initialize", JSON.stringify(initialized, null, 2));
48 yield* Console.log("initialize", initialized);
4949
5050 yield* client.notify("initialized", undefined);
5151
5252 const account = yield* client.request("account/read", {});
53 yield* Console.log("account/read", JSON.stringify(account, null, 2));
53 yield* Console.log("account/read", account);
5454
5555 const skills = yield* client.request("skills/list", {
5656 cwds: [process.cwd()],
5757 });
58 yield* Console.log("skills/list", JSON.stringify(skills, null, 2));
58 yield* Console.log("skills/list", skills);
5959 }).pipe(Effect.provide(codexLayer));
6060});
6161
packages/effect-codex-app-server/tsconfig.json+1-14
......@@ -1,18 +1,5 @@
11{
22 "extends": "../../tsconfig.base.json",
3 "compilerOptions": {
4 "plugins": [
5 {
6 "name": "@effect/language-service",
7 "namespaceImportPackages": ["@effect/platform-node", "effect"],
8 "diagnosticSeverity": {
9 "importFromBarrel": "error",
10 "anyUnknownInErrorContext": "error",
11 "instanceOfSchema": "warning",
12 "deterministicKeys": "warning"
13 }
14 }
15 ]
16 },
3 "compilerOptions": {},
174 "include": ["src", "scripts", "test"]
185}
packages/shared/package.json+1
......@@ -95,6 +95,7 @@
9595 },
9696 "devDependencies": {
9797 "@effect/language-service": "catalog:",
98 "@effect/platform-node": "catalog:",
9899 "@effect/vitest": "catalog:",
99100 "@types/node": "catalog:",
100101 "typescript": "catalog:",
packages/shared/src/Net.test.ts+12-10
......@@ -3,7 +3,7 @@ import * as NodeNet from "node:net";
33import { assert, describe, it } from "@effect/vitest";
44import * as Effect from "effect/Effect";
55
6import * as Net from "./Net.ts";
6import * as NetService from "./Net.ts";
77
88const closeServer = (server: NodeNet.Server) =>
99 Effect.sync(() => {
......@@ -19,19 +19,21 @@ const getPort = (server: NodeNet.Server): number => {
1919 return typeof address === "object" && address !== null ? address.port : 0;
2020};
2121
22const openServer = (host?: string): Effect.Effect<NodeNet.Server, Net.NetError> =>
23 Effect.callback<NodeNet.Server, Net.NetError>((resume) => {
22const openServer = (host?: string): Effect.Effect<NodeNet.Server, NetService.NetError> =>
23 Effect.callback<NodeNet.Server, NetService.NetError>((resume) => {
2424 const server = NodeNet.createServer();
2525 let settled = false;
2626
27 const settle = (effect: Effect.Effect<NodeNet.Server, Net.NetError>) => {
27 const settle = (effect: Effect.Effect<NodeNet.Server, NetService.NetError>) => {
2828 if (settled) return;
2929 settled = true;
3030 resume(effect);
3131 };
3232
3333 server.once("error", (cause) => {
34 settle(Effect.fail(new Net.NetError({ message: "Failed to open test server", cause })));
34 settle(
35 Effect.fail(new NetService.NetError({ message: "Failed to open test server", cause })),
36 );
3537 });
3638
3739 if (host) {
......@@ -43,11 +45,11 @@ const openServer = (host?: string): Effect.Effect<NodeNet.Server, Net.NetError>
4345 return closeServer(server);
4446 });
4547
46it.layer(Net.NetService.layer)("NetService", (it) => {
48it.layer(NetService.layer)("NetService", (it) => {
4749 describe("Net helpers", () => {
4850 it.effect("reserveLoopbackPort returns a positive loopback port", () =>
4951 Effect.gen(function* () {
50 const net = yield* Net.NetService;
52 const net = yield* NetService.NetService;
5153 const port = yield* net.reserveLoopbackPort();
5254
5355 assert.ok(port > 0);
......@@ -59,7 +61,7 @@ it.layer(Net.NetService.layer)("NetService", (it) => {
5961 openServer("127.0.0.1"),
6062 (server) =>
6163 Effect.gen(function* () {
62 const net = yield* Net.NetService;
64 const net = yield* NetService.NetService;
6365 const port = getPort(server);
6466
6567 const available = yield* net.isPortAvailableOnLoopback(port);
......@@ -71,7 +73,7 @@ it.layer(Net.NetService.layer)("NetService", (it) => {
7173
7274 it.effect("findAvailablePort returns preferred when it is free", () =>
7375 Effect.gen(function* () {
74 const net = yield* Net.NetService;
76 const net = yield* NetService.NetService;
7577 const preferred = yield* net.reserveLoopbackPort();
7678
7779 const resolved = yield* net.findAvailablePort(preferred);
......@@ -84,7 +86,7 @@ it.layer(Net.NetService.layer)("NetService", (it) => {
8486 openServer(),
8587 (server) =>
8688 Effect.gen(function* () {
87 const net = yield* Net.NetService;
89 const net = yield* NetService.NetService;
8890 const preferred = getPort(server);
8991
9092 const resolved = yield* net.findAvailablePort(preferred);
packages/shared/src/Net.ts+16-19
......@@ -4,22 +4,21 @@ import * as Data from "effect/Data";
44import * as Effect from "effect/Effect";
55import * as Layer from "effect/Layer";
66import * as Context from "effect/Context";
7import * as Predicate from "effect/Predicate";
78
89export class NetError extends Data.TaggedError("NetError")<{
910 readonly message: string;
1011 readonly cause?: unknown;
1112}> {}
1213
13function isErrnoExceptionWithCode(cause: unknown): cause is {
14const isErrnoExceptionWithCode = (
15 cause: unknown,
16): cause is {
1417 readonly code: string;
15} {
16 return (
17 typeof cause === "object" &&
18 cause !== null &&
19 "code" in cause &&
20 typeof (cause as { readonly code: unknown }).code === "string"
21 );
22}
18} =>
19 Predicate.isObject(cause) &&
20 Predicate.hasProperty(cause, "code") &&
21 Predicate.isString(cause.code);
2322
2423const closeServer = (server: NodeNet.Server) => {
2524 try {
......@@ -85,6 +84,13 @@ export interface NetServiceShape {
8584 readonly findAvailablePort: (preferred: number) => Effect.Effect<number, NetError>;
8685}
8786
87/**
88 * NetService - Service tag for startup networking helpers.
89 */
90export class NetService extends Context.Service<NetService, NetServiceShape>()(
91 "@t3tools/shared/Net/NetService",
92) {}
93
8894export const make = () => {
8995 /**
9096 * Returns true when a TCP server can bind to {host, port}.
......@@ -175,13 +181,4 @@ export const make = () => {
175181 } satisfies NetServiceShape;
176182};
177183
178/**
179 * NetService - Service tag for startup networking helpers.
180 */
181export class NetService extends Context.Service<NetService, NetServiceShape>()(
182 "@t3tools/shared/Net/NetService",
183) {
184 static readonly layer = Layer.sync(NetService, make);
185}
186
187export const layer = NetService.layer;
184export const layer = Layer.sync(NetService, make);
packages/shared/src/logging.ts+1
......@@ -1,3 +1,4 @@
1// @effect-diagnostics nodeBuiltinImport:off
12import fs from "node:fs";
23import path from "node:path";
34
packages/shared/src/observability.test.ts+108-114
......@@ -1,11 +1,10 @@
1import fs from "node:fs";
2import os from "node:os";
3import path from "node:path";
4
51import { assert, describe, it } from "@effect/vitest";
2import * as NodeServices from "@effect/platform-node/NodeServices";
63import * as Effect from "effect/Effect";
4import * as FileSystem from "effect/FileSystem";
75import * as Layer from "effect/Layer";
86import * as Logger from "effect/Logger";
7import * as Path from "effect/Path";
98import * as References from "effect/References";
109import * as Schema from "effect/Schema";
1110import * as Tracer from "effect/Tracer";
......@@ -57,13 +56,14 @@ const makeRecord = (name: string, suffix = ""): TraceRecord => ({
5756 },
5857});
5958
60const readTraceRecords = (tracePath: string) =>
61 fs
62 .readFileSync(tracePath, "utf8")
59const readTraceRecords = Effect.fn("readTraceRecords")(function* (tracePath: string) {
60 const fileSystem = yield* FileSystem.FileSystem;
61 return (yield* fileSystem.readFileString(tracePath))
6362 .trim()
6463 .split("\n")
6564 .filter((line) => line.length > 0)
6665 .map((line) => decodeTraceRecordLine(line));
66});
6767
6868const makeTestLayer = (tracePath: string) =>
6969 Layer.mergeAll(
......@@ -106,9 +106,11 @@ describe("observability", () => {
106106 });
107107
108108 it("normalizes invalid dates without throwing", () => {
109 // @effect-diagnostics-next-line globalDate:off
110 const invalidDate = new Date("not-a-real-date");
109111 assert.deepStrictEqual(
110112 compactTraceAttributes({
111 invalidDate: new Date("not-a-real-date"),
113 invalidDate,
112114 }),
113115 {
114116 invalidDate: "Invalid Date",
......@@ -119,122 +121,116 @@ describe("observability", () => {
119121 it.effect("flushes buffered trace records on close", () =>
120122 Effect.scoped(
121123 Effect.gen(function* () {
122 const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "t3-trace-sink-"));
124 const fileSystem = yield* FileSystem.FileSystem;
125 const path = yield* Path.Path;
126 const tempDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-trace-sink-" });
123127 const tracePath = path.join(tempDir, "shared.trace.ndjson");
124128
125 try {
126 const sink = yield* makeTraceSink({
127 filePath: tracePath,
128 maxBytes: 1024,
129 maxFiles: 2,
130 batchWindowMs: 10_000,
131 });
132
133 sink.push(makeRecord("alpha"));
134 sink.push(makeRecord("beta"));
135 yield* sink.close();
136
137 const lines = readTraceRecords(tracePath);
138
139 assert.equal(lines.length, 2);
140 assert.equal(lines[0]?.name, "alpha");
141 assert.equal(lines[1]?.name, "beta");
142 } finally {
143 fs.rmSync(tempDir, { recursive: true, force: true });
144 }
145 }),
129 const sink = yield* makeTraceSink({
130 filePath: tracePath,
131 maxBytes: 1024,
132 maxFiles: 2,
133 batchWindowMs: 10_000,
134 });
135
136 sink.push(makeRecord("alpha"));
137 sink.push(makeRecord("beta"));
138 yield* sink.close();
139
140 const lines = yield* readTraceRecords(tracePath);
141
142 assert.equal(lines.length, 2);
143 assert.equal(lines[0]?.name, "alpha");
144 assert.equal(lines[1]?.name, "beta");
145 }).pipe(Effect.provide(NodeServices.layer)),
146146 ),
147147 );
148148
149149 it.effect("rotates the trace file when the configured max size is exceeded", () =>
150150 Effect.scoped(
151151 Effect.gen(function* () {
152 const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "t3-trace-sink-"));
152 const fileSystem = yield* FileSystem.FileSystem;
153 const path = yield* Path.Path;
154 const tempDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-trace-sink-" });
153155 const tracePath = path.join(tempDir, "shared.trace.ndjson");
154156
155 try {
156 const sink = yield* makeTraceSink({
157 filePath: tracePath,
158 maxBytes: 180,
159 maxFiles: 2,
160 batchWindowMs: 10_000,
161 });
162
163 for (let index = 0; index < 8; index += 1) {
164 sink.push(makeRecord("rotate", `${index}-${"x".repeat(48)}`));
165 yield* sink.flush;
166 }
167 yield* sink.close();
168
169 const matchingFiles = fs
170 .readdirSync(tempDir)
171 .filter(
172 (entry) =>
173 entry === "shared.trace.ndjson" || entry.startsWith("shared.trace.ndjson."),
174 )
175 .toSorted();
176
177 assert.equal(
178 matchingFiles.some((entry) => entry === "shared.trace.ndjson.1"),
179 true,
180 );
181 assert.equal(
182 matchingFiles.some((entry) => entry === "shared.trace.ndjson.3"),
183 false,
184 );
185 } finally {
186 fs.rmSync(tempDir, { recursive: true, force: true });
157 const sink = yield* makeTraceSink({
158 filePath: tracePath,
159 maxBytes: 180,
160 maxFiles: 2,
161 batchWindowMs: 10_000,
162 });
163
164 for (let index = 0; index < 8; index += 1) {
165 sink.push(makeRecord("rotate", `${index}-${"x".repeat(48)}`));
166 yield* sink.flush;
187167 }
188 }),
168 yield* sink.close();
169
170 const matchingFiles = (yield* fileSystem.readDirectory(tempDir))
171 .filter(
172 (entry) => entry === "shared.trace.ndjson" || entry.startsWith("shared.trace.ndjson."),
173 )
174 .toSorted();
175
176 assert.equal(
177 matchingFiles.some((entry) => entry === "shared.trace.ndjson.1"),
178 true,
179 );
180 assert.equal(
181 matchingFiles.some((entry) => entry === "shared.trace.ndjson.3"),
182 false,
183 );
184 }).pipe(Effect.provide(NodeServices.layer)),
189185 ),
190186 );
191187
192188 it.effect("drops only the invalid trace record when serialization fails", () =>
193189 Effect.scoped(
194190 Effect.gen(function* () {
195 const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "t3-trace-sink-"));
191 const fileSystem = yield* FileSystem.FileSystem;
192 const path = yield* Path.Path;
193 const tempDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-trace-sink-" });
196194 const tracePath = path.join(tempDir, "shared.trace.ndjson");
197195
198 try {
199 const sink = yield* makeTraceSink({
200 filePath: tracePath,
201 maxBytes: 1024,
202 maxFiles: 2,
203 batchWindowMs: 10_000,
204 });
205
206 const circular: Array<unknown> = [];
207 circular.push(circular);
208
209 sink.push(makeRecord("alpha"));
210 sink.push({
211 ...makeRecord("invalid"),
212 attributes: {
213 circular,
214 },
215 } as TraceRecord);
216 sink.push(makeRecord("beta"));
217 yield* sink.close();
218
219 const lines = readTraceRecords(tracePath);
220
221 assert.deepStrictEqual(
222 lines.map((line) => line.name),
223 ["alpha", "beta"],
224 );
225 } finally {
226 fs.rmSync(tempDir, { recursive: true, force: true });
227 }
228 }),
196 const sink = yield* makeTraceSink({
197 filePath: tracePath,
198 maxBytes: 1024,
199 maxFiles: 2,
200 batchWindowMs: 10_000,
201 });
202
203 const circular: Array<unknown> = [];
204 circular.push(circular);
205
206 sink.push(makeRecord("alpha"));
207 sink.push({
208 ...makeRecord("invalid"),
209 attributes: {
210 circular,
211 },
212 } as TraceRecord);
213 sink.push(makeRecord("beta"));
214 yield* sink.close();
215
216 const lines = yield* readTraceRecords(tracePath);
217
218 assert.deepStrictEqual(
219 lines.map((line) => line.name),
220 ["alpha", "beta"],
221 );
222 }).pipe(Effect.provide(NodeServices.layer)),
229223 ),
230224 );
231225
232226 it.effect("writes nested spans to disk and captures log messages as span events", () =>
233 Effect.gen(function* () {
234 const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "t3-local-tracer-"));
235 const tracePath = path.join(tempDir, "shared.trace.ndjson");
227 Effect.scoped(
228 Effect.gen(function* () {
229 const fileSystem = yield* FileSystem.FileSystem;
230 const path = yield* Path.Path;
231 const tempDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-local-tracer-" });
232 const tracePath = path.join(tempDir, "shared.trace.ndjson");
236233
237 try {
238234 yield* Effect.scoped(
239235 Effect.gen(function* () {
240236 const program = Effect.gen(function* () {
......@@ -254,7 +250,7 @@ describe("observability", () => {
254250 }),
255251 );
256252
257 const records = readTraceRecords(tracePath);
253 const records = yield* readTraceRecords(tracePath);
258254 assert.equal(records.length, 2);
259255
260256 const parent = records.find((record) => record.name === "parent-span");
......@@ -281,18 +277,18 @@ describe("observability", () => {
281277 child.events.some((event) => event.attributes["effect.logLevel"] === "INFO"),
282278 true,
283279 );
284 } finally {
285 fs.rmSync(tempDir, { recursive: true, force: true });
286 }
287 }),
280 }).pipe(Effect.provide(NodeServices.layer)),
281 ),
288282 );
289283
290284 it.effect("serializes interrupted spans with an interrupted exit status", () =>
291 Effect.gen(function* () {
292 const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "t3-local-tracer-"));
293 const tracePath = path.join(tempDir, "shared.trace.ndjson");
285 Effect.scoped(
286 Effect.gen(function* () {
287 const fileSystem = yield* FileSystem.FileSystem;
288 const path = yield* Path.Path;
289 const tempDir = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-local-tracer-" });
290 const tracePath = path.join(tempDir, "shared.trace.ndjson");
294291
295 try {
296292 yield* Effect.scoped(
297293 Effect.exit(
298294 Effect.interrupt.pipe(
......@@ -302,13 +298,11 @@ describe("observability", () => {
302298 ),
303299 );
304300
305 const records = readTraceRecords(tracePath);
301 const records = yield* readTraceRecords(tracePath);
306302 assert.equal(records.length, 1);
307303 assert.equal(records[0]?.name, "interrupt-span");
308304 assert.equal(records[0]?.exit?._tag, "Interrupted");
309 } finally {
310 fs.rmSync(tempDir, { recursive: true, force: true });
311 }
312 }),
305 }).pipe(Effect.provide(NodeServices.layer)),
306 ),
313307 );
314308});
packages/shared/src/schemaJson.ts+18
......@@ -84,6 +84,10 @@ export const fromLenientJsonString = new SchemaTransformation.Transformation(
8484 SchemaGetter.stringifyJson(),
8585);
8686
87export const prettyJsonString = SchemaGetter.parseJson<string>().compose(
88 SchemaGetter.stringifyJson({ space: 2 }),
89);
90
8791/**
8892 * Build a schema that decodes a lenient JSON string into `A`.
8993 *
......@@ -92,3 +96,17 @@ export const fromLenientJsonString = new SchemaTransformation.Transformation(
9296 */
9397export const fromLenientJson = <S extends Schema.Top>(schema: S) =>
9498 Schema.String.pipe(Schema.decodeTo(schema, fromLenientJsonString));
99
100/**
101 * Build a JSON string schema that encodes with stable 2-space formatting.
102 *
103 * Decode behavior matches `Schema.fromJsonString(schema)`. Encode behavior
104 * keeps the transformation schema-based while preserving human-readable JSON.
105 */
106export const fromJsonStringPretty = <S extends Schema.Top>(schema: S) =>
107 Schema.fromJsonString(schema).pipe(
108 Schema.encode({
109 decode: prettyJsonString,
110 encode: prettyJsonString,
111 }),
112 );
packages/shared/src/shell.ts+1
......@@ -1,3 +1,4 @@
1// @effect-diagnostics nodeBuiltinImport:off
12import * as NodeOS from "node:os";
23import { execFileSync } from "node:child_process";
34import { accessSync, constants, statSync } from "node:fs";
packages/shared/tsconfig.json+1-14
......@@ -1,18 +1,5 @@
11{
22 "extends": "../../tsconfig.base.json",
3 "compilerOptions": {
4 "plugins": [
5 {
6 "name": "@effect/language-service",
7 "namespaceImportPackages": ["@effect/platform-node", "effect"],
8 "diagnosticSeverity": {
9 "importFromBarrel": "error",
10 "anyUnknownInErrorContext": "error",
11 "instanceOfSchema": "warning",
12 "deterministicKeys": "warning"
13 }
14 }
15 ]
16 },
3 "compilerOptions": {},
174 "include": ["src"]
185}
packages/ssh/src/command.test.ts+1-1
......@@ -7,7 +7,7 @@ import * as Layer from "effect/Layer";
77import * as Result from "effect/Result";
88import * as Sink from "effect/Sink";
99import * as Stream from "effect/Stream";
10import { TestClock } from "effect/testing";
10import * as TestClock from "effect/testing/TestClock";
1111import { ChildProcessSpawner } from "effect/unstable/process";
1212
1313import {
packages/ssh/src/tunnel.test.ts+4-4
......@@ -1,6 +1,6 @@
11import { assert, describe, it } from "@effect/vitest";
22import * as NodeServices from "@effect/platform-node/NodeServices";
3import { NetService } from "@t3tools/shared/Net";
3import * as NetService from "@t3tools/shared/Net";
44import * as Duration from "effect/Duration";
55import * as Effect from "effect/Effect";
66import * as Fiber from "effect/Fiber";
......@@ -8,7 +8,7 @@ import * as Layer from "effect/Layer";
88import * as Result from "effect/Result";
99import * as Sink from "effect/Sink";
1010import * as Stream from "effect/Stream";
11import { TestClock } from "effect/testing";
11import * as TestClock from "effect/testing/TestClock";
1212import { HttpClient, HttpClientResponse } from "effect/unstable/http";
1313import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
1414
......@@ -74,7 +74,7 @@ const testHttpClient = HttpClient.make((request) =>
7474
7575const hangingHttpClient = HttpClient.make(() => Effect.never);
7676
77const testNetService = NetService.of({
77const testNetService = NetService.NetService.of({
7878 canListenOnHost: () => Effect.succeed(true),
7979 isPortAvailableOnLoopback: () => Effect.succeed(true),
8080 reserveLoopbackPort: () => Effect.succeed(41_773),
......@@ -270,7 +270,7 @@ describe("ssh tunnel scripts", () => {
270270 NodeServices.layer,
271271 Layer.succeed(ChildProcessSpawner.ChildProcessSpawner, spawner),
272272 Layer.succeed(HttpClient.HttpClient, testHttpClient),
273 Layer.succeed(NetService, testNetService),
273 Layer.succeed(NetService.NetService, testNetService),
274274 SshPasswordPrompt.disabledLayer,
275275 SshEnvironmentManager.layer(),
276276 );
packages/ssh/src/tunnel.ts+6-6
......@@ -2,7 +2,7 @@ import type {
22 DesktopSshEnvironmentBootstrap,
33 DesktopSshEnvironmentTarget,
44} from "@t3tools/contracts";
5import { type NetError, NetService } from "@t3tools/shared/Net";
5import * as NetService from "@t3tools/shared/Net";
66import { fromLenientJson } from "@t3tools/shared/schemaJson";
77import * as Context from "effect/Context";
88import * as Deferred from "effect/Deferred";
......@@ -82,7 +82,7 @@ type SshEnvironmentEffectContext =
8282 | FileSystem.FileSystem
8383 | Path.Path
8484 | HttpClient.HttpClient
85 | NetService
85 | NetService.NetService
8686 | SshPasswordPrompt;
8787
8888type SshEnvironmentEffectError =
......@@ -92,7 +92,7 @@ type SshEnvironmentEffectError =
9292 | SshPairingError
9393 | SshReadinessError
9494 | SshPasswordPromptError
95 | NetError;
95 | NetService.NetError;
9696
9797function makeSshTunnelCancelledError(target: DesktopSshEnvironmentTarget): SshCommandError {
9898 return new SshCommandError({
......@@ -903,7 +903,7 @@ export const fetchLoopbackSshJson = Effect.fn("ssh/tunnel.fetchLoopbackSshJson")
903903});
904904
905905const reserveLocalTunnelPort = Effect.fn("ssh/tunnel.reserveLocalTunnelPort")(function* () {
906 const net = yield* NetService;
906 const net = yield* NetService.NetService;
907907 return yield* net.reserveLoopbackPort();
908908});
909909
......@@ -923,7 +923,7 @@ const startSshTunnel = Effect.fn("ssh/tunnel.startSshTunnel")(function* (input:
923923 | FileSystem.FileSystem
924924 | Path.Path
925925 | HttpClient.HttpClient
926 | NetService
926 | NetService.NetService
927927 | Scope.Scope
928928> {
929929 const hostSpec = yield* buildSshHostSpecEffect(input.resolvedTarget);
......@@ -1076,7 +1076,7 @@ const startSshTunnel = Effect.fn("ssh/tunnel.startSshTunnel")(function* (input:
10761076 ),
10771077 Effect.tapError((cause) =>
10781078 Effect.gen(function* () {
1079 const net = yield* NetService;
1079 const net = yield* NetService.NetService;
10801080 const processRunningExit = yield* Effect.exit(child.isRunning);
10811081 const localPortAvailableExit = yield* Effect.exit(
10821082 net.canListenOnHost(input.localPort, "127.0.0.1"),
packages/ssh/tsconfig.json+1-14
......@@ -1,18 +1,5 @@
11{
22 "extends": "../../tsconfig.base.json",
3 "compilerOptions": {
4 "plugins": [
5 {
6 "name": "@effect/language-service",
7 "namespaceImportPackages": ["@effect/platform-node", "effect"],
8 "diagnosticSeverity": {
9 "importFromBarrel": "error",
10 "anyUnknownInErrorContext": "error",
11 "instanceOfSchema": "warning",
12 "deterministicKeys": "warning"
13 }
14 }
15 ]
16 },
3 "compilerOptions": {},
174 "include": ["src"]
185}
packages/tailscale/tsconfig.json+1-13
......@@ -1,17 +1,5 @@
11{
22 "extends": "../../tsconfig.base.json",
3 "compilerOptions": {
4 "plugins": [
5 {
6 "name": "@effect/language-service",
7 "diagnosticSeverity": {
8 "importFromBarrel": "error",
9 "anyUnknownInErrorContext": "warning",
10 "instanceOfSchema": "warning",
11 "deterministicKeys": "warning"
12 }
13 }
14 ]
15 },
3 "compilerOptions": {},
164 "include": ["src"]
175}
scripts/apply-web-brand-assets.ts+4-1
......@@ -2,7 +2,10 @@
22
33import * as NodeRuntime from "@effect/platform-node/NodeRuntime";
44import * as NodeServices from "@effect/platform-node/NodeServices";
5import { Effect, FileSystem, Option, Path } from "effect";
5import * as Effect from "effect/Effect";
6import * as FileSystem from "effect/FileSystem";
7import * as Option from "effect/Option";
8import * as Path from "effect/Path";
69import { Argument, Command } from "effect/unstable/cli";
710import { resolveWebIconOverrides, type WebAssetBrand } from "./lib/brand-assets.ts";
811
scripts/build-desktop-artifact.test.ts+3-1
......@@ -1,6 +1,8 @@
11import * as NodeServices from "@effect/platform-node/NodeServices";
22import { assert, it } from "@effect/vitest";
3import { ConfigProvider, Effect, Option } from "effect";
3import * as ConfigProvider from "effect/ConfigProvider";
4import * as Effect from "effect/Effect";
5import * as Option from "effect/Option";
46
57import {
68 resolveBuildOptions,
scripts/build-desktop-artifact.ts+10-12
......@@ -10,18 +10,16 @@ import { resolveCatalogDependencies } from "./lib/resolve-catalog.ts";
1010
1111import * as NodeRuntime from "@effect/platform-node/NodeRuntime";
1212import * as NodeServices from "@effect/platform-node/NodeServices";
13import {
14 Config,
15 Data,
16 Effect,
17 FileSystem,
18 Layer,
19 Logger,
20 Option,
21 Path,
22 Schema,
23 Stream,
24} from "effect";
13import * as Config from "effect/Config";
14import * as Data from "effect/Data";
15import * as Effect from "effect/Effect";
16import * as FileSystem from "effect/FileSystem";
17import * as Layer from "effect/Layer";
18import * as Logger from "effect/Logger";
19import * as Option from "effect/Option";
20import * as Path from "effect/Path";
21import * as Schema from "effect/Schema";
22import * as Stream from "effect/Stream";
2523import { Command, Flag } from "effect/unstable/cli";
2624import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
2725
scripts/dev-runner.test.ts+2-1
......@@ -1,7 +1,8 @@
11import * as NodeServices from "@effect/platform-node/NodeServices";
22import * as NodeOS from "node:os";
33import { assert, describe, it } from "@effect/vitest";
4import { Effect, Path } from "effect";
4import * as Effect from "effect/Effect";
5import * as Path from "effect/Path";
56
67import {
78 checkPortAvailabilityOnHosts,
scripts/dev-runner.ts+16-8
......@@ -4,8 +4,16 @@ import * as NodeOS from "node:os";
44
55import * as NodeRuntime from "@effect/platform-node/NodeRuntime";
66import * as NodeServices from "@effect/platform-node/NodeServices";
7import { NetService } from "@t3tools/shared/Net";
8import { Config, Data, Effect, Hash, Layer, Logger, Option, Path, Schema } from "effect";
7import * as NetService from "@t3tools/shared/Net";
8import * as Config from "effect/Config";
9import * as Data from "effect/Data";
10import * as Effect from "effect/Effect";
11import * as Hash from "effect/Hash";
12import * as Layer from "effect/Layer";
13import * as Logger from "effect/Logger";
14import * as Option from "effect/Option";
15import * as Path from "effect/Path";
16import * as Schema from "effect/Schema";
917import { Argument, Command, Flag } from "effect/unstable/cli";
1018import { ChildProcess } from "effect/unstable/process";
1119
......@@ -238,22 +246,22 @@ export function checkPortAvailabilityOnHosts<R>(
238246 });
239247}
240248
241const defaultCheckPortAvailability: PortAvailabilityCheck<NetService> = (port) =>
249const defaultCheckPortAvailability: PortAvailabilityCheck<NetService.NetService> = (port) =>
242250 Effect.gen(function* () {
243 const net = yield* NetService;
251 const net = yield* NetService.NetService;
244252 return yield* checkPortAvailabilityOnHosts(port, DEV_PORT_PROBE_HOSTS, (candidatePort, host) =>
245253 net.canListenOnHost(candidatePort, host),
246254 );
247255 });
248256
249interface FindFirstAvailableOffsetInput<R = NetService> {
257interface FindFirstAvailableOffsetInput<R = NetService.NetService> {
250258 readonly startOffset: number;
251259 readonly requireServerPort: boolean;
252260 readonly requireWebPort: boolean;
253261 readonly checkPortAvailability?: PortAvailabilityCheck<R>;
254262}
255263
256export function findFirstAvailableOffset<R = NetService>({
264export function findFirstAvailableOffset<R = NetService.NetService>({
257265 startOffset,
258266 requireServerPort,
259267 requireWebPort,
......@@ -300,7 +308,7 @@ export function findFirstAvailableOffset<R = NetService>({
300308 });
301309}
302310
303interface ResolveModePortOffsetsInput<R = NetService> {
311interface ResolveModePortOffsetsInput<R = NetService.NetService> {
304312 readonly mode: DevMode;
305313 readonly startOffset: number;
306314 readonly hasExplicitServerPort: boolean;
......@@ -308,7 +316,7 @@ interface ResolveModePortOffsetsInput<R = NetService> {
308316 readonly checkPortAvailability?: PortAvailabilityCheck<R>;
309317}
310318
311export function resolveModePortOffsets<R = NetService>({
319export function resolveModePortOffsets<R = NetService.NetService>({
312320 mode,
313321 startOffset,
314322 hasExplicitServerPort,
scripts/merge-update-manifests.test.ts+3-1
......@@ -1,6 +1,8 @@
11import * as NodeServices from "@effect/platform-node/NodeServices";
22import { assert, describe, it } from "@effect/vitest";
3import { Effect, FileSystem, Path } from "effect";
3import * as Effect from "effect/Effect";
4import * as FileSystem from "effect/FileSystem";
5import * as Path from "effect/Path";
46import { Command, CliError } from "effect/unstable/cli";
57
68import {
scripts/merge-update-manifests.ts+5-1
......@@ -2,7 +2,11 @@
22
33import * as NodeRuntime from "@effect/platform-node/NodeRuntime";
44import * as NodeServices from "@effect/platform-node/NodeServices";
5import { Effect, FileSystem, Option, Path, Schema } from "effect";
5import * as Effect from "effect/Effect";
6import * as FileSystem from "effect/FileSystem";
7import * as Option from "effect/Option";
8import * as Path from "effect/Path";
9import * as Schema from "effect/Schema";
610import { Argument, Command, Flag } from "effect/unstable/cli";
711
812import {
scripts/mock-update-server.test.ts+5-2
......@@ -1,7 +1,10 @@
11import * as NodeServices from "@effect/platform-node/NodeServices";
2import { NodeHttpServer } from "@effect/platform-node";
2import * as NodeHttpServer from "@effect/platform-node/NodeHttpServer";
33import { assert, it } from "@effect/vitest";
4import { Effect, FileSystem, Layer, Path } from "effect";
4import * as Effect from "effect/Effect";
5import * as FileSystem from "effect/FileSystem";
6import * as Layer from "effect/Layer";
7import * as Path from "effect/Path";
58import { HttpClient, HttpRouter } from "effect/unstable/http";
69
710import { makeMockUpdateRouteLayer } from "./mock-update-server.ts";
scripts/mock-update-server.ts+8-2
......@@ -1,8 +1,14 @@
1// @effect-diagnostics-next-line nodeBuiltinImport:off - NodeHttpServer.layer takes `NodeHttp.createServer` as arg
12import * as NodeHttp from "node:http";
23
3import { NodeHttpServer, NodeRuntime } from "@effect/platform-node";
4import * as NodeHttpServer from "@effect/platform-node/NodeHttpServer";
5import * as NodeRuntime from "@effect/platform-node/NodeRuntime";
46import * as NodeServices from "@effect/platform-node/NodeServices";
5import { Config, Effect, FileSystem, Layer, Path } from "effect";
7import * as Config from "effect/Config";
8import * as Effect from "effect/Effect";
9import * as FileSystem from "effect/FileSystem";
10import * as Layer from "effect/Layer";
11import * as Path from "effect/Path";
612import { HttpRouter, HttpServerRequest, HttpServerResponse } from "effect/unstable/http";
713
814interface MockUpdateServerConfig {
scripts/notify-discord-release.ts+7-1
......@@ -2,7 +2,13 @@
22
33import * as NodeRuntime from "@effect/platform-node/NodeRuntime";
44import * as NodeServices from "@effect/platform-node/NodeServices";
5import { Config, Data, DateTime, Effect, Layer, Logger, Schema } from "effect";
5import * as Config from "effect/Config";
6import * as Data from "effect/Data";
7import * as DateTime from "effect/DateTime";
8import * as Effect from "effect/Effect";
9import * as Layer from "effect/Layer";
10import * as Logger from "effect/Logger";
11import * as Schema from "effect/Schema";
612import { Argument, Command, Flag } from "effect/unstable/cli";
713import {
814 FetchHttpClient,
scripts/release-smoke.ts+4-1
......@@ -1,3 +1,4 @@
1// @effect-diagnostics nodeBuiltinImport:off
12import { execFileSync } from "node:child_process";
23import {
34 cpSync,
......@@ -11,6 +12,8 @@ import {
1112import { tmpdir } from "node:os";
1213import { dirname, join, resolve } from "node:path";
1314import { fileURLToPath } from "node:url";
15import * as Console from "effect/Console";
16import * as Effect from "effect/Effect";
1417
1518const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
1619
......@@ -389,7 +392,7 @@ try {
389392 "Windows release smoke unexpectedly removed the x64 builder debug fixture.",
390393 );
391394
392 console.log("Release smoke checks passed.");
395 Effect.runSync(Console.log("Release smoke checks passed."));
393396} finally {
394397 rmSync(tempRoot, { recursive: true, force: true });
395398}
scripts/resolve-nightly-release.ts+8-2
......@@ -2,7 +2,13 @@
22
33import * as NodeRuntime from "@effect/platform-node/NodeRuntime";
44import * as NodeServices from "@effect/platform-node/NodeServices";
5import { Config, Effect, FileSystem, Option, Path, Schema } from "effect";
5import * as Console from "effect/Console";
6import * as Config from "effect/Config";
7import * as Effect from "effect/Effect";
8import * as FileSystem from "effect/FileSystem";
9import * as Option from "effect/Option";
10import * as Path from "effect/Path";
11import * as Schema from "effect/Schema";
612import { Command, Flag } from "effect/unstable/cli";
713
814interface NightlyReleaseMetadata {
......@@ -93,7 +99,7 @@ const writeOutput = Effect.fn("writeOutput")(function* (
9399 yield* fs.writeFileString(githubOutputPath, serialized, { flag: "a" });
94100 } else {
95101 for (const [key, value] of entries) {
96 console.log(`${key}=${value}`);
102 yield* Console.log(`${key}=${value}`);
97103 }
98104 }
99105});
scripts/resolve-previous-release-tag.ts+7-1
......@@ -2,7 +2,13 @@
22
33import * as NodeRuntime from "@effect/platform-node/NodeRuntime";
44import * as NodeServices from "@effect/platform-node/NodeServices";
5import { Array, Config, Effect, FileSystem, Schema, Stream, String } from "effect";
5import * as Array from "effect/Array";
6import * as Config from "effect/Config";
7import * as Effect from "effect/Effect";
8import * as FileSystem from "effect/FileSystem";
9import * as Schema from "effect/Schema";
10import * as Stream from "effect/Stream";
11import * as String from "effect/String";
612import { Command, Flag } from "effect/unstable/cli";
713import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
814
scripts/tsconfig.json+1-6
......@@ -3,12 +3,7 @@
33 "compilerOptions": {
44 "composite": true,
55 "types": ["node"],
6 "lib": ["ESNext", "esnext.disposable"],
7 "plugins": [
8 {
9 "name": "@effect/language-service"
10 }
11 ]
6 "lib": ["ESNext", "esnext.disposable"]
127 },
138 "include": ["**/*.ts"]
149}
scripts/update-release-package-versions.test.ts+11-13
......@@ -1,8 +1,14 @@
11import * as NodeServices from "@effect/platform-node/NodeServices";
22import { assert, it } from "@effect/vitest";
3import { ConfigProvider, Effect, FileSystem, Layer, Path, Schema, SchemaGetter } from "effect";
3import * as ConfigProvider from "effect/ConfigProvider";
4import * as Effect from "effect/Effect";
5import * as FileSystem from "effect/FileSystem";
6import * as Layer from "effect/Layer";
7import * as Path from "effect/Path";
8import * as Schema from "effect/Schema";
49import { Command, CliError } from "effect/unstable/cli";
510import * as TestConsole from "effect/testing/TestConsole";
11import { fromJsonStringPretty } from "@t3tools/shared/schemaJson";
612
713import {
814 releasePackageFiles,
......@@ -13,17 +19,9 @@ import {
1319const ScriptTestLayer = Layer.mergeAll(NodeServices.layer, TestConsole.layer);
1420const runCli = Command.runWith(updateReleasePackageVersionsCommand, { version: "0.0.0" });
1521const PackageJsonSchema = Schema.Record(Schema.String, Schema.Unknown);
16const PrettyJsonString = SchemaGetter.parseJson<string>().compose(
17 SchemaGetter.stringifyJson({ space: 2 }),
18);
19const PackageJsonPrettyJson = Schema.fromJsonString(PackageJsonSchema).pipe(
20 Schema.encode({
21 decode: PrettyJsonString,
22 encode: PrettyJsonString,
23 }),
24);
25const decodePackageJson = Schema.decodeUnknownEffect(PackageJsonPrettyJson);
26const encodePackageJson = Schema.encodeSync(PackageJsonPrettyJson);
22const PackageJsonPrettyJson = fromJsonStringPretty(PackageJsonSchema);
23const decodePackageJson = Schema.decodeEffect(PackageJsonPrettyJson);
24const encodePackageJson = Schema.encodeEffect(PackageJsonPrettyJson);
2725
2826const writePackageJsonFixtures = Effect.fn("writePackageJsonFixtures")(function* (
2927 rootDir: string,
......@@ -37,7 +35,7 @@ const writePackageJsonFixtures = Effect.fn("writePackageJsonFixtures")(function*
3735 yield* fs.makeDirectory(path.dirname(filePath), { recursive: true });
3836 yield* fs.writeFileString(
3937 filePath,
40 `${encodePackageJson({
38 `${yield* encodePackageJson({
4139 name: relativePath,
4240 version,
4341 private: true,
scripts/update-release-package-versions.ts+9-10
......@@ -2,8 +2,15 @@
22
33import * as NodeRuntime from "@effect/platform-node/NodeRuntime";
44import * as NodeServices from "@effect/platform-node/NodeServices";
5import { Config, Console, Effect, FileSystem, Option, Path, Schema, SchemaGetter } from "effect";
5import * as Config from "effect/Config";
6import * as Console from "effect/Console";
7import * as Effect from "effect/Effect";
8import * as FileSystem from "effect/FileSystem";
9import * as Option from "effect/Option";
10import * as Path from "effect/Path";
11import * as Schema from "effect/Schema";
612import { Argument, Command, Flag } from "effect/unstable/cli";
13import { fromJsonStringPretty } from "@t3tools/shared/schemaJson";
714
815export const releasePackageFiles = [
916 "apps/server/package.json",
......@@ -17,15 +24,7 @@ interface UpdateReleasePackageVersionsOptions {
1724}
1825
1926const PackageJsonSchema = Schema.Record(Schema.String, Schema.Unknown);
20const PrettyJsonString = SchemaGetter.parseJson<string>().compose(
21 SchemaGetter.stringifyJson({ space: 2 }),
22);
23const PackageJsonPrettyJson = Schema.fromJsonString(PackageJsonSchema).pipe(
24 Schema.encode({
25 decode: PrettyJsonString,
26 encode: PrettyJsonString,
27 }),
28);
27const PackageJsonPrettyJson = fromJsonStringPretty(PackageJsonSchema);
2928const decodePackageJson = Schema.decodeUnknownEffect(PackageJsonPrettyJson);
3029const encodePackageJson = Schema.encodeSync(PackageJsonPrettyJson);
3130
tsconfig.base.json+34-1
......@@ -17,6 +17,39 @@
1717 "skipLibCheck": true,
1818 "allowSyntheticDefaultImports": true,
1919 "esModuleInterop": true,
20 "resolveJsonModule": true
20 "resolveJsonModule": true,
21 "plugins": [
22 {
23 "name": "@effect/language-service",
24 "namespaceImportPackages": ["@effect/platform-node", "effect"],
25 "diagnosticSeverity": {
26 "importFromBarrel": "error",
27 "anyUnknownInErrorContext": "error",
28 "unsafeEffectTypeAssertion": "error",
29 "instanceOfSchema": "error",
30 "deterministicKeys": "error",
31 "strictEffectProvide": "off",
32 "missingEffectServiceDependency": "error",
33 "leakingRequirements": "error",
34 "globalErrorInEffectCatch": "error",
35 "globalErrorInEffectFailure": "error",
36 "unknownInEffectCatch": "error",
37 "strictBooleanExpressions": "off",
38 "preferSchemaOverJson": "error",
39 "schemaSyncInEffect": "error",
40 "cryptoRandomUUID": "error",
41 "cryptoRandomUUIDInEffect": "error",
42 "nodeBuiltinImport": "error",
43 "globalDate": "error",
44 "globalDateInEffect": "error",
45 "globalConsole": "error",
46 "globalConsoleInEffect": "error",
47 "globalRandom": "error",
48 "globalRandomInEffect": "error",
49 "globalTimers": "error",
50 "globalTimersInEffect": "error"
51 }
52 }
53 ]
2154 }
2255}