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";...@@ -15,7 +15,7 @@ import * as Schema from "effect/Schema";
15import * as Sink from "effect/Sink";15import * as Sink from "effect/Sink";
16import * as Scope from "effect/Scope";16import * as Scope from "effect/Scope";
17import * as Stream from "effect/Stream";17import * as Stream from "effect/Stream";
18import { TestClock } from "effect/testing";18import * as TestClock from "effect/testing/TestClock";
19import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http";19import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http";
20import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";20import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
2121
apps/desktop/src/ssh/DesktopSshEnvironment.ts+3-3
...@@ -3,7 +3,7 @@ import type {...@@ -3,7 +3,7 @@ import type {
3 DesktopSshEnvironmentBootstrap,3 DesktopSshEnvironmentBootstrap,
4 DesktopSshEnvironmentTarget,4 DesktopSshEnvironmentTarget,
5} from "@t3tools/contracts";5} from "@t3tools/contracts";
6import { type NetError, NetService } from "@t3tools/shared/Net";6import * as NetService from "@t3tools/shared/Net";
7import {7import {
8 SshPasswordPrompt,8 SshPasswordPrompt,
9 type SshPasswordPromptShape,9 type SshPasswordPromptShape,
...@@ -35,7 +35,7 @@ export type DesktopSshEnvironmentRuntimeServices =...@@ -35,7 +35,7 @@ export type DesktopSshEnvironmentRuntimeServices =
35 | FileSystem.FileSystem35 | FileSystem.FileSystem
36 | Path.Path36 | Path.Path
37 | HttpClient.HttpClient37 | HttpClient.HttpClient
38 | NetService;38 | NetService.NetService;
3939
40export type DesktopSshEnvironmentOperationError =40export type DesktopSshEnvironmentOperationError =
41 | SshCommandError41 | SshCommandError
...@@ -44,7 +44,7 @@ export type DesktopSshEnvironmentOperationError =...@@ -44,7 +44,7 @@ export type DesktopSshEnvironmentOperationError =
44 | SshPairingError44 | SshPairingError
45 | SshReadinessError45 | SshReadinessError
46 | SshPasswordPromptError46 | SshPasswordPromptError
47 | NetError;47 | NetService.NetError;
4848
49export type DesktopSshEnvironmentDiscoverError = SshHostDiscoveryError;49export type DesktopSshEnvironmentDiscoverError = SshHostDiscoveryError;
5050
apps/desktop/src/ssh/DesktopSshPasswordPrompts.test.ts+1-1
...@@ -4,7 +4,7 @@ import * as Effect from "effect/Effect";...@@ -4,7 +4,7 @@ import * as Effect from "effect/Effect";
4import * as Fiber from "effect/Fiber";4import * as Fiber from "effect/Fiber";
5import * as Layer from "effect/Layer";5import * as Layer from "effect/Layer";
6import * as Option from "effect/Option";6import * as Option from "effect/Option";
7import { TestClock } from "effect/testing";7import * as TestClock from "effect/testing/TestClock";
8import type * as Electron from "electron";8import type * as Electron from "electron";
99
10import * as ElectronWindow from "../electron/ElectronWindow.ts";10import * 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";...@@ -8,7 +8,7 @@ import * as Effect from "effect/Effect";
8import * as Fiber from "effect/Fiber";8import * as Fiber from "effect/Fiber";
9import * as Layer from "effect/Layer";9import * as Layer from "effect/Layer";
10import * as Option from "effect/Option";10import * as Option from "effect/Option";
11import { TestClock } from "effect/testing";11import * as TestClock from "effect/testing/TestClock";
1212
13import * as DesktopBackendManager from "../backend/DesktopBackendManager.ts";13import * as DesktopBackendManager from "../backend/DesktopBackendManager.ts";
14import * as DesktopConfig from "../app/DesktopConfig.ts";14import * as DesktopConfig from "../app/DesktopConfig.ts";
apps/desktop/tsconfig.json+1-13
...@@ -3,19 +3,7 @@...@@ -3,19 +3,7 @@
3 "compilerOptions": {3 "compilerOptions": {
4 "composite": true,4 "composite": true,
5 "types": ["node", "electron"],5 "types": ["node", "electron"],
6 "lib": ["ESNext", "DOM", "esnext.disposable"],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 ]
19 },7 },
20 "include": ["src", "tsdown.config.ts"]8 "include": ["src", "tsdown.config.ts"]
21}9}
apps/server/integration/OrchestrationEngineHarness.integration.ts+13-14
...@@ -1,3 +1,4 @@...@@ -1,3 +1,4 @@
1// @effect-diagnostics nodeBuiltinImport:off
1import { execFileSync } from "node:child_process";2import { execFileSync } from "node:child_process";
23
3import * as NodeServices from "@effect/platform-node/NodeServices";4import * as NodeServices from "@effect/platform-node/NodeServices";
...@@ -8,20 +9,18 @@ import {...@@ -8,20 +9,18 @@ import {
8 type OrchestrationEvent,9 type OrchestrationEvent,
9 type OrchestrationThread,10 type OrchestrationThread,
10} from "@t3tools/contracts";11} from "@t3tools/contracts";
11import {12import * as Effect from "effect/Effect";
12 Effect,13import * as Exit from "effect/Exit";
13 Exit,14import * as FileSystem from "effect/FileSystem";
14 FileSystem,15import * as Layer from "effect/Layer";
15 Layer,16import * as ManagedRuntime from "effect/ManagedRuntime";
16 ManagedRuntime,17import * as Option from "effect/Option";
17 Option,18import * as Path from "effect/Path";
18 Path,19import * as Ref from "effect/Ref";
19 Ref,20import * as Schedule from "effect/Schedule";
20 Schedule,21import * as Schema from "effect/Schema";
21 Schema,22import * as Scope from "effect/Scope";
22 Scope,23import * as Stream from "effect/Stream";
23 Stream,
24} from "effect";
2524
26import { CheckpointStoreLive } from "../src/checkpointing/Layers/CheckpointStore.ts";25import { CheckpointStoreLive } from "../src/checkpointing/Layers/CheckpointStore.ts";
27import { CheckpointStore } from "../src/checkpointing/Services/CheckpointStore.ts";26import { CheckpointStore } from "../src/checkpointing/Services/CheckpointStore.ts";
apps/server/integration/TestProviderAdapter.integration.ts+7-7
...@@ -1,5 +1,3 @@...@@ -1,5 +1,3 @@
1import { randomUUID } from "node:crypto";
2
3import {1import {
4 ApprovalRequestId,2 ApprovalRequestId,
5 EventId,3 EventId,
...@@ -12,7 +10,10 @@ import {...@@ -12,7 +10,10 @@ import {
12 TurnId,10 TurnId,
13 ProviderDriverKind,11 ProviderDriverKind,
14} from "@t3tools/contracts";12} 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
17import {18import {
18 ProviderAdapterSessionNotFoundError,19 ProviderAdapterSessionNotFoundError,
...@@ -202,7 +203,7 @@ interface MakeTestProviderAdapterHarnessOptions {...@@ -202,7 +203,7 @@ interface MakeTestProviderAdapterHarnessOptions {
202}203}
203204
204function nowIso(): string {205function nowIso(): string {
205 return new Date().toISOString();206 return "2026-01-01T00:00:00.000Z";
206}207}
207208
208function sessionNotFound(209function sessionNotFound(
...@@ -308,10 +309,9 @@ export const makeTestProviderAdapterHarness = (options?: MakeTestProviderAdapter...@@ -308,10 +309,9 @@ export const makeTestProviderAdapterHarness = (options?: MakeTestProviderAdapter
308 for (const fixtureEvent of response.events) {309 for (const fixtureEvent of response.events) {
309 const rawEvent: Record<string, unknown> = {310 const rawEvent: Record<string, unknown> = {
310 ...(fixtureEvent as Record<string, unknown>),311 ...(fixtureEvent as Record<string, unknown>),
311 eventId: randomUUID(),312 eventId: yield* Random.nextUUIDv4,
312 provider,313 provider,
313 sessionId: RuntimeSessionId.make(String(input.threadId)),314 sessionId: RuntimeSessionId.make(String(input.threadId)),
314 createdAt: nowIso(),
315 };315 };
316 rawEvent.threadId = state.snapshot.threadId;316 rawEvent.threadId = state.snapshot.threadId;
317 if (Object.hasOwn(rawEvent, "turnId")) {317 if (Object.hasOwn(rawEvent, "turnId")) {
...@@ -366,7 +366,7 @@ export const makeTestProviderAdapterHarness = (options?: MakeTestProviderAdapter...@@ -366,7 +366,7 @@ export const makeTestProviderAdapterHarness = (options?: MakeTestProviderAdapter
366 if (deferredTurnCompletedEvents.length === 0) {366 if (deferredTurnCompletedEvents.length === 0) {
367 yield* emit({367 yield* emit({
368 type: "turn.completed",368 type: "turn.completed",
369 eventId: EventId.make(randomUUID()),369 eventId: EventId.make(yield* Random.nextUUIDv4),
370 provider,370 provider,
371 createdAt: nowIso(),371 createdAt: nowIso(),
372 threadId: state.snapshot.threadId,372 threadId: state.snapshot.threadId,
apps/server/integration/orchestrationEngine.integration.test.ts+12-5
...@@ -1,3 +1,4 @@...@@ -1,3 +1,4 @@
1// @effect-diagnostics nodeBuiltinImport:off
1import fs from "node:fs";2import fs from "node:fs";
2import path from "node:path";3import path from "node:path";
34
...@@ -17,7 +18,10 @@ import {...@@ -17,7 +18,10 @@ import {
17 ProviderInstanceId,18 ProviderInstanceId,
18} from "@t3tools/contracts";19} from "@t3tools/contracts";
19import { assert, it } from "@effect/vitest";20import { 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
22import type { TestTurnResponse } from "./TestProviderAdapter.integration.ts";26import type { TestTurnResponse } from "./TestProviderAdapter.integration.ts";
23import {27import {
...@@ -47,7 +51,7 @@ const CODEX_PROVIDER = ProviderDriverKind.make("codex");...@@ -47,7 +51,7 @@ const CODEX_PROVIDER = ProviderDriverKind.make("codex");
47const CLAUDE_AGENT_PROVIDER = ProviderDriverKind.make("claudeAgent");51const CLAUDE_AGENT_PROVIDER = ProviderDriverKind.make("claudeAgent");
4852
49function nowIso() {53function nowIso() {
50 return new Date().toISOString();54 return "2026-05-01T00:00:00.000Z";
51}55}
5256
53class IntegrationWaitTimeoutError extends Schema.TaggedErrorClass<IntegrationWaitTimeoutError>()(57class IntegrationWaitTimeoutError extends Schema.TaggedErrorClass<IntegrationWaitTimeoutError>()(
...@@ -64,14 +68,14 @@ function waitForSync<A>(...@@ -64,14 +68,14 @@ function waitForSync<A>(
64 timeoutMs = 10_000,68 timeoutMs = 10_000,
65): Effect.Effect<A, never> {69): Effect.Effect<A, never> {
66 return Effect.gen(function* () {70 return Effect.gen(function* () {
67 const deadline = Date.now() + timeoutMs;71 const deadline = (yield* Clock.currentTimeMillis) + timeoutMs;
6872
69 while (true) {73 while (true) {
70 const value = read();74 const value = read();
71 if (predicate(value)) {75 if (predicate(value)) {
72 return value;76 return value;
73 }77 }
74 if (Date.now() >= deadline) {78 if ((yield* Clock.currentTimeMillis) >= deadline) {
75 return yield* Effect.die(new IntegrationWaitTimeoutError({ description }));79 return yield* Effect.die(new IntegrationWaitTimeoutError({ description }));
76 }80 }
77 yield* Effect.sleep(10);81 yield* Effect.sleep(10);
...@@ -156,6 +160,7 @@ const startTurn = (input: {...@@ -156,6 +160,7 @@ const startTurn = (input: {
156 readonly messageId: string;160 readonly messageId: string;
157 readonly text: string;161 readonly text: string;
158 readonly modelSelection?: ModelSelection;162 readonly modelSelection?: ModelSelection;
163 readonly createdAt?: string;
159}) =>164}) =>
160 input.harness.engine.dispatch({165 input.harness.engine.dispatch({
161 type: "thread.turn.start",166 type: "thread.turn.start",
...@@ -174,7 +179,7 @@ const startTurn = (input: {...@@ -174,7 +179,7 @@ const startTurn = (input: {
174 : {}),179 : {}),
175 interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE,180 interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE,
176 runtimeMode: "approval-required",181 runtimeMode: "approval-required",
177 createdAt: nowIso(),182 createdAt: input.createdAt ?? nowIso(),
178 });183 });
179184
180it.live("runs a single turn end-to-end and persists checkpoint state in sqlite + git", () =>185it.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...@@ -755,6 +760,7 @@ it.live("reverts to an earlier checkpoint and trims checkpoint projections + git
755 commandId: "cmd-turn-start-revert-1",760 commandId: "cmd-turn-start-revert-1",
756 messageId: "msg-user-revert-1",761 messageId: "msg-user-revert-1",
757 text: "First edit",762 text: "First edit",
763 createdAt: "2026-02-24T10:04:59.900Z",
758 });764 });
759765
760 yield* harness.waitForThread(766 yield* harness.waitForThread(
...@@ -813,6 +819,7 @@ it.live("reverts to an earlier checkpoint and trims checkpoint projections + git...@@ -813,6 +819,7 @@ it.live("reverts to an earlier checkpoint and trims checkpoint projections + git
813 commandId: "cmd-turn-start-revert-2",819 commandId: "cmd-turn-start-revert-2",
814 messageId: "msg-user-revert-2",820 messageId: "msg-user-revert-2",
815 text: "Second edit",821 text: "Second edit",
822 createdAt: "2026-02-24T10:05:00.900Z",
816 });823 });
817824
818 yield* harness.waitForThread(825 yield* harness.waitForThread(
apps/server/integration/providerService.integration.test.ts+6-1
...@@ -3,7 +3,12 @@ import { ProviderDriverKind, ProviderInstanceId, ThreadId } from "@t3tools/contr...@@ -3,7 +3,12 @@ import { ProviderDriverKind, ProviderInstanceId, ThreadId } from "@t3tools/contr
3import { DEFAULT_SERVER_SETTINGS } from "@t3tools/contracts/settings";3import { DEFAULT_SERVER_SETTINGS } from "@t3tools/contracts/settings";
4import * as NodeServices from "@effect/platform-node/NodeServices";4import * as NodeServices from "@effect/platform-node/NodeServices";
5import { it, assert } from "@effect/vitest";5import { 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
8import { ProviderAdapterRegistry } from "../src/provider/Services/ProviderAdapterRegistry.ts";13import { ProviderAdapterRegistry } from "../src/provider/Services/ProviderAdapterRegistry.ts";
9import { makeAdapterRegistryMock } from "../src/provider/testUtils/providerAdapterRegistryMock.ts";14import { makeAdapterRegistryMock } from "../src/provider/testUtils/providerAdapterRegistryMock.ts";
apps/server/scripts/acp-mock-agent.ts+1
...@@ -1,4 +1,5 @@...@@ -1,4 +1,5 @@
1#!/usr/bin/env bun1#!/usr/bin/env bun
2// @effect-diagnostics nodeBuiltinImport:off
2import { appendFileSync } from "node:fs";3import { appendFileSync } from "node:fs";
34
4import * as Effect from "effect/Effect";5import * as Effect from "effect/Effect";
apps/server/scripts/cli.ts+13-3
...@@ -1,8 +1,13 @@...@@ -1,8 +1,13 @@
1#!/usr/bin/env node1#!/usr/bin/env node
2
3import * as NodeRuntime from "@effect/platform-node/NodeRuntime";2import * as NodeRuntime from "@effect/platform-node/NodeRuntime";
4import * as NodeServices from "@effect/platform-node/NodeServices";3import * 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";
6import { Command, Flag } from "effect/unstable/cli";11import { Command, Flag } from "effect/unstable/cli";
7import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";12import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
813
...@@ -11,6 +16,7 @@ import {...@@ -11,6 +16,7 @@ import {
11 PUBLISH_ICON_OVERRIDES,16 PUBLISH_ICON_OVERRIDES,
12} from "../../../scripts/lib/brand-assets.ts";17} from "../../../scripts/lib/brand-assets.ts";
13import { resolveCatalogDependencies } from "../../../scripts/lib/resolve-catalog.ts";18import { resolveCatalogDependencies } from "../../../scripts/lib/resolve-catalog.ts";
19import { fromJsonStringPretty } from "@t3tools/shared/schemaJson";
14import rootPackageJson from "../../../package.json" with { type: "json" };20import rootPackageJson from "../../../package.json" with { type: "json" };
15import serverPackageJson from "../package.json" with { type: "json" };21import serverPackageJson from "../package.json" with { type: "json" };
1622
...@@ -30,6 +36,9 @@ interface PackageJson {...@@ -30,6 +36,9 @@ interface PackageJson {
30 overrides: Record<string, string>;36 overrides: Record<string, string>;
31}37}
3238
39const PackageJsonPrettyJson = fromJsonStringPretty(Schema.Unknown);
40const encodePackageJson = Schema.encodeEffect(PackageJsonPrettyJson);
41
33class CliError extends Data.TaggedError("CliError")<{42class CliError extends Data.TaggedError("CliError")<{
34 readonly message: string;43 readonly message: string;
35 readonly cause?: unknown;44 readonly cause?: unknown;
...@@ -227,8 +236,9 @@ const publishCmd = Command.make(...@@ -227,8 +236,9 @@ const publishCmd = Command.make(
227 };236 };
228237
229 const original = yield* fs.readFileString(packageJsonPath);238 const original = yield* fs.readFileString(packageJsonPath);
239 const packageJsonString = yield* encodePackageJson(pkg);
230 yield* fs.writeFileString(backupPath, original);240 yield* fs.writeFileString(backupPath, original);
231 yield* fs.writeFileString(packageJsonPath, `${JSON.stringify(pkg, null, 2)}\n`);241 yield* fs.writeFileString(packageJsonPath, `${packageJsonString}\n`);
232 yield* Effect.log("[cli] Prepared package.json for publish");242 yield* Effect.log("[cli] Prepared package.json for publish");
233243
234 const iconBackups = yield* applyPublishIconOverrides(repoRoot, serverDir);244 const iconBackups = yield* applyPublishIconOverrides(repoRoot, serverDir);
apps/server/scripts/cursor-acp-model-mismatch-probe.ts+8-4
...@@ -1,6 +1,8 @@...@@ -1,6 +1,8 @@
1// @effect-diagnostics nodeBuiltinImport:off
1import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process";2import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process";
2import process from "node:process";3import process from "node:process";
3import readline from "node:readline";4import readline from "node:readline";
5import * as NodeTimers from "node:timers";
46
5type JsonPrimitive = null | boolean | number | string;7type JsonPrimitive = null | boolean | number | string;
6type JsonValue = JsonPrimitive | JsonValue[] | { [key: string]: JsonValue };8type JsonValue = JsonPrimitive | JsonValue[] | { [key: string]: JsonValue };
...@@ -114,7 +116,8 @@ function matchesKeyword(option: SessionConfigOption, keyword: string): boolean {...@@ -114,7 +116,8 @@ function matchesKeyword(option: SessionConfigOption, keyword: string): boolean {
114116
115function sleep(ms: number) {117function sleep(ms: number) {
116 return new Promise<void>((resolve) => {118 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);
118 });121 });
119}122}
120123
...@@ -177,7 +180,8 @@ class JsonRpcChild {...@@ -177,7 +180,8 @@ class JsonRpcChild {
177 const id = this.nextId++;180 const id = this.nextId++;
178181
179 const responsePromise = new Promise<JsonValue | undefined>((resolve, reject) => {182 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(() => {
181 this.pending.delete(id);185 this.pending.delete(id);
182 reject(new Error(`Timed out waiting for ${method} response after ${timeoutMs}ms.`));186 reject(new Error(`Timed out waiting for ${method} response after ${timeoutMs}ms.`));
183 }, timeoutMs);187 }, timeoutMs);
...@@ -185,11 +189,11 @@ class JsonRpcChild {...@@ -185,11 +189,11 @@ class JsonRpcChild {
185 this.pending.set(id, {189 this.pending.set(id, {
186 method,190 method,
187 resolve: (value) => {191 resolve: (value) => {
188 clearTimeout(timeout);192 NodeTimers.clearTimeout(timeout);
189 resolve(value);193 resolve(value);
190 },194 },
191 reject: (error) => {195 reject: (error) => {
192 clearTimeout(timeout);196 NodeTimers.clearTimeout(timeout);
193 reject(error);197 reject(error);
194 },198 },
195 });199 });
apps/server/src/atomicWrite.ts+3-1
...@@ -1,4 +1,6 @@...@@ -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";
2import * as Random from "effect/Random";4import * as Random from "effect/Random";
35
4export const writeFileStringAtomically = (input: {6export const writeFileStringAtomically = (input: {
apps/server/src/attachmentPaths.ts+6-5
...@@ -1,9 +1,10 @@...@@ -1,9 +1,10 @@
1import path from "node:path";1// @effect-diagnostics nodeBuiltinImport:off
2import NodePath from "node:path";
23
3export const ATTACHMENTS_ROUTE_PREFIX = "/attachments";4export const ATTACHMENTS_ROUTE_PREFIX = "/attachments";
45
5export function normalizeAttachmentRelativePath(rawRelativePath: string): string | null {6export function normalizeAttachmentRelativePath(rawRelativePath: string): string | null {
6 const normalized = path.normalize(rawRelativePath).replace(/^[/\\]+/, "");7 const normalized = NodePath.normalize(rawRelativePath).replace(/^[/\\]+/, "");
7 if (normalized.length === 0 || normalized.startsWith("..") || normalized.includes("\0")) {8 if (normalized.length === 0 || normalized.startsWith("..") || normalized.includes("\0")) {
8 return null;9 return null;
9 }10 }
...@@ -19,9 +20,9 @@ export function resolveAttachmentRelativePath(input: {...@@ -19,9 +20,9 @@ export function resolveAttachmentRelativePath(input: {
19 return null;20 return null;
20 }21 }
2122
22 const attachmentsRoot = path.resolve(input.attachmentsDir);23 const attachmentsRoot = NodePath.resolve(input.attachmentsDir);
23 const filePath = path.resolve(path.join(attachmentsRoot, normalizedRelativePath));24 const filePath = NodePath.resolve(NodePath.join(attachmentsRoot, normalizedRelativePath));
24 if (!filePath.startsWith(`${attachmentsRoot}${path.sep}`)) {25 if (!filePath.startsWith(`${attachmentsRoot}${NodePath.sep}`)) {
25 return null;26 return null;
26 }27 }
27 return filePath;28 return filePath;
apps/server/src/attachmentStore.test.ts+1
...@@ -1,3 +1,4 @@...@@ -1,3 +1,4 @@
1// @effect-diagnostics nodeBuiltinImport:off
1import fs from "node:fs";2import fs from "node:fs";
2import os from "node:os";3import os from "node:os";
3import path from "node:path";4import path from "node:path";
apps/server/src/attachmentStore.ts+1
...@@ -1,3 +1,4 @@...@@ -1,3 +1,4 @@
1// @effect-diagnostics nodeBuiltinImport:off
1import { randomUUID } from "node:crypto";2import { randomUUID } from "node:crypto";
2import { existsSync } from "node:fs";3import { existsSync } from "node:fs";
34
apps/server/src/auth/Layers/AuthControlPlane.test.ts+2-1
...@@ -1,6 +1,7 @@...@@ -1,6 +1,7 @@
1import * as NodeServices from "@effect/platform-node/NodeServices";1import * as NodeServices from "@effect/platform-node/NodeServices";
2import { expect, it } from "@effect/vitest";2import { expect, it } from "@effect/vitest";
3import { Effect, Layer } from "effect";3import * as Effect from "effect/Effect";
4import * as Layer from "effect/Layer";
45
5import type { ServerConfigShape } from "../../config.ts";6import type { ServerConfigShape } from "../../config.ts";
6import { ServerConfig } from "../../config.ts";7import { ServerConfig } from "../../config.ts";
apps/server/src/auth/Layers/AuthControlPlane.ts+3-1
...@@ -1,5 +1,7 @@...@@ -1,5 +1,7 @@
1import type { AuthClientSession, AuthPairingLink } from "@t3tools/contracts";1import 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
4import { BootstrapCredentialServiceLive } from "./BootstrapCredentialService.ts";6import { BootstrapCredentialServiceLive } from "./BootstrapCredentialService.ts";
5import { ServerSecretStoreLive } from "./ServerSecretStore.ts";7import { ServerSecretStoreLive } from "./ServerSecretStore.ts";
apps/server/src/auth/Layers/BootstrapCredentialService.test.ts+4-2
...@@ -1,7 +1,9 @@...@@ -1,7 +1,9 @@
1import * as NodeServices from "@effect/platform-node/NodeServices";1import * as NodeServices from "@effect/platform-node/NodeServices";
2import { expect, it } from "@effect/vitest";2import { expect, it } from "@effect/vitest";
3import { Duration, Effect, Layer } from "effect";3import * as Duration from "effect/Duration";
4import { TestClock } from "effect/testing";4import * as Effect from "effect/Effect";
5import * as Layer from "effect/Layer";
6import * as TestClock from "effect/testing/TestClock";
57
6import type { ServerConfigShape } from "../../config.ts";8import type { ServerConfigShape } from "../../config.ts";
7import { ServerConfig } from "../../config.ts";9import { ServerConfig } from "../../config.ts";
apps/server/src/auth/Layers/BootstrapCredentialService.ts+8-2
...@@ -1,6 +1,12 @@...@@ -1,6 +1,12 @@
1import type { AuthPairingLink } from "@t3tools/contracts";1import type { AuthPairingLink } from "@t3tools/contracts";
2import { DateTime, Duration, Effect, Layer, PubSub, Ref, Stream } from "effect";2import * as DateTime from "effect/DateTime";
3import { Option } from "effect";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
5import { ServerConfig } from "../../config.ts";11import { ServerConfig } from "../../config.ts";
6import { AuthPairingLinkRepositoryLive } from "../../persistence/Layers/AuthPairingLinks.ts";12import { AuthPairingLinkRepositoryLive } from "../../persistence/Layers/AuthPairingLinks.ts";
apps/server/src/auth/Layers/ServerAuth.test.ts+2-1
...@@ -1,6 +1,7 @@...@@ -1,6 +1,7 @@
1import * as NodeServices from "@effect/platform-node/NodeServices";1import * as NodeServices from "@effect/platform-node/NodeServices";
2import { expect, it } from "@effect/vitest";2import { expect, it } from "@effect/vitest";
3import { Effect, Layer } from "effect";3import * as Effect from "effect/Effect";
4import * as Layer from "effect/Layer";
45
5import type { ServerConfigShape } from "../../config.ts";6import type { ServerConfigShape } from "../../config.ts";
6import { ServerConfig } from "../../config.ts";7import { ServerConfig } from "../../config.ts";
apps/server/src/auth/Layers/ServerAuth.ts+4-1
...@@ -6,7 +6,10 @@ import {...@@ -6,7 +6,10 @@ import {
6 type AuthSessionState,6 type AuthSessionState,
7 type AuthWebSocketTokenResult,7 type AuthWebSocketTokenResult,
8} from "@t3tools/contracts";8} 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";
10import * as HttpServerRequest from "effect/unstable/http/HttpServerRequest";13import * as HttpServerRequest from "effect/unstable/http/HttpServerRequest";
1114
12import { AuthControlPlane } from "../Services/AuthControlPlane.ts";15import { AuthControlPlane } from "../Services/AuthControlPlane.ts";
apps/server/src/auth/Layers/ServerAuthPolicy.test.ts+2-1
...@@ -1,6 +1,7 @@...@@ -1,6 +1,7 @@
1import * as NodeServices from "@effect/platform-node/NodeServices";1import * as NodeServices from "@effect/platform-node/NodeServices";
2import { expect, it } from "@effect/vitest";2import { expect, it } from "@effect/vitest";
3import { Effect, Layer } from "effect";3import * as Effect from "effect/Effect";
4import * as Layer from "effect/Layer";
45
5import type { ServerConfigShape } from "../../config.ts";6import type { ServerConfigShape } from "../../config.ts";
6import { ServerConfig } from "../../config.ts";7import { ServerConfig } from "../../config.ts";
apps/server/src/auth/Layers/ServerAuthPolicy.ts+2-1
...@@ -1,5 +1,6 @@...@@ -1,5 +1,6 @@
1import type { ServerAuthDescriptor } from "@t3tools/contracts";1import type { ServerAuthDescriptor } from "@t3tools/contracts";
2import { Effect, Layer } from "effect";2import * as Effect from "effect/Effect";
3import * as Layer from "effect/Layer";
34
4import { ServerConfig } from "../../config.ts";5import { ServerConfig } from "../../config.ts";
5import { ServerAuthPolicy, type ServerAuthPolicyShape } from "../Services/ServerAuthPolicy.ts";6import { ServerAuthPolicy, type ServerAuthPolicyShape } from "../Services/ServerAuthPolicy.ts";
apps/server/src/auth/Layers/ServerSecretStore.test.ts+6-1
...@@ -1,6 +1,11 @@...@@ -1,6 +1,11 @@
1import * as NodeServices from "@effect/platform-node/NodeServices";1import * as NodeServices from "@effect/platform-node/NodeServices";
2import { expect, it } from "@effect/vitest";2import { 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";
4import * as PlatformError from "effect/PlatformError";9import * as PlatformError from "effect/PlatformError";
510
6import { ServerConfig } from "../../config.ts";11import { ServerConfig } from "../../config.ts";
apps/server/src/auth/Layers/ServerSecretStore.ts+5-1
...@@ -1,6 +1,10 @@...@@ -1,6 +1,10 @@
1import * as Crypto from "node:crypto";1import * 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";
4import * as PlatformError from "effect/PlatformError";8import * as PlatformError from "effect/PlatformError";
59
6import { ServerConfig } from "../../config.ts";10import { ServerConfig } from "../../config.ts";
apps/server/src/auth/Layers/SessionCredentialService.test.ts+4-2
...@@ -1,7 +1,9 @@...@@ -1,7 +1,9 @@
1import * as NodeServices from "@effect/platform-node/NodeServices";1import * as NodeServices from "@effect/platform-node/NodeServices";
2import { expect, it } from "@effect/vitest";2import { expect, it } from "@effect/vitest";
3import { Duration, Effect, Layer } from "effect";3import * as Duration from "effect/Duration";
4import { TestClock } from "effect/testing";4import * as Effect from "effect/Effect";
5import * as Layer from "effect/Layer";
6import * as TestClock from "effect/testing/TestClock";
57
6import type { ServerConfigShape } from "../../config.ts";8import type { ServerConfigShape } from "../../config.ts";
7import { ServerConfig } from "../../config.ts";9import { ServerConfig } from "../../config.ts";
apps/server/src/auth/Layers/SessionCredentialService.ts+33-5
...@@ -1,6 +1,14 @@...@@ -1,6 +1,14 @@
1import { AuthSessionId, type AuthClientMetadata, type AuthClientSession } from "@t3tools/contracts";1import { AuthSessionId, type AuthClientMetadata, type AuthClientSession } from "@t3tools/contracts";
2import { Clock, DateTime, Duration, Effect, Layer, PubSub, Ref, Schema, Stream } from "effect";2import * as Clock from "effect/Clock";
3import { Option } from "effect";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
5import { ServerConfig } from "../../config.ts";13import { ServerConfig } from "../../config.ts";
6import { AuthSessionRepositoryLive } from "../../persistence/Layers/AuthSessions.ts";14import { AuthSessionRepositoryLive } from "../../persistence/Layers/AuthSessions.ts";
...@@ -192,6 +200,7 @@ export const makeSessionCredentialService = Effect.gen(function* () {...@@ -192,6 +200,7 @@ export const makeSessionCredentialService = Effect.gen(function* () {
192 ),200 ),
193 );201 );
194202
203 const encodeClaims = Schema.encodeEffect(Schema.fromJsonString(SessionClaims));
195 const issue: SessionCredentialServiceShape["issue"] = (input) =>204 const issue: SessionCredentialServiceShape["issue"] = (input) =>
196 Effect.gen(function* () {205 Effect.gen(function* () {
197 const sessionId = AuthSessionId.make(crypto.randomUUID());206 const sessionId = AuthSessionId.make(crypto.randomUUID());
...@@ -209,7 +218,13 @@ export const makeSessionCredentialService = Effect.gen(function* () {...@@ -209,7 +218,13 @@ export const makeSessionCredentialService = Effect.gen(function* () {
209 iat: issuedAt.epochMilliseconds,218 iat: issuedAt.epochMilliseconds,
210 exp: expiresAt.epochMilliseconds,219 exp: expiresAt.epochMilliseconds,
211 };220 };
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 );
213 const signature = signPayload(encodedPayload, signingSecret);228 const signature = signPayload(encodedPayload, signingSecret);
214 const client = input?.client ?? createDefaultClientMetadata();229 const client = input?.client ?? createDefaultClientMetadata();
215 yield* authSessions.create({230 yield* authSessions.create({
...@@ -297,12 +312,19 @@ export const makeSessionCredentialService = Effect.gen(function* () {...@@ -297,12 +312,19 @@ export const makeSessionCredentialService = Effect.gen(function* () {
297 });312 });
298 }313 }
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
300 return {322 return {
301 sessionId: claims.sid,323 sessionId: claims.sid,
302 token,324 token,
303 method: claims.method,325 method: claims.method,
304 client: toClientMetadata(row.value.client),326 client: toClientMetadata(row.value.client),
305 expiresAt: DateTime.makeUnsafe(claims.exp),327 expiresAt: expiresAt.value,
306 subject: claims.sub,328 subject: claims.sub,
307 role: claims.role,329 role: claims.role,
308 } satisfies VerifiedSession;330 } satisfies VerifiedSession;
...@@ -317,6 +339,7 @@ export const makeSessionCredentialService = Effect.gen(function* () {...@@ -317,6 +339,7 @@ export const makeSessionCredentialService = Effect.gen(function* () {
317 ),339 ),
318 );340 );
319341
342 const encodeWsClaims = Schema.encodeEffect(Schema.fromJsonString(WebSocketClaims));
320 const issueWebSocketToken: SessionCredentialServiceShape["issueWebSocketToken"] = (343 const issueWebSocketToken: SessionCredentialServiceShape["issueWebSocketToken"] = (
321 sessionId,344 sessionId,
322 input,345 input,
...@@ -333,7 +356,12 @@ export const makeSessionCredentialService = Effect.gen(function* () {...@@ -333,7 +356,12 @@ export const makeSessionCredentialService = Effect.gen(function* () {
333 iat: issuedAt.epochMilliseconds,356 iat: issuedAt.epochMilliseconds,
334 exp: expiresAt.epochMilliseconds,357 exp: expiresAt.epochMilliseconds,
335 };358 };
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 );
337 const signature = signPayload(encodedPayload, signingSecret);365 const signature = signPayload(encodedPayload, signingSecret);
338 return {366 return {
339 token: `${encodedPayload}.${signature}`,367 token: `${encodedPayload}.${signature}`,
apps/server/src/auth/Services/AuthControlPlane.ts+5-1
...@@ -4,7 +4,11 @@ import type {...@@ -4,7 +4,11 @@ import type {
4 AuthPairingLink,4 AuthPairingLink,
5 AuthSessionId,5 AuthSessionId,
6} from "@t3tools/contracts";6} 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";
8import type { SessionRole } from "./SessionCredentialService.ts";12import type { SessionRole } from "./SessionCredentialService.ts";
913
10export const DEFAULT_SESSION_SUBJECT = "cli-issued-session";14export const DEFAULT_SESSION_SUBJECT = "cli-issued-session";
apps/server/src/auth/Services/BootstrapCredentialService.ts+6-2
...@@ -1,6 +1,10 @@...@@ -1,6 +1,10 @@
1import type { AuthPairingLink, ServerAuthBootstrapMethod } from "@t3tools/contracts";1import type { AuthPairingLink, ServerAuthBootstrapMethod } from "@t3tools/contracts";
2import { Data, DateTime, Duration, Context } from "effect";2import * as Data from "effect/Data";
3import type { Effect, Stream } from "effect";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
5export type BootstrapCredentialRole = "owner" | "client";9export type BootstrapCredentialRole = "owner" | "client";
610
apps/server/src/auth/Services/ServerAuth.ts+4-2
...@@ -12,8 +12,10 @@ import type {...@@ -12,8 +12,10 @@ import type {
12 ServerAuthSessionMethod,12 ServerAuthSessionMethod,
13 AuthWebSocketTokenResult,13 AuthWebSocketTokenResult,
14} from "@t3tools/contracts";14} from "@t3tools/contracts";
15import { Data, DateTime, Context } from "effect";15import * as Data from "effect/Data";
16import type { Effect } from "effect";16import * as DateTime from "effect/DateTime";
17import * as Context from "effect/Context";
18import type * as Effect from "effect/Effect";
17import type * as HttpServerRequest from "effect/unstable/http/HttpServerRequest";19import type * as HttpServerRequest from "effect/unstable/http/HttpServerRequest";
18import type { SessionRole } from "./SessionCredentialService.ts";20import type { SessionRole } from "./SessionCredentialService.ts";
1921
apps/server/src/auth/Services/ServerAuthPolicy.ts+2-2
...@@ -1,6 +1,6 @@...@@ -1,6 +1,6 @@
1import type { ServerAuthDescriptor } from "@t3tools/contracts";1import type { ServerAuthDescriptor } from "@t3tools/contracts";
2import { Context } from "effect";2import * as Context from "effect/Context";
3import type { Effect } from "effect";3import type * as Effect from "effect/Effect";
44
5export interface ServerAuthPolicyShape {5export interface ServerAuthPolicyShape {
6 readonly getDescriptor: () => Effect.Effect<ServerAuthDescriptor>;6 readonly getDescriptor: () => Effect.Effect<ServerAuthDescriptor>;
apps/server/src/auth/Services/ServerSecretStore.ts+3-2
...@@ -1,5 +1,6 @@...@@ -1,5 +1,6 @@
1import { Data, Context } from "effect";1import * as Data from "effect/Data";
2import type { Effect } from "effect";2import * as Context from "effect/Context";
3import type * as Effect from "effect/Effect";
34
4export class SecretStoreError extends Data.TaggedError("SecretStoreError")<{5export class SecretStoreError extends Data.TaggedError("SecretStoreError")<{
5 readonly message: string;6 readonly message: string;
apps/server/src/auth/Services/SessionCredentialService.ts+6-2
...@@ -4,8 +4,12 @@ import type {...@@ -4,8 +4,12 @@ import type {
4 AuthSessionId,4 AuthSessionId,
5 ServerAuthSessionMethod,5 ServerAuthSessionMethod,
6} from "@t3tools/contracts";6} from "@t3tools/contracts";
7import { Data, DateTime, Duration, Context } from "effect";7import * as Data from "effect/Data";
8import type { Effect, Stream } from "effect";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
10export type SessionRole = "owner" | "client";14export type SessionRole = "owner" | "client";
1115
apps/server/src/auth/http.ts+3-1
...@@ -6,7 +6,9 @@ import {...@@ -6,7 +6,9 @@ import {
6 AuthRevokePairingLinkInput,6 AuthRevokePairingLinkInput,
7 type AuthWebSocketTokenResult,7 type AuthWebSocketTokenResult,
8} from "@t3tools/contracts";8} 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";
10import { HttpRouter, HttpServerRequest, HttpServerResponse } from "effect/unstable/http";12import { HttpRouter, HttpServerRequest, HttpServerResponse } from "effect/unstable/http";
1113
12import { AuthError, ServerAuth } from "./Services/ServerAuth.ts";14import { AuthError, ServerAuth } from "./Services/ServerAuth.ts";
apps/server/src/bin.test.ts+7-2
...@@ -1,3 +1,4 @@...@@ -1,3 +1,4 @@
1// @effect-diagnostics-next-line nodeBuiltinImport:off - NodeHttpServer.layer takes `NodeHttp.createServer` as arg
1import * as NodeHttp from "node:http";2import * as NodeHttp from "node:http";
2import { mkdtempSync } from "node:fs";3import { mkdtempSync } from "node:fs";
3import { tmpdir } from "node:os";4import { tmpdir } from "node:os";
...@@ -5,7 +6,7 @@ import { join } from "node:path";...@@ -5,7 +6,7 @@ import { join } from "node:path";
56
6import * as NodeHttpServer from "@effect/platform-node/NodeHttpServer";7import * as NodeHttpServer from "@effect/platform-node/NodeHttpServer";
7import * as NodeServices from "@effect/platform-node/NodeServices";8import * as NodeServices from "@effect/platform-node/NodeServices";
8import { NetService } from "@t3tools/shared/Net";9import * as NetService from "@t3tools/shared/Net";
9import { assert, it } from "@effect/vitest";10import { assert, it } from "@effect/vitest";
10import * as Effect from "effect/Effect";11import * as Effect from "effect/Effect";
11import * as Layer from "effect/Layer";12import * as Layer from "effect/Layer";
...@@ -138,7 +139,7 @@ const withLiveProjectCliServer = <A, E, R>(baseDir: string, run: () => Effect.Ef...@@ -138,7 +139,7 @@ const withLiveProjectCliServer = <A, E, R>(baseDir: string, run: () => Effect.Ef
138 }139 }
139 yield* persistServerRuntimeState({140 yield* persistServerRuntimeState({
140 path: config.serverRuntimeStatePath,141 path: config.serverRuntimeStatePath,
141 state: makePersistedServerRuntimeState({142 state: yield* makePersistedServerRuntimeState({
142 config,143 config,
143 port: address.port,144 port: address.port,
144 }),145 }),
...@@ -179,6 +180,7 @@ it.layer(NodeServices.layer)("bin cli parsing", (it) => {...@@ -179,6 +180,7 @@ it.layer(NodeServices.layer)("bin cli parsing", (it) => {
179 const createdOutput = yield* captureStdout(180 const createdOutput = yield* captureStdout(
180 runCli(["auth", "pairing", "create", "--base-dir", baseDir, "--json"]),181 runCli(["auth", "pairing", "create", "--base-dir", baseDir, "--json"]),
181 );182 );
183 // @effect-diagnostics-next-line preferSchemaOverJson:off
182 const created = JSON.parse(createdOutput.output) as {184 const created = JSON.parse(createdOutput.output) as {
183 readonly id: string;185 readonly id: string;
184 readonly credential: string;186 readonly credential: string;
...@@ -186,6 +188,7 @@ it.layer(NodeServices.layer)("bin cli parsing", (it) => {...@@ -186,6 +188,7 @@ it.layer(NodeServices.layer)("bin cli parsing", (it) => {
186 const listedOutput = yield* captureStdout(188 const listedOutput = yield* captureStdout(
187 runCli(["auth", "pairing", "list", "--base-dir", baseDir, "--json"]),189 runCli(["auth", "pairing", "list", "--base-dir", baseDir, "--json"]),
188 );190 );
191 // @effect-diagnostics-next-line preferSchemaOverJson:off
189 const listed = JSON.parse(listedOutput.output) as ReadonlyArray<{192 const listed = JSON.parse(listedOutput.output) as ReadonlyArray<{
190 readonly id: string;193 readonly id: string;
191 readonly credential?: string;194 readonly credential?: string;
...@@ -207,6 +210,7 @@ it.layer(NodeServices.layer)("bin cli parsing", (it) => {...@@ -207,6 +210,7 @@ it.layer(NodeServices.layer)("bin cli parsing", (it) => {
207 const issuedOutput = yield* captureStdout(210 const issuedOutput = yield* captureStdout(
208 runCli(["auth", "session", "issue", "--base-dir", baseDir, "--json"]),211 runCli(["auth", "session", "issue", "--base-dir", baseDir, "--json"]),
209 );212 );
213 // @effect-diagnostics-next-line preferSchemaOverJson:off
210 const issued = JSON.parse(issuedOutput.output) as {214 const issued = JSON.parse(issuedOutput.output) as {
211 readonly sessionId: string;215 readonly sessionId: string;
212 readonly token: string;216 readonly token: string;
...@@ -215,6 +219,7 @@ it.layer(NodeServices.layer)("bin cli parsing", (it) => {...@@ -215,6 +219,7 @@ it.layer(NodeServices.layer)("bin cli parsing", (it) => {
215 const listedOutput = yield* captureStdout(219 const listedOutput = yield* captureStdout(
216 runCli(["auth", "session", "list", "--base-dir", baseDir, "--json"]),220 runCli(["auth", "session", "list", "--base-dir", baseDir, "--json"]),
217 );221 );
222 // @effect-diagnostics-next-line preferSchemaOverJson:off
218 const listed = JSON.parse(listedOutput.output) as ReadonlyArray<{223 const listed = JSON.parse(listedOutput.output) as ReadonlyArray<{
219 readonly sessionId: string;224 readonly sessionId: string;
220 readonly token?: string;225 readonly token?: string;
apps/server/src/bin.ts+1-1
...@@ -4,7 +4,7 @@ import * as Effect from "effect/Effect";...@@ -4,7 +4,7 @@ import * as Effect from "effect/Effect";
4import * as Layer from "effect/Layer";4import * as Layer from "effect/Layer";
5import { Command } from "effect/unstable/cli";5import { Command } from "effect/unstable/cli";
66
7import { NetService } from "@t3tools/shared/Net";7import * as NetService from "@t3tools/shared/Net";
8import packageJson from "../package.json" with { type: "json" };8import packageJson from "../package.json" with { type: "json" };
9import { authCommand } from "./cli/auth.ts";9import { authCommand } from "./cli/auth.ts";
10import { sharedServerCommandFlags } from "./cli/config.ts";10import { sharedServerCommandFlags } from "./cli/config.ts";
apps/server/src/bootstrap.test.ts+4-2
...@@ -1,13 +1,15 @@...@@ -1,13 +1,15 @@
1// @effect-diagnostics nodeBuiltinImport:off
1import * as NFS from "node:fs";2import * as NFS from "node:fs";
2import * as path from "node:path";3import * as path from "node:path";
3import { execFileSync, spawn } from "node:child_process";4import { execFileSync, spawn } from "node:child_process";
4import * as NodeServices from "@effect/platform-node/NodeServices";5import * as NodeServices from "@effect/platform-node/NodeServices";
5import { assert, it } from "@effect/vitest";6import { assert, it } from "@effect/vitest";
6import { FileSystem, Schema } from "effect";7import * as FileSystem from "effect/FileSystem";
8import * as Schema from "effect/Schema";
7import * as Duration from "effect/Duration";9import * as Duration from "effect/Duration";
8import * as Effect from "effect/Effect";10import * as Effect from "effect/Effect";
9import * as Fiber from "effect/Fiber";11import * as Fiber from "effect/Fiber";
10import { TestClock } from "effect/testing";12import * as TestClock from "effect/testing/TestClock";
11import { vi } from "vitest";13import { vi } from "vitest";
1214
13import { readBootstrapEnvelope, resolveFdPath } from "./bootstrap.ts";15import { readBootstrapEnvelope, resolveFdPath } from "./bootstrap.ts";
apps/server/src/bootstrap.ts+7-1
...@@ -1,9 +1,15 @@...@@ -1,9 +1,15 @@
1// @effect-diagnostics nodeBuiltinImport:off
1import * as NFS from "node:fs";2import * as NFS from "node:fs";
2import * as Net from "node:net";3import * as Net from "node:net";
3import * as readline from "node:readline";4import * as readline from "node:readline";
4import type { Readable } from "node:stream";5import 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";
7import { decodeJsonResult } from "@t3tools/shared/schemaJson";13import { decodeJsonResult } from "@t3tools/shared/schemaJson";
814
9class BootstrapError extends Data.TaggedError("BootstrapError")<{15class BootstrapError extends Data.TaggedError("BootstrapError")<{
apps/server/src/checkpointing/Errors.ts+1-1
...@@ -1,4 +1,4 @@...@@ -1,4 +1,4 @@
1import { Schema } from "effect";1import * as Schema from "effect/Schema";
2import type { ProjectionRepositoryError } from "../persistence/Errors.ts";2import type { ProjectionRepositoryError } from "../persistence/Errors.ts";
3import type { VcsError } from "@t3tools/contracts";3import type { VcsError } from "@t3tools/contracts";
44
apps/server/src/checkpointing/Layers/CheckpointDiffQuery.test.ts+3-1
...@@ -1,5 +1,7 @@...@@ -1,5 +1,7 @@
1import { CheckpointRef, ProjectId, ThreadId, TurnId } from "@t3tools/contracts";1import { 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";
3import { describe, expect, it } from "vitest";5import { describe, expect, it } from "vitest";
46
5import {7import {
apps/server/src/checkpointing/Layers/CheckpointDiffQuery.ts+4-1
...@@ -4,7 +4,10 @@ import {...@@ -4,7 +4,10 @@ import {
4 type OrchestrationGetFullThreadDiffResult,4 type OrchestrationGetFullThreadDiffResult,
5 type OrchestrationGetTurnDiffResult as OrchestrationGetTurnDiffResultType,5 type OrchestrationGetTurnDiffResult as OrchestrationGetTurnDiffResultType,
6} from "@t3tools/contracts";6} 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
9import { ProjectionSnapshotQuery } from "../../orchestration/Services/ProjectionSnapshotQuery.ts";12import { ProjectionSnapshotQuery } from "../../orchestration/Services/ProjectionSnapshotQuery.ts";
10import { CheckpointInvariantError, CheckpointUnavailableError } from "../Errors.ts";13import { CheckpointInvariantError, CheckpointUnavailableError } from "../Errors.ts";
apps/server/src/checkpointing/Layers/CheckpointStore.test.ts+6-1
...@@ -1,8 +1,13 @@...@@ -1,8 +1,13 @@
1// @effect-diagnostics nodeBuiltinImport:off
1import path from "node:path";2import path from "node:path";
23
3import * as NodeServices from "@effect/platform-node/NodeServices";4import * as NodeServices from "@effect/platform-node/NodeServices";
4import { it } from "@effect/vitest";5import { 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";
6import { describe, expect } from "vitest";11import { describe, expect } from "vitest";
712
8import { checkpointRefForThreadTurn } from "../Utils.ts";13import { checkpointRefForThreadTurn } from "../Utils.ts";
apps/server/src/checkpointing/Layers/CheckpointStore.ts+4-1
...@@ -11,7 +11,10 @@...@@ -11,7 +11,10 @@
11 */11 */
12import { randomUUID } from "node:crypto";12import { 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
16import { CheckpointInvariantError } from "../Errors.ts";19import { CheckpointInvariantError } from "../Errors.ts";
17import { VcsProcessExitError } from "@t3tools/contracts";20import { VcsProcessExitError } from "@t3tools/contracts";
apps/server/src/checkpointing/Services/CheckpointDiffQuery.ts+2-2
...@@ -12,8 +12,8 @@ import type {...@@ -12,8 +12,8 @@ import type {
12 OrchestrationGetTurnDiffInput,12 OrchestrationGetTurnDiffInput,
13 OrchestrationGetTurnDiffResult,13 OrchestrationGetTurnDiffResult,
14} from "@t3tools/contracts";14} from "@t3tools/contracts";
15import { Context } from "effect";15import * as Context from "effect/Context";
16import type { Effect } from "effect";16import type * as Effect from "effect/Effect";
1717
18import type { CheckpointServiceError } from "../Errors.ts";18import type { CheckpointServiceError } from "../Errors.ts";
1919
apps/server/src/checkpointing/Services/CheckpointStore.ts+2-2
...@@ -10,8 +10,8 @@...@@ -10,8 +10,8 @@
10 *10 *
11 * @module CheckpointStore11 * @module CheckpointStore
12 */12 */
13import { Context } from "effect";13import * as Context from "effect/Context";
14import type { Effect } from "effect";14import type * as Effect from "effect/Effect";
1515
16import type { CheckpointStoreError } from "../Errors.ts";16import type { CheckpointStoreError } from "../Errors.ts";
17import { CheckpointRef } from "@t3tools/contracts";17import { CheckpointRef } from "@t3tools/contracts";
apps/server/src/checkpointing/Utils.ts+1-1
...@@ -1,4 +1,4 @@...@@ -1,4 +1,4 @@
1import { Encoding } from "effect";1import * as Encoding from "effect/Encoding";
2import { CheckpointRef, ProjectId, type ThreadId } from "@t3tools/contracts";2import { CheckpointRef, ProjectId, type ThreadId } from "@t3tools/contracts";
33
4export const CHECKPOINT_REFS_PREFIX = "refs/t3/checkpoints";4export const CHECKPOINT_REFS_PREFIX = "refs/t3/checkpoints";
apps/server/src/cli/auth.ts+5-1
...@@ -1,5 +1,9 @@...@@ -1,5 +1,9 @@
1import { AuthSessionId } from "@t3tools/contracts";1import { 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";
3import { Argument, Command, Flag, GlobalFlag } from "effect/unstable/cli";7import { Argument, Command, Flag, GlobalFlag } from "effect/unstable/cli";
48
5import { AuthControlPlaneRuntimeLive } from "../auth/Layers/AuthControlPlane.ts";9import { AuthControlPlaneRuntimeLive } from "../auth/Layers/AuthControlPlane.ts";
apps/server/src/cli/config.test.ts+16-9
...@@ -1,13 +1,19 @@...@@ -1,13 +1,19 @@
1import os from "node:os";1import NodeOS from "node:os";
22
3import { assert, expect, it } from "@effect/vitest";3import { 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
6import {12import {
7 DesktopBackendBootstrap,13 DesktopBackendBootstrap,
8 type DesktopBackendBootstrap as DesktopBackendBootstrapValue,14 type DesktopBackendBootstrap as DesktopBackendBootstrapValue,
9} from "@t3tools/contracts";15} from "@t3tools/contracts";
10import { NetService } from "@t3tools/shared/Net";16import * as NetService from "@t3tools/shared/Net";
11import * as NodeServices from "@effect/platform-node/NodeServices";17import * as NodeServices from "@effect/platform-node/NodeServices";
12import { deriveServerPaths } from "../config.ts";18import { deriveServerPaths } from "../config.ts";
13import { resolveServerConfig } from "./config.ts";19import { resolveServerConfig } from "./config.ts";
...@@ -53,7 +59,7 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => {...@@ -53,7 +59,7 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => {
53 it.effect("falls back to effect/config values when flags are omitted", () =>59 it.effect("falls back to effect/config values when flags are omitted", () =>
54 Effect.gen(function* () {60 Effect.gen(function* () {
55 const { join } = yield* Path.Path;61 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");
57 const derivedPaths = yield* deriveServerPaths(baseDir, new URL("http://127.0.0.1:5173"));63 const derivedPaths = yield* deriveServerPaths(baseDir, new URL("http://127.0.0.1:5173"));
58 const resolved = yield* resolveServerConfig(64 const resolved = yield* resolveServerConfig(
59 {65 {
...@@ -119,7 +125,7 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => {...@@ -119,7 +125,7 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => {
119 it.effect("uses CLI flags when provided", () =>125 it.effect("uses CLI flags when provided", () =>
120 Effect.gen(function* () {126 Effect.gen(function* () {
121 const { join } = yield* Path.Path;127 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");
123 const derivedPaths = yield* deriveServerPaths(baseDir, new URL("http://127.0.0.1:4173"));129 const derivedPaths = yield* deriveServerPaths(baseDir, new URL("http://127.0.0.1:4173"));
124 const resolved = yield* resolveServerConfig(130 const resolved = yield* resolveServerConfig(
125 {131 {
...@@ -147,7 +153,7 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => {...@@ -147,7 +153,7 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => {
147 T3CODE_MODE: "desktop",153 T3CODE_MODE: "desktop",
148 T3CODE_PORT: "4001",154 T3CODE_PORT: "4001",
149 T3CODE_HOST: "0.0.0.0",155 T3CODE_HOST: "0.0.0.0",
150 T3CODE_HOME: join(os.tmpdir(), "ignored-base"),156 T3CODE_HOME: join(NodeOS.tmpdir(), "ignored-base"),
151 VITE_DEV_SERVER_URL: "http://127.0.0.1:5173",157 VITE_DEV_SERVER_URL: "http://127.0.0.1:5173",
152 T3CODE_NO_BROWSER: "false",158 T3CODE_NO_BROWSER: "false",
153 T3CODE_AUTO_BOOTSTRAP_PROJECT_FROM_CWD: "false",159 T3CODE_AUTO_BOOTSTRAP_PROJECT_FROM_CWD: "false",
...@@ -185,7 +191,7 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => {...@@ -185,7 +191,7 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => {
185 it.effect("preserves explicit false CLI boolean flags over env and bootstrap values", () =>191 it.effect("preserves explicit false CLI boolean flags over env and bootstrap values", () =>
186 Effect.gen(function* () {192 Effect.gen(function* () {
187 const { join } = yield* Path.Path;193 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");
189 const fd = yield* openBootstrapFd(195 const fd = yield* openBootstrapFd(
190 makeDesktopBootstrap({196 makeDesktopBootstrap({
191 noBrowser: true,197 noBrowser: true,
...@@ -378,7 +384,7 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => {...@@ -378,7 +384,7 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => {
378 it.effect("applies flag then env precedence over bootstrap envelope values", () =>384 it.effect("applies flag then env precedence over bootstrap envelope values", () =>
379 Effect.gen(function* () {385 Effect.gen(function* () {
380 const { join } = yield* Path.Path;386 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");
382 const fd = yield* openBootstrapFd(388 const fd = yield* openBootstrapFd(
383 makeDesktopBootstrap({389 makeDesktopBootstrap({
384 port: 4888,390 port: 4888,
...@@ -459,6 +465,7 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => {...@@ -459,6 +465,7 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => {
459 yield* fs.makeDirectory(path.dirname(derivedPaths.settingsPath), { recursive: true });465 yield* fs.makeDirectory(path.dirname(derivedPaths.settingsPath), { recursive: true });
460 yield* fs.writeFileString(466 yield* fs.writeFileString(
461 derivedPaths.settingsPath,467 derivedPaths.settingsPath,
468 // @effect-diagnostics-next-line preferSchemaOverJson:off
462 `${JSON.stringify({469 `${JSON.stringify({
463 observability: {470 observability: {
464 otlpTracesUrl: "http://localhost:4318/v1/traces",471 otlpTracesUrl: "http://localhost:4318/v1/traces",
...@@ -521,7 +528,7 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => {...@@ -521,7 +528,7 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => {
521 it.effect("forces noBrowser and disables auto-bootstrap for headless startup presentation", () =>528 it.effect("forces noBrowser and disables auto-bootstrap for headless startup presentation", () =>
522 Effect.gen(function* () {529 Effect.gen(function* () {
523 const { join } = yield* Path.Path;530 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");
525 const derivedPaths = yield* deriveServerPaths(baseDir, undefined);532 const derivedPaths = yield* deriveServerPaths(baseDir, undefined);
526533
527 const resolved = yield* resolveServerConfig(534 const resolved = yield* resolveServerConfig(
apps/server/src/cli/config.ts+12-14
...@@ -1,18 +1,16 @@...@@ -1,18 +1,16 @@
1import { NetService } from "@t3tools/shared/Net";1import * as NetService from "@t3tools/shared/Net";
2import { parsePersistedServerObservabilitySettings } from "@t3tools/shared/serverSettings";2import { parsePersistedServerObservabilitySettings } from "@t3tools/shared/serverSettings";
3import { DesktopBackendBootstrap, PortSchema } from "@t3tools/contracts";3import { DesktopBackendBootstrap, PortSchema } from "@t3tools/contracts";
4import {4import * as Config from "effect/Config";
5 Config,5import * as Duration from "effect/Duration";
6 Duration,6import * as Effect from "effect/Effect";
7 Effect,7import * as FileSystem from "effect/FileSystem";
8 FileSystem,8import * as LogLevel from "effect/LogLevel";
9 LogLevel,9import * as Option from "effect/Option";
10 Option,10import * as Path from "effect/Path";
11 Path,11import * as Schema from "effect/Schema";
12 Schema,12import * as SchemaIssue from "effect/SchemaIssue";
13 SchemaIssue,13import * as SchemaTransformation from "effect/SchemaTransformation";
14 SchemaTransformation,
15} from "effect";
16import { Argument, Flag } from "effect/unstable/cli";14import { Argument, Flag } from "effect/unstable/cli";
1715
18import { readBootstrapEnvelope } from "../bootstrap.ts";16import { readBootstrapEnvelope } from "../bootstrap.ts";
...@@ -215,7 +213,7 @@ export const resolveServerConfig = (...@@ -215,7 +213,7 @@ export const resolveServerConfig = (
215 },213 },
216) =>214) =>
217 Effect.gen(function* () {215 Effect.gen(function* () {
218 const { findAvailablePort } = yield* NetService;216 const { findAvailablePort } = yield* NetService.NetService;
219 const path = yield* Path.Path;217 const path = yield* Path.Path;
220 const fs = yield* FileSystem.FileSystem;218 const fs = yield* FileSystem.FileSystem;
221 const env = yield* EnvServerConfig;219 const env = yield* EnvServerConfig;
apps/server/src/cli/project.ts+27-21
...@@ -4,18 +4,18 @@ import {...@@ -4,18 +4,18 @@ import {
4 ProjectId,4 ProjectId,
5 type ClientOrchestrationCommand,5 type ClientOrchestrationCommand,
6} from "@t3tools/contracts";6} from "@t3tools/contracts";
7import {7import * as Console from "effect/Console";
8 Console,8import * as Data from "effect/Data";
9 Duration,9import * as DateTime from "effect/DateTime";
10 Effect,10import * as Duration from "effect/Duration";
11 Exit,11import * as Effect from "effect/Effect";
12 FileSystem,12import * as Exit from "effect/Exit";
13 Layer,13import * as FileSystem from "effect/FileSystem";
14 Option,14import * as Layer from "effect/Layer";
15 Path,15import * as Option from "effect/Option";
16 References,16import * as Path from "effect/Path";
17 Schema,17import * as References from "effect/References";
18} from "effect";18import * as Schema from "effect/Schema";
19import { Argument, Command, Flag, GlobalFlag } from "effect/unstable/cli";19import { Argument, Command, Flag, GlobalFlag } from "effect/unstable/cli";
20import {20import {
21 FetchHttpClient,21 FetchHttpClient,
...@@ -54,6 +54,10 @@ type ProjectCliDispatchCommand = Extract<...@@ -54,6 +54,10 @@ type ProjectCliDispatchCommand = Extract<
54 { type: "project.create" | "project.meta.update" | "project.delete" }54 { type: "project.create" | "project.meta.update" | "project.delete" }
55>;55>;
5656
57class ProjectCommandError extends Data.TaggedError("ProjectCommandError")<{
58 readonly message: string;
59}> {}
60
57const ProjectCliRuntimeLive = Layer.mergeAll(61const ProjectCliRuntimeLive = Layer.mergeAll(
58 WorkspacePathsLive,62 WorkspacePathsLive,
59 OrchestrationLayerLive.pipe(63 OrchestrationLayerLive.pipe(
...@@ -124,7 +128,7 @@ const resolveProjectTitle = Effect.fn("resolveProjectTitle")(function* (...@@ -124,7 +128,7 @@ const resolveProjectTitle = Effect.fn("resolveProjectTitle")(function* (
124 if (trimmed.length > 0) {128 if (trimmed.length > 0) {
125 return trimmed;129 return trimmed;
126 }130 }
127 return yield* Effect.fail(new Error("Project title cannot be empty."));131 return yield* new ProjectCommandError({ message: "Project title cannot be empty." });
128 }132 }
129133
130 const path = yield* Path.Path;134 const path = yield* Path.Path;
...@@ -138,7 +142,7 @@ const findActiveProjectTarget = Effect.fn("findActiveProjectTarget")(function* (...@@ -138,7 +142,7 @@ const findActiveProjectTarget = Effect.fn("findActiveProjectTarget")(function* (
138}) {142}) {
139 const trimmedIdentifier = input.identifier.trim();143 const trimmedIdentifier = input.identifier.trim();
140 if (trimmedIdentifier.length === 0) {144 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." });
142 }146 }
143147
144 const activeProjects = input.snapshot.projects.filter((project) => project.deletedAt === null);148 const activeProjects = input.snapshot.projects.filter((project) => project.deletedAt === null);
...@@ -165,7 +169,9 @@ const findActiveProjectTarget = Effect.fn("findActiveProjectTarget")(function* (...@@ -165,7 +169,9 @@ const findActiveProjectTarget = Effect.fn("findActiveProjectTarget")(function* (
165169
166 const resolved = exactWorkspaceMatch;170 const resolved = exactWorkspaceMatch;
167 if (!resolved) {171 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 });
169 }175 }
170176
171 return {177 return {
...@@ -185,7 +191,7 @@ const fetchLiveOrchestrationSnapshot = (origin: string, bearerToken: string) =>...@@ -185,7 +191,7 @@ const fetchLiveOrchestrationSnapshot = (origin: string, bearerToken: string) =>
185 "2xx": decodeOrchestrationReadModelResponse,191 "2xx": decodeOrchestrationReadModelResponse,
186 orElse: (response) =>192 orElse: (response) =>
187 readErrorMessageFromResponse(response).pipe(193 readErrorMessageFromResponse(response).pipe(
188 Effect.flatMap((message) => Effect.fail(new Error(message))),194 Effect.flatMap((message) => Effect.fail(new ProjectCommandError({ message }))),
189 ),195 ),
190 }),196 }),
191 );197 );
...@@ -206,7 +212,7 @@ const dispatchLiveOrchestrationCommand = (...@@ -206,7 +212,7 @@ const dispatchLiveOrchestrationCommand = (
206 "2xx": () => Effect.void,212 "2xx": () => Effect.void,
207 orElse: (response) =>213 orElse: (response) =>
208 readErrorMessageFromResponse(response).pipe(214 readErrorMessageFromResponse(response).pipe(
209 Effect.flatMap((message) => Effect.fail(new Error(message))),215 Effect.flatMap((message) => Effect.fail(new ProjectCommandError({ message }))),
210 ),216 ),
211 }),217 }),
212 ),218 ),
...@@ -331,9 +337,9 @@ const projectAddCommand = Command.make("add", {...@@ -331,9 +337,9 @@ const projectAddCommand = Command.make("add", {
331 (project) => project.deletedAt === null && project.workspaceRoot === workspaceRoot,337 (project) => project.deletedAt === null && project.workspaceRoot === workspaceRoot,
332 );338 );
333 if (existingProject) {339 if (existingProject) {
334 return yield* Effect.fail(340 return yield* new ProjectCommandError({
335 new Error(`An active project already exists for '${workspaceRoot}'.`),341 message: `An active project already exists for '${workspaceRoot}'.`,
336 );342 });
337 }343 }
338344
339 const title = yield* resolveProjectTitle(workspaceRoot, Option.getOrUndefined(flags.title));345 const title = yield* resolveProjectTitle(workspaceRoot, Option.getOrUndefined(flags.title));
...@@ -345,7 +351,7 @@ const projectAddCommand = Command.make("add", {...@@ -345,7 +351,7 @@ const projectAddCommand = Command.make("add", {
345 title,351 title,
346 workspaceRoot,352 workspaceRoot,
347 defaultModelSelection: getAutoBootstrapDefaultModelSelection(),353 defaultModelSelection: getAutoBootstrapDefaultModelSelection(),
348 createdAt: new Date().toISOString(),354 createdAt: DateTime.formatIso(yield* DateTime.now),
349 });355 });
350 return `Added project ${projectId} (${title}) at ${workspaceRoot}.`;356 return `Added project ${projectId} (${title}) at ${workspaceRoot}.`;
351 }),357 }),
apps/server/src/cli/server.ts+1-1
...@@ -1,4 +1,4 @@...@@ -1,4 +1,4 @@
1import { Effect } from "effect";1import * as Effect from "effect/Effect";
2import { Command, GlobalFlag } from "effect/unstable/cli";2import { Command, GlobalFlag } from "effect/unstable/cli";
33
4import { ServerConfig, type StartupPresentation } from "../config.ts";4import { ServerConfig, type StartupPresentation } from "../config.ts";
apps/server/src/cliAuthFormat.test.ts+8-8
...@@ -1,5 +1,5 @@...@@ -1,5 +1,5 @@
1import { expect, it } from "@effect/vitest";1import { expect, it } from "@effect/vitest";
2import { DateTime } from "effect";2import * as DateTime from "effect/DateTime";
33
4import {4import {
5 formatIssuedPairingCredential,5 formatIssuedPairingCredential,
...@@ -15,8 +15,8 @@ it("formats issued pairing credentials with the secret and optional pair URL", (...@@ -15,8 +15,8 @@ it("formats issued pairing credentials with the secret and optional pair URL", (
15 credential: "secret-pairing-token",15 credential: "secret-pairing-token",
16 role: "client",16 role: "client",
17 subject: "one-time-token",17 subject: "one-time-token",
18 createdAt: DateTime.fromDateUnsafe(new Date("2026-04-08T09:00:00.000Z")),18 createdAt: DateTime.makeUnsafe("2026-04-08T09:00:00.000Z"),
19 expiresAt: DateTime.fromDateUnsafe(new Date("2026-04-08T10:00:00.000Z")),19 expiresAt: DateTime.makeUnsafe("2026-04-08T10:00:00.000Z"),
20 },20 },
21 { baseUrl: "https://example.com", json: false },21 { baseUrl: "https://example.com", json: false },
22 );22 );
...@@ -34,8 +34,8 @@ it("formats pairing listings without exposing the secret token", () => {...@@ -34,8 +34,8 @@ it("formats pairing listings without exposing the secret token", () => {
34 subject: "one-time-token",34 subject: "one-time-token",
35 label: "Phone",35 label: "Phone",
36 role: "client",36 role: "client",
37 createdAt: DateTime.fromDateUnsafe(new Date("2026-04-08T09:00:00.000Z")),37 createdAt: DateTime.makeUnsafe("2026-04-08T09:00:00.000Z"),
38 expiresAt: DateTime.fromDateUnsafe(new Date("2026-04-08T10:00:00.000Z")),38 expiresAt: DateTime.makeUnsafe("2026-04-08T10:00:00.000Z"),
39 },39 },
40 ],40 ],
41 { json: false },41 { json: false },
...@@ -57,7 +57,7 @@ it("formats issued sessions with the bearer token but omits tokens from listings...@@ -57,7 +57,7 @@ it("formats issued sessions with the bearer token but omits tokens from listings
57 label: "deploy-bot",57 label: "deploy-bot",
58 deviceType: "bot",58 deviceType: "bot",
59 },59 },
60 expiresAt: DateTime.fromDateUnsafe(new Date("2026-04-08T10:00:00.000Z")),60 expiresAt: DateTime.makeUnsafe("2026-04-08T10:00:00.000Z"),
61 },61 },
62 { json: false },62 { json: false },
63 );63 );
...@@ -75,8 +75,8 @@ it("formats issued sessions with the bearer token but omits tokens from listings...@@ -75,8 +75,8 @@ it("formats issued sessions with the bearer token but omits tokens from listings
75 },75 },
76 connected: false,76 connected: false,
77 current: false,77 current: false,
78 issuedAt: DateTime.fromDateUnsafe(new Date("2026-04-08T09:00:00.000Z")),78 issuedAt: DateTime.makeUnsafe("2026-04-08T09:00:00.000Z"),
79 expiresAt: DateTime.fromDateUnsafe(new Date("2026-04-08T10:00:00.000Z")),79 expiresAt: DateTime.makeUnsafe("2026-04-08T10:00:00.000Z"),
80 lastConnectedAt: null,80 lastConnectedAt: null,
81 },81 },
82 ],82 ],
apps/server/src/cliAuthFormat.ts+1-1
...@@ -1,5 +1,5 @@...@@ -1,5 +1,5 @@
1import type { AuthClientMetadata, AuthClientSession, AuthPairingLink } from "@t3tools/contracts";1import type { AuthClientMetadata, AuthClientSession, AuthPairingLink } from "@t3tools/contracts";
2import { DateTime } from "effect";2import * as DateTime from "effect/DateTime";
33
4import type { IssuedBearerSession, IssuedPairingLink } from "./auth/Services/AuthControlPlane.ts";4import type { IssuedBearerSession, IssuedPairingLink } from "./auth/Services/AuthControlPlane.ts";
55
apps/server/src/config.ts+7-1
...@@ -6,7 +6,13 @@...@@ -6,7 +6,13 @@
6 *6 *
7 * @module ServerConfig7 * @module ServerConfig
8 */8 */
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
11export const DEFAULT_PORT = 3773;17export const DEFAULT_PORT = 3773;
1218
apps/server/src/diagnostics/TraceDiagnostics.ts+5-14
...@@ -89,24 +89,14 @@ function toNumberValue(value: unknown): number | null {...@@ -89,24 +89,14 @@ function toNumberValue(value: unknown): number | null {
89function unixNanoToDateTime(value: unknown): DateTime.Utc | null {89function unixNanoToDateTime(value: unknown): DateTime.Utc | null {
90 const text = toStringValue(value);90 const text = toStringValue(value);
91 if (!text) return null;91 if (!text) return null;
92
93 try {92 try {
94 const millis = Number(BigInt(text) / 1_000_000n);93 const millis = Number(BigInt(text) / 1_000_000n);
95 if (!Number.isFinite(millis)) return null;94 return Option.getOrNull(DateTime.make(millis));
96 return DateTime.makeUnsafe(millis);
97 } catch {95 } catch {
98 return null;96 return null;
99 }97 }
100}98}
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
110function readExitTag(exit: unknown): string | null {100function readExitTag(exit: unknown): string | null {
111 if (!isRecordObject(exit) || !("_tag" in exit)) return null;101 if (!isRecordObject(exit) || !("_tag" in exit)) return null;
112 return toStringValue(exit._tag);102 return toStringValue(exit._tag);
...@@ -254,10 +244,11 @@ export function aggregateTraceDiagnostics(...@@ -254,10 +244,11 @@ export function aggregateTraceDiagnostics(
254244
255 recordCount += 1;245 recordCount += 1;
256 firstSpanAt =246 firstSpanAt =
257 startedAt && (firstSpanAt === null || isBefore(startedAt, firstSpanAt))247 startedAt && (firstSpanAt === null || DateTime.isLessThan(startedAt, firstSpanAt))
258 ? startedAt248 ? startedAt
259 : firstSpanAt;249 : firstSpanAt;
260 lastSpanAt = lastSpanAt === null || isAfter(endedAt, lastSpanAt) ? endedAt : lastSpanAt;250 lastSpanAt =
251 lastSpanAt === null || DateTime.isGreaterThan(endedAt, lastSpanAt) ? endedAt : lastSpanAt;
261252
262 const exitTag = readExitTag(parsed.exit);253 const exitTag = readExitTag(parsed.exit);
263 const isFailure = exitTag === "Failure";254 const isFailure = exitTag === "Failure";
...@@ -289,7 +280,7 @@ export function aggregateTraceDiagnostics(...@@ -289,7 +280,7 @@ export function aggregateTraceDiagnostics(
289280
290 const failureKey = `${name}\0${cause}`;281 const failureKey = `${name}\0${cause}`;
291 const existing = failuresByKey.get(failureKey);282 const existing = failuresByKey.get(failureKey);
292 const isLatestFailure = !existing || isAfter(endedAt, existing.lastSeenAt);283 const isLatestFailure = !existing || DateTime.isGreaterThan(endedAt, existing.lastSeenAt);
293 failuresByKey.set(failureKey, {284 failuresByKey.set(failureKey, {
294 name,285 name,
295 cause,286 cause,
apps/server/src/environment/Layers/ServerEnvironment.test.ts+6-1
...@@ -1,7 +1,12 @@...@@ -1,7 +1,12 @@
1// @effect-diagnostics nodeBuiltinImport:off
1import * as nodePath from "node:path";2import * as nodePath from "node:path";
2import * as NodeServices from "@effect/platform-node/NodeServices";3import * as NodeServices from "@effect/platform-node/NodeServices";
3import { expect, it } from "@effect/vitest";4import { 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
6import { deriveServerPaths, ServerConfig, type ServerConfigShape } from "../../config.ts";11import { deriveServerPaths, ServerConfig, type ServerConfigShape } from "../../config.ts";
7import { ServerEnvironment } from "../Services/ServerEnvironment.ts";12import { ServerEnvironment } from "../Services/ServerEnvironment.ts";
apps/server/src/environment/Layers/ServerEnvironment.ts+5-1
...@@ -1,5 +1,9 @@...@@ -1,5 +1,9 @@
1import { EnvironmentId, type ExecutionEnvironmentDescriptor } from "@t3tools/contracts";1import { 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
4import { ServerConfig } from "../../config.ts";8import { ServerConfig } from "../../config.ts";
5import { ServerEnvironment, type ServerEnvironmentShape } from "../Services/ServerEnvironment.ts";9import { ServerEnvironment, type ServerEnvironmentShape } from "../Services/ServerEnvironment.ts";
apps/server/src/environment/Layers/ServerEnvironmentLabel.test.ts+2-1
...@@ -1,5 +1,6 @@...@@ -1,5 +1,6 @@
1import { afterEach, describe, expect, it } from "@effect/vitest";1import { afterEach, describe, expect, it } from "@effect/vitest";
2import { Effect, FileSystem } from "effect";2import * as Effect from "effect/Effect";
3import * as FileSystem from "effect/FileSystem";
3import { vi } from "vitest";4import { vi } from "vitest";
45
5vi.mock("../../processRunner.ts", () => ({6vi.mock("../../processRunner.ts", () => ({
apps/server/src/environment/Layers/ServerEnvironmentLabel.ts+2-1
...@@ -1,6 +1,7 @@...@@ -1,6 +1,7 @@
1import * as OS from "node:os";1import * as OS from "node:os";
22
3import { Effect, FileSystem } from "effect";3import * as Effect from "effect/Effect";
4import * as FileSystem from "effect/FileSystem";
45
5import { runProcess } from "../../processRunner.ts";6import { runProcess } from "../../processRunner.ts";
67
apps/server/src/environment/Services/ServerEnvironment.ts+2-2
...@@ -1,6 +1,6 @@...@@ -1,6 +1,6 @@
1import type { EnvironmentId, ExecutionEnvironmentDescriptor } from "@t3tools/contracts";1import type { EnvironmentId, ExecutionEnvironmentDescriptor } from "@t3tools/contracts";
2import { Context } from "effect";2import * as Context from "effect/Context";
3import type { Effect } from "effect";3import type * as Effect from "effect/Effect";
44
5export interface ServerEnvironmentShape {5export interface ServerEnvironmentShape {
6 readonly getEnvironmentId: Effect.Effect<EnvironmentId>;6 readonly getEnvironmentId: Effect.Effect<EnvironmentId>;
apps/server/src/git/GitManager.test.ts+58-4
...@@ -1,10 +1,15 @@...@@ -1,10 +1,15 @@
1// @effect-diagnostics nodeBuiltinImport:off
1import fs from "node:fs";2import fs from "node:fs";
2import path from "node:path";3import path from "node:path";
3import { spawnSync } from "node:child_process";4import { spawnSync } from "node:child_process";
45
5import * as NodeServices from "@effect/platform-node/NodeServices";6import * as NodeServices from "@effect/platform-node/NodeServices";
6import { it } from "@effect/vitest";7import { 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";
8import { ChildProcessSpawner } from "effect/unstable/process";13import { ChildProcessSpawner } from "effect/unstable/process";
9import { expect } from "vitest";14import { expect } from "vitest";
10import type {15import type {
...@@ -32,6 +37,7 @@ import { ServerConfig } from "../config.ts";...@@ -32,6 +37,7 @@ import { ServerConfig } from "../config.ts";
32import { ServerSettingsService } from "../serverSettings.ts";37import { ServerSettingsService } from "../serverSettings.ts";
33import {38import {
34 ProjectSetupScriptRunner,39 ProjectSetupScriptRunner,
40 ProjectSetupScriptRunnerError,
35 type ProjectSetupScriptRunnerInput,41 type ProjectSetupScriptRunnerInput,
36 type ProjectSetupScriptRunnerShape,42 type ProjectSetupScriptRunnerShape,
37} from "../project/Services/ProjectSetupScriptRunner.ts";43} from "../project/Services/ProjectSetupScriptRunner.ts";
...@@ -713,6 +719,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {...@@ -713,6 +719,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
713 const { manager } = yield* makeManager({719 const { manager } = yield* makeManager({
714 ghScenario: {720 ghScenario: {
715 prListSequence: [721 prListSequence: [
722 // @effect-diagnostics-next-line preferSchemaOverJson:off
716 JSON.stringify([723 JSON.stringify([
717 {724 {
718 number: 13,725 number: 13,
...@@ -754,6 +761,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {...@@ -754,6 +761,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
754 const { manager } = yield* makeManager({761 const { manager } = yield* makeManager({
755 ghScenario: {762 ghScenario: {
756 prListSequence: [763 prListSequence: [
764 // @effect-diagnostics-next-line preferSchemaOverJson:off
757 JSON.stringify([765 JSON.stringify([
758 {766 {
759 number: 14,767 number: 14,
...@@ -792,6 +800,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {...@@ -792,6 +800,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
792 const { manager } = yield* makeManager({800 const { manager } = yield* makeManager({
793 ghScenario: {801 ghScenario: {
794 prListSequence: [802 prListSequence: [
803 // @effect-diagnostics-next-line preferSchemaOverJson:off
795 JSON.stringify([804 JSON.stringify([
796 {805 {
797 number: 0,806 number: 0,
...@@ -843,6 +852,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {...@@ -843,6 +852,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
843 const { manager } = yield* makeManager({852 const { manager } = yield* makeManager({
844 ghScenario: {853 ghScenario: {
845 prListSequence: [854 prListSequence: [
855 // @effect-diagnostics-next-line preferSchemaOverJson:off
846 JSON.stringify([856 JSON.stringify([
847 {857 {
848 number: 16,858 number: 16,
...@@ -955,6 +965,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {...@@ -955,6 +965,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
955 };965 };
956 const { manager, ghCalls } = yield* makeManager({966 const { manager, ghCalls } = yield* makeManager({
957 ghScenario: {967 ghScenario: {
968 // @effect-diagnostics-next-line preferSchemaOverJson:off
958 prListSequence: [JSON.stringify([existingPr]), JSON.stringify([existingPr])],969 prListSequence: [JSON.stringify([existingPr]), JSON.stringify([existingPr])],
959 },970 },
960 });971 });
...@@ -981,6 +992,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {...@@ -981,6 +992,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
981 const { manager } = yield* makeManager({992 const { manager } = yield* makeManager({
982 ghScenario: {993 ghScenario: {
983 prListSequence: [994 prListSequence: [
995 // @effect-diagnostics-next-line preferSchemaOverJson:off
984 JSON.stringify([996 JSON.stringify([
985 {997 {
986 number: 1661,998 number: 1661,
...@@ -1034,8 +1046,11 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {...@@ -1034,8 +1046,11 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
1034 const { manager, ghCalls } = yield* makeManager({1046 const { manager, ghCalls } = yield* makeManager({
1035 ghScenario: {1047 ghScenario: {
1036 prListSequence: [1048 prListSequence: [
1049 // @effect-diagnostics-next-line preferSchemaOverJson:off
1037 JSON.stringify([]),1050 JSON.stringify([]),
1051 // @effect-diagnostics-next-line preferSchemaOverJson:off
1038 JSON.stringify([]),1052 JSON.stringify([]),
1053 // @effect-diagnostics-next-line preferSchemaOverJson:off
1039 JSON.stringify([1054 JSON.stringify([
1040 {1055 {
1041 number: 488,1056 number: 488,
...@@ -1110,6 +1125,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {...@@ -1110,6 +1125,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
1110 const { manager, ghCalls } = yield* makeManager({1125 const { manager, ghCalls } = yield* makeManager({
1111 ghScenario: {1126 ghScenario: {
1112 prListByHeadSelector: {1127 prListByHeadSelector: {
1128 // @effect-diagnostics-next-line preferSchemaOverJson:off
1113 "effect-atom": JSON.stringify([1129 "effect-atom": JSON.stringify([
1114 {1130 {
1115 number: 1618,1131 number: 1618,
...@@ -1121,6 +1137,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {...@@ -1121,6 +1137,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
1121 updatedAt: "2026-03-01T10:00:00Z",1137 updatedAt: "2026-03-01T10:00:00Z",
1122 },1138 },
1123 ]),1139 ]),
1140 // @effect-diagnostics-next-line preferSchemaOverJson:off
1124 "upstream/effect-atom": JSON.stringify([1141 "upstream/effect-atom": JSON.stringify([
1125 {1142 {
1126 number: 1518,1143 number: 1518,
...@@ -1132,8 +1149,11 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {...@@ -1132,8 +1149,11 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
1132 updatedAt: "2026-04-01T10:00:00Z",1149 updatedAt: "2026-04-01T10:00:00Z",
1133 },1150 },
1134 ]),1151 ]),
1152 // @effect-diagnostics-next-line preferSchemaOverJson:off
1135 "pingdotgg:effect-atom": JSON.stringify([]),1153 "pingdotgg:effect-atom": JSON.stringify([]),
1154 // @effect-diagnostics-next-line preferSchemaOverJson:off
1136 "my-org/upstream:effect-atom": JSON.stringify([]),1155 "my-org/upstream:effect-atom": JSON.stringify([]),
1156 // @effect-diagnostics-next-line preferSchemaOverJson:off
1137 "pingdotgg:upstream/effect-atom": JSON.stringify([1157 "pingdotgg:upstream/effect-atom": JSON.stringify([
1138 {1158 {
1139 number: 1518,1159 number: 1518,
...@@ -1145,6 +1165,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {...@@ -1145,6 +1165,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
1145 updatedAt: "2026-04-01T10:00:00Z",1165 updatedAt: "2026-04-01T10:00:00Z",
1146 },1166 },
1147 ]),1167 ]),
1168 // @effect-diagnostics-next-line preferSchemaOverJson:off
1148 "my-org/upstream:upstream/effect-atom": JSON.stringify([1169 "my-org/upstream:upstream/effect-atom": JSON.stringify([
1149 {1170 {
1150 number: 1518,1171 number: 1518,
...@@ -1194,6 +1215,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {...@@ -1194,6 +1215,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
1194 const { manager } = yield* makeManager({1215 const { manager } = yield* makeManager({
1195 ghScenario: {1216 ghScenario: {
1196 prListSequence: [1217 prListSequence: [
1218 // @effect-diagnostics-next-line preferSchemaOverJson:off
1197 JSON.stringify([1219 JSON.stringify([
1198 {1220 {
1199 number: 22,1221 number: 22,
...@@ -1231,6 +1253,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {...@@ -1231,6 +1253,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
1231 const { manager } = yield* makeManager({1253 const { manager } = yield* makeManager({
1232 ghScenario: {1254 ghScenario: {
1233 prListSequence: [1255 prListSequence: [
1256 // @effect-diagnostics-next-line preferSchemaOverJson:off
1234 JSON.stringify([1257 JSON.stringify([
1235 {1258 {
1236 number: 23,1259 number: 23,
...@@ -1262,6 +1285,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {...@@ -1262,6 +1285,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
1262 const { manager } = yield* makeManager({1285 const { manager } = yield* makeManager({
1263 ghScenario: {1286 ghScenario: {
1264 prListSequence: [1287 prListSequence: [
1288 // @effect-diagnostics-next-line preferSchemaOverJson:off
1265 JSON.stringify([1289 JSON.stringify([
1266 {1290 {
1267 number: 45,1291 number: 45,
...@@ -1612,6 +1636,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {...@@ -1612,6 +1636,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
1612 ghScenario: {1636 ghScenario: {
1613 prListSequence: [1637 prListSequence: [
1614 "[]",1638 "[]",
1639 // @effect-diagnostics-next-line preferSchemaOverJson:off
1615 JSON.stringify([1640 JSON.stringify([
1616 {1641 {
1617 number: 77,1642 number: 77,
...@@ -1747,6 +1772,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {...@@ -1747,6 +1772,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
1747 ghScenario: {1772 ghScenario: {
1748 prListSequence: [1773 prListSequence: [
1749 "[]",1774 "[]",
1775 // @effect-diagnostics-next-line preferSchemaOverJson:off
1750 JSON.stringify([1776 JSON.stringify([
1751 {1777 {
1752 number: 303,1778 number: 303,
...@@ -1793,6 +1819,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {...@@ -1793,6 +1819,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
1793 ghScenario: {1819 ghScenario: {
1794 prListSequence: [1820 prListSequence: [
1795 "[]",1821 "[]",
1822 // @effect-diagnostics-next-line preferSchemaOverJson:off
1796 JSON.stringify([1823 JSON.stringify([
1797 {1824 {
1798 number: 404,1825 number: 404,
...@@ -1833,6 +1860,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {...@@ -1833,6 +1860,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
1833 const { manager, ghCalls } = yield* makeManager({1860 const { manager, ghCalls } = yield* makeManager({
1834 ghScenario: {1861 ghScenario: {
1835 prListSequence: [1862 prListSequence: [
1863 // @effect-diagnostics-next-line preferSchemaOverJson:off
1836 JSON.stringify([1864 JSON.stringify([
1837 {1865 {
1838 number: 42,1866 number: 42,
...@@ -1886,7 +1914,9 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {...@@ -1886,7 +1914,9 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
1886 const { manager, ghCalls } = yield* makeManager({1914 const { manager, ghCalls } = yield* makeManager({
1887 ghScenario: {1915 ghScenario: {
1888 prListSequence: [1916 prListSequence: [
1917 // @effect-diagnostics-next-line preferSchemaOverJson:off
1889 JSON.stringify([]),1918 JSON.stringify([]),
1919 // @effect-diagnostics-next-line preferSchemaOverJson:off
1890 JSON.stringify([1920 JSON.stringify([
1891 {1921 {
1892 number: 142,1922 number: 142,
...@@ -1963,6 +1993,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {...@@ -1963,6 +1993,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
1963 const { manager, ghCalls } = yield* makeManager({1993 const { manager, ghCalls } = yield* makeManager({
1964 ghScenario: {1994 ghScenario: {
1965 prListByHeadSelector: {1995 prListByHeadSelector: {
1996 // @effect-diagnostics-next-line preferSchemaOverJson:off
1966 "effect-atom": JSON.stringify([1997 "effect-atom": JSON.stringify([
1967 {1998 {
1968 number: 1618,1999 number: 1618,
...@@ -1972,6 +2003,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {...@@ -1972,6 +2003,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
1972 headRefName: "effect-atom",2003 headRefName: "effect-atom",
1973 },2004 },
1974 ]),2005 ]),
2006 // @effect-diagnostics-next-line preferSchemaOverJson:off
1975 "upstream/effect-atom": JSON.stringify([2007 "upstream/effect-atom": JSON.stringify([
1976 {2008 {
1977 number: 1518,2009 number: 1518,
...@@ -1981,8 +2013,11 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {...@@ -1981,8 +2013,11 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
1981 headRefName: "upstream/effect-atom",2013 headRefName: "upstream/effect-atom",
1982 },2014 },
1983 ]),2015 ]),
2016 // @effect-diagnostics-next-line preferSchemaOverJson:off
1984 "pingdotgg:effect-atom": JSON.stringify([]),2017 "pingdotgg:effect-atom": JSON.stringify([]),
2018 // @effect-diagnostics-next-line preferSchemaOverJson:off
1985 "my-org/upstream:effect-atom": JSON.stringify([]),2019 "my-org/upstream:effect-atom": JSON.stringify([]),
2020 // @effect-diagnostics-next-line preferSchemaOverJson:off
1986 "pingdotgg:upstream/effect-atom": JSON.stringify([2021 "pingdotgg:upstream/effect-atom": JSON.stringify([
1987 {2022 {
1988 number: 1518,2023 number: 1518,
...@@ -1992,6 +2027,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {...@@ -1992,6 +2027,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
1992 headRefName: "upstream/effect-atom",2027 headRefName: "upstream/effect-atom",
1993 },2028 },
1994 ]),2029 ]),
2030 // @effect-diagnostics-next-line preferSchemaOverJson:off
1995 "my-org/upstream:upstream/effect-atom": JSON.stringify([2031 "my-org/upstream:upstream/effect-atom": JSON.stringify([
1996 {2032 {
1997 number: 1518,2033 number: 1518,
...@@ -2041,7 +2077,9 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {...@@ -2041,7 +2077,9 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
2041 const { manager, ghCalls } = yield* makeManager({2077 const { manager, ghCalls } = yield* makeManager({
2042 ghScenario: {2078 ghScenario: {
2043 prListByHeadSelector: {2079 prListByHeadSelector: {
2080 // @effect-diagnostics-next-line preferSchemaOverJson:off
2044 "t3code/pr-142/statemachine": JSON.stringify([]),2081 "t3code/pr-142/statemachine": JSON.stringify([]),
2082 // @effect-diagnostics-next-line preferSchemaOverJson:off
2045 statemachine: JSON.stringify([2083 statemachine: JSON.stringify([
2046 {2084 {
2047 number: 41,2085 number: 41,
...@@ -2051,6 +2089,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {...@@ -2051,6 +2089,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
2051 headRefName: "statemachine",2089 headRefName: "statemachine",
2052 },2090 },
2053 ]),2091 ]),
2092 // @effect-diagnostics-next-line preferSchemaOverJson:off
2054 "octocat:statemachine": JSON.stringify([2093 "octocat:statemachine": JSON.stringify([
2055 {2094 {
2056 number: 142,2095 number: 142,
...@@ -2068,6 +2107,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {...@@ -2068,6 +2107,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
2068 },2107 },
2069 },2108 },
2070 ]),2109 ]),
2110 // @effect-diagnostics-next-line preferSchemaOverJson:off
2071 "fork-seed:statemachine": JSON.stringify([]),2111 "fork-seed:statemachine": JSON.stringify([]),
2072 },2112 },
2073 },2113 },
...@@ -2112,6 +2152,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {...@@ -2112,6 +2152,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
2112 const { manager, ghCalls } = yield* makeManager({2152 const { manager, ghCalls } = yield* makeManager({
2113 ghScenario: {2153 ghScenario: {
2114 prListByHeadSelector: {2154 prListByHeadSelector: {
2155 // @effect-diagnostics-next-line preferSchemaOverJson:off
2115 "octocat:statemachine": JSON.stringify([2156 "octocat:statemachine": JSON.stringify([
2116 {2157 {
2117 number: 142,2158 number: 142,
...@@ -2129,8 +2170,11 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {...@@ -2129,8 +2170,11 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
2129 },2170 },
2130 },2171 },
2131 ]),2172 ]),
2173 // @effect-diagnostics-next-line preferSchemaOverJson:off
2132 "fork-seed:statemachine": JSON.stringify([]),2174 "fork-seed:statemachine": JSON.stringify([]),
2175 // @effect-diagnostics-next-line preferSchemaOverJson:off
2133 "t3code/pr-142/statemachine": JSON.stringify([]),2176 "t3code/pr-142/statemachine": JSON.stringify([]),
2177 // @effect-diagnostics-next-line preferSchemaOverJson:off
2134 statemachine: JSON.stringify([]),2178 statemachine: JSON.stringify([]),
2135 },2179 },
2136 },2180 },
...@@ -2170,6 +2214,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {...@@ -2170,6 +2214,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
2170 ghScenario: {2214 ghScenario: {
2171 prListSequence: [2215 prListSequence: [
2172 "[]",2216 "[]",
2217 // @effect-diagnostics-next-line preferSchemaOverJson:off
2173 JSON.stringify([2218 JSON.stringify([
2174 {2219 {
2175 number: 88,2220 number: 88,
...@@ -2215,6 +2260,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {...@@ -2215,6 +2260,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
2215 const { manager, ghCalls } = yield* makeManager({2260 const { manager, ghCalls } = yield* makeManager({
2216 ghScenario: {2261 ghScenario: {
2217 prListSequence: [2262 prListSequence: [
2263 // @effect-diagnostics-next-line preferSchemaOverJson:off
2218 JSON.stringify([2264 JSON.stringify([
2219 {2265 {
2220 number: 1661,2266 number: 1661,
...@@ -2232,6 +2278,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {...@@ -2232,6 +2278,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
2232 },2278 },
2233 },2279 },
2234 ]),2280 ]),
2281 // @effect-diagnostics-next-line preferSchemaOverJson:off
2235 JSON.stringify([2282 JSON.stringify([
2236 {2283 {
2237 number: 188,2284 number: 188,
...@@ -2294,7 +2341,9 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {...@@ -2294,7 +2341,9 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
2294 ghScenario: {2341 ghScenario: {
2295 prListSequenceByHeadSelector: {2342 prListSequenceByHeadSelector: {
2296 "octocat:statemachine": [2343 "octocat:statemachine": [
2344 // @effect-diagnostics-next-line preferSchemaOverJson:off
2297 JSON.stringify([]),2345 JSON.stringify([]),
2346 // @effect-diagnostics-next-line preferSchemaOverJson:off
2298 JSON.stringify([2347 JSON.stringify([
2299 {2348 {
2300 number: 188,2349 number: 188,
...@@ -2313,7 +2362,9 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {...@@ -2313,7 +2362,9 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
2313 },2362 },
2314 ]),2363 ]),
2315 ],2364 ],
2365 // @effect-diagnostics-next-line preferSchemaOverJson:off
2316 "fork-seed:statemachine": [JSON.stringify([])],2366 "fork-seed:statemachine": [JSON.stringify([])],
2367 // @effect-diagnostics-next-line preferSchemaOverJson:off
2317 statemachine: [JSON.stringify([])],2368 statemachine: [JSON.stringify([])],
2318 },2369 },
2319 },2370 },
...@@ -2870,7 +2921,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {...@@ -2870,7 +2921,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
2870 yield* runGit(repoDir, ["add", "existing.txt"]);2921 yield* runGit(repoDir, ["add", "existing.txt"]);
2871 yield* runGit(repoDir, ["commit", "-m", "Existing worktree branch"]);2922 yield* runGit(repoDir, ["commit", "-m", "Existing worktree branch"]);
2872 yield* runGit(repoDir, ["checkout", "main"]);2923 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)}`);
2874 yield* runGit(repoDir, ["worktree", "add", worktreePath, "feature/pr-existing-worktree"]);2925 yield* runGit(repoDir, ["worktree", "add", worktreePath, "feature/pr-existing-worktree"]);
28752926
2876 const setupCalls: ProjectSetupScriptRunnerInput[] = [];2927 const setupCalls: ProjectSetupScriptRunnerInput[] = [];
...@@ -3044,7 +3095,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {...@@ -3044,7 +3095,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
3044 yield* runGit(repoDir, ["commit", "-m", "Reused fork PR branch"]);3095 yield* runGit(repoDir, ["commit", "-m", "Reused fork PR branch"]);
3045 yield* runGit(repoDir, ["push", "-u", "fork-seed", "feature/pr-reused-fork"]);3096 yield* runGit(repoDir, ["push", "-u", "fork-seed", "feature/pr-reused-fork"]);
3046 yield* runGit(repoDir, ["checkout", "main"]);3097 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)}`);
3048 yield* runGit(repoDir, ["worktree", "add", worktreePath, "feature/pr-reused-fork"]);3099 yield* runGit(repoDir, ["worktree", "add", worktreePath, "feature/pr-reused-fork"]);
3049 yield* runGit(worktreePath, ["branch", "--unset-upstream"], true);3100 yield* runGit(worktreePath, ["branch", "--unset-upstream"], true);
30503101
...@@ -3112,7 +3163,8 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {...@@ -3112,7 +3163,8 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
3112 },3163 },
3113 },3164 },
3114 setupScriptRunner: {3165 setupScriptRunner: {
3115 runForThread: () => Effect.fail(new Error("terminal start failed")),3166 runForThread: () =>
3167 Effect.fail(new ProjectSetupScriptRunnerError({ message: "terminal start failed" })),
3116 },3168 },
3117 });3169 });
31183170
...@@ -3289,7 +3341,9 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {...@@ -3289,7 +3341,9 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
3289 const { manager } = yield* makeManager({3341 const { manager } = yield* makeManager({
3290 ghScenario: {3342 ghScenario: {
3291 prListSequence: [3343 prListSequence: [
3344 // @effect-diagnostics-next-line preferSchemaOverJson:off
3292 JSON.stringify([]),3345 JSON.stringify([]),
3346 // @effect-diagnostics-next-line preferSchemaOverJson:off
3293 JSON.stringify([3347 JSON.stringify([
3294 {3348 {
3295 number: 201,3349 number: 201,
apps/server/src/git/GitManager.ts+13-15
...@@ -1,20 +1,18 @@...@@ -1,20 +1,18 @@
1import { randomUUID } from "node:crypto";1import { randomUUID } from "node:crypto";
22
3import {3import * as Arr from "effect/Array";
4 Array as Arr,4import * as Cache from "effect/Cache";
5 Cache,5import * as Context from "effect/Context";
6 Context,6import * as DateTime from "effect/DateTime";
7 DateTime,7import * as Duration from "effect/Duration";
8 Duration,8import * as Effect from "effect/Effect";
9 Effect,9import * as Exit from "effect/Exit";
10 Exit,10import * as FileSystem from "effect/FileSystem";
11 FileSystem,11import * as Layer from "effect/Layer";
12 Layer,12import * as Option from "effect/Option";
13 Option,13import * as Order from "effect/Order";
14 Order,14import * as Path from "effect/Path";
15 Path,15import * as Ref from "effect/Ref";
16 Ref,
17} from "effect";
18import {16import {
19 GitActionProgressEvent,17 GitActionProgressEvent,
20 GitActionProgressPhase,18 GitActionProgressPhase,
apps/server/src/git/GitWorkflowService.test.ts+2-1
...@@ -1,5 +1,6 @@...@@ -1,5 +1,6 @@
1import { assert, describe, it, vi } from "@effect/vitest";1import { 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
4import * as GitManager from "./GitManager.ts";5import * as GitManager from "./GitManager.ts";
5import * as GitWorkflowService from "./GitWorkflowService.ts";6import * as GitWorkflowService from "./GitWorkflowService.ts";
apps/server/src/git/GitWorkflowService.ts+3-1
...@@ -1,4 +1,6 @@...@@ -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
3import {5import {
4 GitManagerError,6 GitManagerError,
apps/server/src/git/Utils.ts+1
...@@ -1,3 +1,4 @@...@@ -1,3 +1,4 @@
1// @effect-diagnostics nodeBuiltinImport:off
1import { existsSync } from "node:fs";2import { existsSync } from "node:fs";
2import { join } from "node:path";3import { join } from "node:path";
34
apps/server/src/http.ts+5-1
...@@ -1,6 +1,10 @@...@@ -1,6 +1,10 @@
1import Mime from "@effect/platform-node/Mime";1import Mime from "@effect/platform-node/Mime";
2import { decodeOtlpTraceRecords } from "@t3tools/shared/observability";2import { 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";
4import { cast } from "effect/Function";8import { cast } from "effect/Function";
5import {9import {
6 HttpBody,10 HttpBody,
apps/server/src/keybindings.test.ts+8-1
...@@ -2,7 +2,13 @@ import { KeybindingCommand, KeybindingRule, KeybindingsConfig } from "@t3tools/c...@@ -2,7 +2,13 @@ import { KeybindingCommand, KeybindingRule, KeybindingsConfig } from "@t3tools/c
2import * as NodeServices from "@effect/platform-node/NodeServices";2import * as NodeServices from "@effect/platform-node/NodeServices";
3import { assert, it } from "@effect/vitest";3import { assert, it } from "@effect/vitest";
4import { assertFailure } from "@effect/vitest/utils";4import { 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";
6import { ServerConfig } from "./config.ts";12import { ServerConfig } from "./config.ts";
713
8import {14import {
...@@ -229,6 +235,7 @@ it.layer(NodeServices.layer)("keybindings", (it) => {...@@ -229,6 +235,7 @@ it.layer(NodeServices.layer)("keybindings", (it) => {
229 const { keybindingsConfigPath } = yield* ServerConfig;235 const { keybindingsConfigPath } = yield* ServerConfig;
230 yield* fs.writeFileString(236 yield* fs.writeFileString(
231 keybindingsConfigPath,237 keybindingsConfigPath,
238 // @effect-diagnostics-next-line preferSchemaOverJson:off
232 JSON.stringify([239 JSON.stringify([
233 { key: "mod+j", command: "terminal.toggle" },240 { key: "mod+j", command: "terminal.toggle" },
234 { key: "mod+shift+d+o", command: "terminal.new" },241 { key: "mod+shift+d+o", command: "terminal.new" },
apps/server/src/keybindings.ts+22-34
...@@ -19,33 +19,30 @@ import {...@@ -19,33 +19,30 @@ import {
19 type ServerUpsertKeybindingInput,19 type ServerUpsertKeybindingInput,
20 type ServerConfigIssue,20 type ServerConfigIssue,
21} from "@t3tools/contracts";21} from "@t3tools/contracts";
22import {22import * as Array from "effect/Array";
23 Array,23import * as Cache from "effect/Cache";
24 Cache,24import * as Cause from "effect/Cause";
25 Cause,25import * as Deferred from "effect/Deferred";
26 Deferred,26import * as Duration from "effect/Duration";
27 Duration,27import * as Effect from "effect/Effect";
28 Effect,28import * as Exit from "effect/Exit";
29 Exit,29import * as FileSystem from "effect/FileSystem";
30 FileSystem,30import * as Path from "effect/Path";
31 Path,31import * as Layer from "effect/Layer";
32 Layer,32import * as Option from "effect/Option";
33 Option,33import * as Predicate from "effect/Predicate";
34 Predicate,34import * as PubSub from "effect/PubSub";
35 PubSub,35import * as Schema from "effect/Schema";
36 Schema,36import * as SchemaIssue from "effect/SchemaIssue";
37 SchemaGetter,37import * as SchemaTransformation from "effect/SchemaTransformation";
38 SchemaIssue,38import * as Ref from "effect/Ref";
39 SchemaTransformation,39import * as Context from "effect/Context";
40 Ref,40import * as Scope from "effect/Scope";
41 Context,41import * as Stream from "effect/Stream";
42 Scope,
43 Stream,
44} from "effect";
45import * as Semaphore from "effect/Semaphore";42import * as Semaphore from "effect/Semaphore";
46import { ServerConfig } from "./config.ts";43import { ServerConfig } from "./config.ts";
47import { writeFileStringAtomically } from "./atomicWrite.ts";44import { writeFileStringAtomically } from "./atomicWrite.ts";
48import { fromLenientJson } from "@t3tools/shared/schemaJson";45import { fromJsonStringPretty, fromLenientJson } from "@t3tools/shared/schemaJson";
49import {46import {
50 DEFAULT_KEYBINDINGS,47 DEFAULT_KEYBINDINGS,
51 DEFAULT_RESOLVED_KEYBINDINGS,48 DEFAULT_RESOLVED_KEYBINDINGS,
...@@ -172,16 +169,7 @@ function encodeWhenAst(node: KeybindingWhenNode): string {...@@ -172,16 +169,7 @@ function encodeWhenAst(node: KeybindingWhenNode): string {
172}169}
173170
174const RawKeybindingsEntries = fromLenientJson(Schema.Array(Schema.Unknown));171const RawKeybindingsEntries = fromLenientJson(Schema.Array(Schema.Unknown));
175const KeybindingsConfigJson = Schema.fromJsonString(KeybindingsConfig);172const KeybindingsConfigPrettyJson = fromJsonStringPretty(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);
185173
186export interface KeybindingsConfigState {174export interface KeybindingsConfigState {
187 readonly keybindings: ResolvedKeybindingsConfig;175 readonly keybindings: ResolvedKeybindingsConfig;
apps/server/src/observability/Attributes.ts+2-1
...@@ -1,4 +1,5 @@...@@ -1,4 +1,5 @@
1import { Cause, Exit } from "effect";1import * as Cause from "effect/Cause";
2import * as Exit from "effect/Exit";
23
3export type MetricAttributeValue = string;4export type MetricAttributeValue = string;
4export type MetricAttributes = Readonly<Record<string, MetricAttributeValue>>;5export type MetricAttributes = Readonly<Record<string, MetricAttributeValue>>;
apps/server/src/observability/Layers/Observability.ts+4-1
...@@ -1,5 +1,8 @@...@@ -1,5 +1,8 @@
1import { makeLocalFileTracer, makeTraceSink } from "@t3tools/shared/observability";1import { 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";
3import { OtlpMetrics, OtlpSerialization, OtlpTracer } from "effect/unstable/observability";6import { OtlpMetrics, OtlpSerialization, OtlpTracer } from "effect/unstable/observability";
47
5import { ServerConfig } from "../../config.ts";8import { ServerConfig } from "../../config.ts";
apps/server/src/observability/Metrics.test.ts+5-2
...@@ -1,7 +1,10 @@...@@ -1,7 +1,10 @@
1import { assert, describe, it } from "@effect/vitest";1import { assert, describe, it } from "@effect/vitest";
2import { ProviderDriverKind } from "@t3tools/contracts";2import { ProviderDriverKind } from "@t3tools/contracts";
3import { Duration, Effect, Fiber, Metric } from "effect";3import * as Duration from "effect/Duration";
4import { TestClock } from "effect/testing";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
6import { withMetrics } from "./Metrics.ts";9import { withMetrics } from "./Metrics.ts";
710
apps/server/src/observability/Metrics.ts+5-1
...@@ -1,4 +1,8 @@...@@ -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";
2import { dual } from "effect/Function";6import { dual } from "effect/Function";
37
4import {8import {
apps/server/src/observability/RpcInstrumentation.test.ts+8-2
...@@ -1,7 +1,13 @@...@@ -1,7 +1,13 @@
1import { assert, describe, it } from "@effect/vitest";1import { assert, describe, it } from "@effect/vitest";
2import { WS_METHODS } from "@t3tools/contracts";2import { WS_METHODS } from "@t3tools/contracts";
3import { Duration, Effect, Exit, Fiber, Metric, Stream, Tracer } from "effect";3import * as Duration from "effect/Duration";
4import { TestClock } from "effect/testing";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
6import {12import {
7 observeRpcEffect,13 observeRpcEffect,
apps/server/src/observability/RpcInstrumentation.ts+7-1
...@@ -1,5 +1,11 @@...@@ -1,5 +1,11 @@
1import { WS_METHODS } from "@t3tools/contracts";1import { 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
4import { outcomeFromExit } from "./Attributes.ts";10import { outcomeFromExit } from "./Attributes.ts";
5import { metricAttributes, rpcRequestDuration, rpcRequestsTotal, withMetrics } from "./Metrics.ts";11import { metricAttributes, rpcRequestDuration, rpcRequestsTotal, withMetrics } from "./Metrics.ts";
apps/server/src/observability/Services/BrowserTraceCollector.ts+2-2
...@@ -1,6 +1,6 @@...@@ -1,6 +1,6 @@
1import type { TraceRecord } from "@t3tools/shared/observability";1import type { TraceRecord } from "@t3tools/shared/observability";
2import { Context } from "effect";2import * as Context from "effect/Context";
3import type { Effect } from "effect";3import type * as Effect from "effect/Effect";
44
5export interface BrowserTraceCollectorShape {5export interface BrowserTraceCollectorShape {
6 readonly record: (records: ReadonlyArray<TraceRecord>) => Effect.Effect<void>;6 readonly record: (records: ReadonlyArray<TraceRecord>) => Effect.Effect<void>;
apps/server/src/open.test.ts+5-2
...@@ -1,7 +1,10 @@...@@ -1,7 +1,10 @@
1import * as NodeServices from "@effect/platform-node/NodeServices";1import * as NodeServices from "@effect/platform-node/NodeServices";
2import { assert, it } from "@effect/vitest";2import { assert, it } from "@effect/vitest";
3import { assertSuccess } from "@effect/vitest/utils";3import { assertSuccess } from "@effect/vitest/utils";
4import { 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
6import {9import {
7 isCommandAvailable,10 isCommandAvailable,
...@@ -488,7 +491,7 @@ it.layer(NodeServices.layer)("launchDetached", (it) => {...@@ -488,7 +491,7 @@ it.layer(NodeServices.layer)("launchDetached", (it) => {
488 it.effect("rejects when command does not exist", () =>491 it.effect("rejects when command does not exist", () =>
489 Effect.gen(function* () {492 Effect.gen(function* () {
490 const result = yield* launchDetached({493 const result = yield* launchDetached({
491 command: `t3code-no-such-command-${Date.now()}`,494 command: `t3code-no-such-command-${yield* Random.nextUUIDv4}`,
492 args: [],495 args: [],
493 }).pipe(Effect.result);496 }).pipe(Effect.result);
494 assert.equal(result._tag, "Failure");497 assert.equal(result._tag, "Failure");
apps/server/src/open.ts+4-1
...@@ -1,3 +1,4 @@...@@ -1,3 +1,4 @@
1// @effect-diagnostics nodeBuiltinImport:off
1/**2/**
2 * Open - Browser/editor launch service interface.3 * Open - Browser/editor launch service interface.
3 *4 *
...@@ -10,7 +11,9 @@ import { spawn } from "node:child_process";...@@ -10,7 +11,9 @@ import { spawn } from "node:child_process";
1011
11import { EDITORS, OpenError, type EditorId } from "@t3tools/contracts";12import { EDITORS, OpenError, type EditorId } from "@t3tools/contracts";
12import { isCommandAvailable, type CommandAvailabilityOptions } from "@t3tools/shared/shell";13import { 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
15// ==============================18// ==============================
16// Definitions19// Definitions
apps/server/src/orchestration/Errors.ts+2-1
...@@ -1,4 +1,5 @@...@@ -1,4 +1,5 @@
1import { SchemaIssue, Schema } from "effect";1import * as SchemaIssue from "effect/SchemaIssue";
2import * as Schema from "effect/Schema";
23
3import type { ProjectionRepositoryError } from "../persistence/Errors.ts";4import type { ProjectionRepositoryError } from "../persistence/Errors.ts";
45
apps/server/src/orchestration/Layers/CheckpointReactor.test.ts+45-37
...@@ -1,3 +1,4 @@...@@ -1,3 +1,4 @@
1// @effect-diagnostics nodeBuiltinImport:off
1import fs from "node:fs";2import fs from "node:fs";
2import os from "node:os";3import os from "node:os";
3import path from "node:path";4import path from "node:path";
...@@ -19,7 +20,14 @@ import {...@@ -19,7 +20,14 @@ import {
19 TurnId,20 TurnId,
20} from "@t3tools/contracts";21} from "@t3tools/contracts";
21import * as NodeServices from "@effect/platform-node/NodeServices";22import * 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";
23import { afterEach, describe, expect, it, vi } from "vitest";31import { afterEach, describe, expect, it, vi } from "vitest";
2432
25import { CheckpointStoreLive } from "../../checkpointing/Layers/CheckpointStore.ts";33import { CheckpointStoreLive } from "../../checkpointing/Layers/CheckpointStore.ts";
...@@ -73,7 +81,7 @@ function createProviderServiceHarness(...@@ -73,7 +81,7 @@ function createProviderServiceHarness(
73 sessionCwd = cwd,81 sessionCwd = cwd,
74 providerName: ProviderSession["provider"] = ProviderDriverKind.make("codex"),82 providerName: ProviderSession["provider"] = ProviderDriverKind.make("codex"),
75) {83) {
76 const now = new Date().toISOString();84 const now = "2026-01-01T00:00:00.000Z";
77 const runtimeEventPubSub = Effect.runSync(PubSub.unbounded<ProviderRuntimeEvent>());85 const runtimeEventPubSub = Effect.runSync(PubSub.unbounded<ProviderRuntimeEvent>());
78 const rollbackConversation = vi.fn(86 const rollbackConversation = vi.fn(
79 (_input: { readonly threadId: ThreadId; readonly numTurns: number }) => Effect.void,87 (_input: { readonly threadId: ThreadId; readonly numTurns: number }) => Effect.void,
...@@ -148,7 +156,7 @@ async function waitForThread(...@@ -148,7 +156,7 @@ async function waitForThread(
148 }) => boolean,156 }) => boolean,
149 timeoutMs = 15_000,157 timeoutMs = 15_000,
150) {158) {
151 const deadline = Date.now() + timeoutMs;159 const deadline = (await Effect.runPromise(Clock.currentTimeMillis)) + timeoutMs;
152 const poll = async (): Promise<{160 const poll = async (): Promise<{
153 latestTurn: { turnId: string } | null;161 latestTurn: { turnId: string } | null;
154 checkpoints: ReadonlyArray<{ checkpointTurnCount: number }>;162 checkpoints: ReadonlyArray<{ checkpointTurnCount: number }>;
...@@ -159,10 +167,10 @@ async function waitForThread(...@@ -159,10 +167,10 @@ async function waitForThread(
159 if (thread && predicate(thread)) {167 if (thread && predicate(thread)) {
160 return thread;168 return thread;
161 }169 }
162 if (Date.now() >= deadline) {170 if ((await Effect.runPromise(Clock.currentTimeMillis)) >= deadline) {
163 throw new Error("Timed out waiting for thread state.");171 throw new Error("Timed out waiting for thread state.");
164 }172 }
165 await new Promise((resolve) => setTimeout(resolve, 10));173 await Effect.runPromise(Effect.sleep("10 millis"));
166 return poll();174 return poll();
167 };175 };
168 return poll();176 return poll();
...@@ -173,7 +181,7 @@ async function waitForEvent(...@@ -173,7 +181,7 @@ async function waitForEvent(
173 predicate: (event: { type: string }) => boolean,181 predicate: (event: { type: string }) => boolean,
174 timeoutMs = 15_000,182 timeoutMs = 15_000,
175) {183) {
176 const deadline = Date.now() + timeoutMs;184 const deadline = (await Effect.runPromise(Clock.currentTimeMillis)) + timeoutMs;
177 const poll = async () => {185 const poll = async () => {
178 const events = await Effect.runPromise(186 const events = await Effect.runPromise(
179 Stream.runCollect(engine.readEvents(0)).pipe(Effect.map((chunk) => Array.from(chunk))),187 Stream.runCollect(engine.readEvents(0)).pipe(Effect.map((chunk) => Array.from(chunk))),
...@@ -181,10 +189,10 @@ async function waitForEvent(...@@ -181,10 +189,10 @@ async function waitForEvent(
181 if (events.some(predicate)) {189 if (events.some(predicate)) {
182 return events;190 return events;
183 }191 }
184 if (Date.now() >= deadline) {192 if ((await Effect.runPromise(Clock.currentTimeMillis)) >= deadline) {
185 throw new Error("Timed out waiting for orchestration event.");193 throw new Error("Timed out waiting for orchestration event.");
186 }194 }
187 await new Promise((resolve) => setTimeout(resolve, 10));195 await Effect.runPromise(Effect.sleep("10 millis"));
188 return poll();196 return poll();
189 };197 };
190 return poll();198 return poll();
...@@ -223,15 +231,15 @@ function gitShowFileAtRef(cwd: string, ref: string, filePath: string): string {...@@ -223,15 +231,15 @@ function gitShowFileAtRef(cwd: string, ref: string, filePath: string): string {
223}231}
224232
225async function waitForGitRefExists(cwd: string, ref: string, timeoutMs = 15_000) {233async function waitForGitRefExists(cwd: string, ref: string, timeoutMs = 15_000) {
226 const deadline = Date.now() + timeoutMs;234 const deadline = (await Effect.runPromise(Clock.currentTimeMillis)) + timeoutMs;
227 const poll = async (): Promise<void> => {235 const poll = async (): Promise<void> => {
228 if (gitRefExists(cwd, ref)) {236 if (gitRefExists(cwd, ref)) {
229 return;237 return;
230 }238 }
231 if (Date.now() >= deadline) {239 if ((await Effect.runPromise(Clock.currentTimeMillis)) >= deadline) {
232 throw new Error(`Timed out waiting for git ref '${ref}'.`);240 throw new Error(`Timed out waiting for git ref '${ref}'.`);
233 }241 }
234 await new Promise((resolve) => setTimeout(resolve, 10));242 await Effect.runPromise(Effect.sleep("10 millis"));
235 return poll();243 return poll();
236 };244 };
237 return poll();245 return poll();
...@@ -342,7 +350,7 @@ describe("CheckpointReactor", () => {...@@ -342,7 +350,7 @@ describe("CheckpointReactor", () => {
342 await Effect.runPromise(reactor.start().pipe(Scope.provide(scope)));350 await Effect.runPromise(reactor.start().pipe(Scope.provide(scope)));
343 const drain = () => Effect.runPromise(reactor.drain);351 const drain = () => Effect.runPromise(reactor.drain);
344352
345 const createdAt = new Date().toISOString();353 const createdAt = "2026-01-01T00:00:00.000Z";
346 await Effect.runPromise(354 await Effect.runPromise(
347 engine.dispatch({355 engine.dispatch({
348 type: "project.create",356 type: "project.create",
...@@ -410,7 +418,7 @@ describe("CheckpointReactor", () => {...@@ -410,7 +418,7 @@ describe("CheckpointReactor", () => {
410418
411 it("captures pre-turn baseline on turn.started and post-turn checkpoint on turn.completed", async () => {419 it("captures pre-turn baseline on turn.started and post-turn checkpoint on turn.completed", async () => {
412 const harness = await createHarness({ seedFilesystemCheckpoints: false });420 const harness = await createHarness({ seedFilesystemCheckpoints: false });
413 const createdAt = new Date().toISOString();421 const createdAt = "2026-01-01T00:00:00.000Z";
414422
415 await Effect.runPromise(423 await Effect.runPromise(
416 harness.engine.dispatch({424 harness.engine.dispatch({
...@@ -435,7 +443,7 @@ describe("CheckpointReactor", () => {...@@ -435,7 +443,7 @@ describe("CheckpointReactor", () => {
435 eventId: EventId.make("evt-turn-started-1"),443 eventId: EventId.make("evt-turn-started-1"),
436 provider: ProviderDriverKind.make("codex"),444 provider: ProviderDriverKind.make("codex"),
437445
438 createdAt: new Date().toISOString(),446 createdAt: "2026-01-01T00:00:00.000Z",
439 threadId: ThreadId.make("thread-1"),447 threadId: ThreadId.make("thread-1"),
440 turnId: asTurnId("turn-1"),448 turnId: asTurnId("turn-1"),
441 });449 });
...@@ -450,7 +458,7 @@ describe("CheckpointReactor", () => {...@@ -450,7 +458,7 @@ describe("CheckpointReactor", () => {
450 eventId: EventId.make("evt-turn-completed-1"),458 eventId: EventId.make("evt-turn-completed-1"),
451 provider: ProviderDriverKind.make("codex"),459 provider: ProviderDriverKind.make("codex"),
452460
453 createdAt: new Date().toISOString(),461 createdAt: "2026-01-01T00:00:00.000Z",
454 threadId: ThreadId.make("thread-1"),462 threadId: ThreadId.make("thread-1"),
455 turnId: asTurnId("turn-1"),463 turnId: asTurnId("turn-1"),
456 payload: { state: "completed" },464 payload: { state: "completed" },
...@@ -495,7 +503,7 @@ describe("CheckpointReactor", () => {...@@ -495,7 +503,7 @@ describe("CheckpointReactor", () => {
495 type: "turn.completed",503 type: "turn.completed",
496 eventId: EventId.make("evt-turn-completed-refresh-local-status"),504 eventId: EventId.make("evt-turn-completed-refresh-local-status"),
497 provider: ProviderDriverKind.make("codex"),505 provider: ProviderDriverKind.make("codex"),
498 createdAt: new Date().toISOString(),506 createdAt: "2026-01-01T00:00:00.000Z",
499 threadId: ThreadId.make("thread-1"),507 threadId: ThreadId.make("thread-1"),
500 turnId: asTurnId("turn-refresh-local-status"),508 turnId: asTurnId("turn-refresh-local-status"),
501 payload: { state: "completed" },509 payload: { state: "completed" },
...@@ -508,7 +516,7 @@ describe("CheckpointReactor", () => {...@@ -508,7 +516,7 @@ describe("CheckpointReactor", () => {
508516
509 it("ignores auxiliary thread turn completion while primary turn is active", async () => {517 it("ignores auxiliary thread turn completion while primary turn is active", async () => {
510 const harness = await createHarness({ seedFilesystemCheckpoints: false });518 const harness = await createHarness({ seedFilesystemCheckpoints: false });
511 const createdAt = new Date().toISOString();519 const createdAt = "2026-01-01T00:00:00.000Z";
512520
513 await Effect.runPromise(521 await Effect.runPromise(
514 harness.engine.dispatch({522 harness.engine.dispatch({
...@@ -533,7 +541,7 @@ describe("CheckpointReactor", () => {...@@ -533,7 +541,7 @@ describe("CheckpointReactor", () => {
533 eventId: EventId.make("evt-turn-started-main"),541 eventId: EventId.make("evt-turn-started-main"),
534 provider: ProviderDriverKind.make("codex"),542 provider: ProviderDriverKind.make("codex"),
535543
536 createdAt: new Date().toISOString(),544 createdAt: "2026-01-01T00:00:00.000Z",
537 threadId: ThreadId.make("thread-1"),545 threadId: ThreadId.make("thread-1"),
538 turnId: asTurnId("turn-main"),546 turnId: asTurnId("turn-main"),
539 });547 });
...@@ -549,7 +557,7 @@ describe("CheckpointReactor", () => {...@@ -549,7 +557,7 @@ describe("CheckpointReactor", () => {
549 eventId: EventId.make("evt-turn-completed-aux"),557 eventId: EventId.make("evt-turn-completed-aux"),
550 provider: ProviderDriverKind.make("codex"),558 provider: ProviderDriverKind.make("codex"),
551559
552 createdAt: new Date().toISOString(),560 createdAt: "2026-01-01T00:00:00.000Z",
553 threadId: ThreadId.make("thread-1"),561 threadId: ThreadId.make("thread-1"),
554 turnId: asTurnId("turn-aux"),562 turnId: asTurnId("turn-aux"),
555 payload: { state: "completed" },563 payload: { state: "completed" },
...@@ -565,7 +573,7 @@ describe("CheckpointReactor", () => {...@@ -565,7 +573,7 @@ describe("CheckpointReactor", () => {
565 eventId: EventId.make("evt-turn-completed-main"),573 eventId: EventId.make("evt-turn-completed-main"),
566 provider: ProviderDriverKind.make("codex"),574 provider: ProviderDriverKind.make("codex"),
567575
568 createdAt: new Date().toISOString(),576 createdAt: "2026-01-01T00:00:00.000Z",
569 threadId: ThreadId.make("thread-1"),577 threadId: ThreadId.make("thread-1"),
570 turnId: asTurnId("turn-main"),578 turnId: asTurnId("turn-main"),
571 payload: { state: "completed" },579 payload: { state: "completed" },
...@@ -583,7 +591,7 @@ describe("CheckpointReactor", () => {...@@ -583,7 +591,7 @@ describe("CheckpointReactor", () => {
583 seedFilesystemCheckpoints: false,591 seedFilesystemCheckpoints: false,
584 providerName: ProviderDriverKind.make("claudeAgent"),592 providerName: ProviderDriverKind.make("claudeAgent"),
585 });593 });
586 const createdAt = new Date().toISOString();594 const createdAt = "2026-01-01T00:00:00.000Z";
587595
588 await Effect.runPromise(596 await Effect.runPromise(
589 harness.engine.dispatch({597 harness.engine.dispatch({
...@@ -607,7 +615,7 @@ describe("CheckpointReactor", () => {...@@ -607,7 +615,7 @@ describe("CheckpointReactor", () => {
607 type: "turn.started",615 type: "turn.started",
608 eventId: EventId.make("evt-turn-started-claude-1"),616 eventId: EventId.make("evt-turn-started-claude-1"),
609 provider: ProviderDriverKind.make("claudeAgent"),617 provider: ProviderDriverKind.make("claudeAgent"),
610 createdAt: new Date().toISOString(),618 createdAt: "2026-01-01T00:00:00.000Z",
611 threadId: ThreadId.make("thread-1"),619 threadId: ThreadId.make("thread-1"),
612 turnId: asTurnId("turn-claude-1"),620 turnId: asTurnId("turn-claude-1"),
613 });621 });
...@@ -621,7 +629,7 @@ describe("CheckpointReactor", () => {...@@ -621,7 +629,7 @@ describe("CheckpointReactor", () => {
621 type: "turn.completed",629 type: "turn.completed",
622 eventId: EventId.make("evt-turn-completed-claude-1"),630 eventId: EventId.make("evt-turn-completed-claude-1"),
623 provider: ProviderDriverKind.make("claudeAgent"),631 provider: ProviderDriverKind.make("claudeAgent"),
624 createdAt: new Date().toISOString(),632 createdAt: "2026-01-01T00:00:00.000Z",
625 threadId: ThreadId.make("thread-1"),633 threadId: ThreadId.make("thread-1"),
626 turnId: asTurnId("turn-claude-1"),634 turnId: asTurnId("turn-claude-1"),
627 payload: { state: "completed" },635 payload: { state: "completed" },
...@@ -641,7 +649,7 @@ describe("CheckpointReactor", () => {...@@ -641,7 +649,7 @@ describe("CheckpointReactor", () => {
641649
642 it("appends capture failure activity when turn diff summary cannot be derived", async () => {650 it("appends capture failure activity when turn diff summary cannot be derived", async () => {
643 const harness = await createHarness({ seedFilesystemCheckpoints: false });651 const harness = await createHarness({ seedFilesystemCheckpoints: false });
644 const createdAt = new Date().toISOString();652 const createdAt = "2026-01-01T00:00:00.000Z";
645653
646 await Effect.runPromise(654 await Effect.runPromise(
647 harness.engine.dispatch({655 harness.engine.dispatch({
...@@ -666,7 +674,7 @@ describe("CheckpointReactor", () => {...@@ -666,7 +674,7 @@ describe("CheckpointReactor", () => {
666 eventId: EventId.make("evt-turn-completed-missing-baseline"),674 eventId: EventId.make("evt-turn-completed-missing-baseline"),
667 provider: ProviderDriverKind.make("codex"),675 provider: ProviderDriverKind.make("codex"),
668676
669 createdAt: new Date().toISOString(),677 createdAt: "2026-01-01T00:00:00.000Z",
670 threadId: ThreadId.make("thread-1"),678 threadId: ThreadId.make("thread-1"),
671 turnId: asTurnId("turn-missing-baseline"),679 turnId: asTurnId("turn-missing-baseline"),
672 payload: { state: "completed" },680 payload: { state: "completed" },
...@@ -706,7 +714,7 @@ describe("CheckpointReactor", () => {...@@ -706,7 +714,7 @@ describe("CheckpointReactor", () => {
706 },714 },
707 interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE,715 interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE,
708 runtimeMode: "approval-required",716 runtimeMode: "approval-required",
709 createdAt: new Date().toISOString(),717 createdAt: "2026-01-01T00:00:00.000Z",
710 }),718 }),
711 );719 );
712720
...@@ -729,7 +737,7 @@ describe("CheckpointReactor", () => {...@@ -729,7 +737,7 @@ describe("CheckpointReactor", () => {
729 seedFilesystemCheckpoints: false,737 seedFilesystemCheckpoints: false,
730 threadWorktreePath: null,738 threadWorktreePath: null,
731 });739 });
732 const createdAt = new Date().toISOString();740 const createdAt = "2026-01-01T00:00:00.000Z";
733741
734 await Effect.runPromise(742 await Effect.runPromise(
735 harness.engine.dispatch({743 harness.engine.dispatch({
...@@ -755,7 +763,7 @@ describe("CheckpointReactor", () => {...@@ -755,7 +763,7 @@ describe("CheckpointReactor", () => {
755 eventId: EventId.make("evt-turn-completed-missing-provider-cwd"),763 eventId: EventId.make("evt-turn-completed-missing-provider-cwd"),
756 provider: ProviderDriverKind.make("codex"),764 provider: ProviderDriverKind.make("codex"),
757765
758 createdAt: new Date().toISOString(),766 createdAt: "2026-01-01T00:00:00.000Z",
759 threadId: ThreadId.make("thread-1"),767 threadId: ThreadId.make("thread-1"),
760 turnId: asTurnId("turn-missing-cwd"),768 turnId: asTurnId("turn-missing-cwd"),
761 payload: { state: "completed" },769 payload: { state: "completed" },
...@@ -776,7 +784,7 @@ describe("CheckpointReactor", () => {...@@ -776,7 +784,7 @@ describe("CheckpointReactor", () => {
776784
777 it("ignores non-v2 checkpoint.captured runtime events", async () => {785 it("ignores non-v2 checkpoint.captured runtime events", async () => {
778 const harness = await createHarness();786 const harness = await createHarness();
779 const createdAt = new Date().toISOString();787 const createdAt = "2026-01-01T00:00:00.000Z";
780788
781 await Effect.runPromise(789 await Effect.runPromise(
782 harness.engine.dispatch({790 harness.engine.dispatch({
...@@ -801,7 +809,7 @@ describe("CheckpointReactor", () => {...@@ -801,7 +809,7 @@ describe("CheckpointReactor", () => {
801 eventId: EventId.make("evt-checkpoint-captured-3"),809 eventId: EventId.make("evt-checkpoint-captured-3"),
802 provider: ProviderDriverKind.make("codex"),810 provider: ProviderDriverKind.make("codex"),
803811
804 createdAt: new Date().toISOString(),812 createdAt: "2026-01-01T00:00:00.000Z",
805 threadId: ThreadId.make("thread-1"),813 threadId: ThreadId.make("thread-1"),
806 turnId: asTurnId("turn-3"),814 turnId: asTurnId("turn-3"),
807 turnCount: 3,815 turnCount: 3,
...@@ -826,7 +834,7 @@ describe("CheckpointReactor", () => {...@@ -826,7 +834,7 @@ describe("CheckpointReactor", () => {
826 seedFilesystemCheckpoints: false,834 seedFilesystemCheckpoints: false,
827 providerSessionCwd: nonRepositorySessionCwd,835 providerSessionCwd: nonRepositorySessionCwd,
828 });836 });
829 const createdAt = new Date().toISOString();837 const createdAt = "2026-01-01T00:00:00.000Z";
830838
831 await Effect.runPromise(839 await Effect.runPromise(
832 harness.engine.dispatch({840 harness.engine.dispatch({
...@@ -851,7 +859,7 @@ describe("CheckpointReactor", () => {...@@ -851,7 +859,7 @@ describe("CheckpointReactor", () => {
851 eventId: EventId.make("evt-runtime-capture-failure"),859 eventId: EventId.make("evt-runtime-capture-failure"),
852 provider: ProviderDriverKind.make("codex"),860 provider: ProviderDriverKind.make("codex"),
853861
854 createdAt: new Date().toISOString(),862 createdAt: "2026-01-01T00:00:00.000Z",
855 threadId: ThreadId.make("thread-1"),863 threadId: ThreadId.make("thread-1"),
856 turnId: asTurnId("turn-runtime-failure"),864 turnId: asTurnId("turn-runtime-failure"),
857 payload: { state: "completed" },865 payload: { state: "completed" },
...@@ -862,7 +870,7 @@ describe("CheckpointReactor", () => {...@@ -862,7 +870,7 @@ describe("CheckpointReactor", () => {
862 eventId: EventId.make("evt-turn-started-after-runtime-failure"),870 eventId: EventId.make("evt-turn-started-after-runtime-failure"),
863 provider: ProviderDriverKind.make("codex"),871 provider: ProviderDriverKind.make("codex"),
864872
865 createdAt: new Date().toISOString(),873 createdAt: "2026-01-01T00:00:00.000Z",
866 threadId: ThreadId.make("thread-1"),874 threadId: ThreadId.make("thread-1"),
867 turnId: asTurnId("turn-after-runtime-failure"),875 turnId: asTurnId("turn-after-runtime-failure"),
868 });876 });
...@@ -878,7 +886,7 @@ describe("CheckpointReactor", () => {...@@ -878,7 +886,7 @@ describe("CheckpointReactor", () => {
878886
879 it("executes provider revert and emits thread.reverted for checkpoint revert requests", async () => {887 it("executes provider revert and emits thread.reverted for checkpoint revert requests", async () => {
880 const harness = await createHarness();888 const harness = await createHarness();
881 const createdAt = new Date().toISOString();889 const createdAt = "2026-01-01T00:00:00.000Z";
882890
883 await Effect.runPromise(891 await Effect.runPromise(
884 harness.engine.dispatch({892 harness.engine.dispatch({
...@@ -959,7 +967,7 @@ describe("CheckpointReactor", () => {...@@ -959,7 +967,7 @@ describe("CheckpointReactor", () => {
959967
960 it("executes provider revert and emits thread.reverted for claude sessions", async () => {968 it("executes provider revert and emits thread.reverted for claude sessions", async () => {
961 const harness = await createHarness({ providerName: ProviderDriverKind.make("claudeAgent") });969 const harness = await createHarness({ providerName: ProviderDriverKind.make("claudeAgent") });
962 const createdAt = new Date().toISOString();970 const createdAt = "2026-01-01T00:00:00.000Z";
963971
964 await Effect.runPromise(972 await Effect.runPromise(
965 harness.engine.dispatch({973 harness.engine.dispatch({
...@@ -1028,7 +1036,7 @@ describe("CheckpointReactor", () => {...@@ -1028,7 +1036,7 @@ describe("CheckpointReactor", () => {
10281036
1029 it("processes consecutive revert requests with deterministic rollback sequencing", async () => {1037 it("processes consecutive revert requests with deterministic rollback sequencing", async () => {
1030 const harness = await createHarness();1038 const harness = await createHarness();
1031 const createdAt = new Date().toISOString();1039 const createdAt = "2026-01-01T00:00:00.000Z";
10321040
1033 await Effect.runPromise(1041 await Effect.runPromise(
1034 harness.engine.dispatch({1042 harness.engine.dispatch({
...@@ -1111,7 +1119,7 @@ describe("CheckpointReactor", () => {...@@ -1111,7 +1119,7 @@ describe("CheckpointReactor", () => {
11111119
1112 it("appends an error activity when revert is requested without an active session", async () => {1120 it("appends an error activity when revert is requested without an active session", async () => {
1113 const harness = await createHarness({ hasSession: false });1121 const harness = await createHarness({ hasSession: false });
1114 const createdAt = new Date().toISOString();1122 const createdAt = "2026-01-01T00:00:00.000Z";
11151123
1116 await Effect.runPromise(1124 await Effect.runPromise(
1117 harness.engine.dispatch({1125 harness.engine.dispatch({
apps/server/src/orchestration/Layers/CheckpointReactor.ts+33-20
...@@ -8,7 +8,12 @@ import {...@@ -8,7 +8,12 @@ import {
8 type OrchestrationEvent,8 type OrchestrationEvent,
9 type ProviderRuntimeEvent,9 type ProviderRuntimeEvent,
10} from "@t3tools/contracts";10} 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";
12import { makeDrainableWorker } from "@t3tools/shared/DrainableWorker";17import { makeDrainableWorker } from "@t3tools/shared/DrainableWorker";
1318
14import { parseTurnDiffFilesFromUnifiedDiff } from "../../checkpointing/Diffs.ts";19import { parseTurnDiffFilesFromUnifiedDiff } from "../../checkpointing/Diffs.ts";
...@@ -28,6 +33,8 @@ import { isGitRepository } from "../../git/Utils.ts";...@@ -28,6 +33,8 @@ import { isGitRepository } from "../../git/Utils.ts";
28import { VcsStatusBroadcaster } from "../../vcs/VcsStatusBroadcaster.ts";33import { VcsStatusBroadcaster } from "../../vcs/VcsStatusBroadcaster.ts";
29import { WorkspaceEntries } from "../../workspace/Services/WorkspaceEntries.ts";34import { WorkspaceEntries } from "../../workspace/Services/WorkspaceEntries.ts";
3035
36const nowIso = Effect.map(DateTime.now, DateTime.formatIso);
37
31type ReactorInput =38type ReactorInput =
32 | {39 | {
33 readonly source: "runtime";40 readonly source: "runtime";
...@@ -584,7 +591,7 @@ const make = Effect.gen(function* () {...@@ -584,7 +591,7 @@ const make = Effect.gen(function* () {
584 const handleRevertRequested = Effect.fn("handleRevertRequested")(function* (591 const handleRevertRequested = Effect.fn("handleRevertRequested")(function* (
585 event: Extract<OrchestrationEvent, { type: "thread.checkpoint-revert-requested" }>,592 event: Extract<OrchestrationEvent, { type: "thread.checkpoint-revert-requested" }>,
586 ) {593 ) {
587 const now = new Date().toISOString();594 const now = DateTime.formatIso(yield* DateTime.now);
588595
589 const thread = yield* resolveThreadDetail(event.payload.threadId);596 const thread = yield* resolveThreadDetail(event.payload.threadId);
590 if (!thread) {597 if (!thread) {
...@@ -717,12 +724,14 @@ const make = Effect.gen(function* () {...@@ -717,12 +724,14 @@ const make = Effect.gen(function* () {
717 if (event.type === "thread.checkpoint-revert-requested") {724 if (event.type === "thread.checkpoint-revert-requested") {
718 yield* handleRevertRequested(event).pipe(725 yield* handleRevertRequested(event).pipe(
719 Effect.catch((error) =>726 Effect.catch((error) =>
720 appendRevertFailureActivity({727 Effect.flatMap(nowIso, (createdAt) =>
721 threadId: event.payload.threadId,728 appendRevertFailureActivity({
722 turnCount: event.payload.turnCount,729 threadId: event.payload.threadId,
723 detail: error.message,730 turnCount: event.payload.turnCount,
724 createdAt: new Date().toISOString(),731 detail: error.message,
725 }),732 createdAt,
733 }),
734 ),
726 ),735 ),
727 );736 );
728 return;737 return;
...@@ -736,12 +745,14 @@ const make = Effect.gen(function* () {...@@ -736,12 +745,14 @@ const make = Effect.gen(function* () {
736 if (event.type === "thread.turn-diff-completed") {745 if (event.type === "thread.turn-diff-completed") {
737 yield* captureCheckpointFromPlaceholder(event).pipe(746 yield* captureCheckpointFromPlaceholder(event).pipe(
738 Effect.catch((error) =>747 Effect.catch((error) =>
739 appendCaptureFailureActivity({748 Effect.flatMap(nowIso, (createdAt) =>
740 threadId: event.payload.threadId,749 appendCaptureFailureActivity({
741 turnId: event.payload.turnId,750 threadId: event.payload.threadId,
742 detail: error.message,751 turnId: event.payload.turnId,
743 createdAt: new Date().toISOString(),752 detail: error.message,
744 }).pipe(Effect.catch(() => Effect.void)),753 createdAt,
754 }).pipe(Effect.catch(() => Effect.void)),
755 ),
745 ),756 ),
746 );757 );
747 }758 }
...@@ -760,12 +771,14 @@ const make = Effect.gen(function* () {...@@ -760,12 +771,14 @@ const make = Effect.gen(function* () {
760 yield* refreshLocalGitStatusFromTurnCompletion(event);771 yield* refreshLocalGitStatusFromTurnCompletion(event);
761 yield* captureCheckpointFromTurnCompletion(event).pipe(772 yield* captureCheckpointFromTurnCompletion(event).pipe(
762 Effect.catch((error) =>773 Effect.catch((error) =>
763 appendCaptureFailureActivity({774 Effect.flatMap(nowIso, (createdAt) =>
764 threadId: event.threadId,775 appendCaptureFailureActivity({
765 turnId,776 threadId: event.threadId,
766 detail: error.message,777 turnId,
767 createdAt: new Date().toISOString(),778 detail: error.message,
768 }).pipe(Effect.catch(() => Effect.void)),779 createdAt,
780 }).pipe(Effect.catch(() => Effect.void)),
781 ),
769 ),782 ),
770 );783 );
771 return;784 return;
apps/server/src/orchestration/Layers/OrchestrationEngine.test.ts+8-3
...@@ -8,9 +8,14 @@ import {...@@ -8,9 +8,14 @@ import {
8 TurnId,8 TurnId,
9 type OrchestrationEvent,9 type OrchestrationEvent,
10 ProviderInstanceId,10 ProviderInstanceId,
11 type OrchestrationReadModel,
12} from "@t3tools/contracts";11} 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";
14import { describe, expect, it } from "vitest";19import { describe, expect, it } from "vitest";
1520
16import { PersistenceSqlError } from "../../persistence/Errors.ts";21import { PersistenceSqlError } from "../../persistence/Errors.ts";
...@@ -69,7 +74,7 @@ async function createOrchestrationSystem() {...@@ -69,7 +74,7 @@ async function createOrchestrationSystem() {
69}74}
7075
71function now() {76function now() {
72 return new Date().toISOString();77 return "2026-01-01T00:00:00.000Z";
73}78}
7479
75const hasMetricSnapshot = (80const hasMetricSnapshot = (
apps/server/src/orchestration/Layers/OrchestrationEngine.ts+26-20
...@@ -5,20 +5,20 @@ import type {...@@ -5,20 +5,20 @@ import type {
5 ThreadId,5 ThreadId,
6} from "@t3tools/contracts";6} from "@t3tools/contracts";
7import { OrchestrationCommand } from "@t3tools/contracts";7import { OrchestrationCommand } from "@t3tools/contracts";
8import {8import * as Cause from "effect/Cause";
9 Cause,9import * as Clock from "effect/Clock";
10 Deferred,10import * as DateTime from "effect/DateTime";
11 Duration,11import * as Deferred from "effect/Deferred";
12 Effect,12import * as Duration from "effect/Duration";
13 Exit,13import * as Effect from "effect/Effect";
14 Layer,14import * as Exit from "effect/Exit";
15 Metric,15import * as Layer from "effect/Layer";
16 Option,16import * as Metric from "effect/Metric";
17 PubSub,17import * as Option from "effect/Option";
18 Queue,18import * as PubSub from "effect/PubSub";
19 Schema,19import * as Queue from "effect/Queue";
20 Stream,20import * as Schema from "effect/Schema";
21} from "effect";21import * as Stream from "effect/Stream";
22import * as SqlClient from "effect/unstable/sql/SqlClient";22import * as SqlClient from "effect/unstable/sql/SqlClient";
2323
24import {24import {
...@@ -78,7 +78,8 @@ const makeOrchestrationEngine = Effect.gen(function* () {...@@ -78,7 +78,8 @@ const makeOrchestrationEngine = Effect.gen(function* () {
78 const projectionPipeline = yield* OrchestrationProjectionPipeline;78 const projectionPipeline = yield* OrchestrationProjectionPipeline;
79 const projectionSnapshotQuery = yield* ProjectionSnapshotQuery;79 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
83 const commandQueue = yield* Queue.unbounded<CommandEnvelope>();84 const commandQueue = yield* Queue.unbounded<CommandEnvelope>();
84 const eventPubSub = yield* PubSub.unbounded<OrchestrationEvent>();85 const eventPubSub = yield* PubSub.unbounded<OrchestrationEvent>();
...@@ -97,7 +98,7 @@ const makeOrchestrationEngine = Effect.gen(function* () {...@@ -97,7 +98,7 @@ const makeOrchestrationEngine = Effect.gen(function* () {
9798
98 const processEnvelope = (envelope: CommandEnvelope): Effect.Effect<void> => {99 const processEnvelope = (envelope: CommandEnvelope): Effect.Effect<void> => {
99 const dispatchStartSequence = commandReadModel.snapshotSequence;100 const dispatchStartSequence = commandReadModel.snapshotSequence;
100 const processingStartedAtMs = Date.now();101 let processingStartedAtMs = 0;
101 const aggregateRef = commandToAggregateRef(envelope.command);102 const aggregateRef = commandToAggregateRef(envelope.command);
102 const baseMetricAttributes = {103 const baseMetricAttributes = {
103 commandType: envelope.command.type,104 commandType: envelope.command.type,
...@@ -120,6 +121,7 @@ const makeOrchestrationEngine = Effect.gen(function* () {...@@ -120,6 +121,7 @@ const makeOrchestrationEngine = Effect.gen(function* () {
120121
121 return Effect.exit(122 return Effect.exit(
122 Effect.gen(function* () {123 Effect.gen(function* () {
124 processingStartedAtMs = yield* Clock.currentTimeMillis;
123 yield* Effect.annotateCurrentSpan({125 yield* Effect.annotateCurrentSpan({
124 "orchestration.command_id": envelope.command.commandId,126 "orchestration.command_id": envelope.command.commandId,
125 "orchestration.command_type": envelope.command.type,127 "orchestration.command_type": envelope.command.type,
...@@ -205,7 +207,7 @@ const makeOrchestrationEngine = Effect.gen(function* () {...@@ -205,7 +207,7 @@ const makeOrchestrationEngine = Effect.gen(function* () {
205 ackEventType: event.type,207 ackEventType: event.type,
206 }),208 }),
207 ),209 ),
208 Duration.millis(Math.max(0, Date.now() - envelope.startedAtMs)),210 Duration.millis(Math.max(0, (yield* Clock.currentTimeMillis) - envelope.startedAtMs)),
209 );211 );
210 }212 }
211 }213 }
...@@ -224,7 +226,7 @@ const makeOrchestrationEngine = Effect.gen(function* () {...@@ -224,7 +226,7 @@ const makeOrchestrationEngine = Effect.gen(function* () {
224 orchestrationCommandDuration,226 orchestrationCommandDuration,
225 metricAttributes(baseMetricAttributes),227 metricAttributes(baseMetricAttributes),
226 ),228 ),
227 Duration.millis(Math.max(0, Date.now() - processingStartedAtMs)),229 Duration.millis(Math.max(0, (yield* Clock.currentTimeMillis) - processingStartedAtMs)),
228 );230 );
229 yield* Metric.update(231 yield* Metric.update(
230 Metric.withAttributes(232 Metric.withAttributes(
...@@ -263,7 +265,7 @@ const makeOrchestrationEngine = Effect.gen(function* () {...@@ -263,7 +265,7 @@ const makeOrchestrationEngine = Effect.gen(function* () {
263 commandId: envelope.command.commandId,265 commandId: envelope.command.commandId,
264 aggregateKind: aggregateRef.aggregateKind,266 aggregateKind: aggregateRef.aggregateKind,
265 aggregateId: aggregateRef.aggregateId,267 aggregateId: aggregateRef.aggregateId,
266 acceptedAt: new Date().toISOString(),268 acceptedAt: yield* nowIso,
267 resultSequence: commandReadModel.snapshotSequence,269 resultSequence: commandReadModel.snapshotSequence,
268 status: "rejected",270 status: "rejected",
269 error: error.message,271 error: error.message,
...@@ -293,7 +295,11 @@ const makeOrchestrationEngine = Effect.gen(function* () {...@@ -293,7 +295,11 @@ const makeOrchestrationEngine = Effect.gen(function* () {
293 const dispatch: OrchestrationEngineShape["dispatch"] = (command) =>295 const dispatch: OrchestrationEngineShape["dispatch"] = (command) =>
294 Effect.gen(function* () {296 Effect.gen(function* () {
295 const result = yield* Deferred.make<{ sequence: number }, OrchestrationDispatchError>();297 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 });
297 return yield* Deferred.await(result);303 return yield* Deferred.await(result);
298 });304 });
299305
apps/server/src/orchestration/Layers/OrchestrationReactor.test.ts+5-1
...@@ -1,4 +1,8 @@...@@ -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";
2import { afterEach, describe, expect, it } from "vitest";6import { afterEach, describe, expect, it } from "vitest";
37
4import { CheckpointReactor } from "../Services/CheckpointReactor.ts";8import { CheckpointReactor } from "../Services/CheckpointReactor.ts";
apps/server/src/orchestration/Layers/OrchestrationReactor.ts+2-1
...@@ -1,4 +1,5 @@...@@ -1,4 +1,5 @@
1import { Effect, Layer } from "effect";1import * as Effect from "effect/Effect";
2import * as Layer from "effect/Layer";
23
3import {4import {
4 OrchestrationReactor,5 OrchestrationReactor,
apps/server/src/orchestration/Layers/ProjectionPipeline.test.ts+23-16
...@@ -11,7 +11,10 @@ import {...@@ -11,7 +11,10 @@ import {
11} from "@t3tools/contracts";11} from "@t3tools/contracts";
12import * as NodeServices from "@effect/platform-node/NodeServices";12import * as NodeServices from "@effect/platform-node/NodeServices";
13import { assert, it } from "@effect/vitest";13import { 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";
15import * as SqlClient from "effect/unstable/sql/SqlClient";18import * as SqlClient from "effect/unstable/sql/SqlClient";
1619
17import { OrchestrationCommandReceiptRepositoryLive } from "../../persistence/Layers/OrchestrationCommandReceipts.ts";20import { OrchestrationCommandReceiptRepositoryLive } from "../../persistence/Layers/OrchestrationCommandReceipts.ts";
...@@ -55,7 +58,7 @@ it.layer(BaseTestLayer)("OrchestrationProjectionPipeline", (it) => {...@@ -55,7 +58,7 @@ it.layer(BaseTestLayer)("OrchestrationProjectionPipeline", (it) => {
55 const projectionPipeline = yield* OrchestrationProjectionPipeline;58 const projectionPipeline = yield* OrchestrationProjectionPipeline;
56 const eventStore = yield* OrchestrationEventStore;59 const eventStore = yield* OrchestrationEventStore;
57 const sql = yield* SqlClient.SqlClient;60 const sql = yield* SqlClient.SqlClient;
58 const now = new Date().toISOString();61 const now = "2026-01-01T00:00:00.000Z";
5962
60 yield* eventStore.append({63 yield* eventStore.append({
61 type: "project.created",64 type: "project.created",
...@@ -180,7 +183,7 @@ it.layer(Layer.fresh(makeProjectionPipelinePrefixedTestLayer("t3-base-")))(...@@ -180,7 +183,7 @@ it.layer(Layer.fresh(makeProjectionPipelinePrefixedTestLayer("t3-base-")))(
180 const projectionPipeline = yield* OrchestrationProjectionPipeline;183 const projectionPipeline = yield* OrchestrationProjectionPipeline;
181 const eventStore = yield* OrchestrationEventStore;184 const eventStore = yield* OrchestrationEventStore;
182 const sql = yield* SqlClient.SqlClient;185 const sql = yield* SqlClient.SqlClient;
183 const now = new Date().toISOString();186 const now = "2026-01-01T00:00:00.000Z";
184187
185 yield* eventStore.append({188 yield* eventStore.append({
186 type: "thread.message-sent",189 type: "thread.message-sent",
...@@ -224,6 +227,7 @@ it.layer(Layer.fresh(makeProjectionPipelinePrefixedTestLayer("t3-base-")))(...@@ -224,6 +227,7 @@ it.layer(Layer.fresh(makeProjectionPipelinePrefixedTestLayer("t3-base-")))(
224 WHERE message_id = 'message-attachments'227 WHERE message_id = 'message-attachments'
225 `;228 `;
226 assert.equal(rows.length, 1);229 assert.equal(rows.length, 1);
230 // @effect-diagnostics-next-line preferSchemaOverJson:off
227 assert.deepEqual(JSON.parse(rows[0]?.attachmentsJson ?? "null"), [231 assert.deepEqual(JSON.parse(rows[0]?.attachmentsJson ?? "null"), [
228 {232 {
229 type: "image",233 type: "image",
...@@ -246,7 +250,7 @@ it.layer(Layer.fresh(makeProjectionPipelinePrefixedTestLayer("t3-projection-atta...@@ -246,7 +250,7 @@ it.layer(Layer.fresh(makeProjectionPipelinePrefixedTestLayer("t3-projection-atta
246 const projectionPipeline = yield* OrchestrationProjectionPipeline;250 const projectionPipeline = yield* OrchestrationProjectionPipeline;
247 const eventStore = yield* OrchestrationEventStore;251 const eventStore = yield* OrchestrationEventStore;
248 const sql = yield* SqlClient.SqlClient;252 const sql = yield* SqlClient.SqlClient;
249 const now = new Date().toISOString();253 const now = "2026-01-01T00:00:00.000Z";
250254
251 yield* eventStore.append({255 yield* eventStore.append({
252 type: "thread.message-sent",256 type: "thread.message-sent",
...@@ -297,6 +301,7 @@ it.layer(Layer.fresh(makeProjectionPipelinePrefixedTestLayer("t3-projection-atta...@@ -297,6 +301,7 @@ it.layer(Layer.fresh(makeProjectionPipelinePrefixedTestLayer("t3-projection-atta
297 WHERE message_id = 'message-attachments-safe'301 WHERE message_id = 'message-attachments-safe'
298 `;302 `;
299 assert.equal(rows.length, 1);303 assert.equal(rows.length, 1);
304 // @effect-diagnostics-next-line preferSchemaOverJson:off
300 assert.deepEqual(JSON.parse(rows[0]?.attachmentsJson ?? "null"), [305 assert.deepEqual(JSON.parse(rows[0]?.attachmentsJson ?? "null"), [
301 {306 {
302 type: "image",307 type: "image",
...@@ -326,8 +331,8 @@ it.layer(BaseTestLayer)("OrchestrationProjectionPipeline", (it) => {...@@ -326,8 +331,8 @@ it.layer(BaseTestLayer)("OrchestrationProjectionPipeline", (it) => {
326 const projectionPipeline = yield* OrchestrationProjectionPipeline;331 const projectionPipeline = yield* OrchestrationProjectionPipeline;
327 const eventStore = yield* OrchestrationEventStore;332 const eventStore = yield* OrchestrationEventStore;
328 const sql = yield* SqlClient.SqlClient;333 const sql = yield* SqlClient.SqlClient;
329 const now = new Date().toISOString();334 const now = "2026-01-01T00:00:00.000Z";
330 const later = new Date(Date.now() + 1_000).toISOString();335 const later = "2026-01-01T00:00:01.000Z";
331336
332 yield* eventStore.append({337 yield* eventStore.append({
333 type: "project.created",338 type: "project.created",
...@@ -441,6 +446,7 @@ it.layer(BaseTestLayer)("OrchestrationProjectionPipeline", (it) => {...@@ -441,6 +446,7 @@ it.layer(BaseTestLayer)("OrchestrationProjectionPipeline", (it) => {
441 WHERE message_id = 'message-clear-attachments'446 WHERE message_id = 'message-clear-attachments'
442 `;447 `;
443 assert.equal(rows.length, 1);448 assert.equal(rows.length, 1);
449 // @effect-diagnostics-next-line preferSchemaOverJson:off
444 assert.deepEqual(JSON.parse(rows[0]?.attachmentsJson ?? "null"), []);450 assert.deepEqual(JSON.parse(rows[0]?.attachmentsJson ?? "null"), []);
445 }),451 }),
446 );452 );
...@@ -454,8 +460,8 @@ it.layer(...@@ -454,8 +460,8 @@ it.layer(
454 const projectionPipeline = yield* OrchestrationProjectionPipeline;460 const projectionPipeline = yield* OrchestrationProjectionPipeline;
455 const eventStore = yield* OrchestrationEventStore;461 const eventStore = yield* OrchestrationEventStore;
456 const sql = yield* SqlClient.SqlClient;462 const sql = yield* SqlClient.SqlClient;
457 const now = new Date().toISOString();463 const now = "2026-01-01T00:00:00.000Z";
458 const later = new Date(Date.now() + 1_000).toISOString();464 const later = "2026-01-01T00:00:01.000Z";
459465
460 yield* eventStore.append({466 yield* eventStore.append({
461 type: "project.created",467 type: "project.created",
...@@ -576,6 +582,7 @@ it.layer(...@@ -576,6 +582,7 @@ it.layer(
576 WHERE message_id = 'message-overwrite'582 WHERE message_id = 'message-overwrite'
577 `;583 `;
578 assert.equal(rows.length, 1);584 assert.equal(rows.length, 1);
585 // @effect-diagnostics-next-line preferSchemaOverJson:off
579 assert.deepEqual(JSON.parse(rows[0]?.attachmentsJson ?? "null"), [586 assert.deepEqual(JSON.parse(rows[0]?.attachmentsJson ?? "null"), [
580 {587 {
581 type: "image",588 type: "image",
...@@ -598,7 +605,7 @@ it.layer(...@@ -598,7 +605,7 @@ it.layer(
598 const eventStore = yield* OrchestrationEventStore;605 const eventStore = yield* OrchestrationEventStore;
599 const path = yield* Path.Path;606 const path = yield* Path.Path;
600 const sql = yield* SqlClient.SqlClient;607 const sql = yield* SqlClient.SqlClient;
601 const now = new Date().toISOString();608 const now = "2026-01-01T00:00:00.000Z";
602609
603 const appendAndProject = (event: Parameters<typeof eventStore.append>[0]) =>610 const appendAndProject = (event: Parameters<typeof eventStore.append>[0]) =>
604 eventStore611 eventStore
...@@ -722,7 +729,7 @@ it.layer(...@@ -722,7 +729,7 @@ it.layer(
722 const projectionPipeline = yield* OrchestrationProjectionPipeline;729 const projectionPipeline = yield* OrchestrationProjectionPipeline;
723 const eventStore = yield* OrchestrationEventStore;730 const eventStore = yield* OrchestrationEventStore;
724 const { attachmentsDir } = yield* ServerConfig;731 const { attachmentsDir } = yield* ServerConfig;
725 const now = new Date().toISOString();732 const now = "2026-01-01T00:00:00.000Z";
726 const threadId = ThreadId.make("Thread Revert.Files");733 const threadId = ThreadId.make("Thread Revert.Files");
727 const keepAttachmentId = "thread-revert-files-00000000-0000-4000-8000-000000000001";734 const keepAttachmentId = "thread-revert-files-00000000-0000-4000-8000-000000000001";
728 const removeAttachmentId = "thread-revert-files-00000000-0000-4000-8000-000000000002";735 const removeAttachmentId = "thread-revert-files-00000000-0000-4000-8000-000000000002";
...@@ -931,7 +938,7 @@ it.layer(Layer.fresh(makeProjectionPipelinePrefixedTestLayer("t3-projection-atta...@@ -931,7 +938,7 @@ it.layer(Layer.fresh(makeProjectionPipelinePrefixedTestLayer("t3-projection-atta
931 const projectionPipeline = yield* OrchestrationProjectionPipeline;938 const projectionPipeline = yield* OrchestrationProjectionPipeline;
932 const eventStore = yield* OrchestrationEventStore;939 const eventStore = yield* OrchestrationEventStore;
933 const { attachmentsDir } = yield* ServerConfig;940 const { attachmentsDir } = yield* ServerConfig;
934 const now = new Date().toISOString();941 const now = "2026-01-01T00:00:00.000Z";
935 const threadId = ThreadId.make("Thread Delete.Files");942 const threadId = ThreadId.make("Thread Delete.Files");
936 const attachmentId = "thread-delete-files-00000000-0000-4000-8000-000000000001";943 const attachmentId = "thread-delete-files-00000000-0000-4000-8000-000000000001";
937 const otherThreadAttachmentId =944 const otherThreadAttachmentId =
...@@ -1063,7 +1070,7 @@ it.layer(Layer.fresh(makeProjectionPipelinePrefixedTestLayer("t3-projection-atta...@@ -1063,7 +1070,7 @@ it.layer(Layer.fresh(makeProjectionPipelinePrefixedTestLayer("t3-projection-atta
1063 const path = yield* Path.Path;1070 const path = yield* Path.Path;
1064 const projectionPipeline = yield* OrchestrationProjectionPipeline;1071 const projectionPipeline = yield* OrchestrationProjectionPipeline;
1065 const eventStore = yield* OrchestrationEventStore;1072 const eventStore = yield* OrchestrationEventStore;
1066 const now = new Date().toISOString();1073 const now = "2026-01-01T00:00:00.000Z";
1067 const { attachmentsDir: attachmentsRootDir, stateDir } = yield* ServerConfig;1074 const { attachmentsDir: attachmentsRootDir, stateDir } = yield* ServerConfig;
1068 const attachmentsSentinelPath = path.join(attachmentsRootDir, "sentinel.txt");1075 const attachmentsSentinelPath = path.join(attachmentsRootDir, "sentinel.txt");
1069 const stateDirSentinelPath = path.join(stateDir, "state-sentinel.txt");1076 const stateDirSentinelPath = path.join(stateDir, "state-sentinel.txt");
...@@ -1103,7 +1110,7 @@ it.layer(BaseTestLayer)("OrchestrationProjectionPipeline", (it) => {...@@ -1103,7 +1110,7 @@ it.layer(BaseTestLayer)("OrchestrationProjectionPipeline", (it) => {
1103 const projectionPipeline = yield* OrchestrationProjectionPipeline;1110 const projectionPipeline = yield* OrchestrationProjectionPipeline;
1104 const eventStore = yield* OrchestrationEventStore;1111 const eventStore = yield* OrchestrationEventStore;
1105 const sql = yield* SqlClient.SqlClient;1112 const sql = yield* SqlClient.SqlClient;
1106 const now = new Date().toISOString();1113 const now = "2026-01-01T00:00:00.000Z";
11071114
1108 yield* eventStore.append({1115 yield* eventStore.append({
1109 type: "project.created",1116 type: "project.created",
...@@ -1230,7 +1237,7 @@ it.layer(BaseTestLayer)("OrchestrationProjectionPipeline", (it) => {...@@ -1230,7 +1237,7 @@ it.layer(BaseTestLayer)("OrchestrationProjectionPipeline", (it) => {
1230 const projectionPipeline = yield* OrchestrationProjectionPipeline;1237 const projectionPipeline = yield* OrchestrationProjectionPipeline;
1231 const eventStore = yield* OrchestrationEventStore;1238 const eventStore = yield* OrchestrationEventStore;
1232 const sql = yield* SqlClient.SqlClient;1239 const sql = yield* SqlClient.SqlClient;
1233 const now = new Date().toISOString();1240 const now = "2026-01-01T00:00:00.000Z";
12341241
1235 yield* eventStore.append({1242 yield* eventStore.append({
1236 type: "project.created",1243 type: "project.created",
...@@ -2187,7 +2194,7 @@ engineLayer("OrchestrationProjectionPipeline via engine dispatch", (it) => {...@@ -2187,7 +2194,7 @@ engineLayer("OrchestrationProjectionPipeline via engine dispatch", (it) => {
2187 Effect.gen(function* () {2194 Effect.gen(function* () {
2188 const engine = yield* OrchestrationEngineService;2195 const engine = yield* OrchestrationEngineService;
2189 const sql = yield* SqlClient.SqlClient;2196 const sql = yield* SqlClient.SqlClient;
2190 const createdAt = new Date().toISOString();2197 const createdAt = "2026-01-01T00:00:00.000Z";
21912198
2192 yield* engine.dispatch({2199 yield* engine.dispatch({
2193 type: "project.create",2200 type: "project.create",
...@@ -2225,7 +2232,7 @@ engineLayer("OrchestrationProjectionPipeline via engine dispatch", (it) => {...@@ -2225,7 +2232,7 @@ engineLayer("OrchestrationProjectionPipeline via engine dispatch", (it) => {
2225 Effect.gen(function* () {2232 Effect.gen(function* () {
2226 const engine = yield* OrchestrationEngineService;2233 const engine = yield* OrchestrationEngineService;
2227 const sql = yield* SqlClient.SqlClient;2234 const sql = yield* SqlClient.SqlClient;
2228 const createdAt = new Date().toISOString();2235 const createdAt = "2026-01-01T00:00:00.000Z";
22292236
2230 yield* engine.dispatch({2237 yield* engine.dispatch({
2231 type: "project.create",2238 type: "project.create",
apps/server/src/orchestration/Layers/ProjectionPipeline.ts+6-1
...@@ -4,7 +4,12 @@ import {...@@ -4,7 +4,12 @@ import {
4 type OrchestrationEvent,4 type OrchestrationEvent,
5 ThreadId,5 ThreadId,
6} from "@t3tools/contracts";6} 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";
8import * as SqlClient from "effect/unstable/sql/SqlClient";13import * as SqlClient from "effect/unstable/sql/SqlClient";
914
10import { toPersistenceSqlError, type ProjectionRepositoryError } from "../../persistence/Errors.ts";15import { toPersistenceSqlError, type ProjectionRepositoryError } from "../../persistence/Errors.ts";
apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.test.ts+2-1
...@@ -8,7 +8,8 @@ import {...@@ -8,7 +8,8 @@ import {
8 ProviderInstanceId,8 ProviderInstanceId,
9} from "@t3tools/contracts";9} from "@t3tools/contracts";
10import { assert, it } from "@effect/vitest";10import { assert, it } from "@effect/vitest";
11import { Effect, Layer } from "effect";11import * as Effect from "effect/Effect";
12import * as Layer from "effect/Layer";
12import * as SqlClient from "effect/unstable/sql/SqlClient";13import * as SqlClient from "effect/unstable/sql/SqlClient";
1314
14import { SqlitePersistenceMemory } from "../../persistence/Layers/Sqlite.ts";15import { SqlitePersistenceMemory } from "../../persistence/Layers/Sqlite.ts";
apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts+8-4
...@@ -23,7 +23,11 @@ import {...@@ -23,7 +23,11 @@ import {
23 ProjectId,23 ProjectId,
24 ThreadId,24 ThreadId,
25} from "@t3tools/contracts";25} 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";
27import * as SqlClient from "effect/unstable/sql/SqlClient";31import * as SqlClient from "effect/unstable/sql/SqlClient";
28import * as SqlSchema from "effect/unstable/sql/SqlSchema";32import * as SqlSchema from "effect/unstable/sql/SqlSchema";
2933
...@@ -1005,7 +1009,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {...@@ -1005,7 +1009,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
1005 snapshotSequence: computeSnapshotSequence(stateRows),1009 snapshotSequence: computeSnapshotSequence(stateRows),
1006 projects,1010 projects,
1007 threads,1011 threads,
1008 updatedAt: updatedAt ?? new Date(0).toISOString(),1012 updatedAt: updatedAt ?? "1970-01-01T00:00:00.000Z",
1009 };1013 };
10101014
1011 return yield* decodeReadModel(snapshot).pipe(1015 return yield* decodeReadModel(snapshot).pipe(
...@@ -1204,7 +1208,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {...@@ -1204,7 +1208,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
1204 snapshotSequence: computeSnapshotSequence(stateRows),1208 snapshotSequence: computeSnapshotSequence(stateRows),
1205 projects,1209 projects,
1206 threads,1210 threads,
1207 updatedAt: updatedAt ?? new Date(0).toISOString(),1211 updatedAt: updatedAt ?? "1970-01-01T00:00:00.000Z",
1208 } satisfies OrchestrationReadModel;1212 } satisfies OrchestrationReadModel;
1209 }),1213 }),
1210 ),1214 ),
...@@ -1326,7 +1330,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {...@@ -1326,7 +1330,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
1326 hasActionableProposedPlan: row.hasActionableProposedPlan > 0,1330 hasActionableProposedPlan: row.hasActionableProposedPlan > 0,
1327 }),1331 }),
1328 ),1332 ),
1329 updatedAt: updatedAt ?? new Date(0).toISOString(),1333 updatedAt: updatedAt ?? "1970-01-01T00:00:00.000Z",
1330 };1334 };
13311335
1332 return yield* decodeShellSnapshot(snapshot).pipe(1336 return yield* decodeShellSnapshot(snapshot).pipe(
apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts+43-35
...@@ -1,3 +1,4 @@...@@ -1,3 +1,4 @@
1// @effect-diagnostics nodeBuiltinImport:off
1import fs from "node:fs";2import fs from "node:fs";
2import os from "node:os";3import os from "node:os";
3import path from "node:path";4import path from "node:path";
...@@ -20,7 +21,13 @@ import {...@@ -20,7 +21,13 @@ import {
20 ThreadId,21 ThreadId,
21 TurnId,22 TurnId,
22} from "@t3tools/contracts";23} 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";
24import { afterEach, describe, expect, it, vi } from "vitest";31import { afterEach, describe, expect, it, vi } from "vitest";
2532
26import { deriveServerPaths, ServerConfig } from "../../config.ts";33import { deriveServerPaths, ServerConfig } from "../../config.ts";
...@@ -47,6 +54,7 @@ import { OrchestrationEngineService } from "../Services/OrchestrationEngine.ts";...@@ -47,6 +54,7 @@ import { OrchestrationEngineService } from "../Services/OrchestrationEngine.ts";
47import { ProviderCommandReactor } from "../Services/ProviderCommandReactor.ts";54import { ProviderCommandReactor } from "../Services/ProviderCommandReactor.ts";
48import { ProjectionSnapshotQuery } from "../Services/ProjectionSnapshotQuery.ts";55import { ProjectionSnapshotQuery } from "../Services/ProjectionSnapshotQuery.ts";
49import * as NodeServices from "@effect/platform-node/NodeServices";56import * as NodeServices from "@effect/platform-node/NodeServices";
57import * as Clock from "effect/Clock";
50import { ServerSettingsService } from "../../serverSettings.ts";58import { ServerSettingsService } from "../../serverSettings.ts";
51import { VcsStatusBroadcaster } from "../../vcs/VcsStatusBroadcaster.ts";59import { VcsStatusBroadcaster } from "../../vcs/VcsStatusBroadcaster.ts";
52import { GitWorkflowService, type GitWorkflowServiceShape } from "../../git/GitWorkflowService.ts";60import { GitWorkflowService, type GitWorkflowServiceShape } from "../../git/GitWorkflowService.ts";
...@@ -63,15 +71,15 @@ async function waitFor(...@@ -63,15 +71,15 @@ async function waitFor(
63 predicate: () => boolean | Promise<boolean>,71 predicate: () => boolean | Promise<boolean>,
64 timeoutMs = 2000,72 timeoutMs = 2000,
65): Promise<void> {73): Promise<void> {
66 const deadline = Date.now() + timeoutMs;74 const deadline = (await Effect.runPromise(Clock.currentTimeMillis)) + timeoutMs;
67 const poll = async (): Promise<void> => {75 const poll = async (): Promise<void> => {
68 if (await predicate()) {76 if (await predicate()) {
69 return;77 return;
70 }78 }
71 if (Date.now() >= deadline) {79 if ((await Effect.runPromise(Clock.currentTimeMillis)) >= deadline) {
72 throw new Error("Timed out waiting for expectation.");80 throw new Error("Timed out waiting for expectation.");
73 }81 }
74 await new Promise((resolve) => setTimeout(resolve, 10));82 await Effect.runPromise(Effect.yieldNow);
75 return poll();83 return poll();
76 };84 };
7785
...@@ -135,7 +143,7 @@ describe("ProviderCommandReactor", () => {...@@ -135,7 +143,7 @@ describe("ProviderCommandReactor", () => {
135 readonly threadModelSelection?: ModelSelection;143 readonly threadModelSelection?: ModelSelection;
136 readonly sessionModelSwitch?: "unsupported" | "in-session";144 readonly sessionModelSwitch?: "unsupported" | "in-session";
137 }) {145 }) {
138 const now = new Date().toISOString();146 const now = "2026-01-01T00:00:00.000Z";
139 const baseDir = input?.baseDir ?? fs.mkdtempSync(path.join(os.tmpdir(), "t3code-reactor-"));147 const baseDir = input?.baseDir ?? fs.mkdtempSync(path.join(os.tmpdir(), "t3code-reactor-"));
140 createdBaseDirs.add(baseDir);148 createdBaseDirs.add(baseDir);
141 const { stateDir } = deriveServerPathsSync(baseDir, undefined);149 const { stateDir } = deriveServerPathsSync(baseDir, undefined);
...@@ -408,7 +416,7 @@ describe("ProviderCommandReactor", () => {...@@ -408,7 +416,7 @@ describe("ProviderCommandReactor", () => {
408416
409 it("reacts to thread.turn.start by ensuring session and sending provider turn", async () => {417 it("reacts to thread.turn.start by ensuring session and sending provider turn", async () => {
410 const harness = await createHarness();418 const harness = await createHarness();
411 const now = new Date().toISOString();419 const now = "2026-01-01T00:00:00.000Z";
412420
413 await Effect.runPromise(421 await Effect.runPromise(
414 harness.engine.dispatch({422 harness.engine.dispatch({
...@@ -447,7 +455,7 @@ describe("ProviderCommandReactor", () => {...@@ -447,7 +455,7 @@ describe("ProviderCommandReactor", () => {
447455
448 it("generates a thread title on the first turn", async () => {456 it("generates a thread title on the first turn", async () => {
449 const harness = await createHarness();457 const harness = await createHarness();
450 const now = new Date().toISOString();458 const now = "2026-01-01T00:00:00.000Z";
451 const seededTitle = "Please investigate reconnect failures after restar...";459 const seededTitle = "Please investigate reconnect failures after restar...";
452 harness.generateThreadTitle.mockReturnValue(Effect.succeed({ title: "Generated title" }));460 harness.generateThreadTitle.mockReturnValue(Effect.succeed({ title: "Generated title" }));
453461
...@@ -497,7 +505,7 @@ describe("ProviderCommandReactor", () => {...@@ -497,7 +505,7 @@ describe("ProviderCommandReactor", () => {
497505
498 it("does not overwrite an existing custom thread title on the first turn", async () => {506 it("does not overwrite an existing custom thread title on the first turn", async () => {
499 const harness = await createHarness();507 const harness = await createHarness();
500 const now = new Date().toISOString();508 const now = "2026-01-01T00:00:00.000Z";
501 const seededTitle = "Please investigate reconnect failures after restar...";509 const seededTitle = "Please investigate reconnect failures after restar...";
502510
503 await Effect.runPromise(511 await Effect.runPromise(
...@@ -537,7 +545,7 @@ describe("ProviderCommandReactor", () => {...@@ -537,7 +545,7 @@ describe("ProviderCommandReactor", () => {
537545
538 it("matches the client-seeded title even when the outgoing prompt is reformatted", async () => {546 it("matches the client-seeded title even when the outgoing prompt is reformatted", async () => {
539 const harness = await createHarness();547 const harness = await createHarness();
540 const now = new Date().toISOString();548 const now = "2026-01-01T00:00:00.000Z";
541 const seededTitle = "Fix reconnect spinner on resume";549 const seededTitle = "Fix reconnect spinner on resume";
542 harness.generateThreadTitle.mockReturnValue(550 harness.generateThreadTitle.mockReturnValue(
543 Effect.succeed({551 Effect.succeed({
...@@ -588,7 +596,7 @@ describe("ProviderCommandReactor", () => {...@@ -588,7 +596,7 @@ describe("ProviderCommandReactor", () => {
588596
589 it("generates a worktree branch name for the first turn", async () => {597 it("generates a worktree branch name for the first turn", async () => {
590 const harness = await createHarness();598 const harness = await createHarness();
591 const now = new Date().toISOString();599 const now = "2026-01-01T00:00:00.000Z";
592600
593 await Effect.runPromise(601 await Effect.runPromise(
594 harness.engine.dispatch({602 harness.engine.dispatch({
...@@ -642,7 +650,7 @@ describe("ProviderCommandReactor", () => {...@@ -642,7 +650,7 @@ describe("ProviderCommandReactor", () => {
642650
643 it("forwards codex model options through session start and turn send", async () => {651 it("forwards codex model options through session start and turn send", async () => {
644 const harness = await createHarness();652 const harness = await createHarness();
645 const now = new Date().toISOString();653 const now = "2026-01-01T00:00:00.000Z";
646654
647 await Effect.runPromise(655 await Effect.runPromise(
648 harness.engine.dispatch({656 harness.engine.dispatch({
...@@ -689,7 +697,7 @@ describe("ProviderCommandReactor", () => {...@@ -689,7 +697,7 @@ describe("ProviderCommandReactor", () => {
689 model: "claude-sonnet-4-6",697 model: "claude-sonnet-4-6",
690 },698 },
691 });699 });
692 const now = new Date().toISOString();700 const now = "2026-01-01T00:00:00.000Z";
693701
694 await Effect.runPromise(702 await Effect.runPromise(
695 harness.engine.dispatch({703 harness.engine.dispatch({
...@@ -739,7 +747,7 @@ describe("ProviderCommandReactor", () => {...@@ -739,7 +747,7 @@ describe("ProviderCommandReactor", () => {
739 model: "claude-opus-4-6",747 model: "claude-opus-4-6",
740 },748 },
741 });749 });
742 const now = new Date().toISOString();750 const now = "2026-01-01T00:00:00.000Z";
743751
744 await Effect.runPromise(752 await Effect.runPromise(
745 harness.engine.dispatch({753 harness.engine.dispatch({
...@@ -784,7 +792,7 @@ describe("ProviderCommandReactor", () => {...@@ -784,7 +792,7 @@ describe("ProviderCommandReactor", () => {
784792
785 it("forwards plan interaction mode to the provider turn request", async () => {793 it("forwards plan interaction mode to the provider turn request", async () => {
786 const harness = await createHarness();794 const harness = await createHarness();
787 const now = new Date().toISOString();795 const now = "2026-01-01T00:00:00.000Z";
788796
789 await Effect.runPromise(797 await Effect.runPromise(
790 harness.engine.dispatch({798 harness.engine.dispatch({
...@@ -822,7 +830,7 @@ describe("ProviderCommandReactor", () => {...@@ -822,7 +830,7 @@ describe("ProviderCommandReactor", () => {
822830
823 it("preserves the active session model when in-session model switching is unsupported", async () => {831 it("preserves the active session model when in-session model switching is unsupported", async () => {
824 const harness = await createHarness({ sessionModelSwitch: "unsupported" });832 const harness = await createHarness({ sessionModelSwitch: "unsupported" });
825 const now = new Date().toISOString();833 const now = "2026-01-01T00:00:00.000Z";
826834
827 await Effect.runPromise(835 await Effect.runPromise(
828 harness.engine.dispatch({836 harness.engine.dispatch({
...@@ -875,7 +883,7 @@ describe("ProviderCommandReactor", () => {...@@ -875,7 +883,7 @@ describe("ProviderCommandReactor", () => {
875 const harness = await createHarness({883 const harness = await createHarness({
876 threadModelSelection: { instanceId: ProviderInstanceId.make("codex"), model: "gpt-5-codex" },884 threadModelSelection: { instanceId: ProviderInstanceId.make("codex"), model: "gpt-5-codex" },
877 });885 });
878 const now = new Date().toISOString();886 const now = "2026-01-01T00:00:00.000Z";
879887
880 await Effect.runPromise(888 await Effect.runPromise(
881 harness.engine.dispatch({889 harness.engine.dispatch({
...@@ -921,7 +929,7 @@ describe("ProviderCommandReactor", () => {...@@ -921,7 +929,7 @@ describe("ProviderCommandReactor", () => {
921929
922 it("reuses the same provider session when runtime mode is unchanged", async () => {930 it("reuses the same provider session when runtime mode is unchanged", async () => {
923 const harness = await createHarness();931 const harness = await createHarness();
924 const now = new Date().toISOString();932 const now = "2026-01-01T00:00:00.000Z";
925933
926 await Effect.runPromise(934 await Effect.runPromise(
927 harness.engine.dispatch({935 harness.engine.dispatch({
...@@ -967,7 +975,7 @@ describe("ProviderCommandReactor", () => {...@@ -967,7 +975,7 @@ describe("ProviderCommandReactor", () => {
967975
968 it("restarts an existing Codex thread on a compatible requested instance", async () => {976 it("restarts an existing Codex thread on a compatible requested instance", async () => {
969 const harness = await createHarness();977 const harness = await createHarness();
970 const now = new Date().toISOString();978 const now = "2026-01-01T00:00:00.000Z";
971979
972 await Effect.runPromise(980 await Effect.runPromise(
973 harness.engine.dispatch({981 harness.engine.dispatch({
...@@ -1009,7 +1017,7 @@ describe("ProviderCommandReactor", () => {...@@ -1009,7 +1017,7 @@ describe("ProviderCommandReactor", () => {
1009 },1017 },
1010 interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE,1018 interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE,
1011 runtimeMode: "approval-required",1019 runtimeMode: "approval-required",
1012 createdAt: new Date().toISOString(),1020 createdAt: "2026-01-01T00:00:00.000Z",
1013 }),1021 }),
1014 );1022 );
10151023
...@@ -1034,7 +1042,7 @@ describe("ProviderCommandReactor", () => {...@@ -1034,7 +1042,7 @@ describe("ProviderCommandReactor", () => {
1034 model: "claude-sonnet-4-6",1042 model: "claude-sonnet-4-6",
1035 },1043 },
1036 });1044 });
1037 const now = new Date().toISOString();1045 const now = "2026-01-01T00:00:00.000Z";
10381046
1039 await Effect.runPromise(1047 await Effect.runPromise(
1040 harness.engine.dispatch({1048 harness.engine.dispatch({
...@@ -1107,7 +1115,7 @@ describe("ProviderCommandReactor", () => {...@@ -1107,7 +1115,7 @@ describe("ProviderCommandReactor", () => {
1107 model: "claude-sonnet-4-6",1115 model: "claude-sonnet-4-6",
1108 },1116 },
1109 });1117 });
1110 const now = new Date().toISOString();1118 const now = "2026-01-01T00:00:00.000Z";
11111119
1112 await Effect.runPromise(1120 await Effect.runPromise(
1113 harness.engine.dispatch({1121 harness.engine.dispatch({
...@@ -1170,7 +1178,7 @@ describe("ProviderCommandReactor", () => {...@@ -1170,7 +1178,7 @@ describe("ProviderCommandReactor", () => {
11701178
1171 it("restarts the provider session when runtime mode is updated on the thread", async () => {1179 it("restarts the provider session when runtime mode is updated on the thread", async () => {
1172 const harness = await createHarness();1180 const harness = await createHarness();
1173 const now = new Date().toISOString();1181 const now = "2026-01-01T00:00:00.000Z";
11741182
1175 await Effect.runPromise(1183 await Effect.runPromise(
1176 harness.engine.dispatch({1184 harness.engine.dispatch({
...@@ -1260,7 +1268,7 @@ describe("ProviderCommandReactor", () => {...@@ -1260,7 +1268,7 @@ describe("ProviderCommandReactor", () => {
1260 model: "claude-opus-4-6",1268 model: "claude-opus-4-6",
1261 },1269 },
1262 });1270 });
1263 const now = new Date().toISOString();1271 const now = "2026-01-01T00:00:00.000Z";
12641272
1265 await Effect.runPromise(1273 await Effect.runPromise(
1266 harness.engine.dispatch({1274 harness.engine.dispatch({
...@@ -1303,7 +1311,7 @@ describe("ProviderCommandReactor", () => {...@@ -1303,7 +1311,7 @@ describe("ProviderCommandReactor", () => {
13031311
1304 it("does not stop the active session when restart fails before rebind", async () => {1312 it("does not stop the active session when restart fails before rebind", async () => {
1305 const harness = await createHarness();1313 const harness = await createHarness();
1306 const now = new Date().toISOString();1314 const now = "2026-01-01T00:00:00.000Z";
13071315
1308 await Effect.runPromise(1316 await Effect.runPromise(
1309 harness.engine.dispatch({1317 harness.engine.dispatch({
...@@ -1336,7 +1344,7 @@ describe("ProviderCommandReactor", () => {...@@ -1336,7 +1344,7 @@ describe("ProviderCommandReactor", () => {
1336 await waitFor(() => harness.sendTurn.mock.calls.length === 1);1344 await waitFor(() => harness.sendTurn.mock.calls.length === 1);
13371345
1338 harness.startSession.mockImplementationOnce(1346 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,
1340 );1348 );
13411349
1342 await Effect.runPromise(1350 await Effect.runPromise(
...@@ -1368,7 +1376,7 @@ describe("ProviderCommandReactor", () => {...@@ -1368,7 +1376,7 @@ describe("ProviderCommandReactor", () => {
13681376
1369 it("rejects provider changes after a thread is already bound to a session provider", async () => {1377 it("rejects provider changes after a thread is already bound to a session provider", async () => {
1370 const harness = await createHarness();1378 const harness = await createHarness();
1371 const now = new Date().toISOString();1379 const now = "2026-01-01T00:00:00.000Z";
13721380
1373 await Effect.runPromise(1381 await Effect.runPromise(
1374 harness.engine.dispatch({1382 harness.engine.dispatch({
...@@ -1440,7 +1448,7 @@ describe("ProviderCommandReactor", () => {...@@ -1440,7 +1448,7 @@ describe("ProviderCommandReactor", () => {
14401448
1441 it("rejects cross-driver provider changes after the existing thread session has stopped", async () => {1449 it("rejects cross-driver provider changes after the existing thread session has stopped", async () => {
1442 const harness = await createHarness();1450 const harness = await createHarness();
1443 const now = new Date().toISOString();1451 const now = "2026-01-01T00:00:00.000Z";
14441452
1445 await Effect.runPromise(1453 await Effect.runPromise(
1446 harness.engine.dispatch({1454 harness.engine.dispatch({
...@@ -1506,7 +1514,7 @@ describe("ProviderCommandReactor", () => {...@@ -1506,7 +1514,7 @@ describe("ProviderCommandReactor", () => {
15061514
1507 it("reacts to thread.turn.interrupt-requested by calling provider interrupt", async () => {1515 it("reacts to thread.turn.interrupt-requested by calling provider interrupt", async () => {
1508 const harness = await createHarness();1516 const harness = await createHarness();
1509 const now = new Date().toISOString();1517 const now = "2026-01-01T00:00:00.000Z";
15101518
1511 await Effect.runPromise(1519 await Effect.runPromise(
1512 harness.engine.dispatch({1520 harness.engine.dispatch({
...@@ -1544,7 +1552,7 @@ describe("ProviderCommandReactor", () => {...@@ -1544,7 +1552,7 @@ describe("ProviderCommandReactor", () => {
15441552
1545 it("starts a fresh session when only projected session state exists", async () => {1553 it("starts a fresh session when only projected session state exists", async () => {
1546 const harness = await createHarness();1554 const harness = await createHarness();
1547 const now = new Date().toISOString();1555 const now = "2026-01-01T00:00:00.000Z";
15481556
1549 await Effect.runPromise(1557 await Effect.runPromise(
1550 harness.engine.dispatch({1558 harness.engine.dispatch({
...@@ -1599,7 +1607,7 @@ describe("ProviderCommandReactor", () => {...@@ -1599,7 +1607,7 @@ describe("ProviderCommandReactor", () => {
15991607
1600 it("rejects active runtime sessions that are missing provider instance ids", async () => {1608 it("rejects active runtime sessions that are missing provider instance ids", async () => {
1601 const harness = await createHarness();1609 const harness = await createHarness();
1602 const now = new Date().toISOString();1610 const now = "2026-01-01T00:00:00.000Z";
16031611
1604 await Effect.runPromise(1612 await Effect.runPromise(
1605 harness.engine.dispatch({1613 harness.engine.dispatch({
...@@ -1670,7 +1678,7 @@ describe("ProviderCommandReactor", () => {...@@ -1670,7 +1678,7 @@ describe("ProviderCommandReactor", () => {
16701678
1671 it("reacts to thread.approval.respond by forwarding provider approval response", async () => {1679 it("reacts to thread.approval.respond by forwarding provider approval response", async () => {
1672 const harness = await createHarness();1680 const harness = await createHarness();
1673 const now = new Date().toISOString();1681 const now = "2026-01-01T00:00:00.000Z";
16741682
1675 await Effect.runPromise(1683 await Effect.runPromise(
1676 harness.engine.dispatch({1684 harness.engine.dispatch({
...@@ -1711,7 +1719,7 @@ describe("ProviderCommandReactor", () => {...@@ -1711,7 +1719,7 @@ describe("ProviderCommandReactor", () => {
17111719
1712 it("reacts to thread.user-input.respond by forwarding structured user input answers", async () => {1720 it("reacts to thread.user-input.respond by forwarding structured user input answers", async () => {
1713 const harness = await createHarness();1721 const harness = await createHarness();
1714 const now = new Date().toISOString();1722 const now = "2026-01-01T00:00:00.000Z";
17151723
1716 await Effect.runPromise(1724 await Effect.runPromise(
1717 harness.engine.dispatch({1725 harness.engine.dispatch({
...@@ -1756,7 +1764,7 @@ describe("ProviderCommandReactor", () => {...@@ -1756,7 +1764,7 @@ describe("ProviderCommandReactor", () => {
17561764
1757 it("surfaces stale provider approval request failures without faking approval resolution", async () => {1765 it("surfaces stale provider approval request failures without faking approval resolution", async () => {
1758 const harness = await createHarness();1766 const harness = await createHarness();
1759 const now = new Date().toISOString();1767 const now = "2026-01-01T00:00:00.000Z";
1760 harness.respondToRequest.mockImplementation(() =>1768 harness.respondToRequest.mockImplementation(() =>
1761 Effect.fail(1769 Effect.fail(
1762 new ProviderAdapterRequestError({1770 new ProviderAdapterRequestError({
...@@ -1851,7 +1859,7 @@ describe("ProviderCommandReactor", () => {...@@ -1851,7 +1859,7 @@ describe("ProviderCommandReactor", () => {
18511859
1852 it("surfaces stale provider user-input failures without faking user-input resolution", async () => {1860 it("surfaces stale provider user-input failures without faking user-input resolution", async () => {
1853 const harness = await createHarness();1861 const harness = await createHarness();
1854 const now = new Date().toISOString();1862 const now = "2026-01-01T00:00:00.000Z";
1855 harness.respondToUserInput.mockImplementation(() =>1863 harness.respondToUserInput.mockImplementation(() =>
1856 Effect.fail(1864 Effect.fail(
1857 new ProviderAdapterRequestError({1865 new ProviderAdapterRequestError({
...@@ -1960,7 +1968,7 @@ describe("ProviderCommandReactor", () => {...@@ -1960,7 +1968,7 @@ describe("ProviderCommandReactor", () => {
19601968
1961 it("reacts to thread.session.stop by stopping provider session and clearing thread session state", async () => {1969 it("reacts to thread.session.stop by stopping provider session and clearing thread session state", async () => {
1962 const harness = await createHarness();1970 const harness = await createHarness();
1963 const now = new Date().toISOString();1971 const now = "2026-01-01T00:00:00.000Z";
19641972
1965 await Effect.runPromise(1973 await Effect.runPromise(
1966 harness.engine.dispatch({1974 harness.engine.dispatch({
apps/server/src/orchestration/Layers/ProviderCommandReactor.ts+9-1
...@@ -13,7 +13,15 @@ import {...@@ -13,7 +13,15 @@ import {
13 type TurnId,13 type TurnId,
14} from "@t3tools/contracts";14} from "@t3tools/contracts";
15import { isTemporaryWorktreeBranch, WORKTREE_BRANCH_PREFIX } from "@t3tools/shared/git";15import { 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";
17import { makeDrainableWorker } from "@t3tools/shared/DrainableWorker";25import { makeDrainableWorker } from "@t3tools/shared/DrainableWorker";
1826
19import { resolveThreadWorkspaceCwd } from "../../checkpointing/Utils.ts";27import { resolveThreadWorkspaceCwd } from "../../checkpointing/Utils.ts";
apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts+69-61
...@@ -1,3 +1,4 @@...@@ -1,3 +1,4 @@
1// @effect-diagnostics nodeBuiltinImport:off
1import fs from "node:fs";2import fs from "node:fs";
2import os from "node:os";3import os from "node:os";
3import path from "node:path";4import path from "node:path";
...@@ -21,7 +22,14 @@ import {...@@ -21,7 +22,14 @@ import {
21 ThreadId,22 ThreadId,
22 TurnId,23 TurnId,
23} from "@t3tools/contracts";24} 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";
25import { afterEach, describe, expect, it } from "vitest";33import { afterEach, describe, expect, it } from "vitest";
2634
27import { OrchestrationEventStoreLive } from "../../persistence/Layers/OrchestrationEventStore.ts";35import { OrchestrationEventStoreLive } from "../../persistence/Layers/OrchestrationEventStore.ts";
...@@ -165,17 +173,17 @@ async function waitForThread(...@@ -165,17 +173,17 @@ async function waitForThread(
165 timeoutMs = 2000,173 timeoutMs = 2000,
166 threadId: ThreadId = asThreadId("thread-1"),174 threadId: ThreadId = asThreadId("thread-1"),
167) {175) {
168 const deadline = Date.now() + timeoutMs;176 const deadline = (await Effect.runPromise(Clock.currentTimeMillis)) + timeoutMs;
169 const poll = async (): Promise<ProviderRuntimeTestThread> => {177 const poll = async (): Promise<ProviderRuntimeTestThread> => {
170 const snapshot = await readModel();178 const snapshot = await readModel();
171 const thread = snapshot.threads.find((entry) => entry.id === threadId);179 const thread = snapshot.threads.find((entry) => entry.id === threadId);
172 if (thread && predicate(thread)) {180 if (thread && predicate(thread)) {
173 return thread;181 return thread;
174 }182 }
175 if (Date.now() >= deadline) {183 if ((await Effect.runPromise(Clock.currentTimeMillis)) >= deadline) {
176 throw new Error("Timed out waiting for thread state");184 throw new Error("Timed out waiting for thread state");
177 }185 }
178 await new Promise((resolve) => setTimeout(resolve, 10));186 await Effect.runPromise(Effect.yieldNow);
179 return poll();187 return poll();
180 };188 };
181 return poll();189 return poll();
...@@ -242,7 +250,7 @@ describe("ProviderRuntimeIngestion", () => {...@@ -242,7 +250,7 @@ describe("ProviderRuntimeIngestion", () => {
242 await Effect.runPromise(ingestion.start().pipe(Scope.provide(scope)));250 await Effect.runPromise(ingestion.start().pipe(Scope.provide(scope)));
243 const drain = () => Effect.runPromise(ingestion.drain);251 const drain = () => Effect.runPromise(ingestion.drain);
244252
245 const createdAt = new Date().toISOString();253 const createdAt = "2026-01-01T00:00:00.000Z";
246 await Effect.runPromise(254 await Effect.runPromise(
247 engine.dispatch({255 engine.dispatch({
248 type: "project.create",256 type: "project.create",
...@@ -312,7 +320,7 @@ describe("ProviderRuntimeIngestion", () => {...@@ -312,7 +320,7 @@ describe("ProviderRuntimeIngestion", () => {
312320
313 it("maps turn started/completed events into thread session updates", async () => {321 it("maps turn started/completed events into thread session updates", async () => {
314 const harness = await createHarness();322 const harness = await createHarness();
315 const now = new Date().toISOString();323 const now = "2026-01-01T00:00:00.000Z";
316324
317 harness.emit({325 harness.emit({
318 type: "turn.started",326 type: "turn.started",
...@@ -333,7 +341,7 @@ describe("ProviderRuntimeIngestion", () => {...@@ -333,7 +341,7 @@ describe("ProviderRuntimeIngestion", () => {
333 eventId: asEventId("evt-turn-completed"),341 eventId: asEventId("evt-turn-completed"),
334 provider: ProviderDriverKind.make("codex"),342 provider: ProviderDriverKind.make("codex"),
335 threadId: asThreadId("thread-1"),343 threadId: asThreadId("thread-1"),
336 createdAt: new Date().toISOString(),344 createdAt: "2026-01-01T00:00:00.000Z",
337 turnId: asTurnId("turn-1"),345 turnId: asTurnId("turn-1"),
338 payload: {346 payload: {
339 state: "failed",347 state: "failed",
...@@ -354,7 +362,7 @@ describe("ProviderRuntimeIngestion", () => {...@@ -354,7 +362,7 @@ describe("ProviderRuntimeIngestion", () => {
354362
355 it("applies provider session.state.changed transitions directly", async () => {363 it("applies provider session.state.changed transitions directly", async () => {
356 const harness = await createHarness();364 const harness = await createHarness();
357 const waitingAt = new Date().toISOString();365 const waitingAt = "2026-01-01T00:00:00.000Z";
358366
359 harness.emit({367 harness.emit({
360 type: "session.state.changed",368 type: "session.state.changed",
...@@ -380,7 +388,7 @@ describe("ProviderRuntimeIngestion", () => {...@@ -380,7 +388,7 @@ describe("ProviderRuntimeIngestion", () => {
380 eventId: asEventId("evt-session-state-error"),388 eventId: asEventId("evt-session-state-error"),
381 provider: ProviderDriverKind.make("codex"),389 provider: ProviderDriverKind.make("codex"),
382 threadId: asThreadId("thread-1"),390 threadId: asThreadId("thread-1"),
383 createdAt: new Date().toISOString(),391 createdAt: "2026-01-01T00:00:00.000Z",
384 payload: {392 payload: {
385 state: "error",393 state: "error",
386 reason: "provider crashed",394 reason: "provider crashed",
...@@ -402,7 +410,7 @@ describe("ProviderRuntimeIngestion", () => {...@@ -402,7 +410,7 @@ describe("ProviderRuntimeIngestion", () => {
402 eventId: asEventId("evt-session-state-stopped"),410 eventId: asEventId("evt-session-state-stopped"),
403 provider: ProviderDriverKind.make("codex"),411 provider: ProviderDriverKind.make("codex"),
404 threadId: asThreadId("thread-1"),412 threadId: asThreadId("thread-1"),
405 createdAt: new Date().toISOString(),413 createdAt: "2026-01-01T00:00:00.000Z",
406 payload: {414 payload: {
407 state: "stopped",415 state: "stopped",
408 },416 },
...@@ -423,7 +431,7 @@ describe("ProviderRuntimeIngestion", () => {...@@ -423,7 +431,7 @@ describe("ProviderRuntimeIngestion", () => {
423 eventId: asEventId("evt-session-state-ready"),431 eventId: asEventId("evt-session-state-ready"),
424 provider: ProviderDriverKind.make("codex"),432 provider: ProviderDriverKind.make("codex"),
425 threadId: asThreadId("thread-1"),433 threadId: asThreadId("thread-1"),
426 createdAt: new Date().toISOString(),434 createdAt: "2026-01-01T00:00:00.000Z",
427 payload: {435 payload: {
428 state: "ready",436 state: "ready",
429 },437 },
...@@ -442,7 +450,7 @@ describe("ProviderRuntimeIngestion", () => {...@@ -442,7 +450,7 @@ describe("ProviderRuntimeIngestion", () => {
442450
443 it("does not clear active turn when session/thread started arrives mid-turn", async () => {451 it("does not clear active turn when session/thread started arrives mid-turn", async () => {
444 const harness = await createHarness();452 const harness = await createHarness();
445 const now = new Date().toISOString();453 const now = "2026-01-01T00:00:00.000Z";
446454
447 harness.emit({455 harness.emit({
448 type: "turn.started",456 type: "turn.started",
...@@ -464,14 +472,14 @@ describe("ProviderRuntimeIngestion", () => {...@@ -464,14 +472,14 @@ describe("ProviderRuntimeIngestion", () => {
464 type: "thread.started",472 type: "thread.started",
465 eventId: asEventId("evt-thread-started-midturn-lifecycle"),473 eventId: asEventId("evt-thread-started-midturn-lifecycle"),
466 provider: ProviderDriverKind.make("codex"),474 provider: ProviderDriverKind.make("codex"),
467 createdAt: new Date().toISOString(),475 createdAt: "2026-01-01T00:00:00.000Z",
468 threadId: asThreadId("thread-1"),476 threadId: asThreadId("thread-1"),
469 });477 });
470 harness.emit({478 harness.emit({
471 type: "session.started",479 type: "session.started",
472 eventId: asEventId("evt-session-started-midturn-lifecycle"),480 eventId: asEventId("evt-session-started-midturn-lifecycle"),
473 provider: ProviderDriverKind.make("codex"),481 provider: ProviderDriverKind.make("codex"),
474 createdAt: new Date().toISOString(),482 createdAt: "2026-01-01T00:00:00.000Z",
475 threadId: asThreadId("thread-1"),483 threadId: asThreadId("thread-1"),
476 });484 });
477485
...@@ -485,7 +493,7 @@ describe("ProviderRuntimeIngestion", () => {...@@ -485,7 +493,7 @@ describe("ProviderRuntimeIngestion", () => {
485 type: "turn.completed",493 type: "turn.completed",
486 eventId: asEventId("evt-turn-completed-midturn-lifecycle"),494 eventId: asEventId("evt-turn-completed-midturn-lifecycle"),
487 provider: ProviderDriverKind.make("codex"),495 provider: ProviderDriverKind.make("codex"),
488 createdAt: new Date().toISOString(),496 createdAt: "2026-01-01T00:00:00.000Z",
489 threadId: asThreadId("thread-1"),497 threadId: asThreadId("thread-1"),
490 turnId: asTurnId("turn-midturn-lifecycle"),498 turnId: asTurnId("turn-midturn-lifecycle"),
491 status: "completed",499 status: "completed",
...@@ -499,7 +507,7 @@ describe("ProviderRuntimeIngestion", () => {...@@ -499,7 +507,7 @@ describe("ProviderRuntimeIngestion", () => {
499507
500 it("accepts claude turn lifecycle when seeded thread id is a synthetic placeholder", async () => {508 it("accepts claude turn lifecycle when seeded thread id is a synthetic placeholder", async () => {
501 const harness = await createHarness();509 const harness = await createHarness();
502 const seededAt = new Date().toISOString();510 const seededAt = "2026-01-01T00:00:00.000Z";
503511
504 await Effect.runPromise(512 await Effect.runPromise(
505 harness.engine.dispatch({513 harness.engine.dispatch({
...@@ -523,7 +531,7 @@ describe("ProviderRuntimeIngestion", () => {...@@ -523,7 +531,7 @@ describe("ProviderRuntimeIngestion", () => {
523 type: "turn.started",531 type: "turn.started",
524 eventId: asEventId("evt-turn-started-claude-placeholder"),532 eventId: asEventId("evt-turn-started-claude-placeholder"),
525 provider: ProviderDriverKind.make("claudeAgent"),533 provider: ProviderDriverKind.make("claudeAgent"),
526 createdAt: new Date().toISOString(),534 createdAt: "2026-01-01T00:00:00.000Z",
527 threadId: asThreadId("thread-1"),535 threadId: asThreadId("thread-1"),
528 turnId: asTurnId("turn-claude-placeholder"),536 turnId: asTurnId("turn-claude-placeholder"),
529 });537 });
...@@ -539,7 +547,7 @@ describe("ProviderRuntimeIngestion", () => {...@@ -539,7 +547,7 @@ describe("ProviderRuntimeIngestion", () => {
539 type: "turn.completed",547 type: "turn.completed",
540 eventId: asEventId("evt-turn-completed-claude-placeholder"),548 eventId: asEventId("evt-turn-completed-claude-placeholder"),
541 provider: ProviderDriverKind.make("claudeAgent"),549 provider: ProviderDriverKind.make("claudeAgent"),
542 createdAt: new Date().toISOString(),550 createdAt: "2026-01-01T00:00:00.000Z",
543 threadId: asThreadId("thread-1"),551 threadId: asThreadId("thread-1"),
544 turnId: asTurnId("turn-claude-placeholder"),552 turnId: asTurnId("turn-claude-placeholder"),
545 status: "completed",553 status: "completed",
...@@ -553,7 +561,7 @@ describe("ProviderRuntimeIngestion", () => {...@@ -553,7 +561,7 @@ describe("ProviderRuntimeIngestion", () => {
553561
554 it("ignores auxiliary turn completions from a different provider thread", async () => {562 it("ignores auxiliary turn completions from a different provider thread", async () => {
555 const harness = await createHarness();563 const harness = await createHarness();
556 const now = new Date().toISOString();564 const now = "2026-01-01T00:00:00.000Z";
557565
558 harness.emit({566 harness.emit({
559 type: "turn.started",567 type: "turn.started",
...@@ -574,7 +582,7 @@ describe("ProviderRuntimeIngestion", () => {...@@ -574,7 +582,7 @@ describe("ProviderRuntimeIngestion", () => {
574 type: "turn.completed",582 type: "turn.completed",
575 eventId: asEventId("evt-turn-completed-aux"),583 eventId: asEventId("evt-turn-completed-aux"),
576 provider: ProviderDriverKind.make("codex"),584 provider: ProviderDriverKind.make("codex"),
577 createdAt: new Date().toISOString(),585 createdAt: "2026-01-01T00:00:00.000Z",
578 threadId: asThreadId("thread-1"),586 threadId: asThreadId("thread-1"),
579 turnId: asTurnId("turn-aux"),587 turnId: asTurnId("turn-aux"),
580 status: "completed",588 status: "completed",
...@@ -590,7 +598,7 @@ describe("ProviderRuntimeIngestion", () => {...@@ -590,7 +598,7 @@ describe("ProviderRuntimeIngestion", () => {
590 type: "turn.completed",598 type: "turn.completed",
591 eventId: asEventId("evt-turn-completed-primary"),599 eventId: asEventId("evt-turn-completed-primary"),
592 provider: ProviderDriverKind.make("codex"),600 provider: ProviderDriverKind.make("codex"),
593 createdAt: new Date().toISOString(),601 createdAt: "2026-01-01T00:00:00.000Z",
594 threadId: asThreadId("thread-1"),602 threadId: asThreadId("thread-1"),
595 turnId: asTurnId("turn-primary"),603 turnId: asTurnId("turn-primary"),
596 status: "completed",604 status: "completed",
...@@ -604,7 +612,7 @@ describe("ProviderRuntimeIngestion", () => {...@@ -604,7 +612,7 @@ describe("ProviderRuntimeIngestion", () => {
604612
605 it("ignores non-active turn completion when runtime omits thread id", async () => {613 it("ignores non-active turn completion when runtime omits thread id", async () => {
606 const harness = await createHarness();614 const harness = await createHarness();
607 const now = new Date().toISOString();615 const now = "2026-01-01T00:00:00.000Z";
608616
609 harness.emit({617 harness.emit({
610 type: "turn.started",618 type: "turn.started",
...@@ -626,7 +634,7 @@ describe("ProviderRuntimeIngestion", () => {...@@ -626,7 +634,7 @@ describe("ProviderRuntimeIngestion", () => {
626 type: "turn.completed",634 type: "turn.completed",
627 eventId: asEventId("evt-turn-completed-guarded-other"),635 eventId: asEventId("evt-turn-completed-guarded-other"),
628 provider: ProviderDriverKind.make("codex"),636 provider: ProviderDriverKind.make("codex"),
629 createdAt: new Date().toISOString(),637 createdAt: "2026-01-01T00:00:00.000Z",
630 threadId: asThreadId("thread-1"),638 threadId: asThreadId("thread-1"),
631 turnId: asTurnId("turn-guarded-other"),639 turnId: asTurnId("turn-guarded-other"),
632 status: "completed",640 status: "completed",
...@@ -642,7 +650,7 @@ describe("ProviderRuntimeIngestion", () => {...@@ -642,7 +650,7 @@ describe("ProviderRuntimeIngestion", () => {
642 type: "turn.completed",650 type: "turn.completed",
643 eventId: asEventId("evt-turn-completed-guarded-main"),651 eventId: asEventId("evt-turn-completed-guarded-main"),
644 provider: ProviderDriverKind.make("codex"),652 provider: ProviderDriverKind.make("codex"),
645 createdAt: new Date().toISOString(),653 createdAt: "2026-01-01T00:00:00.000Z",
646 threadId: asThreadId("thread-1"),654 threadId: asThreadId("thread-1"),
647 turnId: asTurnId("turn-guarded-main"),655 turnId: asTurnId("turn-guarded-main"),
648 status: "completed",656 status: "completed",
...@@ -656,7 +664,7 @@ describe("ProviderRuntimeIngestion", () => {...@@ -656,7 +664,7 @@ describe("ProviderRuntimeIngestion", () => {
656664
657 it("maps canonical content delta/item completed into finalized assistant messages", async () => {665 it("maps canonical content delta/item completed into finalized assistant messages", async () => {
658 const harness = await createHarness();666 const harness = await createHarness();
659 const now = new Date().toISOString();667 const now = "2026-01-01T00:00:00.000Z";
660668
661 harness.emit({669 harness.emit({
662 type: "content.delta",670 type: "content.delta",
...@@ -713,7 +721,7 @@ describe("ProviderRuntimeIngestion", () => {...@@ -713,7 +721,7 @@ describe("ProviderRuntimeIngestion", () => {
713721
714 it("uses assistant item completion detail when no assistant deltas were streamed", async () => {722 it("uses assistant item completion detail when no assistant deltas were streamed", async () => {
715 const harness = await createHarness();723 const harness = await createHarness();
716 const now = new Date().toISOString();724 const now = "2026-01-01T00:00:00.000Z";
717725
718 harness.emit({726 harness.emit({
719 type: "item.completed",727 type: "item.completed",
...@@ -745,7 +753,7 @@ describe("ProviderRuntimeIngestion", () => {...@@ -745,7 +753,7 @@ describe("ProviderRuntimeIngestion", () => {
745753
746 it("preserves completed tool metadata on projected tool activities", async () => {754 it("preserves completed tool metadata on projected tool activities", async () => {
747 const harness = await createHarness();755 const harness = await createHarness();
748 const now = new Date().toISOString();756 const now = "2026-01-01T00:00:00.000Z";
749757
750 harness.emit({758 harness.emit({
751 type: "item.completed",759 type: "item.completed",
...@@ -801,7 +809,7 @@ describe("ProviderRuntimeIngestion", () => {...@@ -801,7 +809,7 @@ describe("ProviderRuntimeIngestion", () => {
801809
802 it("normalizes command execution activities to ran-command summaries", async () => {810 it("normalizes command execution activities to ran-command summaries", async () => {
803 const harness = await createHarness();811 const harness = await createHarness();
804 const now = new Date().toISOString();812 const now = "2026-01-01T00:00:00.000Z";
805813
806 harness.emit({814 harness.emit({
807 type: "item.completed",815 type: "item.completed",
...@@ -843,7 +851,7 @@ describe("ProviderRuntimeIngestion", () => {...@@ -843,7 +851,7 @@ describe("ProviderRuntimeIngestion", () => {
843851
844 it("uses structured read-file paths when available", async () => {852 it("uses structured read-file paths when available", async () => {
845 const harness = await createHarness();853 const harness = await createHarness();
846 const now = new Date().toISOString();854 const now = "2026-01-01T00:00:00.000Z";
847855
848 harness.emit({856 harness.emit({
849 type: "item.completed",857 type: "item.completed",
...@@ -885,7 +893,7 @@ describe("ProviderRuntimeIngestion", () => {...@@ -885,7 +893,7 @@ describe("ProviderRuntimeIngestion", () => {
885893
886 it("projects completed plan items into first-class proposed plans", async () => {894 it("projects completed plan items into first-class proposed plans", async () => {
887 const harness = await createHarness();895 const harness = await createHarness();
888 const now = new Date().toISOString();896 const now = "2026-01-01T00:00:00.000Z";
889897
890 harness.emit({898 harness.emit({
891 type: "turn.proposed.completed",899 type: "turn.proposed.completed",
...@@ -919,7 +927,7 @@ describe("ProviderRuntimeIngestion", () => {...@@ -919,7 +927,7 @@ describe("ProviderRuntimeIngestion", () => {
919 const targetThreadId = asThreadId("thread-implement");927 const targetThreadId = asThreadId("thread-implement");
920 const sourceTurnId = asTurnId("turn-plan-source");928 const sourceTurnId = asTurnId("turn-plan-source");
921 const targetTurnId = asTurnId("turn-plan-implement");929 const targetTurnId = asTurnId("turn-plan-implement");
922 const createdAt = new Date().toISOString();930 const createdAt = "2026-01-01T00:00:00.000Z";
923931
924 await Effect.runPromise(932 await Effect.runPromise(
925 harness.engine.dispatch({933 harness.engine.dispatch({
...@@ -1050,7 +1058,7 @@ describe("ProviderRuntimeIngestion", () => {...@@ -1050,7 +1058,7 @@ describe("ProviderRuntimeIngestion", () => {
1050 },1058 },
1051 interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE,1059 interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE,
1052 runtimeMode: "approval-required",1060 runtimeMode: "approval-required",
1053 createdAt: new Date().toISOString(),1061 createdAt: "2026-01-01T00:00:00.000Z",
1054 }),1062 }),
1055 );1063 );
10561064
...@@ -1075,7 +1083,7 @@ describe("ProviderRuntimeIngestion", () => {...@@ -1075,7 +1083,7 @@ describe("ProviderRuntimeIngestion", () => {
1075 type: "turn.started",1083 type: "turn.started",
1076 eventId: asEventId("evt-plan-target-started"),1084 eventId: asEventId("evt-plan-target-started"),
1077 provider: ProviderDriverKind.make("codex"),1085 provider: ProviderDriverKind.make("codex"),
1078 createdAt: new Date().toISOString(),1086 createdAt: "2026-01-01T00:00:00.000Z",
1079 threadId: targetThreadId,1087 threadId: targetThreadId,
1080 turnId: targetTurnId,1088 turnId: targetTurnId,
1081 });1089 });
...@@ -1106,7 +1114,7 @@ describe("ProviderRuntimeIngestion", () => {...@@ -1106,7 +1114,7 @@ describe("ProviderRuntimeIngestion", () => {
1106 const sourceTurnId = asTurnId("turn-plan-source");1114 const sourceTurnId = asTurnId("turn-plan-source");
1107 const activeTurnId = asTurnId("turn-already-running");1115 const activeTurnId = asTurnId("turn-already-running");
1108 const staleTurnId = asTurnId("turn-stale-start");1116 const staleTurnId = asTurnId("turn-stale-start");
1109 const createdAt = new Date().toISOString();1117 const createdAt = "2026-01-01T00:00:00.000Z";
11101118
1111 await Effect.runPromise(1119 await Effect.runPromise(
1112 harness.engine.dispatch({1120 harness.engine.dispatch({
...@@ -1219,7 +1227,7 @@ describe("ProviderRuntimeIngestion", () => {...@@ -1219,7 +1227,7 @@ describe("ProviderRuntimeIngestion", () => {
1219 },1227 },
1220 interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE,1228 interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE,
1221 runtimeMode: "approval-required",1229 runtimeMode: "approval-required",
1222 createdAt: new Date().toISOString(),1230 createdAt: "2026-01-01T00:00:00.000Z",
1223 }),1231 }),
1224 );1232 );
12251233
...@@ -1227,7 +1235,7 @@ describe("ProviderRuntimeIngestion", () => {...@@ -1227,7 +1235,7 @@ describe("ProviderRuntimeIngestion", () => {
1227 type: "turn.started",1235 type: "turn.started",
1228 eventId: asEventId("evt-turn-started-stale-plan-implementation"),1236 eventId: asEventId("evt-turn-started-stale-plan-implementation"),
1229 provider: ProviderDriverKind.make("codex"),1237 provider: ProviderDriverKind.make("codex"),
1230 createdAt: new Date().toISOString(),1238 createdAt: "2026-01-01T00:00:00.000Z",
1231 threadId: targetThreadId,1239 threadId: targetThreadId,
1232 turnId: staleTurnId,1240 turnId: staleTurnId,
1233 });1241 });
...@@ -1259,7 +1267,7 @@ describe("ProviderRuntimeIngestion", () => {...@@ -1259,7 +1267,7 @@ describe("ProviderRuntimeIngestion", () => {
1259 const sourceTurnId = asTurnId("turn-plan-source");1267 const sourceTurnId = asTurnId("turn-plan-source");
1260 const expectedTurnId = asTurnId("turn-plan-implement");1268 const expectedTurnId = asTurnId("turn-plan-implement");
1261 const replayedTurnId = asTurnId("turn-replayed");1269 const replayedTurnId = asTurnId("turn-replayed");
1262 const createdAt = new Date().toISOString();1270 const createdAt = "2026-01-01T00:00:00.000Z";
12631271
1264 await Effect.runPromise(1272 await Effect.runPromise(
1265 harness.engine.dispatch({1273 harness.engine.dispatch({
...@@ -1381,7 +1389,7 @@ describe("ProviderRuntimeIngestion", () => {...@@ -1381,7 +1389,7 @@ describe("ProviderRuntimeIngestion", () => {
1381 },1389 },
1382 interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE,1390 interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE,
1383 runtimeMode: "approval-required",1391 runtimeMode: "approval-required",
1384 createdAt: new Date().toISOString(),1392 createdAt: "2026-01-01T00:00:00.000Z",
1385 }),1393 }),
1386 );1394 );
13871395
...@@ -1399,7 +1407,7 @@ describe("ProviderRuntimeIngestion", () => {...@@ -1399,7 +1407,7 @@ describe("ProviderRuntimeIngestion", () => {
1399 type: "turn.started",1407 type: "turn.started",
1400 eventId: asEventId("evt-turn-started-unrelated-plan-implementation"),1408 eventId: asEventId("evt-turn-started-unrelated-plan-implementation"),
1401 provider: ProviderDriverKind.make("codex"),1409 provider: ProviderDriverKind.make("codex"),
1402 createdAt: new Date().toISOString(),1410 createdAt: "2026-01-01T00:00:00.000Z",
1403 threadId: targetThreadId,1411 threadId: targetThreadId,
1404 turnId: replayedTurnId,1412 turnId: replayedTurnId,
1405 });1413 });
...@@ -1420,7 +1428,7 @@ describe("ProviderRuntimeIngestion", () => {...@@ -1420,7 +1428,7 @@ describe("ProviderRuntimeIngestion", () => {
14201428
1421 it("finalizes buffered proposed-plan deltas into a first-class proposed plan on turn completion", async () => {1429 it("finalizes buffered proposed-plan deltas into a first-class proposed plan on turn completion", async () => {
1422 const harness = await createHarness();1430 const harness = await createHarness();
1423 const now = new Date().toISOString();1431 const now = "2026-01-01T00:00:00.000Z";
14241432
1425 harness.emit({1433 harness.emit({
1426 type: "turn.started",1434 type: "turn.started",
...@@ -1486,7 +1494,7 @@ describe("ProviderRuntimeIngestion", () => {...@@ -1486,7 +1494,7 @@ describe("ProviderRuntimeIngestion", () => {
14861494
1487 it("buffers assistant deltas by default until completion", async () => {1495 it("buffers assistant deltas by default until completion", async () => {
1488 const harness = await createHarness();1496 const harness = await createHarness();
1489 const now = new Date().toISOString();1497 const now = "2026-01-01T00:00:00.000Z";
14901498
1491 harness.emit({1499 harness.emit({
1492 type: "turn.started",1500 type: "turn.started",
...@@ -1554,7 +1562,7 @@ describe("ProviderRuntimeIngestion", () => {...@@ -1554,7 +1562,7 @@ describe("ProviderRuntimeIngestion", () => {
15541562
1555 it("flushes and completes buffered assistant text when an approval request opens", async () => {1563 it("flushes and completes buffered assistant text when an approval request opens", async () => {
1556 const harness = await createHarness();1564 const harness = await createHarness();
1557 const now = new Date().toISOString();1565 const now = "2026-01-01T00:00:00.000Z";
15581566
1559 harness.emit({1567 harness.emit({
1560 type: "turn.started",1568 type: "turn.started",
...@@ -1614,7 +1622,7 @@ describe("ProviderRuntimeIngestion", () => {...@@ -1614,7 +1622,7 @@ describe("ProviderRuntimeIngestion", () => {
16141622
1615 it("flushes and completes buffered assistant text when user input is requested", async () => {1623 it("flushes and completes buffered assistant text when user input is requested", async () => {
1616 const harness = await createHarness();1624 const harness = await createHarness();
1617 const now = new Date().toISOString();1625 const now = "2026-01-01T00:00:00.000Z";
16181626
1619 harness.emit({1627 harness.emit({
1620 type: "turn.started",1628 type: "turn.started",
...@@ -1980,7 +1988,7 @@ describe("ProviderRuntimeIngestion", () => {...@@ -1980,7 +1988,7 @@ describe("ProviderRuntimeIngestion", () => {
19801988
1981 it("streams assistant deltas when thread.turn.start requests streaming mode", async () => {1989 it("streams assistant deltas when thread.turn.start requests streaming mode", async () => {
1982 const harness = await createHarness({ serverSettings: { enableAssistantStreaming: true } });1990 const harness = await createHarness({ serverSettings: { enableAssistantStreaming: true } });
1983 const now = new Date().toISOString();1991 const now = "2026-01-01T00:00:00.000Z";
19841992
1985 await Effect.runPromise(1993 await Effect.runPromise(
1986 harness.engine.dispatch({1994 harness.engine.dispatch({
...@@ -2072,7 +2080,7 @@ describe("ProviderRuntimeIngestion", () => {...@@ -2072,7 +2080,7 @@ describe("ProviderRuntimeIngestion", () => {
20722080
2073 it("spills oversized buffered deltas and still finalizes full assistant text", async () => {2081 it("spills oversized buffered deltas and still finalizes full assistant text", async () => {
2074 const harness = await createHarness();2082 const harness = await createHarness();
2075 const now = new Date().toISOString();2083 const now = "2026-01-01T00:00:00.000Z";
2076 const oversizedText = "x".repeat(40_000);2084 const oversizedText = "x".repeat(40_000);
20772085
2078 harness.emit({2086 harness.emit({
...@@ -2133,7 +2141,7 @@ describe("ProviderRuntimeIngestion", () => {...@@ -2133,7 +2141,7 @@ describe("ProviderRuntimeIngestion", () => {
21332141
2134 it("does not duplicate assistant completion when item.completed is followed by turn.completed", async () => {2142 it("does not duplicate assistant completion when item.completed is followed by turn.completed", async () => {
2135 const harness = await createHarness();2143 const harness = await createHarness();
2136 const now = new Date().toISOString();2144 const now = "2026-01-01T00:00:00.000Z";
21372145
2138 harness.emit({2146 harness.emit({
2139 type: "turn.started",2147 type: "turn.started",
...@@ -2219,7 +2227,7 @@ describe("ProviderRuntimeIngestion", () => {...@@ -2219,7 +2227,7 @@ describe("ProviderRuntimeIngestion", () => {
22192227
2220 it("maps canonical request events into approval activities with requestKind", async () => {2228 it("maps canonical request events into approval activities with requestKind", async () => {
2221 const harness = await createHarness();2229 const harness = await createHarness();
2222 const now = new Date().toISOString();2230 const now = "2026-01-01T00:00:00.000Z";
22232231
2224 harness.emit({2232 harness.emit({
2225 type: "request.opened",2233 type: "request.opened",
...@@ -2285,7 +2293,7 @@ describe("ProviderRuntimeIngestion", () => {...@@ -2285,7 +2293,7 @@ describe("ProviderRuntimeIngestion", () => {
22852293
2286 it("maps runtime.error into errored session state", async () => {2294 it("maps runtime.error into errored session state", async () => {
2287 const harness = await createHarness();2295 const harness = await createHarness();
2288 const now = new Date().toISOString();2296 const now = "2026-01-01T00:00:00.000Z";
22892297
2290 harness.emit({2298 harness.emit({
2291 type: "runtime.error",2299 type: "runtime.error",
...@@ -2312,7 +2320,7 @@ describe("ProviderRuntimeIngestion", () => {...@@ -2312,7 +2320,7 @@ describe("ProviderRuntimeIngestion", () => {
23122320
2313 it("records runtime.error activities from the typed payload message", async () => {2321 it("records runtime.error activities from the typed payload message", async () => {
2314 const harness = await createHarness();2322 const harness = await createHarness();
2315 const now = new Date().toISOString();2323 const now = "2026-01-01T00:00:00.000Z";
23162324
2317 harness.emit({2325 harness.emit({
2318 type: "runtime.error",2326 type: "runtime.error",
...@@ -2343,7 +2351,7 @@ describe("ProviderRuntimeIngestion", () => {...@@ -2343,7 +2351,7 @@ describe("ProviderRuntimeIngestion", () => {
23432351
2344 it("keeps the session running when a runtime.warning arrives during an active turn", async () => {2352 it("keeps the session running when a runtime.warning arrives during an active turn", async () => {
2345 const harness = await createHarness();2353 const harness = await createHarness();
2346 const now = new Date().toISOString();2354 const now = "2026-01-01T00:00:00.000Z";
23472355
2348 harness.emit({2356 harness.emit({
2349 type: "turn.started",2357 type: "turn.started",
...@@ -2387,7 +2395,7 @@ describe("ProviderRuntimeIngestion", () => {...@@ -2387,7 +2395,7 @@ describe("ProviderRuntimeIngestion", () => {
23872395
2388 it("maps session/thread lifecycle and item.started into session/activity projections", async () => {2396 it("maps session/thread lifecycle and item.started into session/activity projections", async () => {
2389 const harness = await createHarness();2397 const harness = await createHarness();
2390 const now = new Date().toISOString();2398 const now = "2026-01-01T00:00:00.000Z";
23912399
2392 harness.emit({2400 harness.emit({
2393 type: "session.started",2401 type: "session.started",
...@@ -2439,7 +2447,7 @@ describe("ProviderRuntimeIngestion", () => {...@@ -2439,7 +2447,7 @@ describe("ProviderRuntimeIngestion", () => {
24392447
2440 it("consumes P1 runtime events into thread metadata, diff checkpoints, and activities", async () => {2448 it("consumes P1 runtime events into thread metadata, diff checkpoints, and activities", async () => {
2441 const harness = await createHarness();2449 const harness = await createHarness();
2442 const now = new Date().toISOString();2450 const now = "2026-01-01T00:00:00.000Z";
24432451
2444 harness.emit({2452 harness.emit({
2445 type: "thread.metadata.updated",2453 type: "thread.metadata.updated",
...@@ -2573,7 +2581,7 @@ describe("ProviderRuntimeIngestion", () => {...@@ -2573,7 +2581,7 @@ describe("ProviderRuntimeIngestion", () => {
25732581
2574 it("projects context window updates into normalized thread activities", async () => {2582 it("projects context window updates into normalized thread activities", async () => {
2575 const harness = await createHarness();2583 const harness = await createHarness();
2576 const now = new Date().toISOString();2584 const now = "2026-01-01T00:00:00.000Z";
25772585
2578 harness.emit({2586 harness.emit({
2579 type: "thread.token-usage.updated",2587 type: "thread.token-usage.updated",
...@@ -2625,7 +2633,7 @@ describe("ProviderRuntimeIngestion", () => {...@@ -2625,7 +2633,7 @@ describe("ProviderRuntimeIngestion", () => {
26252633
2626 it("projects Codex camelCase token usage payloads into normalized thread activities", async () => {2634 it("projects Codex camelCase token usage payloads into normalized thread activities", async () => {
2627 const harness = await createHarness();2635 const harness = await createHarness();
2628 const now = new Date().toISOString();2636 const now = "2026-01-01T00:00:00.000Z";
26292637
2630 harness.emit({2638 harness.emit({
2631 type: "thread.token-usage.updated",2639 type: "thread.token-usage.updated",
...@@ -2678,7 +2686,7 @@ describe("ProviderRuntimeIngestion", () => {...@@ -2678,7 +2686,7 @@ describe("ProviderRuntimeIngestion", () => {
26782686
2679 it("projects Claude usage snapshots with context window into normalized thread activities", async () => {2687 it("projects Claude usage snapshots with context window into normalized thread activities", async () => {
2680 const harness = await createHarness();2688 const harness = await createHarness();
2681 const now = new Date().toISOString();2689 const now = "2026-01-01T00:00:00.000Z";
26822690
2683 harness.emit({2691 harness.emit({
2684 type: "thread.token-usage.updated",2692 type: "thread.token-usage.updated",
...@@ -2722,7 +2730,7 @@ describe("ProviderRuntimeIngestion", () => {...@@ -2722,7 +2730,7 @@ describe("ProviderRuntimeIngestion", () => {
27222730
2723 it("projects compacted thread state into context compaction activities", async () => {2731 it("projects compacted thread state into context compaction activities", async () => {
2724 const harness = await createHarness();2732 const harness = await createHarness();
2725 const now = new Date().toISOString();2733 const now = "2026-01-01T00:00:00.000Z";
27262734
2727 harness.emit({2735 harness.emit({
2728 type: "thread.state.changed",2736 type: "thread.state.changed",
...@@ -2752,7 +2760,7 @@ describe("ProviderRuntimeIngestion", () => {...@@ -2752,7 +2760,7 @@ describe("ProviderRuntimeIngestion", () => {
27522760
2753 it("projects Codex task lifecycle chunks into thread activities", async () => {2761 it("projects Codex task lifecycle chunks into thread activities", async () => {
2754 const harness = await createHarness();2762 const harness = await createHarness();
2755 const now = new Date().toISOString();2763 const now = "2026-01-01T00:00:00.000Z";
27562764
2757 harness.emit({2765 harness.emit({
2758 type: "task.started",2766 type: "task.started",
...@@ -2855,7 +2863,7 @@ describe("ProviderRuntimeIngestion", () => {...@@ -2855,7 +2863,7 @@ describe("ProviderRuntimeIngestion", () => {
28552863
2856 it("projects structured user input request and resolution as thread activities", async () => {2864 it("projects structured user input request and resolution as thread activities", async () => {
2857 const harness = await createHarness();2865 const harness = await createHarness();
2858 const now = new Date().toISOString();2866 const now = "2026-01-01T00:00:00.000Z";
28592867
2860 harness.emit({2868 harness.emit({
2861 type: "user-input.requested",2869 type: "user-input.requested",
...@@ -2886,7 +2894,7 @@ describe("ProviderRuntimeIngestion", () => {...@@ -2886,7 +2894,7 @@ describe("ProviderRuntimeIngestion", () => {
2886 type: "user-input.resolved",2894 type: "user-input.resolved",
2887 eventId: asEventId("evt-user-input-resolved"),2895 eventId: asEventId("evt-user-input-resolved"),
2888 provider: ProviderDriverKind.make("codex"),2896 provider: ProviderDriverKind.make("codex"),
2889 createdAt: new Date().toISOString(),2897 createdAt: "2026-01-01T00:00:00.000Z",
2890 threadId: asThreadId("thread-1"),2898 threadId: asThreadId("thread-1"),
2891 turnId: asTurnId("turn-user-input"),2899 turnId: asTurnId("turn-user-input"),
2892 requestId: ApprovalRequestId.make("req-user-input-1"),2900 requestId: ApprovalRequestId.make("req-user-input-1"),
...@@ -2928,7 +2936,7 @@ describe("ProviderRuntimeIngestion", () => {...@@ -2928,7 +2936,7 @@ describe("ProviderRuntimeIngestion", () => {
29282936
2929 it("continues processing runtime events after a single event handler failure", async () => {2937 it("continues processing runtime events after a single event handler failure", async () => {
2930 const harness = await createHarness();2938 const harness = await createHarness();
2931 const now = new Date().toISOString();2939 const now = "2026-01-01T00:00:00.000Z";
29322940
2933 harness.emit({2941 harness.emit({
2934 type: "content.delta",2942 type: "content.delta",
...@@ -2948,7 +2956,7 @@ describe("ProviderRuntimeIngestion", () => {...@@ -2948,7 +2956,7 @@ describe("ProviderRuntimeIngestion", () => {
2948 type: "runtime.error",2956 type: "runtime.error",
2949 eventId: asEventId("evt-runtime-error-after-failure"),2957 eventId: asEventId("evt-runtime-error-after-failure"),
2950 provider: ProviderDriverKind.make("codex"),2958 provider: ProviderDriverKind.make("codex"),
2951 createdAt: new Date().toISOString(),2959 createdAt: "2026-01-01T00:00:00.000Z",
2952 threadId: asThreadId("thread-1"),2960 threadId: asThreadId("thread-1"),
2953 turnId: asTurnId("turn-after-failure"),2961 turnId: asTurnId("turn-after-failure"),
2954 payload: {2962 payload: {
apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts+7-1
...@@ -17,7 +17,13 @@ import {...@@ -17,7 +17,13 @@ import {
17 type OrchestrationThreadActivity,17 type OrchestrationThreadActivity,
18 type ProviderRuntimeEvent,18 type ProviderRuntimeEvent,
19} from "@t3tools/contracts";19} 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";
21import { makeDrainableWorker } from "@t3tools/shared/DrainableWorker";27import { makeDrainableWorker } from "@t3tools/shared/DrainableWorker";
2228
23import { ProviderService } from "../../provider/Services/ProviderService.ts";29import { ProviderService } from "../../provider/Services/ProviderService.ts";
apps/server/src/orchestration/Layers/RuntimeReceiptBus.ts+4-1
...@@ -8,7 +8,10 @@...@@ -8,7 +8,10 @@
8 *8 *
9 * @module RuntimeReceiptBus9 * @module RuntimeReceiptBus
10 */10 */
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
13import {16import {
14 RuntimeReceiptBus,17 RuntimeReceiptBus,
apps/server/src/orchestration/Layers/ThreadDeletionReactor.test.ts+3-1
...@@ -1,5 +1,7 @@...@@ -1,5 +1,7 @@
1import { ThreadId } from "@t3tools/contracts";1import { 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";
3import { describe, expect, it } from "vitest";5import { describe, expect, it } from "vitest";
46
5import { logCleanupCauseUnlessInterrupted } from "./ThreadDeletionReactor.ts";7import { logCleanupCauseUnlessInterrupted } from "./ThreadDeletionReactor.ts";
apps/server/src/orchestration/Layers/ThreadDeletionReactor.ts+4-1
...@@ -1,6 +1,9 @@...@@ -1,6 +1,9 @@
1import type { OrchestrationEvent } from "@t3tools/contracts";1import type { OrchestrationEvent } from "@t3tools/contracts";
2import { makeDrainableWorker } from "@t3tools/shared/DrainableWorker";2import { 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
5import { ProviderService } from "../../provider/Services/ProviderService.ts";8import { ProviderService } from "../../provider/Services/ProviderService.ts";
6import { TerminalManager } from "../../terminal/Services/Manager.ts";9import { TerminalManager } from "../../terminal/Services/Manager.ts";
apps/server/src/orchestration/Normalizer.ts+3-1
...@@ -1,4 +1,6 @@...@@ -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";
2import {4import {
3 type ClientOrchestrationCommand,5 type ClientOrchestrationCommand,
4 type OrchestrationCommand,6 type OrchestrationCommand,
apps/server/src/orchestration/Services/CheckpointReactor.ts+3-2
...@@ -6,8 +6,9 @@...@@ -6,8 +6,9 @@
6 *6 *
7 * @module CheckpointReactor7 * @module CheckpointReactor
8 */8 */
9import { Context } from "effect";9import * as Context from "effect/Context";
10import type { Effect, Scope } from "effect";10import type * as Effect from "effect/Effect";
11import type * as Scope from "effect/Scope";
1112
12/**13/**
13 * CheckpointReactorShape - Service API for checkpoint reactor lifecycle.14 * CheckpointReactorShape - Service API for checkpoint reactor lifecycle.
apps/server/src/orchestration/Services/OrchestrationEngine.ts+3-2
...@@ -11,8 +11,9 @@...@@ -11,8 +11,9 @@
11 * @module OrchestrationEngineService11 * @module OrchestrationEngineService
12 */12 */
13import type { OrchestrationCommand, OrchestrationEvent } from "@t3tools/contracts";13import type { OrchestrationCommand, OrchestrationEvent } from "@t3tools/contracts";
14import { Context } from "effect";14import * as Context from "effect/Context";
15import type { Effect, Stream } from "effect";15import type * as Effect from "effect/Effect";
16import type * as Stream from "effect/Stream";
1617
17import type { OrchestrationDispatchError } from "../Errors.ts";18import type { OrchestrationDispatchError } from "../Errors.ts";
18import type { OrchestrationEventStoreError } from "../../persistence/Errors.ts";19import type { OrchestrationEventStoreError } from "../../persistence/Errors.ts";
apps/server/src/orchestration/Services/OrchestrationReactor.ts+3-2
...@@ -6,8 +6,9 @@...@@ -6,8 +6,9 @@
6 *6 *
7 * @module OrchestrationReactor7 * @module OrchestrationReactor
8 */8 */
9import { Context } from "effect";9import * as Context from "effect/Context";
10import type { Effect, Scope } from "effect";10import type * as Effect from "effect/Effect";
11import type * as Scope from "effect/Scope";
1112
12/**13/**
13 * OrchestrationReactorShape - Service API for orchestration reactor lifecycle.14 * OrchestrationReactorShape - Service API for orchestration reactor lifecycle.
apps/server/src/orchestration/Services/ProjectionPipeline.ts+2-2
...@@ -7,8 +7,8 @@...@@ -7,8 +7,8 @@
7 * @module OrchestrationProjectionPipeline7 * @module OrchestrationProjectionPipeline
8 */8 */
9import type { OrchestrationEvent } from "@t3tools/contracts";9import type { OrchestrationEvent } from "@t3tools/contracts";
10import { Context } from "effect";10import * as Context from "effect/Context";
11import type { Effect } from "effect";11import type * as Effect from "effect/Effect";
1212
13import type { ProjectionRepositoryError } from "../../persistence/Errors.ts";13import type { ProjectionRepositoryError } from "../../persistence/Errors.ts";
1414
apps/server/src/orchestration/Services/ProjectionSnapshotQuery.ts+3-3
...@@ -17,9 +17,9 @@ import type {...@@ -17,9 +17,9 @@ import type {
17 ProjectId,17 ProjectId,
18 ThreadId,18 ThreadId,
19} from "@t3tools/contracts";19} from "@t3tools/contracts";
20import { Context } from "effect";20import * as Context from "effect/Context";
21import type { Option } from "effect";21import type * as Option from "effect/Option";
22import type { Effect } from "effect";22import type * as Effect from "effect/Effect";
2323
24import type { ProjectionRepositoryError } from "../../persistence/Errors.ts";24import type { ProjectionRepositoryError } from "../../persistence/Errors.ts";
2525
apps/server/src/orchestration/Services/ProviderCommandReactor.ts+3-2
...@@ -6,8 +6,9 @@...@@ -6,8 +6,9 @@
6 *6 *
7 * @module ProviderCommandReactor7 * @module ProviderCommandReactor
8 */8 */
9import { Context } from "effect";9import * as Context from "effect/Context";
10import type { Effect, Scope } from "effect";10import type * as Effect from "effect/Effect";
11import type * as Scope from "effect/Scope";
1112
12/**13/**
13 * ProviderCommandReactorShape - Service API for provider command reactors.14 * ProviderCommandReactorShape - Service API for provider command reactors.
apps/server/src/orchestration/Services/ProviderRuntimeIngestion.ts+3-2
...@@ -6,8 +6,9 @@...@@ -6,8 +6,9 @@
6 *6 *
7 * @module ProviderRuntimeIngestionService7 * @module ProviderRuntimeIngestionService
8 */8 */
9import { Context } from "effect";9import * as Context from "effect/Context";
10import type { Effect, Scope } from "effect";10import type * as Effect from "effect/Effect";
11import type * as Scope from "effect/Scope";
1112
12/**13/**
13 * ProviderRuntimeIngestionShape - Service API for runtime ingestion lifecycle.14 * ProviderRuntimeIngestionShape - Service API for runtime ingestion lifecycle.
apps/server/src/orchestration/Services/RuntimeReceiptBus.ts+4-2
...@@ -15,8 +15,10 @@...@@ -15,8 +15,10 @@
15 * @module RuntimeReceiptBus15 * @module RuntimeReceiptBus
16 */16 */
17import { CheckpointRef, IsoDateTime, NonNegativeInt, ThreadId, TurnId } from "@t3tools/contracts";17import { CheckpointRef, IsoDateTime, NonNegativeInt, ThreadId, TurnId } from "@t3tools/contracts";
18import { Schema, Context } from "effect";18import * as Schema from "effect/Schema";
19import type { Effect, Stream } from "effect";19import * as Context from "effect/Context";
20import type * as Effect from "effect/Effect";
21import type * as Stream from "effect/Stream";
2022
21export const CheckpointBaselineCapturedReceipt = Schema.Struct({23export const CheckpointBaselineCapturedReceipt = Schema.Struct({
22 type: Schema.Literal("checkpoint.baseline.captured"),24 type: Schema.Literal("checkpoint.baseline.captured"),
apps/server/src/orchestration/Services/ThreadDeletionReactor.ts+3-2
...@@ -6,8 +6,9 @@...@@ -6,8 +6,9 @@
6 *6 *
7 * @module ThreadDeletionReactor7 * @module ThreadDeletionReactor
8 */8 */
9import { Context } from "effect";9import * as Context from "effect/Context";
10import type { Effect, Scope } from "effect";10import type * as Effect from "effect/Effect";
11import type * as Scope from "effect/Scope";
1112
12/**13/**
13 * ThreadDeletionReactorShape - Service API for thread deletion cleanup.14 * ThreadDeletionReactorShape - Service API for thread deletion cleanup.
apps/server/src/orchestration/commandInvariants.test.ts+2-2
...@@ -9,7 +9,7 @@ import {...@@ -9,7 +9,7 @@ import {
9 type OrchestrationReadModel,9 type OrchestrationReadModel,
10 ProviderInstanceId,10 ProviderInstanceId,
11} from "@t3tools/contracts";11} from "@t3tools/contracts";
12import { Effect } from "effect";12import * as Effect from "effect/Effect";
1313
14import {14import {
15 findThreadById,15 findThreadById,
...@@ -19,7 +19,7 @@ import {...@@ -19,7 +19,7 @@ import {
19 requireThreadAbsent,19 requireThreadAbsent,
20} from "./commandInvariants.ts";20} from "./commandInvariants.ts";
2121
22const now = new Date().toISOString();22const now = "2026-01-01T00:00:00.000Z";
2323
24const readModel: OrchestrationReadModel = {24const readModel: OrchestrationReadModel = {
25 snapshotSequence: 2,25 snapshotSequence: 2,
apps/server/src/orchestration/commandInvariants.ts+1-1
...@@ -6,7 +6,7 @@ import type {...@@ -6,7 +6,7 @@ import type {
6 ProjectId,6 ProjectId,
7 ThreadId,7 ThreadId,
8} from "@t3tools/contracts";8} from "@t3tools/contracts";
9import { Effect } from "effect";9import * as Effect from "effect/Effect";
1010
11import { OrchestrationCommandInvariantError } from "./Errors.ts";11import { OrchestrationCommandInvariantError } from "./Errors.ts";
1212
apps/server/src/orchestration/decider.delete.test.ts+2-2
...@@ -9,7 +9,7 @@ import {...@@ -9,7 +9,7 @@ import {
9 type OrchestrationReadModel,9 type OrchestrationReadModel,
10 ProviderInstanceId,10 ProviderInstanceId,
11} from "@t3tools/contracts";11} from "@t3tools/contracts";
12import { Effect } from "effect";12import * as Effect from "effect/Effect";
13import { describe, expect, it } from "vitest";13import { describe, expect, it } from "vitest";
1414
15import { decideOrchestrationCommand } from "./decider.ts";15import { decideOrchestrationCommand } from "./decider.ts";
...@@ -21,7 +21,7 @@ const asProjectId = (value: string): ProjectId => ProjectId.make(value);...@@ -21,7 +21,7 @@ const asProjectId = (value: string): ProjectId => ProjectId.make(value);
21const asThreadId = (value: string): ThreadId => ThreadId.make(value);21const asThreadId = (value: string): ThreadId => ThreadId.make(value);
2222
23async function seedReadModel(): Promise<OrchestrationReadModel> {23async function seedReadModel(): Promise<OrchestrationReadModel> {
24 const now = new Date().toISOString();24 const now = "2026-01-01T00:00:00.000Z";
25 const initial = createEmptyReadModel(now);25 const initial = createEmptyReadModel(now);
26 const withProject = await Effect.runPromise(26 const withProject = await Effect.runPromise(
27 projectEvent(initial, {27 projectEvent(initial, {
apps/server/src/orchestration/decider.projectScripts.test.ts+6-6
...@@ -9,7 +9,7 @@ import {...@@ -9,7 +9,7 @@ import {
9} from "@t3tools/contracts";9} from "@t3tools/contracts";
10import { createModelSelection } from "@t3tools/shared/model";10import { createModelSelection } from "@t3tools/shared/model";
11import { describe, expect, it } from "vitest";11import { describe, expect, it } from "vitest";
12import { Effect } from "effect";12import * as Effect from "effect/Effect";
1313
14import { decideOrchestrationCommand } from "./decider.ts";14import { decideOrchestrationCommand } from "./decider.ts";
15import { createEmptyReadModel, projectEvent } from "./projector.ts";15import { createEmptyReadModel, projectEvent } from "./projector.ts";
...@@ -20,7 +20,7 @@ const asMessageId = (value: string): MessageId => MessageId.make(value);...@@ -20,7 +20,7 @@ const asMessageId = (value: string): MessageId => MessageId.make(value);
2020
21describe("decider project scripts", () => {21describe("decider project scripts", () => {
22 it("emits empty scripts on project.create", async () => {22 it("emits empty scripts on project.create", async () => {
23 const now = new Date().toISOString();23 const now = "2026-01-01T00:00:00.000Z";
24 const readModel = createEmptyReadModel(now);24 const readModel = createEmptyReadModel(now);
2525
26 const result = await Effect.runPromise(26 const result = await Effect.runPromise(
...@@ -43,7 +43,7 @@ describe("decider project scripts", () => {...@@ -43,7 +43,7 @@ describe("decider project scripts", () => {
43 });43 });
4444
45 it("propagates scripts in project.meta.update payload", async () => {45 it("propagates scripts in project.meta.update payload", async () => {
46 const now = new Date().toISOString();46 const now = "2026-01-01T00:00:00.000Z";
47 const initial = createEmptyReadModel(now);47 const initial = createEmptyReadModel(now);
48 const readModel = await Effect.runPromise(48 const readModel = await Effect.runPromise(
49 projectEvent(initial, {49 projectEvent(initial, {
...@@ -97,7 +97,7 @@ describe("decider project scripts", () => {...@@ -97,7 +97,7 @@ describe("decider project scripts", () => {
97 });97 });
9898
99 it("emits user message and turn-start-requested events for thread.turn.start", async () => {99 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";
101 const initial = createEmptyReadModel(now);101 const initial = createEmptyReadModel(now);
102 const withProject = await Effect.runPromise(102 const withProject = await Effect.runPromise(
103 projectEvent(initial, {103 projectEvent(initial, {
...@@ -198,7 +198,7 @@ describe("decider project scripts", () => {...@@ -198,7 +198,7 @@ describe("decider project scripts", () => {
198 });198 });
199199
200 it("emits thread.runtime-mode-set from thread.runtime-mode.set", async () => {200 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";
202 const initial = createEmptyReadModel(now);202 const initial = createEmptyReadModel(now);
203 const withProject = await Effect.runPromise(203 const withProject = await Effect.runPromise(
204 projectEvent(initial, {204 projectEvent(initial, {
...@@ -280,7 +280,7 @@ describe("decider project scripts", () => {...@@ -280,7 +280,7 @@ describe("decider project scripts", () => {
280 });280 });
281281
282 it("emits thread.interaction-mode-set from thread.interaction-mode.set", async () => {282 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";
284 const initial = createEmptyReadModel(now);284 const initial = createEmptyReadModel(now);
285 const withProject = await Effect.runPromise(285 const withProject = await Effect.runPromise(
286 projectEvent(initial, {286 projectEvent(initial, {
apps/server/src/orchestration/decider.ts+11-10
...@@ -3,7 +3,8 @@ import type {...@@ -3,7 +3,8 @@ import type {
3 OrchestrationEvent,3 OrchestrationEvent,
4 OrchestrationReadModel,4 OrchestrationReadModel,
5} from "@t3tools/contracts";5} from "@t3tools/contracts";
6import { DateTime, Effect } from "effect";6import * as DateTime from "effect/DateTime";
7import * as Effect from "effect/Effect";
78
8import { OrchestrationCommandInvariantError } from "./Errors.ts";9import { OrchestrationCommandInvariantError } from "./Errors.ts";
9import {10import {
...@@ -17,7 +18,7 @@ import {...@@ -17,7 +18,7 @@ import {
17} from "./commandInvariants.ts";18} from "./commandInvariants.ts";
18import { projectEvent } from "./projector.ts";19import { projectEvent } from "./projector.ts";
1920
20const currentIso = DateTime.now.pipe(Effect.map(DateTime.formatIso));21const nowIso = Effect.map(DateTime.now, DateTime.formatIso);
2122
22function withEventBase(23function withEventBase(
23 input: Pick<OrchestrationCommand, "commandId"> & {24 input: Pick<OrchestrationCommand, "commandId"> & {
...@@ -116,7 +117,7 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand"...@@ -116,7 +117,7 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand"
116 command,117 command,
117 projectId: command.projectId,118 projectId: command.projectId,
118 });119 });
119 const occurredAt = yield* currentIso;120 const occurredAt = yield* nowIso;
120 return {121 return {
121 ...withEventBase({122 ...withEventBase({
122 aggregateKind: "project",123 aggregateKind: "project",
...@@ -173,7 +174,7 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand"...@@ -173,7 +174,7 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand"
173 });174 });
174 }175 }
175176
176 const occurredAt = yield* currentIso;177 const occurredAt = yield* nowIso;
177 return {178 return {
178 ...withEventBase({179 ...withEventBase({
179 aggregateKind: "project",180 aggregateKind: "project",
...@@ -229,7 +230,7 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand"...@@ -229,7 +230,7 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand"
229 command,230 command,
230 threadId: command.threadId,231 threadId: command.threadId,
231 });232 });
232 const occurredAt = yield* currentIso;233 const occurredAt = yield* nowIso;
233 return {234 return {
234 ...withEventBase({235 ...withEventBase({
235 aggregateKind: "thread",236 aggregateKind: "thread",
...@@ -251,7 +252,7 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand"...@@ -251,7 +252,7 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand"
251 command,252 command,
252 threadId: command.threadId,253 threadId: command.threadId,
253 });254 });
254 const occurredAt = yield* currentIso;255 const occurredAt = yield* nowIso;
255 return {256 return {
256 ...withEventBase({257 ...withEventBase({
257 aggregateKind: "thread",258 aggregateKind: "thread",
...@@ -274,7 +275,7 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand"...@@ -274,7 +275,7 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand"
274 command,275 command,
275 threadId: command.threadId,276 threadId: command.threadId,
276 });277 });
277 const occurredAt = yield* currentIso;278 const occurredAt = yield* nowIso;
278 return {279 return {
279 ...withEventBase({280 ...withEventBase({
280 aggregateKind: "thread",281 aggregateKind: "thread",
...@@ -296,7 +297,7 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand"...@@ -296,7 +297,7 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand"
296 command,297 command,
297 threadId: command.threadId,298 threadId: command.threadId,
298 });299 });
299 const occurredAt = yield* currentIso;300 const occurredAt = yield* nowIso;
300 return {301 return {
301 ...withEventBase({302 ...withEventBase({
302 aggregateKind: "thread",303 aggregateKind: "thread",
...@@ -324,7 +325,7 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand"...@@ -324,7 +325,7 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand"
324 command,325 command,
325 threadId: command.threadId,326 threadId: command.threadId,
326 });327 });
327 const occurredAt = yield* currentIso;328 const occurredAt = yield* nowIso;
328 return {329 return {
329 ...withEventBase({330 ...withEventBase({
330 aggregateKind: "thread",331 aggregateKind: "thread",
...@@ -347,7 +348,7 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand"...@@ -347,7 +348,7 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand"
347 command,348 command,
348 threadId: command.threadId,349 threadId: command.threadId,
349 });350 });
350 const occurredAt = yield* currentIso;351 const occurredAt = yield* nowIso;
351 return {352 return {
352 ...withEventBase({353 ...withEventBase({
353 aggregateKind: "thread",354 aggregateKind: "thread",
apps/server/src/orchestration/http.ts+1-1
...@@ -4,7 +4,7 @@ import {...@@ -4,7 +4,7 @@ import {
4 OrchestrationGetSnapshotError,4 OrchestrationGetSnapshotError,
5 type OrchestrationReadModel,5 type OrchestrationReadModel,
6} from "@t3tools/contracts";6} from "@t3tools/contracts";
7import { Effect } from "effect";7import * as Effect from "effect/Effect";
8import { HttpRouter, HttpServerRequest, HttpServerResponse } from "effect/unstable/http";8import { HttpRouter, HttpServerRequest, HttpServerResponse } from "effect/unstable/http";
99
10import { ServerAuth } from "../auth/Services/ServerAuth.ts";10import { ServerAuth } from "../auth/Services/ServerAuth.ts";
apps/server/src/orchestration/projector.test.ts+6-6
...@@ -6,7 +6,7 @@ import {...@@ -6,7 +6,7 @@ import {
6 ThreadId,6 ThreadId,
7 type OrchestrationEvent,7 type OrchestrationEvent,
8} from "@t3tools/contracts";8} from "@t3tools/contracts";
9import { Effect } from "effect";9import * as Effect from "effect/Effect";
10import { describe, expect, it } from "vitest";10import { describe, expect, it } from "vitest";
1111
12import { createEmptyReadModel, projectEvent } from "./projector.ts";12import { createEmptyReadModel, projectEvent } from "./projector.ts";
...@@ -40,7 +40,7 @@ function makeEvent(input: {...@@ -40,7 +40,7 @@ function makeEvent(input: {
4040
41describe("orchestration projector", () => {41describe("orchestration projector", () => {
42 it("applies thread.created events", async () => {42 it("applies thread.created events", async () => {
43 const now = new Date().toISOString();43 const now = "2026-01-01T00:00:00.000Z";
44 const model = createEmptyReadModel(now);44 const model = createEmptyReadModel(now);
4545
46 const next = await Effect.runPromise(46 const next = await Effect.runPromise(
...@@ -100,7 +100,7 @@ describe("orchestration projector", () => {...@@ -100,7 +100,7 @@ describe("orchestration projector", () => {
100 });100 });
101101
102 it("fails when event payload cannot be decoded by runtime schema", async () => {102 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";
104 const model = createEmptyReadModel(now);104 const model = createEmptyReadModel(now);
105105
106 await expect(106 await expect(
...@@ -134,8 +134,8 @@ describe("orchestration projector", () => {...@@ -134,8 +134,8 @@ describe("orchestration projector", () => {
134 });134 });
135135
136 it("applies thread.archived and thread.unarchived events", async () => {136 it("applies thread.archived and thread.unarchived events", async () => {
137 const now = new Date().toISOString();137 const now = "2026-01-01T00:00:00.000Z";
138 const later = new Date(Date.parse(now) + 1_000).toISOString();138 const later = "2026-01-01T00:00:01.000Z";
139 const created = await Effect.runPromise(139 const created = await Effect.runPromise(
140 projectEvent(140 projectEvent(
141 createEmptyReadModel(now),141 createEmptyReadModel(now),
...@@ -206,7 +206,7 @@ describe("orchestration projector", () => {...@@ -206,7 +206,7 @@ describe("orchestration projector", () => {
206 });206 });
207207
208 it("keeps projector forward-compatible for unhandled event types", async () => {208 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";
210 const model = createEmptyReadModel(now);210 const model = createEmptyReadModel(now);
211211
212 const next = await Effect.runPromise(212 const next = await Effect.runPromise(
apps/server/src/orchestration/projector.ts+2-1
...@@ -5,7 +5,8 @@ import {...@@ -5,7 +5,8 @@ import {
5 OrchestrationSession,5 OrchestrationSession,
6 OrchestrationThread,6 OrchestrationThread,
7} from "@t3tools/contracts";7} from "@t3tools/contracts";
8import { Effect, Schema } from "effect";8import * as Effect from "effect/Effect";
9import * as Schema from "effect/Schema";
910
10import { toProjectorDecodeError, type OrchestrationProjectorDecodeError } from "./Errors.ts";11import { toProjectorDecodeError, type OrchestrationProjectorDecodeError } from "./Errors.ts";
11import {12import {
apps/server/src/orchestration/runtimeLayer.ts+1-1
...@@ -1,4 +1,4 @@...@@ -1,4 +1,4 @@
1import { Layer } from "effect";1import * as Layer from "effect/Layer";
22
3import { OrchestrationCommandReceiptRepositoryLive } from "../persistence/Layers/OrchestrationCommandReceipts.ts";3import { OrchestrationCommandReceiptRepositoryLive } from "../persistence/Layers/OrchestrationCommandReceipts.ts";
4import { OrchestrationEventStoreLive } from "../persistence/Layers/OrchestrationEventStore.ts";4import { 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 @@...@@ -1,5 +1,6 @@
1import * as OS from "node:os";1import * as NodeOS from "node:os";
2import { Effect, Path } from "effect";2import * as Effect from "effect/Effect";
3import * as Path from "effect/Path";
3import {4import {
4 readPathFromLoginShell,5 readPathFromLoginShell,
5 readEnvironmentFromWindowsShell,6 readEnvironmentFromWindowsShell,
...@@ -17,7 +18,9 @@ type WindowsCommandAvailabilityChecker = (...@@ -17,7 +18,9 @@ type WindowsCommandAvailabilityChecker = (
17) => boolean;18) => boolean;
1819
19function logPathHydrationWarning(message: string, error?: unknown): void {20function 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 );
21}24}
2225
23export function fixPath(26export function fixPath(
...@@ -84,10 +87,10 @@ export function fixPath(...@@ -84,10 +87,10 @@ export function fixPath(
84export const expandHomePath = Effect.fn(function* (input: string) {87export const expandHomePath = Effect.fn(function* (input: string) {
85 const { join } = yield* Path.Path;88 const { join } = yield* Path.Path;
86 if (input === "~") {89 if (input === "~") {
87 return OS.homedir();90 return NodeOS.homedir();
88 }91 }
89 if (input.startsWith("~/") || input.startsWith("~\\")) {92 if (input.startsWith("~/") || input.startsWith("~\\")) {
90 return join(OS.homedir(), input.slice(2));93 return join(NodeOS.homedir(), input.slice(2));
91 }94 }
92 return input;95 return input;
93});96});
...@@ -95,7 +98,7 @@ export const expandHomePath = Effect.fn(function* (input: string) {...@@ -95,7 +98,7 @@ export const expandHomePath = Effect.fn(function* (input: string) {
95export const resolveBaseDir = Effect.fn(function* (raw: string | undefined) {98export const resolveBaseDir = Effect.fn(function* (raw: string | undefined) {
96 const { join, resolve } = yield* Path.Path;99 const { join, resolve } = yield* Path.Path;
97 if (!raw || raw.trim().length === 0) {100 if (!raw || raw.trim().length === 0) {
98 return join(OS.homedir(), ".t3");101 return join(NodeOS.homedir(), ".t3");
99 }102 }
100 return resolve(yield* expandHomePath(raw.trim()));103 return resolve(yield* expandHomePath(raw.trim()));
101});104});
apps/server/src/pathExpansion.test.ts+1
...@@ -1,3 +1,4 @@...@@ -1,3 +1,4 @@
1// @effect-diagnostics nodeBuiltinImport:off
1import { homedir } from "node:os";2import { homedir } from "node:os";
2import { join } from "node:path";3import { join } from "node:path";
3import { describe, expect, it } from "vitest";4import { describe, expect, it } from "vitest";
apps/server/src/pathExpansion.ts+1
...@@ -1,3 +1,4 @@...@@ -1,3 +1,4 @@
1// @effect-diagnostics nodeBuiltinImport:off
1import { homedir } from "node:os";2import { homedir } from "node:os";
2import { join } from "node:path";3import { join } from "node:path";
34
apps/server/src/persistence/Errors.ts+2-1
...@@ -1,4 +1,5 @@...@@ -1,4 +1,5 @@
1import { Schema, SchemaIssue } from "effect";1import * as Schema from "effect/Schema";
2import * as SchemaIssue from "effect/SchemaIssue";
23
3// ===============================4// ===============================
4// Core Persistence Errors5// Core Persistence Errors
apps/server/src/persistence/Layers/AuthPairingLinks.ts+3-1
...@@ -1,6 +1,8 @@...@@ -1,6 +1,8 @@
1import * as SqlClient from "effect/unstable/sql/SqlClient";1import * as SqlClient from "effect/unstable/sql/SqlClient";
2import * as SqlSchema from "effect/unstable/sql/SqlSchema";2import * 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
5import {7import {
6 toPersistenceDecodeError,8 toPersistenceDecodeError,
apps/server/src/persistence/Layers/AuthSessions.ts+4-1
...@@ -1,7 +1,10 @@...@@ -1,7 +1,10 @@
1import { AuthSessionId } from "@t3tools/contracts";1import { AuthSessionId } from "@t3tools/contracts";
2import * as SqlClient from "effect/unstable/sql/SqlClient";2import * as SqlClient from "effect/unstable/sql/SqlClient";
3import * as SqlSchema from "effect/unstable/sql/SqlSchema";3import * 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
6import {9import {
7 toPersistenceDecodeError,10 toPersistenceDecodeError,
apps/server/src/persistence/Layers/OrchestrationCommandReceipts.ts+2-1
...@@ -1,6 +1,7 @@...@@ -1,6 +1,7 @@
1import * as SqlClient from "effect/unstable/sql/SqlClient";1import * as SqlClient from "effect/unstable/sql/SqlClient";
2import * as SqlSchema from "effect/unstable/sql/SqlSchema";2import * 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
5import { toPersistenceSqlError } from "../Errors.ts";6import { toPersistenceSqlError } from "../Errors.ts";
67
apps/server/src/persistence/Layers/OrchestrationEventStore.test.ts+6-3
...@@ -1,6 +1,9 @@...@@ -1,6 +1,9 @@
1import { CommandId, EventId, ProjectId } from "@t3tools/contracts";1import { CommandId, EventId, ProjectId } from "@t3tools/contracts";
2import { assert, it } from "@effect/vitest";2import { 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";
4import * as SqlClient from "effect/unstable/sql/SqlClient";7import * as SqlClient from "effect/unstable/sql/SqlClient";
58
6import { PersistenceDecodeError } from "../Errors.ts";9import { PersistenceDecodeError } from "../Errors.ts";
...@@ -17,7 +20,7 @@ layer("OrchestrationEventStore", (it) => {...@@ -17,7 +20,7 @@ layer("OrchestrationEventStore", (it) => {
17 Effect.gen(function* () {20 Effect.gen(function* () {
18 const eventStore = yield* OrchestrationEventStore;21 const eventStore = yield* OrchestrationEventStore;
19 const sql = yield* SqlClient.SqlClient;22 const sql = yield* SqlClient.SqlClient;
20 const now = new Date().toISOString();23 const now = "2026-01-01T00:00:00.000Z";
2124
22 const appended = yield* eventStore.append({25 const appended = yield* eventStore.append({
23 type: "project.created",26 type: "project.created",
...@@ -69,7 +72,7 @@ layer("OrchestrationEventStore", (it) => {...@@ -69,7 +72,7 @@ layer("OrchestrationEventStore", (it) => {
69 Effect.gen(function* () {72 Effect.gen(function* () {
70 const eventStore = yield* OrchestrationEventStore;73 const eventStore = yield* OrchestrationEventStore;
71 const sql = yield* SqlClient.SqlClient;74 const sql = yield* SqlClient.SqlClient;
72 const now = new Date().toISOString();75 const now = "2026-01-01T00:00:00.000Z";
7376
74 yield* sql`77 yield* sql`
75 INSERT INTO orchestration_events (78 INSERT INTO orchestration_events (
apps/server/src/persistence/Layers/OrchestrationEventStore.ts+4-1
...@@ -13,7 +13,10 @@ import {...@@ -13,7 +13,10 @@ import {
13} from "@t3tools/contracts";13} from "@t3tools/contracts";
14import * as SqlClient from "effect/unstable/sql/SqlClient";14import * as SqlClient from "effect/unstable/sql/SqlClient";
15import * as SqlSchema from "effect/unstable/sql/SqlSchema";15import * 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
18import {21import {
19 toPersistenceDecodeError,22 toPersistenceDecodeError,
apps/server/src/persistence/Layers/ProjectionCheckpoints.ts+5-1
...@@ -1,7 +1,11 @@...@@ -1,7 +1,11 @@
1import { OrchestrationCheckpointFile } from "@t3tools/contracts";1import { OrchestrationCheckpointFile } from "@t3tools/contracts";
2import * as SqlClient from "effect/unstable/sql/SqlClient";2import * as SqlClient from "effect/unstable/sql/SqlClient";
3import * as SqlSchema from "effect/unstable/sql/SqlSchema";3import * 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
6import { toPersistenceDecodeError, toPersistenceSqlError } from "../Errors.ts";10import { toPersistenceDecodeError, toPersistenceSqlError } from "../Errors.ts";
7import {11import {
apps/server/src/persistence/Layers/ProjectionPendingApprovals.ts+2-1
...@@ -1,6 +1,7 @@...@@ -1,6 +1,7 @@
1import * as SqlClient from "effect/unstable/sql/SqlClient";1import * as SqlClient from "effect/unstable/sql/SqlClient";
2import * as SqlSchema from "effect/unstable/sql/SqlSchema";2import * 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
5import { toPersistenceSqlError } from "../Errors.ts";6import { toPersistenceSqlError } from "../Errors.ts";
6import {7import {
apps/server/src/persistence/Layers/ProjectionProjects.ts+4-1
...@@ -1,6 +1,9 @@...@@ -1,6 +1,9 @@
1import * as SqlClient from "effect/unstable/sql/SqlClient";1import * as SqlClient from "effect/unstable/sql/SqlClient";
2import * as SqlSchema from "effect/unstable/sql/SqlSchema";2import * 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
5import { ModelSelection, ProjectScript } from "@t3tools/contracts";8import { ModelSelection, ProjectScript } from "@t3tools/contracts";
6import { toPersistenceSqlError } from "../Errors.ts";9import { toPersistenceSqlError } from "../Errors.ts";
apps/server/src/persistence/Layers/ProjectionRepositories.test.ts+7-3
...@@ -1,6 +1,8 @@...@@ -1,6 +1,8 @@
1import { ProjectId, ThreadId, ProviderInstanceId } from "@t3tools/contracts";1import { ProjectId, ThreadId, ProviderInstanceId } from "@t3tools/contracts";
2import { assert, it } from "@effect/vitest";2import { 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";
4import * as SqlClient from "effect/unstable/sql/SqlClient";6import * as SqlClient from "effect/unstable/sql/SqlClient";
57
6import { SqlitePersistenceMemory } from "./Sqlite.ts";8import { SqlitePersistenceMemory } from "./Sqlite.ts";
...@@ -46,11 +48,12 @@ projectionRepositoriesLayer("Projection repositories", (it) => {...@@ -46,11 +48,12 @@ projectionRepositoriesLayer("Projection repositories", (it) => {
46 `;48 `;
47 const row = rows[0];49 const row = rows[0];
48 if (!row) {50 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.");
50 }52 }
5153
52 assert.strictEqual(54 assert.strictEqual(
53 row.defaultModelSelection,55 row.defaultModelSelection,
56 // @effect-diagnostics-next-line preferSchemaOverJson:off
54 JSON.stringify({57 JSON.stringify({
55 instanceId: ProviderInstanceId.make("codex"),58 instanceId: ProviderInstanceId.make("codex"),
56 model: "gpt-5.4",59 model: "gpt-5.4",
...@@ -104,11 +107,12 @@ projectionRepositoriesLayer("Projection repositories", (it) => {...@@ -104,11 +107,12 @@ projectionRepositoriesLayer("Projection repositories", (it) => {
104 `;107 `;
105 const row = rows[0];108 const row = rows[0];
106 if (!row) {109 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.");
108 }111 }
109112
110 assert.strictEqual(113 assert.strictEqual(
111 row.modelSelection,114 row.modelSelection,
115 // @effect-diagnostics-next-line preferSchemaOverJson:off
112 JSON.stringify({116 JSON.stringify({
113 instanceId: ProviderInstanceId.make("claudeAgent"),117 instanceId: ProviderInstanceId.make("claudeAgent"),
114 model: "claude-opus-4-6",118 model: "claude-opus-4-6",
apps/server/src/persistence/Layers/ProjectionState.ts+3-1
...@@ -1,7 +1,9 @@...@@ -1,7 +1,9 @@
1import { NonNegativeInt } from "@t3tools/contracts";1import { NonNegativeInt } from "@t3tools/contracts";
2import * as SqlClient from "effect/unstable/sql/SqlClient";2import * as SqlClient from "effect/unstable/sql/SqlClient";
3import * as SqlSchema from "effect/unstable/sql/SqlSchema";3import * 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
6import { toPersistenceSqlError } from "../Errors.ts";8import { toPersistenceSqlError } from "../Errors.ts";
79
apps/server/src/persistence/Layers/ProjectionThreadActivities.ts+4-1
...@@ -1,7 +1,10 @@...@@ -1,7 +1,10 @@
1import * as SqlClient from "effect/unstable/sql/SqlClient";1import * as SqlClient from "effect/unstable/sql/SqlClient";
2import * as SqlSchema from "effect/unstable/sql/SqlSchema";2import * as SqlSchema from "effect/unstable/sql/SqlSchema";
3import { NonNegativeInt } from "@t3tools/contracts";3import { 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
6import { toPersistenceDecodeError, toPersistenceSqlError } from "../Errors.ts";9import { toPersistenceDecodeError, toPersistenceSqlError } from "../Errors.ts";
710
apps/server/src/persistence/Layers/ProjectionThreadMessages.test.ts+2-1
...@@ -1,6 +1,7 @@...@@ -1,6 +1,7 @@
1import { MessageId, ThreadId } from "@t3tools/contracts";1import { MessageId, ThreadId } from "@t3tools/contracts";
2import { assert, it } from "@effect/vitest";2import { assert, it } from "@effect/vitest";
3import { Effect, Layer } from "effect";3import * as Effect from "effect/Effect";
4import * as Layer from "effect/Layer";
45
5import { ProjectionThreadMessageRepository } from "../Services/ProjectionThreadMessages.ts";6import { ProjectionThreadMessageRepository } from "../Services/ProjectionThreadMessages.ts";
6import { ProjectionThreadMessageRepositoryLive } from "./ProjectionThreadMessages.ts";7import { ProjectionThreadMessageRepositoryLive } from "./ProjectionThreadMessages.ts";
apps/server/src/persistence/Layers/ProjectionThreadMessages.ts+5-1
...@@ -1,6 +1,10 @@...@@ -1,6 +1,10 @@
1import * as SqlClient from "effect/unstable/sql/SqlClient";1import * as SqlClient from "effect/unstable/sql/SqlClient";
2import * as SqlSchema from "effect/unstable/sql/SqlSchema";2import * 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";
4import { ChatAttachment } from "@t3tools/contracts";8import { ChatAttachment } from "@t3tools/contracts";
59
6import { toPersistenceSqlError } from "../Errors.ts";10import { toPersistenceSqlError } from "../Errors.ts";
apps/server/src/persistence/Layers/ProjectionThreadProposedPlans.ts+2-1
...@@ -1,4 +1,5 @@...@@ -1,4 +1,5 @@
1import { Effect, Layer } from "effect";1import * as Effect from "effect/Effect";
2import * as Layer from "effect/Layer";
2import * as SqlClient from "effect/unstable/sql/SqlClient";3import * as SqlClient from "effect/unstable/sql/SqlClient";
3import * as SqlSchema from "effect/unstable/sql/SqlSchema";4import * as SqlSchema from "effect/unstable/sql/SqlSchema";
45
apps/server/src/persistence/Layers/ProjectionThreadSessions.ts+2-1
...@@ -1,6 +1,7 @@...@@ -1,6 +1,7 @@
1import * as SqlClient from "effect/unstable/sql/SqlClient";1import * as SqlClient from "effect/unstable/sql/SqlClient";
2import * as SqlSchema from "effect/unstable/sql/SqlSchema";2import * 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
5import { toPersistenceSqlError } from "../Errors.ts";6import { toPersistenceSqlError } from "../Errors.ts";
67
apps/server/src/persistence/Layers/ProjectionThreads.ts+4-1
...@@ -1,6 +1,9 @@...@@ -1,6 +1,9 @@
1import * as SqlClient from "effect/unstable/sql/SqlClient";1import * as SqlClient from "effect/unstable/sql/SqlClient";
2import * as SqlSchema from "effect/unstable/sql/SqlSchema";2import * 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
5import { toPersistenceSqlError } from "../Errors.ts";8import { toPersistenceSqlError } from "../Errors.ts";
6import {9import {
apps/server/src/persistence/Layers/ProjectionTurns.ts+5-1
...@@ -1,7 +1,11 @@...@@ -1,7 +1,11 @@
1import { OrchestrationCheckpointFile } from "@t3tools/contracts";1import { OrchestrationCheckpointFile } from "@t3tools/contracts";
2import * as SqlClient from "effect/unstable/sql/SqlClient";2import * as SqlClient from "effect/unstable/sql/SqlClient";
3import * as SqlSchema from "effect/unstable/sql/SqlSchema";3import * 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
6import { toPersistenceDecodeError, toPersistenceSqlError } from "../Errors.ts";10import { toPersistenceDecodeError, toPersistenceSqlError } from "../Errors.ts";
7import {11import {
apps/server/src/persistence/Layers/ProviderSessionRuntime.ts+5-1
...@@ -1,7 +1,11 @@...@@ -1,7 +1,11 @@
1import { ThreadId } from "@t3tools/contracts";1import { ThreadId } from "@t3tools/contracts";
2import * as SqlClient from "effect/unstable/sql/SqlClient";2import * as SqlClient from "effect/unstable/sql/SqlClient";
3import * as SqlSchema from "effect/unstable/sql/SqlSchema";3import * 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
6import {10import {
7 toPersistenceDecodeError,11 toPersistenceDecodeError,
apps/server/src/persistence/Layers/Sqlite.ts+4-1
...@@ -1,4 +1,7 @@...@@ -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";
2import * as SqlClient from "effect/unstable/sql/SqlClient";5import * as SqlClient from "effect/unstable/sql/SqlClient";
36
4import { runMigrations } from "../Migrations.ts";7import { runMigrations } from "../Migrations.ts";
apps/server/src/persistence/Migrations/016_CanonicalizeModelSelections.test.ts+9-8
...@@ -1,5 +1,6 @@...@@ -1,5 +1,6 @@
1import { assert, it } from "@effect/vitest";1import { assert, it } from "@effect/vitest";
2import { Effect, Layer } from "effect";2import * as Effect from "effect/Effect";
3import * as Layer from "effect/Layer";
3import * as SqlClient from "effect/unstable/sql/SqlClient";4import * as SqlClient from "effect/unstable/sql/SqlClient";
45
5import { runMigrations } from "../Migrations.ts";6import { runMigrations } from "../Migrations.ts";
...@@ -264,7 +265,7 @@ layer("016_CanonicalizeModelSelections", (it) => {...@@ -264,7 +265,7 @@ layer("016_CanonicalizeModelSelections", (it) => {
264 FROM orchestration_events265 FROM orchestration_events
265 ORDER BY rowid ASC266 ORDER BY rowid ASC
266 `;267 `;
267268 // @effect-diagnostics-next-line preferSchemaOverJson:off
268 assert.deepStrictEqual(JSON.parse(eventRows[0]!.payloadJson), {269 assert.deepStrictEqual(JSON.parse(eventRows[0]!.payloadJson), {
269 projectId: "project-1",270 projectId: "project-1",
270 title: "Project",271 title: "Project",
...@@ -280,7 +281,7 @@ layer("016_CanonicalizeModelSelections", (it) => {...@@ -280,7 +281,7 @@ layer("016_CanonicalizeModelSelections", (it) => {
280 createdAt: "2026-01-01T00:00:00.000Z",281 createdAt: "2026-01-01T00:00:00.000Z",
281 updatedAt: "2026-01-01T00:00:00.000Z",282 updatedAt: "2026-01-01T00:00:00.000Z",
282 });283 });
283284 // @effect-diagnostics-next-line preferSchemaOverJson:off
284 assert.deepStrictEqual(JSON.parse(eventRows[1]!.payloadJson), {285 assert.deepStrictEqual(JSON.parse(eventRows[1]!.payloadJson), {
285 projectId: "project-2",286 projectId: "project-2",
286 title: "Fallback Project",287 title: "Fallback Project",
...@@ -296,7 +297,7 @@ layer("016_CanonicalizeModelSelections", (it) => {...@@ -296,7 +297,7 @@ layer("016_CanonicalizeModelSelections", (it) => {
296 createdAt: "2026-01-01T00:00:00.000Z",297 createdAt: "2026-01-01T00:00:00.000Z",
297 updatedAt: "2026-01-01T00:00:00.000Z",298 updatedAt: "2026-01-01T00:00:00.000Z",
298 });299 });
299300 // @effect-diagnostics-next-line preferSchemaOverJson:off
300 assert.deepStrictEqual(JSON.parse(eventRows[2]!.payloadJson), {301 assert.deepStrictEqual(JSON.parse(eventRows[2]!.payloadJson), {
301 projectId: "project-3",302 projectId: "project-3",
302 title: "Null Model Project",303 title: "Null Model Project",
...@@ -306,7 +307,7 @@ layer("016_CanonicalizeModelSelections", (it) => {...@@ -306,7 +307,7 @@ layer("016_CanonicalizeModelSelections", (it) => {
306 createdAt: "2026-01-01T00:00:00.000Z",307 createdAt: "2026-01-01T00:00:00.000Z",
307 updatedAt: "2026-01-01T00:00:00.000Z",308 updatedAt: "2026-01-01T00:00:00.000Z",
308 });309 });
309310 // @effect-diagnostics-next-line preferSchemaOverJson:off
310 assert.deepStrictEqual(JSON.parse(eventRows[3]!.payloadJson), {311 assert.deepStrictEqual(JSON.parse(eventRows[3]!.payloadJson), {
311 threadId: "thread-1",312 threadId: "thread-1",
312 projectId: "project-1",313 projectId: "project-1",
...@@ -326,7 +327,7 @@ layer("016_CanonicalizeModelSelections", (it) => {...@@ -326,7 +327,7 @@ layer("016_CanonicalizeModelSelections", (it) => {
326 createdAt: "2026-01-01T00:00:00.000Z",327 createdAt: "2026-01-01T00:00:00.000Z",
327 updatedAt: "2026-01-01T00:00:00.000Z",328 updatedAt: "2026-01-01T00:00:00.000Z",
328 });329 });
329330 // @effect-diagnostics-next-line preferSchemaOverJson:off
330 assert.deepStrictEqual(JSON.parse(eventRows[4]!.payloadJson), {331 assert.deepStrictEqual(JSON.parse(eventRows[4]!.payloadJson), {
331 threadId: "thread-2",332 threadId: "thread-2",
332 projectId: "project-1",333 projectId: "project-1",
...@@ -345,7 +346,7 @@ layer("016_CanonicalizeModelSelections", (it) => {...@@ -345,7 +346,7 @@ layer("016_CanonicalizeModelSelections", (it) => {
345 createdAt: "2026-01-01T00:00:00.000Z",346 createdAt: "2026-01-01T00:00:00.000Z",
346 updatedAt: "2026-01-01T00:00:00.000Z",347 updatedAt: "2026-01-01T00:00:00.000Z",
347 });348 });
348349 // @effect-diagnostics-next-line preferSchemaOverJson:off
349 assert.deepStrictEqual(JSON.parse(eventRows[5]!.payloadJson), {350 assert.deepStrictEqual(JSON.parse(eventRows[5]!.payloadJson), {
350 threadId: "thread-1",351 threadId: "thread-1",
351 turnId: "turn-1",352 turnId: "turn-1",
...@@ -359,7 +360,7 @@ layer("016_CanonicalizeModelSelections", (it) => {...@@ -359,7 +360,7 @@ layer("016_CanonicalizeModelSelections", (it) => {
359 },360 },
360 deliveryMode: "buffered",361 deliveryMode: "buffered",
361 });362 });
362363 // @effect-diagnostics-next-line preferSchemaOverJson:off
363 assert.deepStrictEqual(JSON.parse(eventRows[6]!.payloadJson), {364 assert.deepStrictEqual(JSON.parse(eventRows[6]!.payloadJson), {
364 threadId: "thread-3",365 threadId: "thread-3",
365 projectId: "project-1",366 projectId: "project-1",
apps/server/src/persistence/Migrations/019_ProjectionSnapshotLookupIndexes.test.ts+2-1
...@@ -1,5 +1,6 @@...@@ -1,5 +1,6 @@
1import { assert, it } from "@effect/vitest";1import { assert, it } from "@effect/vitest";
2import { Effect, Layer } from "effect";2import * as Effect from "effect/Effect";
3import * as Layer from "effect/Layer";
3import * as SqlClient from "effect/unstable/sql/SqlClient";4import * as SqlClient from "effect/unstable/sql/SqlClient";
45
5import { runMigrations } from "../Migrations.ts";6import { runMigrations } from "../Migrations.ts";
apps/server/src/persistence/Migrations/024_BackfillProjectionThreadShellSummary.test.ts+2-1
...@@ -1,5 +1,6 @@...@@ -1,5 +1,6 @@
1import { assert, it } from "@effect/vitest";1import { assert, it } from "@effect/vitest";
2import { Effect, Layer } from "effect";2import * as Effect from "effect/Effect";
3import * as Layer from "effect/Layer";
3import * as SqlClient from "effect/unstable/sql/SqlClient";4import * as SqlClient from "effect/unstable/sql/SqlClient";
45
5import { runMigrations } from "../Migrations.ts";6import { runMigrations } from "../Migrations.ts";
apps/server/src/persistence/Migrations/025_CleanupInvalidProjectionPendingApprovals.test.ts+2-1
...@@ -1,5 +1,6 @@...@@ -1,5 +1,6 @@
1import { assert, it } from "@effect/vitest";1import { assert, it } from "@effect/vitest";
2import { Effect, Layer } from "effect";2import * as Effect from "effect/Effect";
3import * as Layer from "effect/Layer";
3import * as SqlClient from "effect/unstable/sql/SqlClient";4import * as SqlClient from "effect/unstable/sql/SqlClient";
45
5import { runMigrations } from "../Migrations.ts";6import { runMigrations } from "../Migrations.ts";
apps/server/src/persistence/Migrations/026_CanonicalizeModelSelectionOptions.test.ts+2-1
...@@ -1,5 +1,6 @@...@@ -1,5 +1,6 @@
1import { assert, it } from "@effect/vitest";1import { assert, it } from "@effect/vitest";
2import { Effect, Layer } from "effect";2import * as Effect from "effect/Effect";
3import * as Layer from "effect/Layer";
3import * as SqlClient from "effect/unstable/sql/SqlClient";4import * as SqlClient from "effect/unstable/sql/SqlClient";
45
5import { runMigrations } from "../Migrations.ts";6import { runMigrations } from "../Migrations.ts";
apps/server/src/persistence/Migrations/027_028_ProviderInstanceIdColumns.test.ts+2-1
...@@ -1,5 +1,6 @@...@@ -1,5 +1,6 @@
1import { assert, it } from "@effect/vitest";1import { assert, it } from "@effect/vitest";
2import { Effect, Layer } from "effect";2import * as Effect from "effect/Effect";
3import * as Layer from "effect/Layer";
3import * as SqlClient from "effect/unstable/sql/SqlClient";4import * as SqlClient from "effect/unstable/sql/SqlClient";
45
5import { runMigrations } from "../Migrations.ts";6import { runMigrations } from "../Migrations.ts";
apps/server/src/persistence/Migrations/029_ProjectionThreadDetailOrderingIndexes.test.ts+2-1
...@@ -1,5 +1,6 @@...@@ -1,5 +1,6 @@
1import { assert, it } from "@effect/vitest";1import { assert, it } from "@effect/vitest";
2import { Effect, Layer } from "effect";2import * as Effect from "effect/Effect";
3import * as Layer from "effect/Layer";
3import * as SqlClient from "effect/unstable/sql/SqlClient";4import * as SqlClient from "effect/unstable/sql/SqlClient";
45
5import { runMigrations } from "../Migrations.ts";6import { runMigrations } from "../Migrations.ts";
apps/server/src/persistence/NodeSqliteClient.test.ts+1-1
...@@ -1,5 +1,5 @@...@@ -1,5 +1,5 @@
1import { assert, it } from "@effect/vitest";1import { assert, it } from "@effect/vitest";
2import { Effect } from "effect";2import * as Effect from "effect/Effect";
3import * as SqlClient from "effect/unstable/sql/SqlClient";3import * as SqlClient from "effect/unstable/sql/SqlClient";
44
5import * as SqliteClient from "./NodeSqliteClient.ts";5import * as SqliteClient from "./NodeSqliteClient.ts";
apps/server/src/persistence/Services/AuthPairingLinks.ts+4-2
...@@ -1,5 +1,7 @@...@@ -1,5 +1,7 @@
1import { Option, Schema, Context } from "effect";1import * as Option from "effect/Option";
2import type { Effect } from "effect";2import * as Schema from "effect/Schema";
3import * as Context from "effect/Context";
4import type * as Effect from "effect/Effect";
35
4import type { AuthPairingLinkRepositoryError } from "../Errors.ts";6import type { AuthPairingLinkRepositoryError } from "../Errors.ts";
57
apps/server/src/persistence/Services/AuthSessions.ts+4-2
...@@ -1,6 +1,8 @@...@@ -1,6 +1,8 @@
1import { AuthClientMetadataDeviceType, AuthSessionId } from "@t3tools/contracts";1import { AuthClientMetadataDeviceType, AuthSessionId } from "@t3tools/contracts";
2import { Option, Schema, Context } from "effect";2import * as Option from "effect/Option";
3import type { Effect } from "effect";3import * as Schema from "effect/Schema";
4import * as Context from "effect/Context";
5import type * as Effect from "effect/Effect";
46
5import type { AuthSessionRepositoryError } from "../Errors.ts";7import type { AuthSessionRepositoryError } from "../Errors.ts";
68
apps/server/src/persistence/Services/OrchestrationCommandReceipts.ts+4-2
...@@ -15,8 +15,10 @@ import {...@@ -15,8 +15,10 @@ import {
15 ProjectId,15 ProjectId,
16 ThreadId,16 ThreadId,
17} from "@t3tools/contracts";17} from "@t3tools/contracts";
18import { Option, Schema, Context } from "effect";18import * as Option from "effect/Option";
19import type { Effect } from "effect";19import * as Schema from "effect/Schema";
20import * as Context from "effect/Context";
21import type * as Effect from "effect/Effect";
2022
21import type { OrchestrationCommandReceiptRepositoryError } from "../Errors.ts";23import type { OrchestrationCommandReceiptRepositoryError } from "../Errors.ts";
2224
apps/server/src/persistence/Services/OrchestrationEventStore.ts+3-2
...@@ -10,8 +10,9 @@...@@ -10,8 +10,9 @@
10 * @module OrchestrationEventStore10 * @module OrchestrationEventStore
11 */11 */
12import { OrchestrationEvent } from "@t3tools/contracts";12import { OrchestrationEvent } from "@t3tools/contracts";
13import { Context } from "effect";13import * as Context from "effect/Context";
14import type { Effect, Stream } from "effect";14import type * as Effect from "effect/Effect";
15import type * as Stream from "effect/Stream";
1516
16import type { OrchestrationEventStoreError } from "../Errors.ts";17import type { OrchestrationEventStoreError } from "../Errors.ts";
1718
apps/server/src/persistence/Services/ProjectionCheckpoints.ts+4-2
...@@ -16,8 +16,10 @@ import {...@@ -16,8 +16,10 @@ import {
16 ThreadId,16 ThreadId,
17 TurnId,17 TurnId,
18} from "@t3tools/contracts";18} from "@t3tools/contracts";
19import { Option, Context, Schema } from "effect";19import * as Option from "effect/Option";
20import type { Effect } from "effect";20import * as Context from "effect/Context";
21import * as Schema from "effect/Schema";
22import type * as Effect from "effect/Effect";
2123
22import type { ProjectionRepositoryError } from "../Errors.ts";24import type { ProjectionRepositoryError } from "../Errors.ts";
2325
apps/server/src/persistence/Services/ProjectionPendingApprovals.ts+4-2
...@@ -14,8 +14,10 @@ import {...@@ -14,8 +14,10 @@ import {
14 ThreadId,14 ThreadId,
15 TurnId,15 TurnId,
16} from "@t3tools/contracts";16} from "@t3tools/contracts";
17import { Option, Schema, Context } from "effect";17import * as Option from "effect/Option";
18import type { Effect } from "effect";18import * as Schema from "effect/Schema";
19import * as Context from "effect/Context";
20import type * as Effect from "effect/Effect";
1921
20import type { ProjectionRepositoryError } from "../Errors.ts";22import type { ProjectionRepositoryError } from "../Errors.ts";
2123
apps/server/src/persistence/Services/ProjectionProjects.ts+4-2
...@@ -7,8 +7,10 @@...@@ -7,8 +7,10 @@
7 * @module ProjectionProjectRepository7 * @module ProjectionProjectRepository
8 */8 */
9import { IsoDateTime, ModelSelection, ProjectId, ProjectScript } from "@t3tools/contracts";9import { IsoDateTime, ModelSelection, ProjectId, ProjectScript } from "@t3tools/contracts";
10import { Option, Schema, Context } from "effect";10import * as Option from "effect/Option";
11import type { Effect } from "effect";11import * as Schema from "effect/Schema";
12import * as Context from "effect/Context";
13import type * as Effect from "effect/Effect";
1214
13import type { ProjectionRepositoryError } from "../Errors.ts";15import type { ProjectionRepositoryError } from "../Errors.ts";
1416
apps/server/src/persistence/Services/ProjectionState.ts+4-2
...@@ -7,8 +7,10 @@...@@ -7,8 +7,10 @@
7 * @module ProjectionStateRepository7 * @module ProjectionStateRepository
8 */8 */
9import { IsoDateTime, NonNegativeInt } from "@t3tools/contracts";9import { IsoDateTime, NonNegativeInt } from "@t3tools/contracts";
10import { Option, Schema, Context } from "effect";10import * as Option from "effect/Option";
11import type { Effect } from "effect";11import * as Schema from "effect/Schema";
12import * as Context from "effect/Context";
13import type * as Effect from "effect/Effect";
1214
13import type { ProjectionRepositoryError } from "../Errors.ts";15import type { ProjectionRepositoryError } from "../Errors.ts";
1416
apps/server/src/persistence/Services/ProjectionThreadActivities.ts+3-2
...@@ -14,8 +14,9 @@ import {...@@ -14,8 +14,9 @@ import {
14 ThreadId,14 ThreadId,
15 TurnId,15 TurnId,
16} from "@t3tools/contracts";16} from "@t3tools/contracts";
17import { Schema, Context } from "effect";17import * as Schema from "effect/Schema";
18import type { Effect } from "effect";18import * as Context from "effect/Context";
19import type * as Effect from "effect/Effect";
1920
20import type { ProjectionRepositoryError } from "../Errors.ts";21import type { ProjectionRepositoryError } from "../Errors.ts";
2122
apps/server/src/persistence/Services/ProjectionThreadMessages.ts+4-3
...@@ -14,9 +14,10 @@ import {...@@ -14,9 +14,10 @@ import {
14 TurnId,14 TurnId,
15 IsoDateTime,15 IsoDateTime,
16} from "@t3tools/contracts";16} from "@t3tools/contracts";
17import { Schema, Context } from "effect";17import * as Schema from "effect/Schema";
18import type { Option } from "effect";18import * as Context from "effect/Context";
19import type { Effect } from "effect";19import type * as Option from "effect/Option";
20import type * as Effect from "effect/Effect";
2021
21import type { ProjectionRepositoryError } from "../Errors.ts";22import type { ProjectionRepositoryError } from "../Errors.ts";
2223
apps/server/src/persistence/Services/ProjectionThreadProposedPlans.ts+3-2
...@@ -5,8 +5,9 @@ import {...@@ -5,8 +5,9 @@ import {
5 TrimmedNonEmptyString,5 TrimmedNonEmptyString,
6 TurnId,6 TurnId,
7} from "@t3tools/contracts";7} from "@t3tools/contracts";
8import { Schema, Context } from "effect";8import * as Schema from "effect/Schema";
9import type { Effect } from "effect";9import * as Context from "effect/Context";
10import type * as Effect from "effect/Effect";
1011
11import type { ProjectionRepositoryError } from "../Errors.ts";12import type { ProjectionRepositoryError } from "../Errors.ts";
1213
apps/server/src/persistence/Services/ProjectionThreadSessions.ts+4-2
...@@ -14,8 +14,10 @@ import {...@@ -14,8 +14,10 @@ import {
14 ThreadId,14 ThreadId,
15 TurnId,15 TurnId,
16} from "@t3tools/contracts";16} from "@t3tools/contracts";
17import { Option, Schema, Context } from "effect";17import * as Option from "effect/Option";
18import type { Effect } from "effect";18import * as Schema from "effect/Schema";
19import * as Context from "effect/Context";
20import type * as Effect from "effect/Effect";
1921
20import type { ProjectionRepositoryError } from "../Errors.ts";22import type { ProjectionRepositoryError } from "../Errors.ts";
2123
apps/server/src/persistence/Services/ProjectionThreads.ts+4-2
...@@ -16,8 +16,10 @@ import {...@@ -16,8 +16,10 @@ import {
16 ThreadId,16 ThreadId,
17 TurnId,17 TurnId,
18} from "@t3tools/contracts";18} from "@t3tools/contracts";
19import { Option, Schema, Context } from "effect";19import * as Option from "effect/Option";
20import type { Effect } from "effect";20import * as Schema from "effect/Schema";
21import * as Context from "effect/Context";
22import type * as Effect from "effect/Effect";
2123
22import type { ProjectionRepositoryError } from "../Errors.ts";24import type { ProjectionRepositoryError } from "../Errors.ts";
2325
apps/server/src/persistence/Services/ProjectionTurns.ts+4-2
...@@ -17,8 +17,10 @@ import {...@@ -17,8 +17,10 @@ import {
17 ThreadId,17 ThreadId,
18 TurnId,18 TurnId,
19} from "@t3tools/contracts";19} from "@t3tools/contracts";
20import { Option, Schema, Context } from "effect";20import * as Option from "effect/Option";
21import type { Effect } from "effect";21import * as Schema from "effect/Schema";
22import * as Context from "effect/Context";
23import type * as Effect from "effect/Effect";
2224
23import type { ProjectionRepositoryError } from "../Errors.ts";25import type { ProjectionRepositoryError } from "../Errors.ts";
2426
apps/server/src/persistence/Services/ProviderSessionRuntime.ts+4-2
...@@ -12,8 +12,10 @@ import {...@@ -12,8 +12,10 @@ import {
12 RuntimeMode,12 RuntimeMode,
13 ThreadId,13 ThreadId,
14} from "@t3tools/contracts";14} from "@t3tools/contracts";
15import { Option, Schema, Context } from "effect";15import * as Option from "effect/Option";
16import type { Effect } from "effect";16import * as Schema from "effect/Schema";
17import * as Context from "effect/Context";
18import type * as Effect from "effect/Effect";
1719
18import type { ProviderSessionRuntimeRepositoryError } from "../Errors.ts";20import type { ProviderSessionRuntimeRepositoryError } from "../Errors.ts";
1921
apps/server/src/processRunner.ts+9-6
...@@ -1,5 +1,6 @@...@@ -1,5 +1,6 @@
1// @effect-diagnostics nodeBuiltinImport:off
1import { type ChildProcess as ChildProcessHandle, spawn, spawnSync } from "node:child_process";2import { type ChildProcess as ChildProcessHandle, spawn, spawnSync } from "node:child_process";
23import * as NodeTimers from "node:timers";
3export interface ProcessRunOptions {4export interface ProcessRunOptions {
4 cwd?: string | undefined;5 cwd?: string | undefined;
5 timeoutMs?: number | undefined;6 timeoutMs?: number | undefined;
...@@ -163,12 +164,14 @@ export async function runProcess(...@@ -163,12 +164,14 @@ export async function runProcess(
163 let stderrTruncated = false;164 let stderrTruncated = false;
164 let timedOut = false;165 let timedOut = false;
165 let settled = false;166 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(() => {
169 timedOut = true;171 timedOut = true;
170 killChild(child, "SIGTERM");172 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(() => {
172 killChild(child, "SIGKILL");175 killChild(child, "SIGKILL");
173 }, 1_000);176 }, 1_000);
174 }, timeoutMs);177 }, timeoutMs);
...@@ -176,9 +179,9 @@ export async function runProcess(...@@ -176,9 +179,9 @@ export async function runProcess(
176 const finalize = (callback: () => void): void => {179 const finalize = (callback: () => void): void => {
177 if (settled) return;180 if (settled) return;
178 settled = true;181 settled = true;
179 clearTimeout(timeoutTimer);182 NodeTimers.clearTimeout(timeoutTimer);
180 if (forceKillTimer) {183 if (forceKillTimer) {
181 clearTimeout(forceKillTimer);184 NodeTimers.clearTimeout(forceKillTimer);
182 }185 }
183 callback();186 callback();
184 };187 };
apps/server/src/project/Layers/ProjectFaviconResolver.test.ts+4-1
...@@ -1,6 +1,9 @@...@@ -1,6 +1,9 @@
1import * as NodeServices from "@effect/platform-node/NodeServices";1import * as NodeServices from "@effect/platform-node/NodeServices";
2import { it, describe, expect } from "@effect/vitest";2import { 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
5import { ProjectFaviconResolver } from "../Services/ProjectFaviconResolver.ts";8import { ProjectFaviconResolver } from "../Services/ProjectFaviconResolver.ts";
6import { ProjectFaviconResolverLive } from "./ProjectFaviconResolver.ts";9import { ProjectFaviconResolverLive } from "./ProjectFaviconResolver.ts";
apps/server/src/project/Layers/ProjectFaviconResolver.ts+4-1
...@@ -1,4 +1,7 @@...@@ -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
3import {6import {
4 ProjectFaviconResolver,7 ProjectFaviconResolver,
apps/server/src/project/Layers/ProjectSetupScriptRunner.test.ts+3-1
...@@ -1,5 +1,7 @@...@@ -1,5 +1,7 @@
1import { ProjectId, type OrchestrationProject } from "@t3tools/contracts";1import { 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";
3import { describe, expect, it, vi } from "vitest";5import { describe, expect, it, vi } from "vitest";
46
5import { ProjectionSnapshotQuery } from "../../orchestration/Services/ProjectionSnapshotQuery.ts";7import { ProjectionSnapshotQuery } from "../../orchestration/Services/ProjectionSnapshotQuery.ts";
apps/server/src/project/Layers/ProjectSetupScriptRunner.ts+27-3
...@@ -1,12 +1,15 @@...@@ -1,12 +1,15 @@
1import { ProjectId } from "@t3tools/contracts";1import { ProjectId } from "@t3tools/contracts";
2import { projectScriptRuntimeEnv, setupProjectScript } from "@t3tools/shared/projectScripts";2import { 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
5import { ProjectionSnapshotQuery } from "../../orchestration/Services/ProjectionSnapshotQuery.ts";7import { ProjectionSnapshotQuery } from "../../orchestration/Services/ProjectionSnapshotQuery.ts";
6import { TerminalManager } from "../../terminal/Services/Manager.ts";8import { TerminalManager } from "../../terminal/Services/Manager.ts";
7import {9import {
8 type ProjectSetupScriptRunnerShape,10 type ProjectSetupScriptRunnerShape,
9 ProjectSetupScriptRunner,11 ProjectSetupScriptRunner,
12 ProjectSetupScriptRunnerError,
10} from "../Services/ProjectSetupScriptRunner.ts";13} from "../Services/ProjectSetupScriptRunner.ts";
1114
12const makeProjectSetupScriptRunner = Effect.gen(function* () {15const makeProjectSetupScriptRunner = Effect.gen(function* () {
...@@ -29,7 +32,9 @@ const makeProjectSetupScriptRunner = Effect.gen(function* () {...@@ -29,7 +32,9 @@ const makeProjectSetupScriptRunner = Effect.gen(function* () {
29 null;32 null;
3033
31 if (!project) {34 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 });
33 }38 }
3439
35 const script = setupProjectScript(project.scripts);40 const script = setupProjectScript(project.scripts);
...@@ -66,7 +71,26 @@ const makeProjectSetupScriptRunner = Effect.gen(function* () {...@@ -66,7 +71,26 @@ const makeProjectSetupScriptRunner = Effect.gen(function* () {
66 terminalId,71 terminalId,
67 cwd,72 cwd,
68 } as const;73 } 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
71 return {95 return {
72 runForThread,96 runForThread,
apps/server/src/project/Layers/RepositoryIdentityResolver.test.ts+6-2
...@@ -1,9 +1,13 @@...@@ -1,9 +1,13 @@
1// @effect-diagnostics nodeBuiltinImport:off
1import { realpathSync } from "node:fs";2import { realpathSync } from "node:fs";
23
3import * as NodeServices from "@effect/platform-node/NodeServices";4import * as NodeServices from "@effect/platform-node/NodeServices";
4import { expect, it } from "@effect/vitest";5import { expect, it } from "@effect/vitest";
5import { Duration, Effect, FileSystem, Layer } from "effect";6import * as Duration from "effect/Duration";
6import { TestClock } from "effect/testing";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
8import { runProcess } from "../../processRunner.ts";12import { runProcess } from "../../processRunner.ts";
9import { RepositoryIdentityResolver } from "../Services/RepositoryIdentityResolver.ts";13import { RepositoryIdentityResolver } from "../Services/RepositoryIdentityResolver.ts";
apps/server/src/project/Layers/RepositoryIdentityResolver.ts+5-1
...@@ -1,5 +1,9 @@...@@ -1,5 +1,9 @@
1import type { RepositoryIdentity } from "@t3tools/contracts";1import 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";
3import {7import {
4 detectSourceControlProviderFromGitRemoteUrl,8 detectSourceControlProviderFromGitRemoteUrl,
5 normalizeGitRemoteUrl,9 normalizeGitRemoteUrl,
apps/server/src/project/Services/ProjectFaviconResolver.ts+2-2
...@@ -6,8 +6,8 @@...@@ -6,8 +6,8 @@
6 *6 *
7 * @module ProjectFaviconResolver7 * @module ProjectFaviconResolver
8 */8 */
9import { Context } from "effect";9import * as Context from "effect/Context";
10import type { Effect } from "effect";10import type * as Effect from "effect/Effect";
1111
12/**12/**
13 * ProjectFaviconResolverShape - Service API for project favicon lookup.13 * ProjectFaviconResolverShape - Service API for project favicon lookup.
apps/server/src/project/Services/ProjectSetupScriptRunner.ts+10-3
...@@ -1,5 +1,6 @@...@@ -1,5 +1,6 @@
1import { Context } from "effect";1import * as Context from "effect/Context";
2import type { Effect } from "effect";2import * as Data from "effect/Data";
3import type * as Effect from "effect/Effect";
34
4export interface ProjectSetupScriptRunnerResultNoScript {5export interface ProjectSetupScriptRunnerResultNoScript {
5 readonly status: "no-script";6 readonly status: "no-script";
...@@ -25,10 +26,16 @@ export interface ProjectSetupScriptRunnerInput {...@@ -25,10 +26,16 @@ export interface ProjectSetupScriptRunnerInput {
25 readonly preferredTerminalId?: string;26 readonly preferredTerminalId?: string;
26}27}
2728
29export class ProjectSetupScriptRunnerError extends Data.TaggedError(
30 "ProjectSetupScriptRunnerError",
31)<{
32 readonly message: string;
33}> {}
34
28export interface ProjectSetupScriptRunnerShape {35export interface ProjectSetupScriptRunnerShape {
29 readonly runForThread: (36 readonly runForThread: (
30 input: ProjectSetupScriptRunnerInput,37 input: ProjectSetupScriptRunnerInput,
31 ) => Effect.Effect<ProjectSetupScriptRunnerResult, Error>;38 ) => Effect.Effect<ProjectSetupScriptRunnerResult, ProjectSetupScriptRunnerError>;
32}39}
3340
34export class ProjectSetupScriptRunner extends Context.Service<41export class ProjectSetupScriptRunner extends Context.Service<
apps/server/src/project/Services/RepositoryIdentityResolver.ts+2-2
...@@ -1,6 +1,6 @@...@@ -1,6 +1,6 @@
1import type { RepositoryIdentity } from "@t3tools/contracts";1import type { RepositoryIdentity } from "@t3tools/contracts";
2import { Context } from "effect";2import * as Context from "effect/Context";
3import type { Effect } from "effect";3import type * as Effect from "effect/Effect";
44
5export interface RepositoryIdentityResolverShape {5export interface RepositoryIdentityResolverShape {
6 readonly resolve: (cwd: string) => Effect.Effect<RepositoryIdentity | null>;6 readonly resolve: (cwd: string) => Effect.Effect<RepositoryIdentity | null>;
apps/server/src/provider/Drivers/ClaudeDriver.ts+9-2
...@@ -13,7 +13,13 @@...@@ -13,7 +13,13 @@
13 * @module provider/Drivers/ClaudeDriver13 * @module provider/Drivers/ClaudeDriver
14 */14 */
15import { ClaudeSettings, ProviderDriverKind, type ServerProvider } from "@t3tools/contracts";15import { 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";
17import { HttpClient } from "effect/unstable/http";23import { HttpClient } from "effect/unstable/http";
18import { ChildProcessSpawner } from "effect/unstable/process";24import { ChildProcessSpawner } from "effect/unstable/process";
1925
...@@ -159,7 +165,8 @@ export const ClaudeDriver: ProviderDriver<ClaudeSettings, ClaudeDriverEnv> = {...@@ -159,7 +165,8 @@ export const ClaudeDriver: ProviderDriver<ClaudeSettings, ClaudeDriverEnv> = {
159 getSettings: Effect.succeed(effectiveConfig),165 getSettings: Effect.succeed(effectiveConfig),
160 streamSettings: Stream.never,166 streamSettings: Stream.never,
161 haveSettingsChanged: () => false,167 haveSettingsChanged: () => false,
162 initialSnapshot: (settings) => stampIdentity(makePendingClaudeProvider(settings)),168 initialSnapshot: (settings) =>
169 makePendingClaudeProvider(settings).pipe(Effect.map(stampIdentity)),
163 checkProvider,170 checkProvider,
164 enrichSnapshot: ({ snapshot, publishSnapshot }) =>171 enrichSnapshot: ({ snapshot, publishSnapshot }) =>
165 enrichProviderSnapshotWithVersionAdvisory(snapshot, maintenanceCapabilities).pipe(172 enrichProviderSnapshotWithVersionAdvisory(snapshot, maintenanceCapabilities).pipe(
apps/server/src/provider/Drivers/ClaudeHome.test.ts+2-1
...@@ -2,7 +2,8 @@ import * as NodeOS from "node:os";...@@ -2,7 +2,8 @@ import * as NodeOS from "node:os";
22
3import * as NodeServices from "@effect/platform-node/NodeServices";3import * as NodeServices from "@effect/platform-node/NodeServices";
4import { describe, expect, it } from "@effect/vitest";4import { describe, expect, it } from "@effect/vitest";
5import { Effect, Path } from "effect";5import * as Effect from "effect/Effect";
6import * as Path from "effect/Path";
67
7import {8import {
8 makeClaudeCapabilitiesCacheKey,9 makeClaudeCapabilitiesCacheKey,
apps/server/src/provider/Drivers/ClaudeHome.ts+2-1
...@@ -1,7 +1,8 @@...@@ -1,7 +1,8 @@
1import * as NodeOS from "node:os";1import * as NodeOS from "node:os";
22
3import type { ClaudeSettings } from "@t3tools/contracts";3import type { ClaudeSettings } from "@t3tools/contracts";
4import { Effect, Path } from "effect";4import * as Effect from "effect/Effect";
5import * as Path from "effect/Path";
56
6import { expandHomePath } from "../../pathExpansion.ts";7import { expandHomePath } from "../../pathExpansion.ts";
78
apps/server/src/provider/Drivers/CodexDriver.ts+8-2
...@@ -22,7 +22,12 @@...@@ -22,7 +22,12 @@
22 * @module provider/Drivers/CodexDriver22 * @module provider/Drivers/CodexDriver
23 */23 */
24import { CodexSettings, ProviderDriverKind, type ServerProvider } from "@t3tools/contracts";24import { 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";
26import { HttpClient } from "effect/unstable/http";31import { HttpClient } from "effect/unstable/http";
27import { ChildProcessSpawner } from "effect/unstable/process";32import { ChildProcessSpawner } from "effect/unstable/process";
2833
...@@ -160,7 +165,8 @@ export const CodexDriver: ProviderDriver<CodexSettings, CodexDriverEnv> = {...@@ -160,7 +165,8 @@ export const CodexDriver: ProviderDriver<CodexSettings, CodexDriverEnv> = {
160 getSettings: Effect.succeed(effectiveConfig),165 getSettings: Effect.succeed(effectiveConfig),
161 streamSettings: Stream.never,166 streamSettings: Stream.never,
162 haveSettingsChanged: () => false,167 haveSettingsChanged: () => false,
163 initialSnapshot: (settings) => stampIdentity(makePendingCodexProvider(settings)),168 initialSnapshot: (settings) =>
169 makePendingCodexProvider(settings).pipe(Effect.map(stampIdentity)),
164 checkProvider,170 checkProvider,
165 enrichSnapshot: ({ snapshot, publishSnapshot }) =>171 enrichSnapshot: ({ snapshot, publishSnapshot }) =>
166 enrichProviderSnapshotWithVersionAdvisory(snapshot, maintenanceCapabilities).pipe(172 enrichProviderSnapshotWithVersionAdvisory(snapshot, maintenanceCapabilities).pipe(
apps/server/src/provider/Drivers/CodexHomeLayout.test.ts+4-1
...@@ -1,6 +1,9 @@...@@ -1,6 +1,9 @@
1import * as NodeServices from "@effect/platform-node/NodeServices";1import * as NodeServices from "@effect/platform-node/NodeServices";
2import { describe, expect, it } from "@effect/vitest";2import { 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
5import { CodexSettings } from "@t3tools/contracts";8import { CodexSettings } from "@t3tools/contracts";
6import {9import {
apps/server/src/provider/Drivers/CodexHomeLayout.ts+4-1
...@@ -1,7 +1,10 @@...@@ -1,7 +1,10 @@
1import * as NodeOS from "node:os";1import * as NodeOS from "node:os";
22
3import { ProviderDriverKind, type CodexSettings } from "@t3tools/contracts";3import { 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";
5import * as PlatformError from "effect/PlatformError";8import * as PlatformError from "effect/PlatformError";
69
7import { expandHomePath } from "../../pathExpansion.ts";10import { expandHomePath } from "../../pathExpansion.ts";
apps/server/src/provider/Drivers/CursorDriver.ts+8-2
...@@ -13,7 +13,12 @@...@@ -13,7 +13,12 @@
13 * @module provider/Drivers/CursorDriver13 * @module provider/Drivers/CursorDriver
14 */14 */
15import { CursorSettings, ProviderDriverKind, type ServerProvider } from "@t3tools/contracts";15import { 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";
17import { HttpClient } from "effect/unstable/http";22import { HttpClient } from "effect/unstable/http";
18import { ChildProcessSpawner } from "effect/unstable/process";23import { ChildProcessSpawner } from "effect/unstable/process";
1924
...@@ -128,7 +133,8 @@ export const CursorDriver: ProviderDriver<CursorSettings, CursorDriverEnv> = {...@@ -128,7 +133,8 @@ export const CursorDriver: ProviderDriver<CursorSettings, CursorDriverEnv> = {
128 getSettings: Effect.succeed(effectiveConfig),133 getSettings: Effect.succeed(effectiveConfig),
129 streamSettings: Stream.never,134 streamSettings: Stream.never,
130 haveSettingsChanged: () => false,135 haveSettingsChanged: () => false,
131 initialSnapshot: (settings) => stampIdentity(buildInitialCursorProviderSnapshot(settings)),136 initialSnapshot: (settings) =>
137 buildInitialCursorProviderSnapshot(settings).pipe(Effect.map(stampIdentity)),
132 checkProvider,138 checkProvider,
133 // Preserve the background ACP model-capability probe that used to139 // Preserve the background ACP model-capability probe that used to
134 // live on `CursorProviderLive`. Only fires when the snapshot reports140 // live on `CursorProviderLive`. Only fires when the snapshot reports
apps/server/src/provider/Drivers/OpenCodeDriver.ts+8-2
...@@ -13,7 +13,12 @@...@@ -13,7 +13,12 @@
13 * @module provider/Drivers/OpenCodeDriver13 * @module provider/Drivers/OpenCodeDriver
14 */14 */
15import { OpenCodeSettings, ProviderDriverKind, type ServerProvider } from "@t3tools/contracts";15import { 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";
17import { HttpClient } from "effect/unstable/http";22import { HttpClient } from "effect/unstable/http";
18import { ChildProcessSpawner } from "effect/unstable/process";23import { ChildProcessSpawner } from "effect/unstable/process";
1924
...@@ -139,7 +144,8 @@ export const OpenCodeDriver: ProviderDriver<OpenCodeSettings, OpenCodeDriverEnv>...@@ -139,7 +144,8 @@ export const OpenCodeDriver: ProviderDriver<OpenCodeSettings, OpenCodeDriverEnv>
139 getSettings: Effect.succeed(effectiveConfig),144 getSettings: Effect.succeed(effectiveConfig),
140 streamSettings: Stream.never,145 streamSettings: Stream.never,
141 haveSettingsChanged: () => false,146 haveSettingsChanged: () => false,
142 initialSnapshot: (settings) => stampIdentity(makePendingOpenCodeProvider(settings)),147 initialSnapshot: (settings) =>
148 makePendingOpenCodeProvider(settings).pipe(Effect.map(stampIdentity)),
143 checkProvider,149 checkProvider,
144 enrichSnapshot: ({ snapshot, publishSnapshot }) =>150 enrichSnapshot: ({ snapshot, publishSnapshot }) =>
145 enrichProviderSnapshotWithVersionAdvisory(snapshot, maintenanceCapabilities).pipe(151 enrichProviderSnapshotWithVersionAdvisory(snapshot, maintenanceCapabilities).pipe(
apps/server/src/provider/Errors.ts+1-1
...@@ -1,4 +1,4 @@...@@ -1,4 +1,4 @@
1import { Schema } from "effect";1import * as Schema from "effect/Schema";
22
3import type { CheckpointServiceError } from "../checkpointing/Errors.ts";3import type { CheckpointServiceError } from "../checkpointing/Errors.ts";
44
apps/server/src/provider/Layers/ClaudeAdapter.test.ts+11-2
...@@ -1,3 +1,4 @@...@@ -1,3 +1,4 @@
1// @effect-diagnostics nodeBuiltinImport:off
1import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";2import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
2import os from "node:os";3import os from "node:os";
3import path from "node:path";4import path from "node:path";
...@@ -22,7 +23,14 @@ import {...@@ -22,7 +23,14 @@ import {
22} from "@t3tools/contracts";23} from "@t3tools/contracts";
23import { createModelSelection } from "@t3tools/shared/model";24import { createModelSelection } from "@t3tools/shared/model";
24import { assert, describe, it } from "@effect/vitest";25import { 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
27import { attachmentRelativePath } from "../../attachmentStore.ts";35import { attachmentRelativePath } from "../../attachmentStore.ts";
28import { ServerConfig } from "../../config.ts";36import { ServerConfig } from "../../config.ts";
...@@ -1463,7 +1471,8 @@ describe("ClaudeAdapterLive", () => {...@@ -1463,7 +1471,8 @@ describe("ClaudeAdapterLive", () => {
1463 yield* Effect.yieldNow;1471 yield* Effect.yieldNow;
1464 yield* Effect.yieldNow;1472 yield* Effect.yieldNow;
1465 yield* Effect.yieldNow;1473 yield* Effect.yieldNow;
1466 yield* Effect.promise(() => new Promise((resolve) => setTimeout(resolve, 50)));1474 yield* TestClock.adjust("50 millis");
1475 yield* Effect.yieldNow;
14671476
1468 runtimeEventsFiber.interruptUnsafe();1477 runtimeEventsFiber.interruptUnsafe();
14691478
apps/server/src/provider/Layers/ClaudeAdapter.ts+65-36
...@@ -51,20 +51,19 @@ import {...@@ -51,20 +51,19 @@ import {
51 getProviderOptionDescriptors,51 getProviderOptionDescriptors,
52 resolvePromptInjectedEffort,52 resolvePromptInjectedEffort,
53} from "@t3tools/shared/model";53} from "@t3tools/shared/model";
54import {54import * as Cause from "effect/Cause";
55 Cause,55import * as DateTime from "effect/DateTime";
56 DateTime,56import * as Deferred from "effect/Deferred";
57 Deferred,57import * as Effect from "effect/Effect";
58 Effect,58import * as Exit from "effect/Exit";
59 Exit,59import * as FileSystem from "effect/FileSystem";
60 FileSystem,60import * as Fiber from "effect/Fiber";
61 Fiber,61import * as Path from "effect/Path";
62 Path,62import * as Queue from "effect/Queue";
63 Queue,63import * as Random from "effect/Random";
64 Random,64import * as Ref from "effect/Ref";
65 Ref,65import * as Schema from "effect/Schema";
66 Stream,66import * as Stream from "effect/Stream";
67} from "effect";
6867
69import { resolveAttachmentPath } from "../../attachmentStore.ts";68import { resolveAttachmentPath } from "../../attachmentStore.ts";
70import { ServerConfig } from "../../config.ts";69import { ServerConfig } from "../../config.ts";
...@@ -222,11 +221,22 @@ function toMessage(cause: unknown, fallback: string): string {...@@ -222,11 +221,22 @@ function toMessage(cause: unknown, fallback: string): string {
222 return fallback;221 return fallback;
223}222}
224223
225function toError(cause: unknown, fallback: string): Error {224function toProcessError(
226 return cause instanceof Error ? cause : new Error(toMessage(cause, fallback));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 });
227}235}
228236
229function normalizeClaudeStreamMessages(cause: Cause.Cause<Error>): ReadonlyArray<string> {237function normalizeClaudeStreamMessages(
238 cause: Cause.Cause<{ readonly message: string }>,
239): ReadonlyArray<string> {
230 const errors = Cause.prettyErrors(cause)240 const errors = Cause.prettyErrors(cause)
231 .map((error) => error.message.trim())241 .map((error) => error.message.trim())
232 .filter((message) => message.length > 0);242 .filter((message) => message.length > 0);
...@@ -252,18 +262,23 @@ function isClaudeInterruptedMessage(message: string): boolean {...@@ -252,18 +262,23 @@ function isClaudeInterruptedMessage(message: string): boolean {
252 );262 );
253}263}
254264
255function isClaudeInterruptedCause(cause: Cause.Cause<Error>): boolean {265function isClaudeInterruptedCause(cause: Cause.Cause<{ readonly message: string }>): boolean {
256 return (266 return (
257 Cause.hasInterruptsOnly(cause) ||267 Cause.hasInterruptsOnly(cause) ||
258 normalizeClaudeStreamMessages(cause).some(isClaudeInterruptedMessage)268 normalizeClaudeStreamMessages(cause).some(isClaudeInterruptedMessage)
259 );269 );
260}270}
261271
262function messageFromClaudeStreamCause(cause: Cause.Cause<Error>, fallback: string): string {272function messageFromClaudeStreamCause(
273 cause: Cause.Cause<{ readonly message: string }>,
274 fallback: string,
275): string {
263 return normalizeClaudeStreamMessages(cause)[0] ?? fallback;276 return normalizeClaudeStreamMessages(cause)[0] ?? fallback;
264}277}
265278
266function interruptionMessageFromClaudeCause(cause: Cause.Cause<Error>): string {279function interruptionMessageFromClaudeCause(
280 cause: Cause.Cause<{ readonly message: string }>,
281): string {
267 const message = messageFromClaudeStreamCause(cause, "Claude runtime interrupted.");282 const message = messageFromClaudeStreamCause(cause, "Claude runtime interrupted.");
268 return isClaudeInterruptedMessage(message) ? "Claude runtime interrupted." : message;283 return isClaudeInterruptedMessage(message) ? "Claude runtime interrupted." : message;
269}284}
...@@ -530,7 +545,7 @@ function summarizeToolRequest(toolName: string, input: Record<string, unknown>):...@@ -530,7 +545,7 @@ function summarizeToolRequest(toolName: string, input: Record<string, unknown>):
530 }545 }
531 }546 }
532547
533 const serialized = JSON.stringify(input);548 const serialized = Schema.encodeUnknownSync(Schema.UnknownFromJsonString)(input);
534 if (serialized.length <= 400) {549 if (serialized.length <= 400) {
535 return `${toolName}: ${serialized}`;550 return `${toolName}: ${serialized}`;
536 }551 }
...@@ -796,7 +811,7 @@ function exitPlanCaptureKey(input: {...@@ -796,7 +811,7 @@ function exitPlanCaptureKey(input: {
796811
797function tryParseJsonRecord(value: string): Record<string, unknown> | undefined {812function tryParseJsonRecord(value: string): Record<string, unknown> | undefined {
798 try {813 try {
799 const parsed = JSON.parse(value);814 const parsed = Schema.decodeUnknownSync(Schema.UnknownFromJsonString)(value);
800 return parsed && typeof parsed === "object" && !Array.isArray(parsed)815 return parsed && typeof parsed === "object" && !Array.isArray(parsed)
801 ? (parsed as Record<string, unknown>)816 ? (parsed as Record<string, unknown>)
802 : undefined;817 : undefined;
...@@ -807,7 +822,7 @@ function tryParseJsonRecord(value: string): Record<string, unknown> | undefined...@@ -807,7 +822,7 @@ function tryParseJsonRecord(value: string): Record<string, unknown> | undefined
807822
808function toolInputFingerprint(input: Record<string, unknown>): string | undefined {823function toolInputFingerprint(input: Record<string, unknown>): string | undefined {
809 try {824 try {
810 return JSON.stringify(input);825 return Schema.encodeUnknownSync(Schema.UnknownFromJsonString)(input);
811 } catch {826 } catch {
812 return undefined;827 return undefined;
813 }828 }
...@@ -1020,7 +1035,7 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (...@@ -1020,7 +1035,7 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (
1020 return;1035 return;
1021 }1036 }
10221037
1023 const observedAt = new Date().toISOString();1038 const observedAt = yield* nowIso;
1024 const itemId = sdkNativeItemId(message);1039 const itemId = sdkNativeItemId(message);
10251040
1026 yield* nativeEventLogger.write(1041 yield* nativeEventLogger.write(
...@@ -1030,7 +1045,7 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (...@@ -1030,7 +1045,7 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (
1030 id:1045 id:
1031 "uuid" in message && typeof message.uuid === "string"1046 "uuid" in message && typeof message.uuid === "string"
1032 ? message.uuid1047 ? message.uuid
1033 : crypto.randomUUID(),1048 : yield* Random.nextUUIDv4,
1034 kind: "notification",1049 kind: "notification",
1035 provider: PROVIDER,1050 provider: PROVIDER,
1036 createdAt: observedAt,1051 createdAt: observedAt,
...@@ -2353,9 +2368,11 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (...@@ -2353,9 +2368,11 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (
2353 }2368 }
2354 });2369 });
23552370
2356 const runSdkStream = (context: ClaudeSessionContext): Effect.Effect<void, Error> =>2371 const runSdkStream = (
2372 context: ClaudeSessionContext,
2373 ): Effect.Effect<void, ProviderAdapterProcessError> =>
2357 Stream.fromAsyncIterable(context.query, (cause) =>2374 Stream.fromAsyncIterable(context.query, (cause) =>
2358 toError(cause, "Claude runtime stream failed."),2375 toProcessError(cause, "Claude runtime stream failed.", context.session.threadId),
2359 ).pipe(2376 ).pipe(
2360 Stream.takeWhile(() => !context.stopped),2377 Stream.takeWhile(() => !context.stopped),
2361 Stream.runForEach((message) => handleSdkMessage(context, message)),2378 Stream.runForEach((message) => handleSdkMessage(context, message)),
...@@ -2363,7 +2380,7 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (...@@ -2363,7 +2380,7 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (
23632380
2364 const handleStreamExit = Effect.fn("handleStreamExit")(function* (2381 const handleStreamExit = Effect.fn("handleStreamExit")(function* (
2365 context: ClaudeSessionContext,2382 context: ClaudeSessionContext,
2366 exit: Exit.Exit<void, Error>,2383 exit: Exit.Exit<void, ProviderAdapterProcessError>,
2367 ) {2384 ) {
2368 if (context.stopped) {2385 if (context.stopped) {
2369 return;2386 return;
...@@ -2432,12 +2449,20 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (...@@ -2432,12 +2449,20 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (
2432 yield* Fiber.interrupt(streamFiber);2449 yield* Fiber.interrupt(streamFiber);
2433 }2450 }
24342451
2435 // @effect-diagnostics-next-line tryCatchInEffectGen:off2452 yield* Effect.try({
2436 try {2453 try: () => context.query.close(),
2437 context.query.close();2454 catch: (cause) =>
2438 } catch (cause) {2455 new ProviderAdapterProcessError({
2439 yield* emitRuntimeError(context, "Failed to close Claude runtime query.", cause);2456 provider: PROVIDER,
2440 }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
2442 const updatedAt = yield* nowIso;2467 const updatedAt = yield* nowIso;
2443 context.session = {2468 context.session = {
...@@ -2916,8 +2941,12 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (...@@ -2916,8 +2941,12 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (
2916 "claude.query.include_partial_messages": true,2941 "claude.query.include_partial_messages": true,
2917 "claude.query.additional_directories": input.cwd ? [input.cwd] : [],2942 "claude.query.additional_directories": input.cwd ? [input.cwd] : [],
2918 "claude.query.setting_sources": [...CLAUDE_SETTING_SOURCES],2943 "claude.query.setting_sources": [...CLAUDE_SETTING_SOURCES],
2919 "claude.query.settings_json": JSON.stringify(settings),2944 "claude.query.settings_json": Schema.encodeUnknownSync(Schema.UnknownFromJsonString)(
2920 "claude.query.extra_args_json": JSON.stringify(extraArgs),2945 settings,
2946 ),
2947 "claude.query.extra_args_json": Schema.encodeUnknownSync(Schema.UnknownFromJsonString)(
2948 extraArgs,
2949 ),
2921 "claude.query.path_to_executable": claudeBinaryPath,2950 "claude.query.path_to_executable": claudeBinaryPath,
2922 });2951 });
29232952
apps/server/src/provider/Layers/ClaudeProvider.ts+37-28
...@@ -6,7 +6,11 @@ import {...@@ -6,7 +6,11 @@ import {
6 type ServerProviderModel,6 type ServerProviderModel,
7 type ServerProviderSlashCommand,7 type ServerProviderSlashCommand,
8} from "@t3tools/contracts";8} 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";
10import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";14import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
11import {15import {
12 createModelCapabilities,16 createModelCapabilities,
...@@ -521,7 +525,7 @@ export const checkClaudeProviderStatus = Effect.fn("checkClaudeProviderStatus")(...@@ -521,7 +525,7 @@ export const checkClaudeProviderStatus = Effect.fn("checkClaudeProviderStatus")(
521 never,525 never,
522 ChildProcessSpawner.ChildProcessSpawner | Path.Path526 ChildProcessSpawner.ChildProcessSpawner | Path.Path
523> {527> {
524 const checkedAt = new Date().toISOString();528 const checkedAt = DateTime.formatIso(yield* DateTime.now);
525 const allModels = providerModelsFromSettings(529 const allModels = providerModelsFromSettings(
526 BUILT_IN_MODELS,530 BUILT_IN_MODELS,
527 PROVIDER,531 PROVIDER,
...@@ -664,19 +668,39 @@ export const checkClaudeProviderStatus = Effect.fn("checkClaudeProviderStatus")(...@@ -664,19 +668,39 @@ export const checkClaudeProviderStatus = Effect.fn("checkClaudeProviderStatus")(
664 });668 });
665});669});
666670
667export const makePendingClaudeProvider = (claudeSettings: ClaudeSettings): ServerProviderDraft => {671const nowIso = Effect.map(DateTime.now, DateTime.formatIso);
668 const checkedAt = new Date().toISOString();672
669 const models = providerModelsFromSettings(673export const makePendingClaudeProvider = (
670 BUILT_IN_MODELS,674 claudeSettings: ClaudeSettings,
671 PROVIDER,675): Effect.Effect<ServerProviderDraft> =>
672 claudeSettings.customModels,676 Effect.gen(function* () {
673 DEFAULT_CLAUDE_MODEL_CAPABILITIES,677 const checkedAt = yield* nowIso;
674 );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) {
677 return buildServerProvider({701 return buildServerProvider({
678 presentation: CLAUDE_PRESENTATION,702 presentation: CLAUDE_PRESENTATION,
679 enabled: false,703 enabled: true,
680 checkedAt,704 checkedAt,
681 models,705 models,
682 probe: {706 probe: {
...@@ -684,24 +708,9 @@ export const makePendingClaudeProvider = (claudeSettings: ClaudeSettings): Serve...@@ -684,24 +708,9 @@ export const makePendingClaudeProvider = (claudeSettings: ClaudeSettings): Serve
684 version: null,708 version: null,
685 status: "warning",709 status: "warning",
686 auth: { status: "unknown" },710 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.",
688 },712 },
689 });713 });
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 },
704 });714 });
705};
706715
707export { probeClaudeCapabilities };716export { probeClaudeCapabilities };
apps/server/src/provider/Layers/CodexAdapter.test.ts+28-18
...@@ -1,3 +1,4 @@...@@ -1,3 +1,4 @@
1// @effect-diagnostics nodeBuiltinImport:off
1import assert from "node:assert/strict";2import assert from "node:assert/strict";
2import fs from "node:fs";3import fs from "node:fs";
3import os from "node:os";4import os from "node:os";
...@@ -21,7 +22,16 @@ import { createModelSelection } from "@t3tools/shared/model";...@@ -21,7 +22,16 @@ import { createModelSelection } from "@t3tools/shared/model";
21import * as NodeServices from "@effect/platform-node/NodeServices";22import * as NodeServices from "@effect/platform-node/NodeServices";
22import { it, vi } from "@effect/vitest";23import { 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";
25import * as CodexErrors from "effect-codex-app-server/errors";35import * as CodexErrors from "effect-codex-app-server/errors";
2636
27import { ServerConfig } from "../../config.ts";37import { ServerConfig } from "../../config.ts";
...@@ -49,7 +59,7 @@ const asItemId = (value: string): ProviderItemId => ProviderItemId.make(value);...@@ -49,7 +59,7 @@ const asItemId = (value: string): ProviderItemId => ProviderItemId.make(value);
4959
50class FakeCodexRuntime implements CodexSessionRuntimeShape {60class FakeCodexRuntime implements CodexSessionRuntimeShape {
51 private readonly eventQueue = Effect.runSync(Queue.unbounded<ProviderEvent>());61 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
54 public readonly startImpl = vi.fn(() =>64 public readonly startImpl = vi.fn(() =>
55 Promise.resolve({65 Promise.resolve({
...@@ -446,7 +456,7 @@ lifecycleLayer("CodexAdapterLive lifecycle", (it) => {...@@ -446,7 +456,7 @@ lifecycleLayer("CodexAdapterLive lifecycle", (it) => {
446 id: asEventId("evt-msg-complete"),456 id: asEventId("evt-msg-complete"),
447 kind: "notification",457 kind: "notification",
448 provider: ProviderDriverKind.make("codex"),458 provider: ProviderDriverKind.make("codex"),
449 createdAt: new Date().toISOString(),459 createdAt: "2026-01-01T00:00:00.000Z",
450 method: "item/completed",460 method: "item/completed",
451 threadId: asThreadId("thread-1"),461 threadId: asThreadId("thread-1"),
452 turnId: asTurnId("turn-1"),462 turnId: asTurnId("turn-1"),
...@@ -489,7 +499,7 @@ lifecycleLayer("CodexAdapterLive lifecycle", (it) => {...@@ -489,7 +499,7 @@ lifecycleLayer("CodexAdapterLive lifecycle", (it) => {
489 id: asEventId("evt-plan-complete"),499 id: asEventId("evt-plan-complete"),
490 kind: "notification",500 kind: "notification",
491 provider: ProviderDriverKind.make("codex"),501 provider: ProviderDriverKind.make("codex"),
492 createdAt: new Date().toISOString(),502 createdAt: "2026-01-01T00:00:00.000Z",
493 method: "item/completed",503 method: "item/completed",
494 threadId: asThreadId("thread-1"),504 threadId: asThreadId("thread-1"),
495 turnId: asTurnId("turn-1"),505 turnId: asTurnId("turn-1"),
...@@ -531,7 +541,7 @@ lifecycleLayer("CodexAdapterLive lifecycle", (it) => {...@@ -531,7 +541,7 @@ lifecycleLayer("CodexAdapterLive lifecycle", (it) => {
531 id: asEventId("evt-plan-delta"),541 id: asEventId("evt-plan-delta"),
532 kind: "notification",542 kind: "notification",
533 provider: ProviderDriverKind.make("codex"),543 provider: ProviderDriverKind.make("codex"),
534 createdAt: new Date().toISOString(),544 createdAt: "2026-01-01T00:00:00.000Z",
535 method: "item/plan/delta",545 method: "item/plan/delta",
536 threadId: asThreadId("thread-1"),546 threadId: asThreadId("thread-1"),
537 turnId: asTurnId("turn-1"),547 turnId: asTurnId("turn-1"),
...@@ -569,7 +579,7 @@ lifecycleLayer("CodexAdapterLive lifecycle", (it) => {...@@ -569,7 +579,7 @@ lifecycleLayer("CodexAdapterLive lifecycle", (it) => {
569 kind: "session",579 kind: "session",
570 provider: ProviderDriverKind.make("codex"),580 provider: ProviderDriverKind.make("codex"),
571 threadId: asThreadId("thread-1"),581 threadId: asThreadId("thread-1"),
572 createdAt: new Date().toISOString(),582 createdAt: "2026-01-01T00:00:00.000Z",
573 method: "session/closed",583 method: "session/closed",
574 message: "Session stopped",584 message: "Session stopped",
575 };585 };
...@@ -600,7 +610,7 @@ lifecycleLayer("CodexAdapterLive lifecycle", (it) => {...@@ -600,7 +610,7 @@ lifecycleLayer("CodexAdapterLive lifecycle", (it) => {
600 kind: "notification",610 kind: "notification",
601 provider: ProviderDriverKind.make("codex"),611 provider: ProviderDriverKind.make("codex"),
602 threadId: asThreadId("thread-1"),612 threadId: asThreadId("thread-1"),
603 createdAt: new Date().toISOString(),613 createdAt: "2026-01-01T00:00:00.000Z",
604 method: "error",614 method: "error",
605 turnId: asTurnId("turn-1"),615 turnId: asTurnId("turn-1"),
606 payload: {616 payload: {
...@@ -638,7 +648,7 @@ lifecycleLayer("CodexAdapterLive lifecycle", (it) => {...@@ -638,7 +648,7 @@ lifecycleLayer("CodexAdapterLive lifecycle", (it) => {
638 kind: "notification",648 kind: "notification",
639 provider: ProviderDriverKind.make("codex"),649 provider: ProviderDriverKind.make("codex"),
640 threadId: asThreadId("thread-1"),650 threadId: asThreadId("thread-1"),
641 createdAt: new Date().toISOString(),651 createdAt: "2026-01-01T00:00:00.000Z",
642 method: "process/stderr",652 method: "process/stderr",
643 turnId: asTurnId("turn-1"),653 turnId: asTurnId("turn-1"),
644 message: "The filename or extension is too long. (os error 206)",654 message: "The filename or extension is too long. (os error 206)",
...@@ -672,7 +682,7 @@ lifecycleLayer("CodexAdapterLive lifecycle", (it) => {...@@ -672,7 +682,7 @@ lifecycleLayer("CodexAdapterLive lifecycle", (it) => {
672 kind: "notification",682 kind: "notification",
673 provider: ProviderDriverKind.make("codex"),683 provider: ProviderDriverKind.make("codex"),
674 threadId: asThreadId("thread-1"),684 threadId: asThreadId("thread-1"),
675 createdAt: new Date().toISOString(),685 createdAt: "2026-01-01T00:00:00.000Z",
676 method: "thread/realtime/started",686 method: "thread/realtime/started",
677 payload: {687 payload: {
678 threadId: "thread-1",688 threadId: "thread-1",
...@@ -706,7 +716,7 @@ lifecycleLayer("CodexAdapterLive lifecycle", (it) => {...@@ -706,7 +716,7 @@ lifecycleLayer("CodexAdapterLive lifecycle", (it) => {
706 kind: "notification",716 kind: "notification",
707 provider: ProviderDriverKind.make("codex"),717 provider: ProviderDriverKind.make("codex"),
708 threadId: asThreadId("thread-1"),718 threadId: asThreadId("thread-1"),
709 createdAt: new Date().toISOString(),719 createdAt: "2026-01-01T00:00:00.000Z",
710 method: "process/stderr",720 method: "process/stderr",
711 turnId: asTurnId("turn-1"),721 turnId: asTurnId("turn-1"),
712 message:722 message:
...@@ -742,7 +752,7 @@ lifecycleLayer("CodexAdapterLive lifecycle", (it) => {...@@ -742,7 +752,7 @@ lifecycleLayer("CodexAdapterLive lifecycle", (it) => {
742 kind: "notification",752 kind: "notification",
743 provider: ProviderDriverKind.make("codex"),753 provider: ProviderDriverKind.make("codex"),
744 threadId: asThreadId("thread-1"),754 threadId: asThreadId("thread-1"),
745 createdAt: new Date().toISOString(),755 createdAt: "2026-01-01T00:00:00.000Z",
746 method: "serverRequest/resolved",756 method: "serverRequest/resolved",
747 requestKind: "command",757 requestKind: "command",
748 requestId: ApprovalRequestId.make("req-1"),758 requestId: ApprovalRequestId.make("req-1"),
...@@ -777,7 +787,7 @@ lifecycleLayer("CodexAdapterLive lifecycle", (it) => {...@@ -777,7 +787,7 @@ lifecycleLayer("CodexAdapterLive lifecycle", (it) => {
777 kind: "notification",787 kind: "notification",
778 provider: ProviderDriverKind.make("codex"),788 provider: ProviderDriverKind.make("codex"),
779 threadId: asThreadId("thread-1"),789 threadId: asThreadId("thread-1"),
780 createdAt: new Date().toISOString(),790 createdAt: "2026-01-01T00:00:00.000Z",
781 method: "serverRequest/resolved",791 method: "serverRequest/resolved",
782 requestKind: "file-read",792 requestKind: "file-read",
783 requestId: ApprovalRequestId.make("req-file-read-1"),793 requestId: ApprovalRequestId.make("req-file-read-1"),
...@@ -812,7 +822,7 @@ lifecycleLayer("CodexAdapterLive lifecycle", (it) => {...@@ -812,7 +822,7 @@ lifecycleLayer("CodexAdapterLive lifecycle", (it) => {
812 kind: "notification",822 kind: "notification",
813 provider: ProviderDriverKind.make("codex"),823 provider: ProviderDriverKind.make("codex"),
814 threadId: asThreadId("thread-1"),824 threadId: asThreadId("thread-1"),
815 createdAt: new Date().toISOString(),825 createdAt: "2026-01-01T00:00:00.000Z",
816 method: "item/tool/requestUserInput/answered",826 method: "item/tool/requestUserInput/answered",
817 payload: {827 payload: {
818 answers: {828 answers: {
...@@ -852,7 +862,7 @@ lifecycleLayer("CodexAdapterLive lifecycle", (it) => {...@@ -852,7 +862,7 @@ lifecycleLayer("CodexAdapterLive lifecycle", (it) => {
852 kind: "notification",862 kind: "notification",
853 provider: ProviderDriverKind.make("codex"),863 provider: ProviderDriverKind.make("codex"),
854 threadId: asThreadId("thread-1"),864 threadId: asThreadId("thread-1"),
855 createdAt: new Date().toISOString(),865 createdAt: "2026-01-01T00:00:00.000Z",
856 method: "windowsSandbox/setupCompleted",866 method: "windowsSandbox/setupCompleted",
857 message: "Sandbox setup failed",867 message: "Sandbox setup failed",
858 payload: {868 payload: {
...@@ -897,7 +907,7 @@ lifecycleLayer("CodexAdapterLive lifecycle", (it) => {...@@ -897,7 +907,7 @@ lifecycleLayer("CodexAdapterLive lifecycle", (it) => {
897 kind: "request",907 kind: "request",
898 provider: ProviderDriverKind.make("codex"),908 provider: ProviderDriverKind.make("codex"),
899 threadId: asThreadId("thread-1"),909 threadId: asThreadId("thread-1"),
900 createdAt: new Date().toISOString(),910 createdAt: "2026-01-01T00:00:00.000Z",
901 method: "item/tool/requestUserInput",911 method: "item/tool/requestUserInput",
902 requestId: ApprovalRequestId.make("req-user-input-1"),912 requestId: ApprovalRequestId.make("req-user-input-1"),
903 payload: {913 payload: {
...@@ -924,7 +934,7 @@ lifecycleLayer("CodexAdapterLive lifecycle", (it) => {...@@ -924,7 +934,7 @@ lifecycleLayer("CodexAdapterLive lifecycle", (it) => {
924 kind: "notification",934 kind: "notification",
925 provider: ProviderDriverKind.make("codex"),935 provider: ProviderDriverKind.make("codex"),
926 threadId: asThreadId("thread-1"),936 threadId: asThreadId("thread-1"),
927 createdAt: new Date().toISOString(),937 createdAt: "2026-01-01T00:00:00.000Z",
928 method: "item/tool/requestUserInput/answered",938 method: "item/tool/requestUserInput/answered",
929 requestId: ApprovalRequestId.make("req-user-input-1"),939 requestId: ApprovalRequestId.make("req-user-input-1"),
930 payload: {940 payload: {
...@@ -965,7 +975,7 @@ lifecycleLayer("CodexAdapterLive lifecycle", (it) => {...@@ -965,7 +975,7 @@ lifecycleLayer("CodexAdapterLive lifecycle", (it) => {
965 provider: ProviderDriverKind.make("codex"),975 provider: ProviderDriverKind.make("codex"),
966 threadId: asThreadId("thread-1"),976 threadId: asThreadId("thread-1"),
967 turnId: asTurnId("turn-1"),977 turnId: asTurnId("turn-1"),
968 createdAt: new Date().toISOString(),978 createdAt: "2026-01-01T00:00:00.000Z",
969 method: "thread/tokenUsage/updated",979 method: "thread/tokenUsage/updated",
970 payload: {980 payload: {
971 threadId: "thread-1",981 threadId: "thread-1",
...@@ -1147,7 +1157,7 @@ it.effect("flushes managed native logs when the adapter layer shuts down", () =>...@@ -1147,7 +1157,7 @@ it.effect("flushes managed native logs when the adapter layer shuts down", () =>
1147 kind: "notification",1157 kind: "notification",
1148 provider: ProviderDriverKind.make("codex"),1158 provider: ProviderDriverKind.make("codex"),
1149 threadId: asThreadId("thread-logger"),1159 threadId: asThreadId("thread-logger"),
1150 createdAt: new Date().toISOString(),1160 createdAt: "2026-01-01T00:00:00.000Z",
1151 method: "process/stderr",1161 method: "process/stderr",
1152 message: "native flush test",1162 message: "native flush test",
1153 } satisfies ProviderEvent);1163 } satisfies ProviderEvent);
apps/server/src/provider/Layers/CodexAdapter.ts+8-1
...@@ -24,7 +24,14 @@ import {...@@ -24,7 +24,14 @@ import {
24 ThreadId,24 ThreadId,
25 ProviderSendTurnInput,25 ProviderSendTurnInput,
26} from "@t3tools/contracts";26} 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";
28import { ChildProcessSpawner } from "effect/unstable/process";35import { ChildProcessSpawner } from "effect/unstable/process";
29import * as CodexErrors from "effect-codex-app-server/errors";36import * as CodexErrors from "effect-codex-app-server/errors";
30import * as EffectCodexSchema from "effect-codex-app-server/schema";37import * as EffectCodexSchema from "effect-codex-app-server/schema";
apps/server/src/provider/Layers/CodexProvider.ts+33-23
...@@ -1,4 +1,11 @@...@@ -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";
2import { ChildProcessSpawner } from "effect/unstable/process";9import { ChildProcessSpawner } from "effect/unstable/process";
3import * as CodexClient from "effect-codex-app-server/client";10import * as CodexClient from "effect-codex-app-server/client";
4import * as CodexSchema from "effect-codex-app-server/schema";11import * as CodexSchema from "effect-codex-app-server/schema";
...@@ -325,14 +332,33 @@ const emptyCodexModelsFromSettings = (codexSettings: CodexSettings): ServerProvi...@@ -325,14 +332,33 @@ const emptyCodexModelsFromSettings = (codexSettings: CodexSettings): ServerProvi
325 capabilities: null,332 capabilities: null,
326 }));333 }));
327334
328const makePendingCodexProvider = (codexSettings: CodexSettings): ServerProviderDraft => {335const makePendingCodexProvider = (
329 const checkedAt = new Date().toISOString();336 codexSettings: CodexSettings,
330 const models = emptyCodexModelsFromSettings(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) {
333 return buildServerProvider({359 return buildServerProvider({
334 presentation: CODEX_PRESENTATION,360 presentation: CODEX_PRESENTATION,
335 enabled: false,361 enabled: true,
336 checkedAt,362 checkedAt,
337 models,363 models,
338 skills: [],364 skills: [],
...@@ -341,26 +367,10 @@ const makePendingCodexProvider = (codexSettings: CodexSettings): ServerProviderD...@@ -341,26 +367,10 @@ const makePendingCodexProvider = (codexSettings: CodexSettings): ServerProviderD
341 version: null,367 version: null,
342 status: "warning",368 status: "warning",
343 auth: { status: "unknown" },369 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.",
345 },371 },
346 });372 });
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 },
362 });373 });
363};
364374
365function accountProbeStatus(account: CodexAppServerProviderSnapshot["account"]): {375function accountProbeStatus(account: CodexAppServerProviderSnapshot["account"]): {
366 readonly status: Exclude<ServerProviderState, "disabled">;376 readonly status: Exclude<ServerProviderState, "disabled">;
apps/server/src/provider/Layers/CodexSessionRuntime.test.ts+2-1
...@@ -1,6 +1,7 @@...@@ -1,6 +1,7 @@
1import assert from "node:assert/strict";1import assert from "node:assert/strict";
22
3import { Effect, Schema } from "effect";3import * as Effect from "effect/Effect";
4import * as Schema from "effect/Schema";
4import { describe, it } from "vitest";5import { describe, it } from "vitest";
5import { ThreadId } from "@t3tools/contracts";6import { ThreadId } from "@t3tools/contracts";
6import * as CodexErrors from "effect-codex-app-server/errors";7import * as CodexErrors from "effect-codex-app-server/errors";
apps/server/src/provider/Layers/CodexSessionRuntime.ts+30-14
...@@ -17,7 +17,17 @@ import {...@@ -17,7 +17,17 @@ import {
17 TurnId,17 TurnId,
18} from "@t3tools/contracts";18} from "@t3tools/contracts";
19import { normalizeModelSlug } from "@t3tools/shared/model";19import { normalizeModelSlug } from "@t3tools/shared/model";
20import { 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";
21import * as SchemaIssue from "effect/SchemaIssue";31import * as SchemaIssue from "effect/SchemaIssue";
22import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";32import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
23import * as CodexClient from "effect-codex-app-server/client";33import * as CodexClient from "effect-codex-app-server/client";
...@@ -653,11 +663,14 @@ function updateSession(...@@ -653,11 +663,14 @@ function updateSession(
653 sessionRef: Ref.Ref<ProviderSession>,663 sessionRef: Ref.Ref<ProviderSession>,
654 updates: Partial<ProviderSession>,664 updates: Partial<ProviderSession>,
655): Effect.Effect<void> {665): Effect.Effect<void> {
656 return Ref.update(sessionRef, (session) => ({666 return Effect.gen(function* () {
657 ...session,667 const updatedAt = DateTime.formatIso(yield* DateTime.now);
658 ...updates,668 yield* Ref.update(sessionRef, (session) => ({
659 updatedAt: new Date().toISOString(),669 ...session,
660 }));670 ...updates,
671 updatedAt,
672 }));
673 });
661}674}
662675
663function parseThreadSnapshot(676function parseThreadSnapshot(
...@@ -724,7 +737,9 @@ export const makeCodexSessionRuntime = (...@@ -724,7 +737,9 @@ export const makeCodexSessionRuntime = (
724 Effect.provide(clientContext),737 Effect.provide(clientContext),
725 );738 );
726 const serverNotifications = yield* Queue.unbounded<CodexServerNotification>();739 const serverNotifications = yield* Queue.unbounded<CodexServerNotification>();
740 const nowIso = Effect.map(DateTime.now, DateTime.formatIso);
727741
742 const sessionCreatedAt = yield* nowIso;
728 const initialSession = {743 const initialSession = {
729 provider: PROVIDER,744 provider: PROVIDER,
730 ...(options.providerInstanceId ? { providerInstanceId: options.providerInstanceId } : {}),745 ...(options.providerInstanceId ? { providerInstanceId: options.providerInstanceId } : {}),
...@@ -734,22 +749,23 @@ export const makeCodexSessionRuntime = (...@@ -734,22 +749,23 @@ export const makeCodexSessionRuntime = (
734 ...(options.model ? { model: options.model } : {}),749 ...(options.model ? { model: options.model } : {}),
735 threadId: options.threadId,750 threadId: options.threadId,
736 ...(options.resumeCursor !== undefined ? { resumeCursor: options.resumeCursor } : {}),751 ...(options.resumeCursor !== undefined ? { resumeCursor: options.resumeCursor } : {}),
737 createdAt: new Date().toISOString(),752 createdAt: sessionCreatedAt,
738 updatedAt: new Date().toISOString(),753 updatedAt: sessionCreatedAt,
739 } satisfies ProviderSession;754 } satisfies ProviderSession;
740 const sessionRef = yield* Ref.make<ProviderSession>(initialSession);755 const sessionRef = yield* Ref.make<ProviderSession>(initialSession);
741 const offerEvent = (event: ProviderEvent) => Queue.offer(events, event).pipe(Effect.asVoid);756 const offerEvent = (event: ProviderEvent) => Queue.offer(events, event).pipe(Effect.asVoid);
742757
743 const emitEvent = (event: Omit<ProviderEvent, "id" | "provider" | "createdAt">) =>758 const emitEvent = (event: Omit<ProviderEvent, "id" | "provider" | "createdAt">) =>
744 Effect.flatMap(Random.nextUUIDv4, (id) =>759 Effect.gen(function* () {
745 offerEvent({760 const id = yield* Random.nextUUIDv4;
761 return yield* offerEvent({
746 id: EventId.make(id),762 id: EventId.make(id),
747 provider: PROVIDER,763 provider: PROVIDER,
748 ...(options.providerInstanceId ? { providerInstanceId: options.providerInstanceId } : {}),764 ...(options.providerInstanceId ? { providerInstanceId: options.providerInstanceId } : {}),
749 createdAt: new Date().toISOString(),765 createdAt: yield* nowIso,
750 ...event,766 ...event,
751 }),767 });
752 );768 });
753 const emitSessionEvent = (method: string, message: string) =>769 const emitSessionEvent = (method: string, message: string) =>
754 emitEvent({770 emitEvent({
755 kind: "session",771 kind: "session",
...@@ -1177,7 +1193,7 @@ export const makeCodexSessionRuntime = (...@@ -1177,7 +1193,7 @@ export const makeCodexSessionRuntime = (
1177 cwd: opened.cwd,1193 cwd: opened.cwd,
1178 model: opened.model,1194 model: opened.model,
1179 resumeCursor: { threadId: providerThreadId },1195 resumeCursor: { threadId: providerThreadId },
1180 updatedAt: new Date().toISOString(),1196 updatedAt: yield* nowIso,
1181 } satisfies ProviderSession;1197 } satisfies ProviderSession;
1182 yield* Ref.set(sessionRef, session);1198 yield* Ref.set(sessionRef, session);
1183 yield* emitSessionEvent("session/ready", "Codex App Server session ready.");1199 yield* emitSessionEvent("session/ready", "Codex App Server session ready.");
apps/server/src/provider/Layers/CursorAdapter.test.ts+14-6
...@@ -1,3 +1,4 @@...@@ -1,3 +1,4 @@
1// @effect-diagnostics nodeBuiltinImport:off
1import * as path from "node:path";2import * as path from "node:path";
2import * as os from "node:os";3import * as os from "node:os";
3import { chmod, mkdtemp, readFile, writeFile } from "node:fs/promises";4import { chmod, mkdtemp, readFile, writeFile } from "node:fs/promises";
...@@ -5,7 +6,13 @@ import { fileURLToPath } from "node:url";...@@ -5,7 +6,13 @@ import { fileURLToPath } from "node:url";
56
6import * as NodeServices from "@effect/platform-node/NodeServices";7import * as NodeServices from "@effect/platform-node/NodeServices";
7import { assert, it } from "@effect/vitest";8import { 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";
9import { createModelSelection } from "@t3tools/shared/model";16import { createModelSelection } from "@t3tools/shared/model";
1017
11import {18import {
...@@ -98,7 +105,7 @@ async function waitForFileContent(filePath: string, attempts = 40) {...@@ -98,7 +105,7 @@ async function waitForFileContent(filePath: string, attempts = 40) {
98 return raw;105 return raw;
99 }106 }
100 } catch {}107 } catch {}
101 await new Promise((resolve) => setTimeout(resolve, 50));108 await Effect.runPromise(Effect.yieldNow);
102 }109 }
103 throw new Error(`Timed out waiting for file content at ${filePath}`);110 throw new Error(`Timed out waiting for file content at ${filePath}`);
104}111}
...@@ -113,10 +120,11 @@ async function waitForFileContent(filePath: string, attempts = 40) {...@@ -113,10 +120,11 @@ async function waitForFileContent(filePath: string, attempts = 40) {
113// tests assumed.120// tests assumed.
114const makeResolveCursorSettings = Effect.gen(function* () {121const makeResolveCursorSettings = Effect.gen(function* () {
115 const serverSettings = yield* ServerSettingsService;122 const serverSettings = yield* ServerSettingsService;
116 // @effect-diagnostics effect/returnEffectInGen:off123 return yield* Effect.succeed(
117 return serverSettings.getSettings.pipe(124 serverSettings.getSettings.pipe(
118 Effect.map((snapshot) => snapshot.providers.cursor),125 Effect.map((snapshot) => snapshot.providers.cursor),
119 Effect.orDie,126 Effect.orDie,
127 ),
120 );128 );
121});129});
122130
apps/server/src/provider/Layers/CursorAdapter.ts+23-21
...@@ -3,7 +3,6 @@...@@ -3,7 +3,6 @@
3 *3 *
4 * @module CursorAdapterLive4 * @module CursorAdapterLive
5 */5 */
6import * as nodePath from "node:path";
76
8import {7import {
9 ApprovalRequestId,8 ApprovalRequestId,
...@@ -22,21 +21,21 @@ import {...@@ -22,21 +21,21 @@ import {
22 type ThreadId,21 type ThreadId,
23 TurnId,22 TurnId,
24} from "@t3tools/contracts";23} from "@t3tools/contracts";
25import {24import * as DateTime from "effect/DateTime";
26 DateTime,25import * as Deferred from "effect/Deferred";
27 Deferred,26import * as Effect from "effect/Effect";
28 Effect,27import * as Exit from "effect/Exit";
29 Exit,28import * as Fiber from "effect/Fiber";
30 Fiber,29import * as FileSystem from "effect/FileSystem";
31 FileSystem,30import * as Option from "effect/Option";
32 Option,31import * as Path from "effect/Path";
33 PubSub,32import * as PubSub from "effect/PubSub";
34 Random,33import * as Random from "effect/Random";
35 Scope,34import * as Schema from "effect/Schema";
36 Semaphore,35import * as Scope from "effect/Scope";
37 Stream,36import * as Semaphore from "effect/Semaphore";
38 SynchronizedRef,37import * as Stream from "effect/Stream";
39} from "effect";38import * as SynchronizedRef from "effect/SynchronizedRef";
40import { ChildProcessSpawner } from "effect/unstable/process";39import { ChildProcessSpawner } from "effect/unstable/process";
41import type * as EffectAcpSchema from "effect-acp/schema";40import type * as EffectAcpSchema from "effect-acp/schema";
4241
...@@ -306,6 +305,7 @@ export function makeCursorAdapter(...@@ -306,6 +305,7 @@ export function makeCursorAdapter(
306 return Effect.gen(function* () {305 return Effect.gen(function* () {
307 const boundInstanceId = options?.instanceId ?? ProviderInstanceId.make("cursor");306 const boundInstanceId = options?.instanceId ?? ProviderInstanceId.make("cursor");
308 const fileSystem = yield* FileSystem.FileSystem;307 const fileSystem = yield* FileSystem.FileSystem;
308 const path = yield* Path.Path;
309 const childProcessSpawner = yield* ChildProcessSpawner.ChildProcessSpawner;309 const childProcessSpawner = yield* ChildProcessSpawner.ChildProcessSpawner;
310 const serverConfig = yield* Effect.service(ServerConfig);310 const serverConfig = yield* Effect.service(ServerConfig);
311 const nativeEventLogger =311 const nativeEventLogger =
...@@ -358,12 +358,12 @@ export function makeCursorAdapter(...@@ -358,12 +358,12 @@ export function makeCursorAdapter(
358 ) =>358 ) =>
359 Effect.gen(function* () {359 Effect.gen(function* () {
360 if (!nativeEventLogger) return;360 if (!nativeEventLogger) return;
361 const observedAt = new Date().toISOString();361 const observedAt = yield* nowIso;
362 yield* nativeEventLogger.write(362 yield* nativeEventLogger.write(
363 {363 {
364 observedAt,364 observedAt,
365 event: {365 event: {
366 id: crypto.randomUUID(),366 id: yield* Random.nextUUIDv4,
367 kind: "notification",367 kind: "notification",
368 provider: PROVIDER,368 provider: PROVIDER,
369 createdAt: observedAt,369 createdAt: observedAt,
...@@ -390,7 +390,7 @@ export function makeCursorAdapter(...@@ -390,7 +390,7 @@ export function makeCursorAdapter(
390 method: string,390 method: string,
391 ) =>391 ) =>
392 Effect.gen(function* () {392 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)}`;
394 if (ctx.lastPlanFingerprint === fingerprint) {394 if (ctx.lastPlanFingerprint === fingerprint) {
395 return;395 return;
396 }396 }
...@@ -460,7 +460,7 @@ export function makeCursorAdapter(...@@ -460,7 +460,7 @@ export function makeCursorAdapter(
460 });460 });
461 }461 }
462462
463 const cwd = nodePath.resolve(input.cwd.trim());463 const cwd = path.resolve(input.cwd.trim());
464 const cursorModelSelection =464 const cursorModelSelection =
465 input.modelSelection?.instanceId === boundInstanceId ? input.modelSelection : undefined;465 input.modelSelection?.instanceId === boundInstanceId ? input.modelSelection : undefined;
466 const existing = sessions.get(input.threadId);466 const existing = sessions.get(input.threadId);
...@@ -638,7 +638,9 @@ export function makeCursorAdapter(...@@ -638,7 +638,9 @@ export function makeCursorAdapter(
638 turnId: ctx?.activeTurnId,638 turnId: ctx?.activeTurnId,
639 requestId: runtimeRequestId,639 requestId: runtimeRequestId,
640 permissionRequest,640 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),
642 args: params,644 args: params,
643 source: "acp.jsonrpc",645 source: "acp.jsonrpc",
644 method: "session/request_permission",646 method: "session/request_permission",
apps/server/src/provider/Layers/CursorProvider.test.ts+14-4
...@@ -1,7 +1,9 @@...@@ -1,7 +1,9 @@
1import * as NodeOS from "node:os";1import * as NodeOS from "node:os";
22
3import * as NodeServices from "@effect/platform-node/NodeServices";3import * 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";
5import { describe, expect, it } from "vitest";7import { describe, expect, it } from "vitest";
6import type * as EffectAcpSchema from "effect-acp/schema";8import type * as EffectAcpSchema from "effect-acp/schema";
7import type { CursorSettings, ServerProviderModel } from "@t3tools/contracts";9import type { CursorSettings, ServerProviderModel } from "@t3tools/contracts";
...@@ -68,12 +70,16 @@ const makeMockAgentWrapper = Effect.fn("makeMockAgentWrapper")(function* (...@@ -68,12 +70,16 @@ const makeMockAgentWrapper = Effect.fn("makeMockAgentWrapper")(function* (
68 prefix: "cursor-provider-mock-",70 prefix: "cursor-provider-mock-",
69 });71 });
70 const wrapperPath = path.join(dir, "fake-agent.sh");72 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);
71 const envExports = Object.entries(extraEnv ?? {})77 const envExports = Object.entries(extraEnv ?? {})
72 .map(([key, value]) => `export ${key}=${JSON.stringify(value)}`)78 .map(([key, value]) => `export ${key}=${JSON.stringify(value)}`)
73 .join("\n");79 .join("\n");
74 const script = `#!/bin/sh80 const script = `#!/bin/sh
75${envExports}81${envExports}
76exec ${JSON.stringify("bun")} ${JSON.stringify(mockAgentPath)} "$@"82exec ${bunCommand} ${mockAgentPathJson} "$@"
77`;83`;
78 yield* fileSystem.writeFileString(wrapperPath, script);84 yield* fileSystem.writeFileString(wrapperPath, script);
79 yield* fileSystem.chmod(wrapperPath, 0o755);85 yield* fileSystem.chmod(wrapperPath, 0o755);
...@@ -89,13 +95,17 @@ const makeMockAgentWithAboutWrapper = Effect.fn("makeMockAgentWithAboutWrapper")...@@ -89,13 +95,17 @@ const makeMockAgentWithAboutWrapper = Effect.fn("makeMockAgentWithAboutWrapper")
89 prefix: "cursor-provider-about-mock-",95 prefix: "cursor-provider-about-mock-",
90 });96 });
91 const wrapperPath = path.join(dir, "fake-agent.sh");97 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);
92 const script = `#!/bin/sh102 const script = `#!/bin/sh
93if [ "$1" = "about" ]; then103if [ "$1" = "about" ]; then
94 printf 'CLI Version 2026.04.09-f2b0fcd\\n'104 printf 'CLI Version 2026.04.09-f2b0fcd\\n'
95 printf 'User Email cursor@example.com\\n'105 printf 'User Email cursor@example.com\\n'
96 exit 0106 exit 0
97fi107fi
98exec ${JSON.stringify("bun")} ${JSON.stringify(mockAgentPath)} "$@"108exec ${bunCommand} ${mockAgentPathJson} "$@"
99`;109`;
100 yield* fileSystem.writeFileString(wrapperPath, script);110 yield* fileSystem.writeFileString(wrapperPath, script);
101 yield* fileSystem.chmod(wrapperPath, 0o755);111 yield* fileSystem.chmod(wrapperPath, 0o755);
...@@ -118,7 +128,7 @@ const waitForFileContent = Effect.fn("waitForFileContent")(function* (...@@ -118,7 +128,7 @@ const waitForFileContent = Effect.fn("waitForFileContent")(function* (
118 }128 }
119 yield* Effect.sleep("50 millis");129 yield* Effect.sleep("50 millis");
120 }130 }
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}`);
122});132});
123133
124const makeProviderStatusEnvFixture = Effect.fn("makeProviderStatusEnvFixture")(function* () {134const makeProviderStatusEnvFixture = Effect.fn("makeProviderStatusEnvFixture")(function* () {
apps/server/src/provider/Layers/CursorProvider.ts+35-26
...@@ -1,5 +1,4 @@...@@ -1,5 +1,4 @@
1import * as nodeOs from "node:os";1import * as NodeOs from "node:os";
2
3import type {2import type {
4 CursorSettings,3 CursorSettings,
5 ModelCapabilities,4 ModelCapabilities,
...@@ -11,7 +10,15 @@ import type {...@@ -11,7 +10,15 @@ import type {
11} from "@t3tools/contracts";10} from "@t3tools/contracts";
12import { ProviderDriverKind } from "@t3tools/contracts";11import { ProviderDriverKind } from "@t3tools/contracts";
13import type * as EffectAcpSchema from "effect-acp/schema";12import 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";
15import { HttpClient } from "effect/unstable/http";22import { HttpClient } from "effect/unstable/http";
16import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";23import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
17import {24import {
...@@ -58,38 +65,40 @@ export const CURSOR_PARAMETERIZED_MODEL_PICKER_CAPABILITIES = {...@@ -58,38 +65,40 @@ export const CURSOR_PARAMETERIZED_MODEL_PICKER_CAPABILITIES = {
5865
59export function buildInitialCursorProviderSnapshot(66export function buildInitialCursorProviderSnapshot(
60 cursorSettings: CursorSettings,67 cursorSettings: CursorSettings,
61): ServerProviderDraft {68): Effect.Effect<ServerProviderDraft> {
62 const checkedAt = new Date().toISOString();69 return Effect.gen(function* () {
63 const models = getCursorFallbackModels(cursorSettings);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) {
66 return buildServerProvider({89 return buildServerProvider({
67 presentation: CURSOR_PRESENTATION,90 presentation: CURSOR_PRESENTATION,
68 enabled: false,91 enabled: true,
69 checkedAt,92 checkedAt,
70 models,93 models,
71 probe: {94 probe: {
72 installed: false,95 installed: true,
73 version: null,96 version: null,
74 status: "warning",97 status: "warning",
75 auth: { status: "unknown" },98 auth: { status: "unknown" },
76 message: "Cursor is disabled in T3 Code settings.",99 message: "Checking Cursor Agent availability...",
77 },100 },
78 });101 });
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 },
93 });102 });
94}103}
95104
...@@ -854,7 +863,7 @@ function isCursorAboutJsonFormatUnsupported(result: CommandResult): boolean {...@@ -854,7 +863,7 @@ function isCursorAboutJsonFormatUnsupported(result: CommandResult): boolean {
854const readCursorCliConfigChannel = Effect.fn("readCursorCliConfigChannel")(function* () {863const readCursorCliConfigChannel = Effect.fn("readCursorCliConfigChannel")(function* () {
855 const fileSystem = yield* FileSystem.FileSystem;864 const fileSystem = yield* FileSystem.FileSystem;
856 const path = yield* Path.Path;865 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");
858 const raw = yield* fileSystem.readFileString(configPath).pipe(Effect.orElseSucceed(() => ""));867 const raw = yield* fileSystem.readFileString(configPath).pipe(Effect.orElseSucceed(() => ""));
859 return parseCursorCliConfigChannel(raw);868 return parseCursorCliConfigChannel(raw);
860});869});
...@@ -1085,7 +1094,7 @@ export const checkCursorProviderStatus = Effect.fn("checkCursorProviderStatus")(...@@ -1085,7 +1094,7 @@ export const checkCursorProviderStatus = Effect.fn("checkCursorProviderStatus")(
1085 never,1094 never,
1086 ChildProcessSpawner.ChildProcessSpawner | FileSystem.FileSystem | Path.Path1095 ChildProcessSpawner.ChildProcessSpawner | FileSystem.FileSystem | Path.Path
1087> {1096> {
1088 const checkedAt = new Date().toISOString();1097 const checkedAt = DateTime.formatIso(yield* DateTime.now);
1089 const fallbackModels = getCursorFallbackModels(cursorSettings);1098 const fallbackModels = getCursorFallbackModels(cursorSettings);
10901099
1091 if (!cursorSettings.enabled) {1100 if (!cursorSettings.enabled) {
apps/server/src/provider/Layers/EventNdjsonLogger.test.ts+2-1
...@@ -1,10 +1,11 @@...@@ -1,10 +1,11 @@
1// @effect-diagnostics nodeBuiltinImport:off
1import fs from "node:fs";2import fs from "node:fs";
2import os from "node:os";3import os from "node:os";
3import path from "node:path";4import path from "node:path";
45
5import { ThreadId } from "@t3tools/contracts";6import { ThreadId } from "@t3tools/contracts";
6import { assert, describe, it } from "@effect/vitest";7import { assert, describe, it } from "@effect/vitest";
7import { Effect } from "effect";8import * as Effect from "effect/Effect";
89
9import { makeEventNdjsonLogger } from "./EventNdjsonLogger.ts";10import { makeEventNdjsonLogger } from "./EventNdjsonLogger.ts";
1011
apps/server/src/provider/Layers/EventNdjsonLogger.ts+6-1
...@@ -1,3 +1,4 @@...@@ -1,3 +1,4 @@
1// @effect-diagnostics nodeBuiltinImport:off
1/**2/**
2 * Provider event logger helper.3 * Provider event logger helper.
3 *4 *
...@@ -10,7 +11,11 @@ import path from "node:path";...@@ -10,7 +11,11 @@ import path from "node:path";
1011
11import type { ThreadId } from "@t3tools/contracts";12import type { ThreadId } from "@t3tools/contracts";
12import { RotatingFileSink } from "@t3tools/shared/logging";13import { 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
15import { toSafeThreadAttachmentSegment } from "../../attachmentStore.ts";20import { toSafeThreadAttachmentSegment } from "../../attachmentStore.ts";
1621
apps/server/src/provider/Layers/OpenCodeAdapter.test.ts+14-6
...@@ -1,8 +1,16 @@...@@ -1,8 +1,16 @@
1import assert from "node:assert/strict";1import assert from "node:assert/strict";
2
3import * as NodeServices from "@effect/platform-node/NodeServices";2import * as NodeServices from "@effect/platform-node/NodeServices";
4import { it } from "@effect/vitest";3import { 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";
6import { beforeEach } from "vitest";14import { beforeEach } from "vitest";
715
8import {16import {
...@@ -216,8 +224,8 @@ beforeEach(() => {...@@ -216,8 +224,8 @@ beforeEach(() => {
216 runtimeMock.reset();224 runtimeMock.reset();
217});225});
218226
219const sleep = (ms: number) =>227const advanceTestClock = (ms: number) =>
220 Effect.promise(() => new Promise<void>((resolve) => setTimeout(resolve, ms)));228 TestClock.adjust(`${ms} millis`).pipe(Effect.andThen(Effect.yieldNow));
221229
222it.layer(OpenCodeAdapterTestLayer)("OpenCodeAdapterLive", (it) => {230it.layer(OpenCodeAdapterTestLayer)("OpenCodeAdapterLive", (it) => {
223 it.effect("reuses a configured OpenCode server URL instead of spawning a local server", () =>231 it.effect("reuses a configured OpenCode server URL instead of spawning a local server", () =>
...@@ -648,7 +656,7 @@ it.layer(OpenCodeAdapterTestLayer)("OpenCodeAdapterLive", (it) => {...@@ -648,7 +656,7 @@ it.layer(OpenCodeAdapterTestLayer)("OpenCodeAdapterLive", (it) => {
648 threadId: asThreadId("thread-native-log"),656 threadId: asThreadId("thread-native-log"),
649 runtimeMode: "full-access",657 runtimeMode: "full-access",
650 });658 });
651 yield* sleep(10);659 yield* advanceTestClock(10);
652 return started;660 return started;
653 }).pipe(Effect.provide(adapterLayer));661 }).pipe(Effect.provide(adapterLayer));
654662
...@@ -736,7 +744,7 @@ it.layer(OpenCodeAdapterTestLayer)("OpenCodeAdapterLive", (it) => {...@@ -736,7 +744,7 @@ it.layer(OpenCodeAdapterTestLayer)("OpenCodeAdapterLive", (it) => {
736 threadId: asThreadId("thread-native-log-failure"),744 threadId: asThreadId("thread-native-log-failure"),
737 runtimeMode: "full-access",745 runtimeMode: "full-access",
738 });746 });
739 yield* sleep(10);747 yield* advanceTestClock(10);
740 return {748 return {
741 sessions: yield* adapter.listSessions(),749 sessions: yield* adapter.listSessions(),
742 closeCallsDuringRun: [...runtimeMock.state.closeCalls],750 closeCallsDuringRun: [...runtimeMock.state.closeCalls],
apps/server/src/provider/Layers/OpenCodeAdapter.ts+52-39
...@@ -12,7 +12,15 @@ import {...@@ -12,7 +12,15 @@ import {
12 TurnId,12 TurnId,
13 type UserInputQuestion,13 type UserInputQuestion,
14} from "@t3tools/contracts";14} 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";
16import type { OpencodeClient, Part, PermissionRequest, QuestionRequest } from "@opencode-ai/sdk/v2";24import type { OpencodeClient, Part, PermissionRequest, QuestionRequest } from "@opencode-ai/sdk/v2";
17import { getModelSelectionStringOptionValue } from "@t3tools/shared/model";25import { getModelSelectionStringOptionValue } from "@t3tools/shared/model";
1826
...@@ -40,6 +48,7 @@ import {...@@ -40,6 +48,7 @@ import {
40 toOpenCodeQuestionAnswers,48 toOpenCodeQuestionAnswers,
41 type OpenCodeServerConnection,49 type OpenCodeServerConnection,
42} from "../opencodeRuntime.ts";50} from "../opencodeRuntime.ts";
51import * as Option from "effect/Option";
4352
44const PROVIDER = ProviderDriverKind.make("opencode");53const PROVIDER = ProviderDriverKind.make("opencode");
4554
...@@ -95,9 +104,7 @@ export interface OpenCodeAdapterLiveOptions {...@@ -95,9 +104,7 @@ export interface OpenCodeAdapterLiveOptions {
95 readonly nativeEventLogger?: EventNdjsonLogger;104 readonly nativeEventLogger?: EventNdjsonLogger;
96}105}
97106
98function nowIso(): string {107const nowIso = Effect.map(DateTime.now, DateTime.formatIso);
99 return new Date().toISOString();
100}
101108
102/**109/**
103 * Map a tagged OpenCodeRuntimeError produced by {@link runOpenCodeSdk} into110 * Map a tagged OpenCodeRuntimeError produced by {@link runOpenCodeSdk} into
...@@ -140,12 +147,14 @@ const buildEventBase = (input: {...@@ -140,12 +147,14 @@ const buildEventBase = (input: {
140 "eventId" | "provider" | "threadId" | "createdAt" | "turnId" | "itemId" | "requestId" | "raw"147 "eventId" | "provider" | "threadId" | "createdAt" | "turnId" | "itemId" | "requestId" | "raw"
141 >148 >
142> =>149> =>
143 Random.nextUUIDv4.pipe(150 Effect.gen(function* () {
144 Effect.map((uuid) => ({151 const uuid = yield* Random.nextUUIDv4;
152 const createdAt = input.createdAt ?? (yield* nowIso);
153 return {
145 eventId: EventId.make(uuid),154 eventId: EventId.make(uuid),
146 provider: PROVIDER,155 provider: PROVIDER,
147 threadId: input.threadId,156 threadId: input.threadId,
148 createdAt: input.createdAt ?? nowIso(),157 createdAt,
149 ...(input.turnId ? { turnId: input.turnId } : {}),158 ...(input.turnId ? { turnId: input.turnId } : {}),
150 ...(input.itemId ? { itemId: RuntimeItemId.make(input.itemId) } : {}),159 ...(input.itemId ? { itemId: RuntimeItemId.make(input.itemId) } : {}),
151 ...(input.requestId ? { requestId: RuntimeRequestId.make(input.requestId) } : {}),160 ...(input.requestId ? { requestId: RuntimeRequestId.make(input.requestId) } : {}),
...@@ -157,8 +166,8 @@ const buildEventBase = (input: {...@@ -157,8 +166,8 @@ const buildEventBase = (input: {
157 },166 },
158 }167 }
159 : {}),168 : {}),
160 })),169 };
161 );170 });
162171
163function toToolLifecycleItemType(toolName: string): ToolLifecycleItemType {172function toToolLifecycleItemType(toolName: string): ToolLifecycleItemType {
164 const normalized = toolName.toLowerCase();173 const normalized = toolName.toLowerCase();
...@@ -347,12 +356,13 @@ export function appendOpenCodeAssistantTextDelta(...@@ -347,12 +356,13 @@ export function appendOpenCodeAssistantTextDelta(
347 };356 };
348}357}
349358
350function isoFromEpochMs(value: number | undefined): string | undefined {359const isoFromEpochMs = (value: number) =>
351 if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {360 DateTime.make(value).pipe(
352 return undefined;361 Option.match({
353 }362 onNone: () => undefined,
354 return new Date(value).toISOString();363 onSome: DateTime.formatIso,
355}364 }),
365 );
356366
357function messageRoleForPart(367function messageRoleForPart(
358 context: OpenCodeSessionContext,368 context: OpenCodeSessionContext,
...@@ -408,21 +418,24 @@ function updateProviderSession(...@@ -408,21 +418,24 @@ function updateProviderSession(
408 readonly clearActiveTurnId?: boolean;418 readonly clearActiveTurnId?: boolean;
409 readonly clearLastError?: boolean;419 readonly clearLastError?: boolean;
410 },420 },
411): ProviderSession {421): Effect.Effect<ProviderSession> {
412 const nextSession = {422 return Effect.gen(function* () {
413 ...context.session,423 const updatedAt = yield* nowIso;
414 ...patch,424 const nextSession = {
415 updatedAt: nowIso(),425 ...context.session,
416 } as ProviderSession & Record<string, unknown>;426 ...patch,
417 const mutableSession = nextSession as Record<string, unknown>;427 updatedAt,
418 if (options?.clearActiveTurnId) {428 } as ProviderSession & Record<string, unknown>;
419 delete mutableSession.activeTurnId;429 const mutableSession = nextSession as Record<string, unknown>;
420 }430 if (options?.clearActiveTurnId) {
421 if (options?.clearLastError) {431 delete mutableSession.activeTurnId;
422 delete mutableSession.lastError;432 }
423 }433 if (options?.clearLastError) {
424 context.session = nextSession;434 delete mutableSession.lastError;
425 return nextSession;435 }
436 context.session = nextSession;
437 return nextSession;
438 });
426}439}
427440
428const stopOpenCodeContext = Effect.fn("stopOpenCodeContext")(function* (441const stopOpenCodeContext = Effect.fn("stopOpenCodeContext")(function* (
...@@ -592,8 +605,8 @@ export function makeOpenCodeAdapter(...@@ -592,8 +605,8 @@ export function makeOpenCodeAdapter(
592 turnId,605 turnId,
593 itemId: part.id,606 itemId: part.id,
594 createdAt:607 createdAt:
595 part.type === "text" || part.type === "reasoning"608 (part.type === "text" || part.type === "reasoning") && part.time !== undefined
596 ? isoFromEpochMs(part.time?.start)609 ? isoFromEpochMs(part.time.start)
597 : undefined,610 : undefined,
598 raw,611 raw,
599 })),612 })),
...@@ -642,7 +655,7 @@ export function makeOpenCodeAdapter(...@@ -642,7 +655,7 @@ export function makeOpenCodeAdapter(
642655
643 const turnId = context.activeTurnId;656 const turnId = context.activeTurnId;
644 yield* writeNativeEventBestEffort(context.session.threadId, {657 yield* writeNativeEventBestEffort(context.session.threadId, {
645 observedAt: nowIso(),658 observedAt: yield* nowIso,
646 event: {659 event: {
647 provider: PROVIDER,660 provider: PROVIDER,
648 threadId: context.session.threadId,661 threadId: context.session.threadId,
...@@ -863,7 +876,7 @@ export function makeOpenCodeAdapter(...@@ -863,7 +876,7 @@ export function makeOpenCodeAdapter(
863876
864 case "session.status": {877 case "session.status": {
865 if (event.properties.status.type === "busy") {878 if (event.properties.status.type === "busy") {
866 updateProviderSession(context, {879 yield* updateProviderSession(context, {
867 status: "running",880 status: "running",
868 activeTurnId: turnId,881 activeTurnId: turnId,
869 });882 });
...@@ -887,7 +900,7 @@ export function makeOpenCodeAdapter(...@@ -887,7 +900,7 @@ export function makeOpenCodeAdapter(
887900
888 if (event.properties.status.type === "idle" && turnId) {901 if (event.properties.status.type === "idle" && turnId) {
889 context.activeTurnId = undefined;902 context.activeTurnId = undefined;
890 updateProviderSession(context, { status: "ready" }, { clearActiveTurnId: true });903 yield* updateProviderSession(context, { status: "ready" }, { clearActiveTurnId: true });
891 yield* emit({904 yield* emit({
892 ...(yield* buildEventBase({905 ...(yield* buildEventBase({
893 threadId: context.session.threadId,906 threadId: context.session.threadId,
...@@ -907,7 +920,7 @@ export function makeOpenCodeAdapter(...@@ -907,7 +920,7 @@ export function makeOpenCodeAdapter(
907 const message = sessionErrorMessage(event.properties.error);920 const message = sessionErrorMessage(event.properties.error);
908 const activeTurnId = context.activeTurnId;921 const activeTurnId = context.activeTurnId;
909 context.activeTurnId = undefined;922 context.activeTurnId = undefined;
910 updateProviderSession(923 yield* updateProviderSession(
911 context,924 context,
912 {925 {
913 status: "error",926 status: "error",
...@@ -1084,7 +1097,7 @@ export function makeOpenCodeAdapter(...@@ -1084,7 +1097,7 @@ export function makeOpenCodeAdapter(
1084 return raceWinner.session;1097 return raceWinner.session;
1085 }1098 }
10861099
1087 const createdAt = nowIso();1100 const createdAt = yield* nowIso;
1088 const session: ProviderSession = {1101 const session: ProviderSession = {
1089 provider: PROVIDER,1102 provider: PROVIDER,
1090 providerInstanceId: boundInstanceId,1103 providerInstanceId: boundInstanceId,
...@@ -1185,7 +1198,7 @@ export function makeOpenCodeAdapter(...@@ -1185,7 +1198,7 @@ export function makeOpenCodeAdapter(
1185 context.activeTurnId = turnId;1198 context.activeTurnId = turnId;
1186 context.activeAgent = agent ?? (input.interactionMode === "plan" ? "plan" : undefined);1199 context.activeAgent = agent ?? (input.interactionMode === "plan" ? "plan" : undefined);
1187 context.activeVariant = variant;1200 context.activeVariant = variant;
1188 updateProviderSession(1201 yield* updateProviderSession(
1189 context,1202 context,
1190 {1203 {
1191 status: "running",1204 status: "running",
...@@ -1223,7 +1236,7 @@ export function makeOpenCodeAdapter(...@@ -1223,7 +1236,7 @@ export function makeOpenCodeAdapter(
1223 context.activeTurnId = undefined;1236 context.activeTurnId = undefined;
1224 context.activeAgent = undefined;1237 context.activeAgent = undefined;
1225 context.activeVariant = undefined;1238 context.activeVariant = undefined;
1226 updateProviderSession(1239 yield* updateProviderSession(
1227 context,1240 context,
1228 {1241 {
1229 status: "ready",1242 status: "ready",
apps/server/src/provider/Layers/OpenCodeProvider.test.ts+3-1
...@@ -2,7 +2,9 @@ import assert from "node:assert/strict";...@@ -2,7 +2,9 @@ import assert from "node:assert/strict";
22
3import * as NodeServices from "@effect/platform-node/NodeServices";3import * as NodeServices from "@effect/platform-node/NodeServices";
4import { it } from "@effect/vitest";4import { 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";
6import { beforeEach } from "vitest";8import { beforeEach } from "vitest";
79
8import { OpenCodeSettings } from "@t3tools/contracts";10import { OpenCodeSettings } from "@t3tools/contracts";
apps/server/src/provider/Layers/OpenCodeProvider.ts+35-31
...@@ -4,7 +4,10 @@ import {...@@ -4,7 +4,10 @@ import {
4 type OpenCodeSettings,4 type OpenCodeSettings,
5 type ServerProviderModel,5 type ServerProviderModel,
6} from "@t3tools/contracts";6} 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
9import { createModelCapabilities } from "@t3tools/shared/model";12import { createModelCapabilities } from "@t3tools/shared/model";
10import {13import {
...@@ -249,19 +252,38 @@ function flattenOpenCodeModels(input: OpenCodeInventory): ReadonlyArray<ServerPr...@@ -249,19 +252,38 @@ function flattenOpenCodeModels(input: OpenCodeInventory): ReadonlyArray<ServerPr
249252
250export const makePendingOpenCodeProvider = (253export const makePendingOpenCodeProvider = (
251 openCodeSettings: OpenCodeSettings,254 openCodeSettings: OpenCodeSettings,
252): ServerProviderDraft => {255): Effect.Effect<ServerProviderDraft> =>
253 const checkedAt = new Date().toISOString();256 Effect.gen(function* () {
254 const models = providerModelsFromSettings(257 const checkedAt = yield* Effect.map(DateTime.now, DateTime.formatIso);
255 [],258 const models = providerModelsFromSettings(
256 PROVIDER,259 [],
257 openCodeSettings.customModels,260 PROVIDER,
258 DEFAULT_OPENCODE_MODEL_CAPABILITIES,261 openCodeSettings.customModels,
259 );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) {
262 return buildServerProvider({284 return buildServerProvider({
263 presentation: OPENCODE_PRESENTATION,285 presentation: OPENCODE_PRESENTATION,
264 enabled: false,286 enabled: true,
265 checkedAt,287 checkedAt,
266 models,288 models,
267 probe: {289 probe: {
...@@ -269,28 +291,10 @@ export const makePendingOpenCodeProvider = (...@@ -269,28 +291,10 @@ export const makePendingOpenCodeProvider = (
269 version: null,291 version: null,
270 status: "warning",292 status: "warning",
271 auth: { status: "unknown" },293 auth: { status: "unknown" },
272 message:294 message: "OpenCode provider status has not been checked in this session yet.",
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.",
276 },295 },
277 });296 });
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 },
292 });297 });
293};
294298
295export const checkOpenCodeProviderStatus = Effect.fn("checkOpenCodeProviderStatus")(function* (299export const checkOpenCodeProviderStatus = Effect.fn("checkOpenCodeProviderStatus")(function* (
296 openCodeSettings: OpenCodeSettings,300 openCodeSettings: OpenCodeSettings,
...@@ -298,7 +302,7 @@ export const checkOpenCodeProviderStatus = Effect.fn("checkOpenCodeProviderStatu...@@ -298,7 +302,7 @@ export const checkOpenCodeProviderStatus = Effect.fn("checkOpenCodeProviderStatu
298 environment: NodeJS.ProcessEnv = process.env,302 environment: NodeJS.ProcessEnv = process.env,
299): Effect.fn.Return<ServerProviderDraft, never, OpenCodeRuntime> {303): Effect.fn.Return<ServerProviderDraft, never, OpenCodeRuntime> {
300 const openCodeRuntime = yield* OpenCodeRuntime;304 const openCodeRuntime = yield* OpenCodeRuntime;
301 const checkedAt = new Date().toISOString();305 const checkedAt = DateTime.formatIso(yield* DateTime.now);
302 const customModels = openCodeSettings.customModels;306 const customModels = openCodeSettings.customModels;
303 const isExternalServer = openCodeSettings.serverUrl.trim().length > 0;307 const isExternalServer = openCodeSettings.serverUrl.trim().length > 0;
304308
apps/server/src/provider/Layers/ProviderAdapterRegistry.test.ts+4-1
...@@ -5,7 +5,10 @@ import {...@@ -5,7 +5,10 @@ import {
5} from "@t3tools/contracts";5} from "@t3tools/contracts";
6import { it, assert, vi } from "@effect/vitest";6import { 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
10import type { ClaudeAdapterShape } from "../Services/ClaudeAdapter.ts";13import type { ClaudeAdapterShape } from "../Services/ClaudeAdapter.ts";
11import type { CodexAdapterShape } from "../Services/CodexAdapter.ts";14import type { CodexAdapterShape } from "../Services/CodexAdapter.ts";
apps/server/src/provider/Layers/ProviderAdapterRegistry.ts+2-1
...@@ -20,7 +20,8 @@ import {...@@ -20,7 +20,8 @@ import {
20 ProviderInstanceId,20 ProviderInstanceId,
21 type ProviderDriverKind,21 type ProviderDriverKind,
22} from "@t3tools/contracts";22} from "@t3tools/contracts";
23import { Effect, Layer } from "effect";23import * as Effect from "effect/Effect";
24import * as Layer from "effect/Layer";
2425
25import { ProviderUnsupportedError } from "../Errors.ts";26import { ProviderUnsupportedError } from "../Errors.ts";
26import { ProviderInstanceRegistry } from "../Services/ProviderInstanceRegistry.ts";27import { ProviderInstanceRegistry } from "../Services/ProviderInstanceRegistry.ts";
apps/server/src/provider/Layers/ProviderEventLoggers.ts+3-1
...@@ -27,7 +27,9 @@...@@ -27,7 +27,9 @@
27 *27 *
28 * @module provider/Layers/ProviderEventLoggers28 * @module provider/Layers/ProviderEventLoggers
29 */29 */
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
32import { ServerConfig } from "../../config.ts";34import { ServerConfig } from "../../config.ts";
33import { type EventNdjsonLogger, makeEventNdjsonLogger } from "./EventNdjsonLogger.ts";35import { type EventNdjsonLogger, makeEventNdjsonLogger } from "./EventNdjsonLogger.ts";
apps/server/src/provider/Layers/ProviderInstanceRegistryHydration.ts+3-1
...@@ -47,7 +47,9 @@ import {...@@ -47,7 +47,9 @@ import {
47 type ProviderInstanceConfigMap,47 type ProviderInstanceConfigMap,
48 ServerSettings,48 ServerSettings,
49} from "@t3tools/contracts";49} 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
52import { ServerSettingsService } from "../../serverSettings.ts";54import { ServerSettingsService } from "../../serverSettings.ts";
53import { BUILT_IN_DRIVERS, type BuiltInDriversEnv } from "../builtInDrivers.ts";55import { BUILT_IN_DRIVERS, type BuiltInDriversEnv } from "../builtInDrivers.ts";
apps/server/src/provider/Layers/ProviderInstanceRegistryLive.test.ts+2-1
...@@ -33,7 +33,8 @@ import {...@@ -33,7 +33,8 @@ import {
33 type ProviderInstanceConfigMap,33 type ProviderInstanceConfigMap,
34 ProviderInstanceId,34 ProviderInstanceId,
35} from "@t3tools/contracts";35} from "@t3tools/contracts";
36import { Effect, Layer } from "effect";36import * as Effect from "effect/Effect";
37import * as Layer from "effect/Layer";
37import { HttpClient, HttpClientResponse } from "effect/unstable/http";38import { HttpClient, HttpClientResponse } from "effect/unstable/http";
3839
39import { ServerConfig } from "../../config.ts";40import { ServerConfig } from "../../config.ts";
apps/server/src/provider/Layers/ProviderInstanceRegistryLive.ts+13-4
...@@ -40,7 +40,16 @@ import {...@@ -40,7 +40,16 @@ import {
40 type ProviderDriverKind,40 type ProviderDriverKind,
41 type ServerProvider,41 type ServerProvider,
42} from "@t3tools/contracts";42} 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
45import { buildUnavailableProviderSnapshot } from "../unavailableProviderSnapshot.ts";54import { buildUnavailableProviderSnapshot } from "../unavailableProviderSnapshot.ts";
46import {55import {
...@@ -115,7 +124,7 @@ const buildEntry = <R>(input: {...@@ -115,7 +124,7 @@ const buildEntry = <R>(input: {
115 if (!driver) {124 if (!driver) {
116 return {125 return {
117 kind: "unavailable" as const,126 kind: "unavailable" as const,
118 snapshot: buildUnavailableProviderSnapshot({127 snapshot: yield* buildUnavailableProviderSnapshot({
119 driverKind: entry.driver,128 driverKind: entry.driver,
120 instanceId,129 instanceId,
121 displayName: entry.displayName,130 displayName: entry.displayName,
...@@ -137,7 +146,7 @@ const buildEntry = <R>(input: {...@@ -137,7 +146,7 @@ const buildEntry = <R>(input: {
137 });146 });
138 return {147 return {
139 kind: "unavailable" as const,148 kind: "unavailable" as const,
140 snapshot: buildUnavailableProviderSnapshot({149 snapshot: yield* buildUnavailableProviderSnapshot({
141 driverKind: entry.driver,150 driverKind: entry.driver,
142 instanceId,151 instanceId,
143 displayName: entry.displayName,152 displayName: entry.displayName,
...@@ -175,7 +184,7 @@ const buildEntry = <R>(input: {...@@ -175,7 +184,7 @@ const buildEntry = <R>(input: {
175 yield* Scope.close(childScope, Exit.void).pipe(Effect.ignore);184 yield* Scope.close(childScope, Exit.void).pipe(Effect.ignore);
176 return {185 return {
177 kind: "unavailable" as const,186 kind: "unavailable" as const,
178 snapshot: buildUnavailableProviderSnapshot({187 snapshot: yield* buildUnavailableProviderSnapshot({
179 driverKind: entry.driver,188 driverKind: entry.driver,
180 instanceId,189 instanceId,
181 displayName: entry.displayName,190 displayName: entry.displayName,
apps/server/src/provider/Layers/ProviderRegistry.test.ts+23-26
...@@ -1,6 +1,15 @@...@@ -1,6 +1,15 @@
1import * as NodeServices from "@effect/platform-node/NodeServices";1import * as NodeServices from "@effect/platform-node/NodeServices";
2import { describe, it, assert, live } from "@effect/vitest";2import { describe, it, assert } from "@effect/vitest";
3import { Effect, Exit, Layer, PubSub, Ref, Schema, Scope, Sink, Stream } from "effect";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";
4import * as CodexErrors from "effect-codex-app-server/errors";13import * as CodexErrors from "effect-codex-app-server/errors";
5import {14import {
6 ClaudeSettings,15 ClaudeSettings,
...@@ -696,7 +705,8 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T...@@ -696,7 +705,8 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T
696 attempt < 50 && cachedProvider?.checkedAt !== refreshedProvider.checkedAt;705 attempt < 50 && cachedProvider?.checkedAt !== refreshedProvider.checkedAt;
697 attempt += 1706 attempt += 1
698 ) {707 ) {
699 yield* Effect.sleep("10 millis");708 yield* TestClock.adjust("10 millis");
709 yield* Effect.yieldNow;
700 cachedProvider = yield* readProviderStatusCache(filePath);710 cachedProvider = yield* readProviderStatusCache(filePath);
701 }711 }
702712
...@@ -840,8 +850,7 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T...@@ -840,8 +850,7 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T
840 const changes = yield* PubSub.unbounded<void>();850 const changes = yield* PubSub.unbounded<void>();
841 const instancesRef = yield* Ref.make<ReadonlyArray<ProviderInstance>>([codexInstance]);851 const instancesRef = yield* Ref.make<ReadonlyArray<ProviderInstance>>([codexInstance]);
842 const failNextList = yield* Ref.make(false);852 const failNextList = yield* Ref.make(false);
843 const wait = (millis: number) =>853 const wait = () => Effect.yieldNow;
844 Effect.promise<void>(() => new Promise((resolve) => setTimeout(resolve, millis)));
845 const instanceRegistryLayer = Layer.succeed(ProviderInstanceRegistry, {854 const instanceRegistryLayer = Layer.succeed(ProviderInstanceRegistry, {
846 getInstance: (instanceId) =>855 getInstance: (instanceId) =>
847 Ref.get(instancesRef).pipe(856 Ref.get(instancesRef).pipe(
...@@ -892,7 +901,7 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T...@@ -892,7 +901,7 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T
892 !providers.some((provider) => provider.instanceId === claudeInstanceId);901 !providers.some((provider) => provider.instanceId === claudeInstanceId);
893 attempt += 1902 attempt += 1
894 ) {903 ) {
895 yield* wait(10);904 yield* wait();
896 providers = yield* registry.getProviders;905 providers = yield* registry.getProviders;
897 }906 }
898907
...@@ -917,7 +926,7 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T...@@ -917,7 +926,7 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T
917 // assertions below fail.926 // assertions below fail.
918 it.effect("propagates real Codex probe failures to the aggregator at boot", () =>927 it.effect("propagates real Codex probe failures to the aggregator at boot", () =>
919 Effect.gen(function* () {928 Effect.gen(function* () {
920 const missingBinary = `t3code_codex_missing_${process.pid}_${Date.now()}`;929 const missingBinary = `t3code_codex_missing_`;
921 const serverSettings = yield* makeMutableServerSettingsService(930 const serverSettings = yield* makeMutableServerSettingsService(
922 Schema.decodeSync(ServerSettings)(931 Schema.decodeSync(ServerSettings)(
923 deepMerge(DEFAULT_SERVER_SETTINGS, {932 deepMerge(DEFAULT_SERVER_SETTINGS, {
...@@ -1013,20 +1022,10 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T...@@ -1013,20 +1022,10 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T
1013 // rebuilt instance's refresh (previous bug mode), the aggregator1022 // rebuilt instance's refresh (previous bug mode), the aggregator
1014 // keeps the old snapshot and this test fails.1023 // keeps the old snapshot and this test fails.
1015 //1024 //
1016 // `live` (imported from `@effect/vitest`) is used instead of1025 it.effect("re-probes when settings change the codex binaryPath", () =>
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", () =>
1027 Effect.gen(function* () {1026 Effect.gen(function* () {
1028 const firstMissing = `t3code_codex_first_${process.pid}_${Date.now()}`;1027 const firstMissing = `t3code_codex_first_`;
1029 const secondMissing = `t3code_codex_second_${process.pid}_${Date.now()}`;1028 const secondMissing = `t3code_codex_second_`;
1030 const serverSettings = yield* makeMutableServerSettingsService(1029 const serverSettings = yield* makeMutableServerSettingsService(
1031 Schema.decodeSync(ServerSettings)(1030 Schema.decodeSync(ServerSettings)(
1032 deepMerge(DEFAULT_SERVER_SETTINGS, {1031 deepMerge(DEFAULT_SERVER_SETTINGS, {
...@@ -1093,11 +1092,8 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T...@@ -1093,11 +1092,8 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T
1093 },1092 },
1094 });1093 });
10951094
1096 // Poll with real timers (via `it.live`) until `checkedAt`1095 // Poll with TestClock until `checkedAt` advances or we hit a
1097 // advances or we hit a generous 3-second ceiling. Anything1096 // generous virtual 3-second ceiling.
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.
1101 const refreshed = yield* Effect.gen(function* () {1097 const refreshed = yield* Effect.gen(function* () {
1102 for (let attempts = 0; attempts < 60; attempts += 1) {1098 for (let attempts = 0; attempts < 60; attempts += 1) {
1103 const providers = yield* registry.getProviders;1099 const providers = yield* registry.getProviders;
...@@ -1105,7 +1101,8 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T...@@ -1105,7 +1101,8 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsService.layerTest(), T
1105 if (codex !== undefined && codex.checkedAt !== initialCheckedAt) {1101 if (codex !== undefined && codex.checkedAt !== initialCheckedAt) {
1106 return providers;1102 return providers;
1107 }1103 }
1108 yield* Effect.sleep("50 millis");1104 yield* TestClock.adjust("50 millis");
1105 yield* Effect.yieldNow;
1109 }1106 }
1110 return yield* registry.getProviders;1107 return yield* registry.getProviders;
1111 });1108 });
apps/server/src/provider/Layers/ProviderRegistry.ts+9-1
...@@ -29,7 +29,15 @@ import {...@@ -29,7 +29,15 @@ import {
29 type ServerProvider,29 type ServerProvider,
30 type ServerProviderUpdateState,30 type ServerProviderUpdateState,
31} from "@t3tools/contracts";31} 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";
33import * as Semaphore from "effect/Semaphore";41import * as Semaphore from "effect/Semaphore";
3442
35import { ServerConfig } from "../../config.ts";43import { ServerConfig } from "../../config.ts";
apps/server/src/provider/Layers/ProviderService.test.ts+31-20
...@@ -1,3 +1,4 @@...@@ -1,3 +1,4 @@
1// @effect-diagnostics nodeBuiltinImport:off
1import fs from "node:fs";2import fs from "node:fs";
2import os from "node:os";3import os from "node:os";
3import path from "node:path";4import path from "node:path";
...@@ -21,7 +22,17 @@ import {...@@ -21,7 +22,17 @@ import {
21import { createModelSelection } from "@t3tools/shared/model";22import { createModelSelection } from "@t3tools/shared/model";
22import { it, assert, vi } from "@effect/vitest";23import { 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";
25import * as SqlClient from "effect/unstable/sql/SqlClient";36import * as SqlClient from "effect/unstable/sql/SqlClient";
2637
27import {38import {
...@@ -83,7 +94,7 @@ function makeFakeCodexAdapter(provider: ProviderDriverKind = CODEX_DRIVER) {...@@ -83,7 +94,7 @@ function makeFakeCodexAdapter(provider: ProviderDriverKind = CODEX_DRIVER) {
8394
84 const startSession = vi.fn((input: ProviderSessionStartInput) =>95 const startSession = vi.fn((input: ProviderSessionStartInput) =>
85 Effect.sync(() => {96 Effect.sync(() => {
86 const now = new Date().toISOString();97 const now = "2026-01-01T00:00:00.000Z";
87 const session: ProviderSession = {98 const session: ProviderSession = {
88 provider,99 provider,
89 ...(input.providerInstanceId !== undefined100 ...(input.providerInstanceId !== undefined
...@@ -246,8 +257,8 @@ function makeFakeCodexAdapter(provider: ProviderDriverKind = CODEX_DRIVER) {...@@ -246,8 +257,8 @@ function makeFakeCodexAdapter(provider: ProviderDriverKind = CODEX_DRIVER) {
246 };257 };
247}258}
248259
249const sleep = (ms: number) =>260const advanceTestClock = (ms: number) =>
250 Effect.promise(() => new Promise<void>((resolve) => setTimeout(resolve, ms)));261 TestClock.adjust(`${ms} millis`).pipe(Effect.andThen(Effect.yieldNow));
251262
252const hasMetricSnapshot = (263const hasMetricSnapshot = (
253 snapshots: ReadonlyArray<Metric.Metric.Snapshot>,264 snapshots: ReadonlyArray<Metric.Metric.Snapshot>,
...@@ -572,18 +583,18 @@ it.effect("ProviderServiceLive writes canonical events to the emitting thread se...@@ -572,18 +583,18 @@ it.effect("ProviderServiceLive writes canonical events to the emitting thread se
572583
573 yield* Effect.gen(function* () {584 yield* Effect.gen(function* () {
574 yield* ProviderService;585 yield* ProviderService;
575 yield* sleep(10);586 yield* advanceTestClock(10);
576 codex.emit({587 codex.emit({
577 eventId: asEventId("evt-canonical-thread-segment"),588 eventId: asEventId("evt-canonical-thread-segment"),
578 provider: ProviderDriverKind.make("codex"),589 provider: ProviderDriverKind.make("codex"),
579 threadId: asThreadId("thread-canonical-thread-segment"),590 threadId: asThreadId("thread-canonical-thread-segment"),
580 createdAt: new Date().toISOString(),591 createdAt: "2026-01-01T00:00:00.000Z",
581 type: "turn.completed",592 type: "turn.completed",
582 payload: {593 payload: {
583 state: "completed",594 state: "completed",
584 },595 },
585 });596 });
586 yield* sleep(20);597 yield* advanceTestClock(20);
587 }).pipe(Effect.provide(providerLayer));598 }).pipe(Effect.provide(providerLayer));
588599
589 assert.equal(canonicalEvents.length, 1);600 assert.equal(canonicalEvents.length, 1);
...@@ -704,7 +715,7 @@ it.effect(...@@ -704,7 +715,7 @@ it.effect(
704 ...existing,715 ...existing,
705 status: "ready",716 status: "ready",
706 resumeCursor: updatedResumeCursor,717 resumeCursor: updatedResumeCursor,
707 updatedAt: new Date(Date.now() + 1_000).toISOString(),718 updatedAt: "2026-01-01T00:00:01.000Z",
708 }));719 }));
709 return session;720 return session;
710 }).pipe(Effect.provide(firstProviderLayer));721 }).pipe(Effect.provide(firstProviderLayer));
...@@ -1403,20 +1414,20 @@ fanout.layer("ProviderServiceLive fanout", (it) => {...@@ -1403,20 +1414,20 @@ fanout.layer("ProviderServiceLive fanout", (it) => {
1403 const consumer = yield* Stream.runForEach(provider.streamEvents, (event) =>1414 const consumer = yield* Stream.runForEach(provider.streamEvents, (event) =>
1404 Ref.update(eventsRef, (current) => [...current, event]),1415 Ref.update(eventsRef, (current) => [...current, event]),
1405 ).pipe(Effect.forkChild);1416 ).pipe(Effect.forkChild);
1406 yield* sleep(50);1417 yield* advanceTestClock(50);
14071418
1408 const completedEvent: LegacyProviderRuntimeEvent = {1419 const completedEvent: LegacyProviderRuntimeEvent = {
1409 type: "turn.completed",1420 type: "turn.completed",
1410 eventId: asEventId("evt-1"),1421 eventId: asEventId("evt-1"),
1411 provider: ProviderDriverKind.make("codex"),1422 provider: ProviderDriverKind.make("codex"),
1412 createdAt: new Date().toISOString(),1423 createdAt: "2026-01-01T00:00:00.000Z",
1413 threadId: session.threadId,1424 threadId: session.threadId,
1414 turnId: asTurnId("turn-1"),1425 turnId: asTurnId("turn-1"),
1415 status: "completed",1426 status: "completed",
1416 };1427 };
14171428
1418 fanout.codex.emit(completedEvent);1429 fanout.codex.emit(completedEvent);
1419 yield* sleep(50);1430 yield* advanceTestClock(50);
14201431
1421 const events = yield* Ref.get(eventsRef);1432 const events = yield* Ref.get(eventsRef);
1422 yield* Fiber.interrupt(consumer);1433 yield* Fiber.interrupt(consumer);
...@@ -1450,13 +1461,13 @@ fanout.layer("ProviderServiceLive fanout", (it) => {...@@ -1450,13 +1461,13 @@ fanout.layer("ProviderServiceLive fanout", (it) => {
1450 Stream.runForEach((event) => Ref.update(receivedRef, (current) => [...current, event])),1461 Stream.runForEach((event) => Ref.update(receivedRef, (current) => [...current, event])),
1451 Effect.forkChild,1462 Effect.forkChild,
1452 );1463 );
1453 yield* sleep(50);1464 yield* advanceTestClock(50);
14541465
1455 fanout.codex.emit({1466 fanout.codex.emit({
1456 type: "tool.started",1467 type: "tool.started",
1457 eventId: asEventId("evt-seq-1"),1468 eventId: asEventId("evt-seq-1"),
1458 provider: ProviderDriverKind.make("codex"),1469 provider: ProviderDriverKind.make("codex"),
1459 createdAt: new Date().toISOString(),1470 createdAt: "2026-01-01T00:00:00.000Z",
1460 threadId: session.threadId,1471 threadId: session.threadId,
1461 turnId: asTurnId("turn-1"),1472 turnId: asTurnId("turn-1"),
1462 toolKind: "command",1473 toolKind: "command",
...@@ -1466,7 +1477,7 @@ fanout.layer("ProviderServiceLive fanout", (it) => {...@@ -1466,7 +1477,7 @@ fanout.layer("ProviderServiceLive fanout", (it) => {
1466 type: "tool.completed",1477 type: "tool.completed",
1467 eventId: asEventId("evt-seq-2"),1478 eventId: asEventId("evt-seq-2"),
1468 provider: ProviderDriverKind.make("codex"),1479 provider: ProviderDriverKind.make("codex"),
1469 createdAt: new Date().toISOString(),1480 createdAt: "2026-01-01T00:00:00.000Z",
1470 threadId: session.threadId,1481 threadId: session.threadId,
1471 turnId: asTurnId("turn-1"),1482 turnId: asTurnId("turn-1"),
1472 toolKind: "command",1483 toolKind: "command",
...@@ -1476,7 +1487,7 @@ fanout.layer("ProviderServiceLive fanout", (it) => {...@@ -1476,7 +1487,7 @@ fanout.layer("ProviderServiceLive fanout", (it) => {
1476 type: "turn.completed",1487 type: "turn.completed",
1477 eventId: asEventId("evt-seq-3"),1488 eventId: asEventId("evt-seq-3"),
1478 provider: ProviderDriverKind.make("codex"),1489 provider: ProviderDriverKind.make("codex"),
1479 createdAt: new Date().toISOString(),1490 createdAt: "2026-01-01T00:00:00.000Z",
1480 threadId: session.threadId,1491 threadId: session.threadId,
1481 turnId: asTurnId("turn-1"),1492 turnId: asTurnId("turn-1"),
1482 status: "completed",1493 status: "completed",
...@@ -1515,14 +1526,14 @@ fanout.layer("ProviderServiceLive fanout", (it) => {...@@ -1515,14 +1526,14 @@ fanout.layer("ProviderServiceLive fanout", (it) => {
1515 Stream.runForEach(() => Effect.fail("listener crash")),1526 Stream.runForEach(() => Effect.fail("listener crash")),
1516 Effect.forkChild,1527 Effect.forkChild,
1517 );1528 );
1518 yield* sleep(50);1529 yield* advanceTestClock(50);
15191530
1520 const events: ReadonlyArray<LegacyProviderRuntimeEvent> = [1531 const events: ReadonlyArray<LegacyProviderRuntimeEvent> = [
1521 {1532 {
1522 type: "tool.completed",1533 type: "tool.completed",
1523 eventId: asEventId("evt-ordered-1"),1534 eventId: asEventId("evt-ordered-1"),
1524 provider: ProviderDriverKind.make("codex"),1535 provider: ProviderDriverKind.make("codex"),
1525 createdAt: new Date().toISOString(),1536 createdAt: "2026-01-01T00:00:00.000Z",
1526 threadId: session.threadId,1537 threadId: session.threadId,
1527 turnId: asTurnId("turn-1"),1538 turnId: asTurnId("turn-1"),
1528 toolKind: "command",1539 toolKind: "command",
...@@ -1533,7 +1544,7 @@ fanout.layer("ProviderServiceLive fanout", (it) => {...@@ -1533,7 +1544,7 @@ fanout.layer("ProviderServiceLive fanout", (it) => {
1533 type: "message.delta",1544 type: "message.delta",
1534 eventId: asEventId("evt-ordered-2"),1545 eventId: asEventId("evt-ordered-2"),
1535 provider: ProviderDriverKind.make("codex"),1546 provider: ProviderDriverKind.make("codex"),
1536 createdAt: new Date().toISOString(),1547 createdAt: "2026-01-01T00:00:00.000Z",
1537 threadId: session.threadId,1548 threadId: session.threadId,
1538 turnId: asTurnId("turn-1"),1549 turnId: asTurnId("turn-1"),
1539 delta: "hello",1550 delta: "hello",
...@@ -1542,7 +1553,7 @@ fanout.layer("ProviderServiceLive fanout", (it) => {...@@ -1542,7 +1553,7 @@ fanout.layer("ProviderServiceLive fanout", (it) => {
1542 type: "turn.completed",1553 type: "turn.completed",
1543 eventId: asEventId("evt-ordered-3"),1554 eventId: asEventId("evt-ordered-3"),
1544 provider: ProviderDriverKind.make("codex"),1555 provider: ProviderDriverKind.make("codex"),
1545 createdAt: new Date().toISOString(),1556 createdAt: "2026-01-01T00:00:00.000Z",
1546 threadId: session.threadId,1557 threadId: session.threadId,
1547 turnId: asTurnId("turn-1"),1558 turnId: asTurnId("turn-1"),
1548 status: "completed",1559 status: "completed",
...@@ -1758,7 +1769,7 @@ validation.layer("ProviderServiceLive validation", (it) => {...@@ -1758,7 +1769,7 @@ validation.layer("ProviderServiceLive validation", (it) => {
17581769
1759 validation.codex.startSession.mockImplementationOnce((input: ProviderSessionStartInput) =>1770 validation.codex.startSession.mockImplementationOnce((input: ProviderSessionStartInput) =>
1760 Effect.sync(() => {1771 Effect.sync(() => {
1761 const now = new Date().toISOString();1772 const now = "2026-01-01T00:00:00.000Z";
1762 return {1773 return {
1763 provider: ProviderDriverKind.make("codex"),1774 provider: ProviderDriverKind.make("codex"),
1764 status: "ready",1775 status: "ready",
apps/server/src/provider/Layers/ProviderService.ts+37-20
...@@ -24,7 +24,16 @@ import {...@@ -24,7 +24,16 @@ import {
24 type ProviderRuntimeEvent,24 type ProviderRuntimeEvent,
25 type ProviderSession,25 type ProviderSession,
26} from "@t3tools/contracts";26} 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
29import {38import {
30 increment,39 increment,
...@@ -199,6 +208,7 @@ const makeProviderService = Effect.fn("makeProviderService")(function* (...@@ -199,6 +208,7 @@ const makeProviderService = Effect.fn("makeProviderService")(function* (
199 const registry = yield* ProviderAdapterRegistry;208 const registry = yield* ProviderAdapterRegistry;
200 const directory = yield* ProviderSessionDirectory;209 const directory = yield* ProviderSessionDirectory;
201 const runtimeEventPubSub = yield* PubSub.unbounded<ProviderRuntimeEvent>();210 const runtimeEventPubSub = yield* PubSub.unbounded<ProviderRuntimeEvent>();
211 const nowIso = Effect.map(DateTime.now, DateTime.formatIso);
202212
203 const publishRuntimeEvent = (event: ProviderRuntimeEvent): Effect.Effect<void> =>213 const publishRuntimeEvent = (event: ProviderRuntimeEvent): Effect.Effect<void> =>
204 Effect.succeed(event).pipe(214 Effect.succeed(event).pipe(
...@@ -655,7 +665,7 @@ const makeProviderService = Effect.fn("makeProviderService")(function* (...@@ -655,7 +665,7 @@ const makeProviderService = Effect.fn("makeProviderService")(function* (
655 ...(input.modelSelection !== undefined ? { modelSelection: input.modelSelection } : {}),665 ...(input.modelSelection !== undefined ? { modelSelection: input.modelSelection } : {}),
656 activeTurnId: turn.turnId,666 activeTurnId: turn.turnId,
657 lastRuntimeEvent: "provider.sendTurn",667 lastRuntimeEvent: "provider.sendTurn",
658 lastRuntimeEventAt: new Date().toISOString(),668 lastRuntimeEventAt: yield* nowIso,
659 },669 },
660 });670 });
661 yield* analytics.record("provider.turn.sent", {671 yield* analytics.record("provider.turn.sent", {
...@@ -977,27 +987,34 @@ const makeProviderService = Effect.fn("makeProviderService")(function* (...@@ -977,27 +987,34 @@ const makeProviderService = Effect.fn("makeProviderService")(function* (
977 ),987 ),
978 ).pipe(Effect.map((sessionsByAdapter) => sessionsByAdapter.flatMap((sessions) => sessions)));988 ).pipe(Effect.map((sessionsByAdapter) => sessionsByAdapter.flatMap((sessions) => sessions)));
979 yield* Effect.forEach(activeSessions, (session) =>989 yield* Effect.forEach(activeSessions, (session) =>
980 upsertSessionBinding(session, session.threadId, {990 Effect.flatMap(nowIso, (lastRuntimeEventAt) =>
981 lastRuntimeEvent: "provider.stopAll",991 upsertSessionBinding(session, session.threadId, {
982 lastRuntimeEventAt: new Date().toISOString(),992 lastRuntimeEvent: "provider.stopAll",
983 }),993 lastRuntimeEventAt,
994 }),
995 ),
984 ).pipe(Effect.asVoid);996 ).pipe(Effect.asVoid);
985 yield* Effect.forEach(currentAdapters, ([, adapter]) => adapter.stopAll()).pipe(Effect.asVoid);997 yield* Effect.forEach(currentAdapters, ([, adapter]) => adapter.stopAll()).pipe(Effect.asVoid);
986 const bindings = yield* directory.listBindings().pipe(Effect.orElseSucceed(() => []));998 const bindings = yield* directory.listBindings().pipe(Effect.orElseSucceed(() => []));
987 yield* Effect.forEach(bindings, (binding) => {999 yield* Effect.forEach(bindings, (binding) =>
988 const providerInstanceId = dieOnMissingBindingInstanceId("ProviderService.stopAll", binding);1000 Effect.gen(function* () {
989 return directory.upsert({1001 const providerInstanceId = dieOnMissingBindingInstanceId(
990 threadId: binding.threadId,1002 "ProviderService.stopAll",
991 provider: binding.provider,1003 binding,
992 providerInstanceId,1004 );
993 status: "stopped",1005 return yield* directory.upsert({
994 runtimePayload: {1006 threadId: binding.threadId,
995 activeTurnId: null,1007 provider: binding.provider,
996 lastRuntimeEvent: "provider.stopAll",1008 providerInstanceId,
997 lastRuntimeEventAt: new Date().toISOString(),1009 status: "stopped",
998 },1010 runtimePayload: {
999 });1011 activeTurnId: null,
1000 }).pipe(Effect.asVoid);1012 lastRuntimeEvent: "provider.stopAll",
1013 lastRuntimeEventAt: yield* nowIso,
1014 },
1015 });
1016 }),
1017 ).pipe(Effect.asVoid);
1001 yield* analytics.record("provider.sessions.stopped_all", {1018 yield* analytics.record("provider.sessions.stopped_all", {
1002 sessionCount: threadIds.length,1019 sessionCount: threadIds.length,
1003 });1020 });
apps/server/src/provider/Layers/ProviderSessionDirectory.test.ts+5-2
...@@ -1,3 +1,4 @@...@@ -1,3 +1,4 @@
1// @effect-diagnostics nodeBuiltinImport:off
1import fs from "node:fs";2import fs from "node:fs";
2import os from "node:os";3import os from "node:os";
3import path from "node:path";4import path from "node:path";
...@@ -6,7 +7,9 @@ import * as NodeServices from "@effect/platform-node/NodeServices";...@@ -6,7 +7,9 @@ import * as NodeServices from "@effect/platform-node/NodeServices";
6import { ProviderDriverKind, ThreadId } from "@t3tools/contracts";7import { ProviderDriverKind, ThreadId } from "@t3tools/contracts";
7import { it, assert } from "@effect/vitest";8import { it, assert } from "@effect/vitest";
8import { assertSome } from "@effect/vitest/utils";9import { 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";
10import * as SqlClient from "effect/unstable/sql/SqlClient";13import * as SqlClient from "effect/unstable/sql/SqlClient";
1114
12import {15import {
...@@ -209,7 +212,7 @@ it.layer(makeDirectoryLayer(SqlitePersistenceMemory))("ProviderSessionDirectoryL...@@ -209,7 +212,7 @@ it.layer(makeDirectoryLayer(SqlitePersistenceMemory))("ProviderSessionDirectoryL
209 adapterKey: "claudeAgent",212 adapterKey: "claudeAgent",
210 runtimeMode: "full-access",213 runtimeMode: "full-access",
211 status: "running",214 status: "running",
212 lastSeenAt: new Date().toISOString(),215 lastSeenAt: "2026-01-01T00:00:00.000Z",
213 resumeCursor: null,216 resumeCursor: null,
214 runtimePayload: null,217 runtimePayload: null,
215 });218 });
apps/server/src/provider/Layers/ProviderSessionDirectory.ts+6-2
...@@ -1,5 +1,9 @@...@@ -1,5 +1,9 @@
1import { defaultInstanceIdForDriver, ProviderDriverKind, type ThreadId } from "@t3tools/contracts";1import { 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
4import type { ProviderSessionRuntime } from "../../persistence/Services/ProviderSessionRuntime.ts";8import type { ProviderSessionRuntime } from "../../persistence/Services/ProviderSessionRuntime.ts";
5import { ProviderSessionRuntimeRepository } from "../../persistence/Services/ProviderSessionRuntime.ts";9import { ProviderSessionRuntimeRepository } from "../../persistence/Services/ProviderSessionRuntime.ts";
...@@ -110,7 +114,7 @@ const makeProviderSessionDirectory = Effect.gen(function* () {...@@ -110,7 +114,7 @@ const makeProviderSessionDirectory = Effect.gen(function* () {
110 });114 });
111 }115 }
112116
113 const now = new Date().toISOString();117 const now = DateTime.formatIso(yield* DateTime.now);
114 const providerChanged =118 const providerChanged =
115 existingRuntime !== undefined && existingRuntime.providerName !== binding.provider;119 existingRuntime !== undefined && existingRuntime.providerName !== binding.provider;
116 const providerInstanceId =120 const providerInstanceId =
apps/server/src/provider/Layers/ProviderSessionReaper.test.ts+26-14
...@@ -6,7 +6,15 @@ import {...@@ -6,7 +6,15 @@ import {
6 ProviderDriverKind,6 ProviderDriverKind,
7 ProviderInstanceId,7 ProviderInstanceId,
8} from "@t3tools/contracts";8} 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";
10import { afterEach, describe, expect, it, vi } from "vitest";18import { afterEach, describe, expect, it, vi } from "vitest";
1119
12import { ProjectionSnapshotQuery } from "../../orchestration/Services/ProjectionSnapshotQuery.ts";20import { ProjectionSnapshotQuery } from "../../orchestration/Services/ProjectionSnapshotQuery.ts";
...@@ -28,21 +36,25 @@ async function waitFor(...@@ -28,21 +36,25 @@ async function waitFor(
28 predicate: () => boolean | Promise<boolean>,36 predicate: () => boolean | Promise<boolean>,
29 timeoutMs = 2_000,37 timeoutMs = 2_000,
30): Promise<void> {38): Promise<void> {
31 const deadline = Date.now() + timeoutMs;39 const deadline = (await Effect.runPromise(Clock.currentTimeMillis)) + timeoutMs;
32 const poll = async (): Promise<void> => {40 const poll = async (): Promise<void> => {
33 if (await predicate()) {41 if (await predicate()) {
34 return;42 return;
35 }43 }
36 if (Date.now() >= deadline) {44 if ((await Effect.runPromise(Clock.currentTimeMillis)) >= deadline) {
37 throw new Error("Timed out waiting for expectation.");45 throw new Error("Timed out waiting for expectation.");
38 }46 }
39 await new Promise((resolve) => setTimeout(resolve, 10));47 await Effect.runPromise(Effect.yieldNow);
40 return poll();48 return poll();
41 };49 };
4250
43 return poll();51 return poll();
44}52}
4553
54const drainFibers = Effect.forEach(Array.from({ length: 10 }), () => Effect.yieldNow, {
55 discard: true,
56});
57
46const unsupported = () => Effect.die(new Error("Unsupported provider call in test")) as never;58const unsupported = () => Effect.die(new Error("Unsupported provider call in test")) as never;
4759
48function makeReadModel(60function makeReadModel(
...@@ -59,7 +71,7 @@ function makeReadModel(...@@ -59,7 +71,7 @@ function makeReadModel(
59 } | null;71 } | null;
60 }>,72 }>,
61) {73) {
62 const now = new Date().toISOString();74 const now = "2026-01-01T00:00:00.000Z";
63 const projectId = ProjectId.make("project-provider-session-reaper");75 const projectId = ProjectId.make("project-provider-session-reaper");
6476
65 return {77 return {
...@@ -207,7 +219,7 @@ describe("ProviderSessionReaper", () => {...@@ -207,7 +219,7 @@ describe("ProviderSessionReaper", () => {
207219
208 it("reaps stale persisted sessions without active turns", async () => {220 it("reaps stale persisted sessions without active turns", async () => {
209 const threadId = ThreadId.make("thread-reaper-stale");221 const threadId = ThreadId.make("thread-reaper-stale");
210 const now = new Date().toISOString();222 const now = "2026-01-01T00:00:00.000Z";
211 const harness = await createHarness({223 const harness = await createHarness({
212 readModel: makeReadModel([224 readModel: makeReadModel([
213 {225 {
...@@ -255,7 +267,7 @@ describe("ProviderSessionReaper", () => {...@@ -255,7 +267,7 @@ describe("ProviderSessionReaper", () => {
255 it("skips stale sessions when the thread still has an active turn", async () => {267 it("skips stale sessions when the thread still has an active turn", async () => {
256 const threadId = ThreadId.make("thread-reaper-active-turn");268 const threadId = ThreadId.make("thread-reaper-active-turn");
257 const turnId = TurnId.make("turn-reaper-active");269 const turnId = TurnId.make("turn-reaper-active");
258 const now = new Date().toISOString();270 const now = "2026-01-01T00:00:00.000Z";
259 const harness = await createHarness({271 const harness = await createHarness({
260 readModel: makeReadModel([272 readModel: makeReadModel([
261 {273 {
...@@ -293,7 +305,7 @@ describe("ProviderSessionReaper", () => {...@@ -293,7 +305,7 @@ describe("ProviderSessionReaper", () => {
293 const reaper = await runtime!.runPromise(Effect.service(ProviderSessionReaper));305 const reaper = await runtime!.runPromise(Effect.service(ProviderSessionReaper));
294 scope = await Effect.runPromise(Scope.make("sequential"));306 scope = await Effect.runPromise(Scope.make("sequential"));
295 await Effect.runPromise(reaper.start().pipe(Scope.provide(scope)));307 await Effect.runPromise(reaper.start().pipe(Scope.provide(scope)));
296 await new Promise((resolve) => setTimeout(resolve, 50));308 await Effect.runPromise(drainFibers);
297309
298 expect(harness.stopSession).not.toHaveBeenCalled();310 expect(harness.stopSession).not.toHaveBeenCalled();
299 const remaining = await runtime!.runPromise(repository.getByThreadId({ threadId }));311 const remaining = await runtime!.runPromise(repository.getByThreadId({ threadId }));
...@@ -302,7 +314,7 @@ describe("ProviderSessionReaper", () => {...@@ -302,7 +314,7 @@ describe("ProviderSessionReaper", () => {
302314
303 it("does not reap sessions that are still within the inactivity threshold", async () => {315 it("does not reap sessions that are still within the inactivity threshold", async () => {
304 const threadId = ThreadId.make("thread-reaper-fresh");316 const threadId = ThreadId.make("thread-reaper-fresh");
305 const now = new Date().toISOString();317 const now = DateTime.formatIso(await Effect.runPromise(DateTime.now));
306 const harness = await createHarness({318 const harness = await createHarness({
307 readModel: makeReadModel([319 readModel: makeReadModel([
308 {320 {
...@@ -340,7 +352,7 @@ describe("ProviderSessionReaper", () => {...@@ -340,7 +352,7 @@ describe("ProviderSessionReaper", () => {
340 const reaper = await runtime!.runPromise(Effect.service(ProviderSessionReaper));352 const reaper = await runtime!.runPromise(Effect.service(ProviderSessionReaper));
341 scope = await Effect.runPromise(Scope.make("sequential"));353 scope = await Effect.runPromise(Scope.make("sequential"));
342 await Effect.runPromise(reaper.start().pipe(Scope.provide(scope)));354 await Effect.runPromise(reaper.start().pipe(Scope.provide(scope)));
343 await new Promise((resolve) => setTimeout(resolve, 50));355 await Effect.runPromise(drainFibers);
344356
345 expect(harness.stopSession).not.toHaveBeenCalled();357 expect(harness.stopSession).not.toHaveBeenCalled();
346 const remaining = await runtime!.runPromise(repository.getByThreadId({ threadId }));358 const remaining = await runtime!.runPromise(repository.getByThreadId({ threadId }));
...@@ -349,7 +361,7 @@ describe("ProviderSessionReaper", () => {...@@ -349,7 +361,7 @@ describe("ProviderSessionReaper", () => {
349361
350 it("skips persisted sessions that are already marked stopped", async () => {362 it("skips persisted sessions that are already marked stopped", async () => {
351 const threadId = ThreadId.make("thread-reaper-stopped");363 const threadId = ThreadId.make("thread-reaper-stopped");
352 const now = new Date().toISOString();364 const now = "2026-01-01T00:00:00.000Z";
353 const harness = await createHarness({365 const harness = await createHarness({
354 readModel: makeReadModel([366 readModel: makeReadModel([
355 {367 {
...@@ -387,7 +399,7 @@ describe("ProviderSessionReaper", () => {...@@ -387,7 +399,7 @@ describe("ProviderSessionReaper", () => {
387 const reaper = await runtime!.runPromise(Effect.service(ProviderSessionReaper));399 const reaper = await runtime!.runPromise(Effect.service(ProviderSessionReaper));
388 scope = await Effect.runPromise(Scope.make("sequential"));400 scope = await Effect.runPromise(Scope.make("sequential"));
389 await Effect.runPromise(reaper.start().pipe(Scope.provide(scope)));401 await Effect.runPromise(reaper.start().pipe(Scope.provide(scope)));
390 await new Promise((resolve) => setTimeout(resolve, 50));402 await Effect.runPromise(drainFibers);
391403
392 expect(harness.stopSession).not.toHaveBeenCalled();404 expect(harness.stopSession).not.toHaveBeenCalled();
393 const remaining = await runtime!.runPromise(repository.getByThreadId({ threadId }));405 const remaining = await runtime!.runPromise(repository.getByThreadId({ threadId }));
...@@ -397,7 +409,7 @@ describe("ProviderSessionReaper", () => {...@@ -397,7 +409,7 @@ describe("ProviderSessionReaper", () => {
397 it("continues reaping other sessions when one stop attempt fails", async () => {409 it("continues reaping other sessions when one stop attempt fails", async () => {
398 const failedThreadId = ThreadId.make("thread-reaper-stop-failure");410 const failedThreadId = ThreadId.make("thread-reaper-stop-failure");
399 const reapedThreadId = ThreadId.make("thread-reaper-stop-success");411 const reapedThreadId = ThreadId.make("thread-reaper-stop-success");
400 const now = new Date().toISOString();412 const now = "2026-01-01T00:00:00.000Z";
401 const harness = await createHarness({413 const harness = await createHarness({
402 readModel: makeReadModel([414 readModel: makeReadModel([
403 {415 {
...@@ -483,7 +495,7 @@ describe("ProviderSessionReaper", () => {...@@ -483,7 +495,7 @@ describe("ProviderSessionReaper", () => {
483 it("continues reaping other sessions when one stop attempt defects", async () => {495 it("continues reaping other sessions when one stop attempt defects", async () => {
484 const defectThreadId = ThreadId.make("thread-reaper-stop-defect");496 const defectThreadId = ThreadId.make("thread-reaper-stop-defect");
485 const reapedThreadId = ThreadId.make("thread-reaper-stop-after-defect");497 const reapedThreadId = ThreadId.make("thread-reaper-stop-after-defect");
486 const now = new Date().toISOString();498 const now = "2026-01-01T00:00:00.000Z";
487 const harness = await createHarness({499 const harness = await createHarness({
488 readModel: makeReadModel([500 readModel: makeReadModel([
489 {501 {
apps/server/src/provider/Layers/ProviderSessionReaper.ts+7-2
...@@ -1,4 +1,9 @@...@@ -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
3import { ProjectionSnapshotQuery } from "../../orchestration/Services/ProjectionSnapshotQuery.ts";8import { ProjectionSnapshotQuery } from "../../orchestration/Services/ProjectionSnapshotQuery.ts";
4import { ProviderSessionDirectory } from "../Services/ProviderSessionDirectory.ts";9import { ProviderSessionDirectory } from "../Services/ProviderSessionDirectory.ts";
...@@ -30,7 +35,7 @@ const makeProviderSessionReaper = (options?: ProviderSessionReaperLiveOptions) =...@@ -30,7 +35,7 @@ const makeProviderSessionReaper = (options?: ProviderSessionReaperLiveOptions) =
3035
31 const sweep = Effect.gen(function* () {36 const sweep = Effect.gen(function* () {
32 const bindings = yield* directory.listBindings();37 const bindings = yield* directory.listBindings();
33 const now = Date.now();38 const now = yield* Clock.currentTimeMillis;
34 let reapedCount = 0;39 let reapedCount = 0;
3540
36 for (const binding of bindings) {41 for (const binding of bindings) {
apps/server/src/provider/Layers/scopedSafeTeardown.test.ts+3-1
...@@ -1,5 +1,7 @@...@@ -1,5 +1,7 @@
1import { it } from "@effect/vitest";1import { 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";
3import { describe, expect } from "vitest";5import { describe, expect } from "vitest";
46
5import { scopedSafeTeardown } from "./scopedSafeTeardown.ts";7import { scopedSafeTeardown } from "./scopedSafeTeardown.ts";
apps/server/src/provider/Layers/scopedSafeTeardown.ts+3-1
...@@ -34,7 +34,9 @@...@@ -34,7 +34,9 @@
34 *34 *
35 * @module provider/Layers/scopedSafeTeardown35 * @module provider/Layers/scopedSafeTeardown
36 */36 */
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
39/**41/**
40 * Run `effect` with a freshly made `Scope.Scope`, guaranteeing that42 * Run `effect` with a freshly made `Scope.Scope`, guaranteeing that
apps/server/src/provider/ProviderDriver.ts+3-1
...@@ -26,7 +26,9 @@ import type {...@@ -26,7 +26,9 @@ import type {
26 ProviderInstanceEnvironment,26 ProviderInstanceEnvironment,
27 ProviderInstanceId,27 ProviderInstanceId,
28} from "@t3tools/contracts";28} 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
31import type { TextGenerationShape } from "../textGeneration/TextGeneration.ts";33import type { TextGenerationShape } from "../textGeneration/TextGeneration.ts";
32import type { ProviderAdapterError, ProviderDriverError } from "./Errors.ts";34import type { ProviderAdapterError, ProviderDriverError } from "./Errors.ts";
apps/server/src/provider/Services/ProviderAdapter.ts+2-2
...@@ -20,8 +20,8 @@ import type {...@@ -20,8 +20,8 @@ import type {
20 ProviderTurnStartResult,20 ProviderTurnStartResult,
21 TurnId,21 TurnId,
22} from "@t3tools/contracts";22} from "@t3tools/contracts";
23import type { Effect } from "effect";23import type * as Effect from "effect/Effect";
24import type { Stream } from "effect";24import type * as Stream from "effect/Stream";
2525
26export type ProviderSessionModelSwitchMode = "in-session" | "unsupported";26export type ProviderSessionModelSwitchMode = "in-session" | "unsupported";
2727
apps/server/src/provider/Services/ProviderAdapterRegistry.ts+5-2
...@@ -20,8 +20,11 @@...@@ -20,8 +20,11 @@
20 * @module ProviderAdapterRegistry20 * @module ProviderAdapterRegistry
21 */21 */
22import type { ProviderDriverKind, ProviderInstanceId } from "@t3tools/contracts";22import type { ProviderDriverKind, ProviderInstanceId } from "@t3tools/contracts";
23import { Context } from "effect";23import * as Context from "effect/Context";
24import type { Effect, PubSub, Scope, Stream } from "effect";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
26import type { ProviderAdapterError, ProviderUnsupportedError } from "../Errors.ts";29import type { ProviderAdapterError, ProviderUnsupportedError } from "../Errors.ts";
27import type { ProviderAdapterShape } from "./ProviderAdapter.ts";30import type { ProviderAdapterShape } from "./ProviderAdapter.ts";
apps/server/src/provider/Services/ProviderInstanceRegistry.ts+5-2
...@@ -18,8 +18,11 @@...@@ -18,8 +18,11 @@
18 * @module provider/Services/ProviderInstanceRegistry18 * @module provider/Services/ProviderInstanceRegistry
19 */19 */
20import type { ProviderInstanceId, ServerProvider } from "@t3tools/contracts";20import type { ProviderInstanceId, ServerProvider } from "@t3tools/contracts";
21import { Context } from "effect";21import * as Context from "effect/Context";
22import type { Effect, PubSub, Scope, Stream } from "effect";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
24import type { ProviderInstance } from "../ProviderDriver.ts";27import type { ProviderInstance } from "../ProviderDriver.ts";
2528
apps/server/src/provider/Services/ProviderInstanceRegistryMutator.ts+2-2
...@@ -29,8 +29,8 @@...@@ -29,8 +29,8 @@
29 * @module provider/Services/ProviderInstanceRegistryMutator29 * @module provider/Services/ProviderInstanceRegistryMutator
30 */30 */
31import type { ProviderInstanceConfigMap } from "@t3tools/contracts";31import type { ProviderInstanceConfigMap } from "@t3tools/contracts";
32import { Context } from "effect";32import * as Context from "effect/Context";
33import type { Effect } from "effect";33import type * as Effect from "effect/Effect";
3434
35export interface ProviderInstanceRegistryMutatorShape {35export interface ProviderInstanceRegistryMutatorShape {
36 /**36 /**
apps/server/src/provider/Services/ProviderRegistry.ts+3-2
...@@ -12,8 +12,9 @@ import type {...@@ -12,8 +12,9 @@ import type {
12 ServerProvider,12 ServerProvider,
13 ServerProviderUpdateState,13 ServerProviderUpdateState,
14} from "@t3tools/contracts";14} from "@t3tools/contracts";
15import { Context } from "effect";15import * as Context from "effect/Context";
16import type { Effect, Stream } from "effect";16import type * as Effect from "effect/Effect";
17import type * as Stream from "effect/Stream";
17import type { ProviderMaintenanceCapabilities } from "../providerMaintenance.ts";18import type { ProviderMaintenanceCapabilities } from "../providerMaintenance.ts";
1819
19export type ProviderMaintenanceActionKind = "update";20export type ProviderMaintenanceActionKind = "update";
apps/server/src/provider/Services/ProviderService.ts+3-2
...@@ -24,8 +24,9 @@ import type {...@@ -24,8 +24,9 @@ import type {
24 ThreadId,24 ThreadId,
25 ProviderTurnStartResult,25 ProviderTurnStartResult,
26} from "@t3tools/contracts";26} from "@t3tools/contracts";
27import { Context } from "effect";27import * as Context from "effect/Context";
28import type { Effect, Stream } from "effect";28import type * as Effect from "effect/Effect";
29import type * as Stream from "effect/Stream";
2930
30import type { ProviderServiceError } from "../Errors.ts";31import type { ProviderServiceError } from "../Errors.ts";
31import type { ProviderAdapterCapabilities } from "./ProviderAdapter.ts";32import type { ProviderAdapterCapabilities } from "./ProviderAdapter.ts";
apps/server/src/provider/Services/ProviderSessionDirectory.ts+3-2
...@@ -5,8 +5,9 @@ import type {...@@ -5,8 +5,9 @@ import type {
5 RuntimeMode,5 RuntimeMode,
6 ThreadId,6 ThreadId,
7} from "@t3tools/contracts";7} from "@t3tools/contracts";
8import { Option, Context } from "effect";8import * as Option from "effect/Option";
9import type { Effect } from "effect";9import * as Context from "effect/Context";
10import type * as Effect from "effect/Effect";
1011
11import type {12import type {
12 ProviderSessionDirectoryPersistenceError,13 ProviderSessionDirectoryPersistenceError,
apps/server/src/provider/Services/ProviderSessionReaper.ts+3-2
...@@ -1,5 +1,6 @@...@@ -1,5 +1,6 @@
1import { Context } from "effect";1import * as Context from "effect/Context";
2import type { Effect, Scope } from "effect";2import type * as Effect from "effect/Effect";
3import type * as Scope from "effect/Scope";
34
4export interface ProviderSessionReaperShape {5export interface ProviderSessionReaperShape {
5 /**6 /**
apps/server/src/provider/Services/ServerProvider.ts+2-1
...@@ -1,5 +1,6 @@...@@ -1,5 +1,6 @@
1import type { ServerProvider } from "@t3tools/contracts";1import 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";
3import type { ProviderMaintenanceCapabilities } from "../providerMaintenance.ts";4import type { ProviderMaintenanceCapabilities } from "../providerMaintenance.ts";
45
5export interface ServerProviderShape {6export interface ServerProviderShape {
apps/server/src/provider/acp/AcpAdapterSupport.ts+1-1
...@@ -3,7 +3,7 @@ import {...@@ -3,7 +3,7 @@ import {
3 type ProviderDriverKind,3 type ProviderDriverKind,
4 type ThreadId,4 type ThreadId,
5} from "@t3tools/contracts";5} from "@t3tools/contracts";
6import { Schema } from "effect";6import * as Schema from "effect/Schema";
7import * as EffectAcpErrors from "effect-acp/errors";7import * as EffectAcpErrors from "effect-acp/errors";
88
9import {9import {
apps/server/src/provider/acp/AcpJsonRpcConnection.test.ts+3-1
...@@ -1,3 +1,4 @@...@@ -1,3 +1,4 @@
1// @effect-diagnostics nodeBuiltinImport:off
1import * as path from "node:path";2import * as path from "node:path";
2import * as os from "node:os";3import * as os from "node:os";
3import { fileURLToPath } from "node:url";4import { fileURLToPath } from "node:url";
...@@ -5,7 +6,8 @@ import { mkdtempSync, readFileSync, rmSync } from "node:fs";...@@ -5,7 +6,8 @@ import { mkdtempSync, readFileSync, rmSync } from "node:fs";
56
6import * as NodeServices from "@effect/platform-node/NodeServices";7import * as NodeServices from "@effect/platform-node/NodeServices";
7import { it } from "@effect/vitest";8import { it } from "@effect/vitest";
8import { Effect, Stream } from "effect";9import * as Effect from "effect/Effect";
10import * as Stream from "effect/Stream";
9import { describe, expect } from "vitest";11import { describe, expect } from "vitest";
1012
11import { AcpSessionRuntime, type AcpSessionRequestLogEvent } from "./AcpSessionRuntime.ts";13import { AcpSessionRuntime, type AcpSessionRequestLogEvent } from "./AcpSessionRuntime.ts";
apps/server/src/provider/acp/AcpNativeLogging.ts+6-3
...@@ -1,5 +1,8 @@...@@ -1,5 +1,8 @@
1import type { ProviderDriverKind, ThreadId } from "@t3tools/contracts";1import 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";
3import type * as EffectAcpProtocol from "effect-acp/protocol";6import type * as EffectAcpProtocol from "effect-acp/protocol";
47
5import type { EventNdjsonLogger } from "../Layers/EventNdjsonLogger.ts";8import type { EventNdjsonLogger } from "../Layers/EventNdjsonLogger.ts";
...@@ -14,12 +17,12 @@ function writeNativeAcpLog(input: {...@@ -14,12 +17,12 @@ function writeNativeAcpLog(input: {
14}): Effect.Effect<void, never> {17}): Effect.Effect<void, never> {
15 return Effect.gen(function* () {18 return Effect.gen(function* () {
16 if (!input.nativeEventLogger) return;19 if (!input.nativeEventLogger) return;
17 const observedAt = new Date().toISOString();20 const observedAt = DateTime.formatIso(yield* DateTime.now);
18 yield* input.nativeEventLogger.write(21 yield* input.nativeEventLogger.write(
19 {22 {
20 observedAt,23 observedAt,
21 event: {24 event: {
22 id: crypto.randomUUID(),25 id: yield* Random.nextUUIDv4,
23 kind: input.kind,26 kind: input.kind,
24 provider: input.provider,27 provider: input.provider,
25 createdAt: observedAt,28 createdAt: observedAt,
apps/server/src/provider/acp/AcpSessionRuntime.ts+15-5
...@@ -1,4 +1,14 @@...@@ -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";
2import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";12import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
3import * as EffectAcpClient from "effect-acp/client";13import * as EffectAcpClient from "effect-acp/client";
4import * as EffectAcpErrors from "effect-acp/errors";14import * as EffectAcpErrors from "effect-acp/errors";
...@@ -248,7 +258,7 @@ const makeAcpSessionRuntime = (...@@ -248,7 +258,7 @@ const makeAcpSessionRuntime = (
248 }258 }
249 return yield* new EffectAcpErrors.AcpTransportError({259 return yield* new EffectAcpErrors.AcpTransportError({
250 detail: "ACP session runtime has not been started",260 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",
252 });262 });
253 });263 });
254264
...@@ -267,7 +277,7 @@ const makeAcpSessionRuntime = (...@@ -267,7 +277,7 @@ const makeAcpSessionRuntime = (
267 }277 }
268 return yield* new EffectAcpErrors.AcpRequestError({278 return yield* new EffectAcpErrors.AcpRequestError({
269 code: -32602,279 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`,
271 data: {281 data: {
272 configId: configOption.id,282 configId: configOption.id,
273 expectedType: "boolean",283 expectedType: "boolean",
...@@ -278,7 +288,7 @@ const makeAcpSessionRuntime = (...@@ -278,7 +288,7 @@ const makeAcpSessionRuntime = (
278 if (typeof value !== "string") {288 if (typeof value !== "string") {
279 return yield* new EffectAcpErrors.AcpRequestError({289 return yield* new EffectAcpErrors.AcpRequestError({
280 code: -32602,290 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`,
282 data: {292 data: {
283 configId: configOption.id,293 configId: configOption.id,
284 expectedType: "string",294 expectedType: "string",
...@@ -292,7 +302,7 @@ const makeAcpSessionRuntime = (...@@ -292,7 +302,7 @@ const makeAcpSessionRuntime = (
292 }302 }
293 return yield* new EffectAcpErrors.AcpRequestError({303 return yield* new EffectAcpErrors.AcpRequestError({
294 code: -32602,304 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(", ")}`,
296 data: {306 data: {
297 configId: configOption.id,307 configId: configOption.id,
298 allowedValues,308 allowedValues,
apps/server/src/provider/acp/CursorAcpCliProbe.test.ts+12-7
...@@ -4,7 +4,8 @@...@@ -4,7 +4,8 @@
4 */4 */
5import * as NodeServices from "@effect/platform-node/NodeServices";5import * as NodeServices from "@effect/platform-node/NodeServices";
6import { it } from "@effect/vitest";6import { it } from "@effect/vitest";
7import { Effect } from "effect";7import * as Console from "effect/Console";
8import * as Effect from "effect/Effect";
8import { describe, expect } from "vitest";9import { describe, expect } from "vitest";
9import type * as EffectAcpSchema from "effect-acp/schema";10import 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",...@@ -44,12 +45,14 @@ describe.runIf(process.env.T3_CURSOR_ACP_PROBE === "1")("Cursor ACP CLI probe",
44 const runtime = yield* AcpSessionRuntime;45 const runtime = yield* AcpSessionRuntime;
45 const started = yield* runtime.start();46 const started = yield* runtime.start();
46 const result = started.sessionSetupResult;47 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
49 expect(typeof started.sessionId).toBe("string");51 expect(typeof started.sessionId).toBe("string");
5052
51 const configOptions = result.configOptions;53 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
54 if (Array.isArray(configOptions)) {57 if (Array.isArray(configOptions)) {
55 const modelConfig = configOptions.find((opt) => opt.category === "model");58 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",...@@ -59,9 +62,11 @@ describe.runIf(process.env.T3_CURSOR_ACP_PROBE === "1")("Cursor ACP CLI probe",
59 opt.category === "model_option" ||62 opt.category === "model_option" ||
60 opt.category === "model_config",63 opt.category === "model_config",
61 );64 );
62 console.log("Model config option:", JSON.stringify(modelConfig, null, 2));65 // @effect-diagnostics-next-line preferSchemaOverJson:off
63 console.log(66 yield* Console.log("Model config option:", JSON.stringify(modelConfig, null, 2));
67 yield* Console.log(
64 "Parameterized model config options:",68 "Parameterized model config options:",
69 // @effect-diagnostics-next-line preferSchemaOverJson:off
65 JSON.stringify(parameterizedOptions, null, 2),70 JSON.stringify(parameterizedOptions, null, 2),
66 );71 );
67 expect(modelConfig).toBeDefined();72 expect(modelConfig).toBeDefined();
...@@ -107,8 +112,8 @@ describe.runIf(process.env.T3_CURSOR_ACP_PROBE === "1")("Cursor ACP CLI probe",...@@ -107,8 +112,8 @@ describe.runIf(process.env.T3_CURSOR_ACP_PROBE === "1")("Cursor ACP CLI probe",
107112
108 const setResult: EffectAcpSchema.SetSessionConfigOptionResponse =113 const setResult: EffectAcpSchema.SetSessionConfigOptionResponse =
109 yield* runtime.setConfigOption(modelConfigId, "gpt-5.4");114 yield* runtime.setConfigOption(modelConfigId, "gpt-5.4");
110115 // @effect-diagnostics-next-line preferSchemaOverJson:off
111 console.log("session/set_config_option result:", JSON.stringify(setResult, null, 2));116 yield* Console.log("session/set_config_option result:", JSON.stringify(setResult, null, 2));
112117
113 if (Array.isArray(setResult.configOptions)) {118 if (Array.isArray(setResult.configOptions)) {
114 const modelConfig = setResult.configOptions.find((opt) => opt.category === "model");119 const modelConfig = setResult.configOptions.find((opt) => opt.category === "model");
apps/server/src/provider/acp/CursorAcpExtension.ts+1-1
...@@ -3,7 +3,7 @@...@@ -3,7 +3,7 @@
3 * Additional reference provided by the Cursor team: https://anysphere.enterprise.slack.com/files/U068SSJE141/F0APT1HSZRP/cursor-acp-extension-method-schemas.md3 * Additional reference provided by the Cursor team: https://anysphere.enterprise.slack.com/files/U068SSJE141/F0APT1HSZRP/cursor-acp-extension-method-schemas.md
4 */4 */
5import type { UserInputQuestion } from "@t3tools/contracts";5import type { UserInputQuestion } from "@t3tools/contracts";
6import { Schema } from "effect";6import * as Schema from "effect/Schema";
77
8const CursorAskQuestionOption = Schema.Struct({8const CursorAskQuestionOption = Schema.Struct({
9 id: Schema.String,9 id: Schema.String,
apps/server/src/provider/acp/CursorAcpSupport.test.ts+4-6
...@@ -1,4 +1,4 @@...@@ -1,4 +1,4 @@
1import { Effect } from "effect";1import * as Effect from "effect/Effect";
2import type * as EffectAcpSchema from "effect-acp/schema";2import type * as EffectAcpSchema from "effect-acp/schema";
3import { describe, expect, it } from "vitest";3import { describe, expect, it } from "vitest";
44
...@@ -105,11 +105,9 @@ describe("applyCursorAcpModelSelection", () => {...@@ -105,11 +105,9 @@ describe("applyCursorAcpModelSelection", () => {
105 { id: "fastMode", value: true },105 { id: "fastMode", value: true },
106 ],106 ],
107 mapError: ({ step, configId, cause }) =>107 mapError: ({ step, configId, cause }) =>
108 new Error(108 step === "set-config-option"
109 step === "set-config-option"109 ? `failed to set config option ${configId}: ${cause.message}`
110 ? `failed to set config option ${configId}: ${cause.message}`110 : `failed to set model: ${cause.message}`,
111 : `failed to set model: ${cause.message}`,
112 ),
113 }),111 }),
114 );112 );
115113
apps/server/src/provider/acp/CursorAcpSupport.ts+3-1
...@@ -1,5 +1,7 @@...@@ -1,5 +1,7 @@
1import { type CursorSettings, type ProviderOptionSelection } from "@t3tools/contracts";1import { 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";
3import { ChildProcessSpawner } from "effect/unstable/process";5import { ChildProcessSpawner } from "effect/unstable/process";
4import type * as EffectAcpErrors from "effect-acp/errors";6import type * as EffectAcpErrors from "effect-acp/errors";
57
apps/server/src/provider/builtInProviderCatalog.ts+1-1
...@@ -1,5 +1,5 @@...@@ -1,5 +1,5 @@
1import type { ProviderDriverKind, ProviderInstanceId, ServerProvider } from "@t3tools/contracts";1import type { ProviderDriverKind, ProviderInstanceId, ServerProvider } from "@t3tools/contracts";
2import type { Stream } from "effect";2import type * as Stream from "effect/Stream";
3import type { ServerProviderShape } from "./Services/ServerProvider.ts";3import type { ServerProviderShape } from "./Services/ServerProvider.ts";
44
5export type ProviderSnapshotSource = {5export type ProviderSnapshotSource = {
apps/server/src/provider/makeManagedServerProvider.test.ts+10-5
...@@ -1,7 +1,12 @@...@@ -1,7 +1,12 @@
1import { describe, it, assert } from "@effect/vitest";1import { describe, it, assert } from "@effect/vitest";
2import { ProviderDriverKind, ProviderInstanceId, type ServerProvider } from "@t3tools/contracts";2import { ProviderDriverKind, ProviderInstanceId, type ServerProvider } from "@t3tools/contracts";
3import { createModelCapabilities } from "@t3tools/shared/model";3import { 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
6import { makeManagedServerProvider } from "./makeManagedServerProvider.ts";11import { makeManagedServerProvider } from "./makeManagedServerProvider.ts";
712
...@@ -108,7 +113,7 @@ describe("makeManagedServerProvider", () => {...@@ -108,7 +113,7 @@ describe("makeManagedServerProvider", () => {
108 getSettings: Effect.succeed({ enabled: true }),113 getSettings: Effect.succeed({ enabled: true }),
109 streamSettings: Stream.empty,114 streamSettings: Stream.empty,
110 haveSettingsChanged: (previous, next) => previous.enabled !== next.enabled,115 haveSettingsChanged: (previous, next) => previous.enabled !== next.enabled,
111 initialSnapshot: () => initialSnapshot,116 initialSnapshot: () => Effect.succeed(initialSnapshot),
112 checkProvider: Ref.update(checkCalls, (count) => count + 1).pipe(117 checkProvider: Ref.update(checkCalls, (count) => count + 1).pipe(
113 Effect.flatMap(() => Deferred.await(releaseCheck)),118 Effect.flatMap(() => Deferred.await(releaseCheck)),
114 Effect.as(refreshedSnapshot),119 Effect.as(refreshedSnapshot),
...@@ -150,7 +155,7 @@ describe("makeManagedServerProvider", () => {...@@ -150,7 +155,7 @@ describe("makeManagedServerProvider", () => {
150 getSettings: Ref.get(settingsRef),155 getSettings: Ref.get(settingsRef),
151 streamSettings: Stream.fromPubSub(settingsChanges),156 streamSettings: Stream.fromPubSub(settingsChanges),
152 haveSettingsChanged: (previous, next) => previous.enabled !== next.enabled,157 haveSettingsChanged: (previous, next) => previous.enabled !== next.enabled,
153 initialSnapshot: () => initialSnapshot,158 initialSnapshot: () => Effect.succeed(initialSnapshot),
154 checkProvider: Ref.updateAndGet(checkCalls, (count) => count + 1).pipe(159 checkProvider: Ref.updateAndGet(checkCalls, (count) => count + 1).pipe(
155 Effect.flatMap((count) =>160 Effect.flatMap((count) =>
156 count === 1161 count === 1
...@@ -192,7 +197,7 @@ describe("makeManagedServerProvider", () => {...@@ -192,7 +197,7 @@ describe("makeManagedServerProvider", () => {
192 getSettings: Effect.succeed({ enabled: true }),197 getSettings: Effect.succeed({ enabled: true }),
193 streamSettings: Stream.empty,198 streamSettings: Stream.empty,
194 haveSettingsChanged: (previous, next) => previous.enabled !== next.enabled,199 haveSettingsChanged: (previous, next) => previous.enabled !== next.enabled,
195 initialSnapshot: () => initialSnapshot,200 initialSnapshot: () => Effect.succeed(initialSnapshot),
196 checkProvider: Deferred.await(releaseCheck).pipe(Effect.as(refreshedSnapshot)),201 checkProvider: Deferred.await(releaseCheck).pipe(Effect.as(refreshedSnapshot)),
197 enrichSnapshot: ({ publishSnapshot }) =>202 enrichSnapshot: ({ publishSnapshot }) =>
198 Deferred.await(releaseEnrichment).pipe(203 Deferred.await(releaseEnrichment).pipe(
...@@ -233,7 +238,7 @@ describe("makeManagedServerProvider", () => {...@@ -233,7 +238,7 @@ describe("makeManagedServerProvider", () => {
233 getSettings: Effect.succeed({ enabled: true }),238 getSettings: Effect.succeed({ enabled: true }),
234 streamSettings: Stream.empty,239 streamSettings: Stream.empty,
235 haveSettingsChanged: (previous, next) => previous.enabled !== next.enabled,240 haveSettingsChanged: (previous, next) => previous.enabled !== next.enabled,
236 initialSnapshot: () => initialSnapshot,241 initialSnapshot: () => Effect.succeed(initialSnapshot),
237 checkProvider: Ref.updateAndGet(refreshCount, (count) => count + 1).pipe(242 checkProvider: Ref.updateAndGet(refreshCount, (count) => count + 1).pipe(
238 Effect.flatMap((count) =>243 Effect.flatMap((count) =>
239 count === 1244 count === 1
apps/server/src/provider/makeManagedServerProvider.ts+10-3
...@@ -1,5 +1,12 @@...@@ -1,5 +1,12 @@
1import type { ServerProvider } from "@t3tools/contracts";1import 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";
3import * as Semaphore from "effect/Semaphore";10import * as Semaphore from "effect/Semaphore";
411
5import type { ServerProviderShape } from "./Services/ServerProvider.ts";12import type { ServerProviderShape } from "./Services/ServerProvider.ts";
...@@ -17,7 +24,7 @@ export const makeManagedServerProvider = Effect.fn("makeManagedServerProvider")(...@@ -17,7 +24,7 @@ export const makeManagedServerProvider = Effect.fn("makeManagedServerProvider")(
17 readonly getSettings: Effect.Effect<Settings>;24 readonly getSettings: Effect.Effect<Settings>;
18 readonly streamSettings: Stream.Stream<Settings>;25 readonly streamSettings: Stream.Stream<Settings>;
19 readonly haveSettingsChanged: (previous: Settings, next: Settings) => boolean;26 readonly haveSettingsChanged: (previous: Settings, next: Settings) => boolean;
20 readonly initialSnapshot: (settings: Settings) => ServerProvider;27 readonly initialSnapshot: (settings: Settings) => Effect.Effect<ServerProvider>;
21 readonly checkProvider: Effect.Effect<ServerProvider, ServerSettingsError>;28 readonly checkProvider: Effect.Effect<ServerProvider, ServerSettingsError>;
22 readonly enrichSnapshot?: (input: {29 readonly enrichSnapshot?: (input: {
23 readonly settings: Settings;30 readonly settings: Settings;
...@@ -33,7 +40,7 @@ export const makeManagedServerProvider = Effect.fn("makeManagedServerProvider")(...@@ -33,7 +40,7 @@ export const makeManagedServerProvider = Effect.fn("makeManagedServerProvider")(
33 PubSub.shutdown,40 PubSub.shutdown,
34 );41 );
35 const initialSettings = yield* input.getSettings;42 const initialSettings = yield* input.getSettings;
36 const initialSnapshot = input.initialSnapshot(initialSettings);43 const initialSnapshot = yield* input.initialSnapshot(initialSettings);
37 const snapshotStateRef = yield* Ref.make<ProviderSnapshotState>({44 const snapshotStateRef = yield* Ref.make<ProviderSnapshotState>({
38 snapshot: initialSnapshot,45 snapshot: initialSnapshot,
39 enrichmentGeneration: 0,46 enrichmentGeneration: 0,
apps/server/src/provider/opencodeRuntime.ts+19-20
...@@ -11,27 +11,26 @@ import {...@@ -11,27 +11,26 @@ import {
11 type QuestionAnswer,11 type QuestionAnswer,
12 type QuestionRequest,12 type QuestionRequest,
13} from "@opencode-ai/sdk/v2";13} from "@opencode-ai/sdk/v2";
14import {14import * as Cause from "effect/Cause";
15 Cause,15import * as Context from "effect/Context";
16 Context,16import * as Data from "effect/Data";
17 Data,17import * as Deferred from "effect/Deferred";
18 Deferred,18import * as Effect from "effect/Effect";
19 Effect,19import * as Exit from "effect/Exit";
20 Exit,20import * as Fiber from "effect/Fiber";
21 Fiber,21import * as Layer from "effect/Layer";
22 Layer,22import * as Option from "effect/Option";
23 Option,23import * as P from "effect/Predicate";
24 Predicate as P,24import * as Ref from "effect/Ref";
25 Ref,25import * as Result from "effect/Result";
26 Result,26import * as Scope from "effect/Scope";
27 Scope,27import * as Schema from "effect/Schema";
28 Stream,28import * as Stream from "effect/Stream";
29} from "effect";
30import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";29import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
3130
32import { isWindowsCommandNotFound } from "../processRunner.ts";31import { isWindowsCommandNotFound } from "../processRunner.ts";
33import { collectStreamAsString } from "./providerSnapshot.ts";32import { collectStreamAsString } from "./providerSnapshot.ts";
34import { NetService } from "@t3tools/shared/Net";33import * as NetService from "@t3tools/shared/Net";
3534
36const OPENCODE_SERVER_READY_PREFIX = "opencode server listening";35const OPENCODE_SERVER_READY_PREFIX = "opencode server listening";
37const DEFAULT_OPENCODE_SERVER_TIMEOUT_MS = 5_000;36const DEFAULT_OPENCODE_SERVER_TIMEOUT_MS = 5_000;
...@@ -66,7 +65,7 @@ export function openCodeRuntimeErrorDetail(cause: unknown): string {...@@ -66,7 +65,7 @@ export function openCodeRuntimeErrorDetail(cause: unknown): string {
66 const status = (anyCause.response as { status?: number } | undefined)?.status;65 const status = (anyCause.response as { status?: number } | undefined)?.status;
67 const body = anyCause.error ?? anyCause.data ?? anyCause.body;66 const body = anyCause.error ?? anyCause.data ?? anyCause.body;
68 try {67 try {
69 return `status=${status ?? "?"} body=${JSON.stringify(body ?? cause)}`;68 return `status=${status ?? "?"} body=${Schema.encodeUnknownSync(Schema.UnknownFromJsonString)(body ?? cause)}`;
70 } catch {69 } catch {
71 /* fall through */70 /* fall through */
72 }71 }
...@@ -270,7 +269,7 @@ function ensureRuntimeError(...@@ -270,7 +269,7 @@ function ensureRuntimeError(
270269
271const makeOpenCodeRuntime = Effect.gen(function* () {270const makeOpenCodeRuntime = Effect.gen(function* () {
272 const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;271 const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
273 const netService = yield* NetService;272 const netService = yield* NetService.NetService;
274273
275 const runOpenCodeCommand: OpenCodeRuntimeShape["runOpenCodeCommand"] = (input) =>274 const runOpenCodeCommand: OpenCodeRuntimeShape["runOpenCodeCommand"] = (input) =>
276 Effect.gen(function* () {275 Effect.gen(function* () {
...@@ -337,7 +336,7 @@ const makeOpenCodeRuntime = Effect.gen(function* () {...@@ -337,7 +336,7 @@ const makeOpenCodeRuntime = Effect.gen(function* () {
337 shell: process.platform === "win32",336 shell: process.platform === "win32",
338 env: {337 env: {
339 ...(input.environment ?? process.env),338 ...(input.environment ?? process.env),
340 OPENCODE_CONFIG_CONTENT: JSON.stringify({}),339 OPENCODE_CONFIG_CONTENT: Schema.encodeUnknownSync(Schema.UnknownFromJsonString)({}),
341 },340 },
342 }),341 }),
343 )342 )
apps/server/src/provider/providerMaintenance.test.ts+3-1
...@@ -1,10 +1,12 @@...@@ -1,10 +1,12 @@
1// @effect-diagnostics nodeBuiltinImport:off
1import { afterEach, describe, expect, it } from "@effect/vitest";2import { afterEach, describe, expect, it } from "@effect/vitest";
2import { chmodSync, mkdirSync, symlinkSync, writeFileSync } from "node:fs";3import { chmodSync, mkdirSync, symlinkSync, writeFileSync } from "node:fs";
3import * as NodeServices from "@effect/platform-node/NodeServices";4import * as NodeServices from "@effect/platform-node/NodeServices";
4import os from "node:os";5import os from "node:os";
5import path from "node:path";6import path from "node:path";
6import { ProviderDriverKind } from "@t3tools/contracts";7import { ProviderDriverKind } from "@t3tools/contracts";
7import { Effect, Random } from "effect";8import * as Effect from "effect/Effect";
9import * as Random from "effect/Random";
8import {10import {
9 clearLatestProviderVersionCacheForTests,11 clearLatestProviderVersionCacheForTests,
10 createProviderVersionAdvisory,12 createProviderVersionAdvisory,
apps/server/src/provider/providerMaintenance.ts+5-1
...@@ -4,7 +4,11 @@ import {...@@ -4,7 +4,11 @@ import {
4 type ServerProviderVersionAdvisory,4 type ServerProviderVersionAdvisory,
5} from "@t3tools/contracts";5} from "@t3tools/contracts";
6import { resolveCommandPath } from "@t3tools/shared/shell";6import { 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";
8import { HttpClient, HttpClientRequest } from "effect/unstable/http";12import { HttpClient, HttpClientRequest } from "effect/unstable/http";
913
10import { compareCliVersions } from "./cliVersion.ts";14import { compareCliVersions } from "./cliVersion.ts";
apps/server/src/provider/providerMaintenanceCommandCoordinator.ts+2-1
...@@ -1,4 +1,5 @@...@@ -1,4 +1,5 @@
1import { Effect, Ref } from "effect";1import * as Effect from "effect/Effect";
2import * as Ref from "effect/Ref";
2import * as Semaphore from "effect/Semaphore";3import * as Semaphore from "effect/Semaphore";
34
4export interface ProviderMaintenanceCommandCoordinatorShape<E> {5export interface ProviderMaintenanceCommandCoordinatorShape<E> {
apps/server/src/provider/providerMaintenanceRunner.test.ts+9-1
...@@ -6,7 +6,15 @@ import {...@@ -6,7 +6,15 @@ import {
6 type ServerProviderUpdateState,6 type ServerProviderUpdateState,
7} from "@t3tools/contracts";7} from "@t3tools/contracts";
8import { ServerProviderUpdateError } from "@t3tools/contracts";8import { 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";
10import { HttpClient, HttpClientResponse } from "effect/unstable/http";18import { HttpClient, HttpClientResponse } from "effect/unstable/http";
11import { ChildProcessSpawner } from "effect/unstable/process";19import { ChildProcessSpawner } from "effect/unstable/process";
1220
apps/server/src/provider/providerMaintenanceRunner.ts+23-3
...@@ -7,7 +7,16 @@ import {...@@ -7,7 +7,16 @@ import {
7 type ServerProviderUpdatedPayload,7 type ServerProviderUpdatedPayload,
8 type ServerProviderUpdateState,8 type ServerProviderUpdateState,
9} from "@t3tools/contracts";9} 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";
11import { HttpClient } from "effect/unstable/http";20import { HttpClient } from "effect/unstable/http";
12import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";21import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
1322
...@@ -45,6 +54,11 @@ export class ProviderMaintenanceRunner extends Context.Service<...@@ -45,6 +54,11 @@ export class ProviderMaintenanceRunner extends Context.Service<
45 ProviderMaintenanceRunnerShape54 ProviderMaintenanceRunnerShape
46>()("t3/provider/ProviderMaintenanceRunner") {}55>()("t3/provider/ProviderMaintenanceRunner") {}
4756
57class ProviderMaintenanceCommandError extends Data.TaggedError("ProviderMaintenanceCommandError")<{
58 readonly message: string;
59 readonly cause?: unknown;
60}> {}
61
48interface VerifiedProviderRefresh {62interface VerifiedProviderRefresh {
49 readonly providers: ReadonlyArray<ServerProvider>;63 readonly providers: ReadonlyArray<ServerProvider>;
50 readonly verifiedProviders: ReadonlyArray<ServerProvider>;64 readonly verifiedProviders: ReadonlyArray<ServerProvider>;
...@@ -65,7 +79,10 @@ const runProviderMaintenanceCommandWithSpawner = Effect.fn("ProviderMaintenanceR...@@ -65,7 +79,10 @@ const runProviderMaintenanceCommandWithSpawner = Effect.fn("ProviderMaintenanceR
65 .pipe(79 .pipe(
66 Effect.mapError(80 Effect.mapError(
67 (cause) =>81 (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 }),
69 ),86 ),
70 );87 );
71 yield* Effect.addFinalizer(() => child.kill().pipe(Effect.ignore));88 yield* Effect.addFinalizer(() => child.kill().pipe(Effect.ignore));
...@@ -86,7 +103,10 @@ const runProviderMaintenanceCommandWithSpawner = Effect.fn("ProviderMaintenanceR...@@ -86,7 +103,10 @@ const runProviderMaintenanceCommandWithSpawner = Effect.fn("ProviderMaintenanceR
86 ).pipe(103 ).pipe(
87 Effect.mapError(104 Effect.mapError(
88 (cause) =>105 (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 }),
90 ),110 ),
91 );111 );
92112
apps/server/src/provider/providerSnapshot.ts+11-3
...@@ -8,7 +8,9 @@ import type {...@@ -8,7 +8,9 @@ import type {
8 ServerProviderModel,8 ServerProviderModel,
9 ServerProviderState,9 ServerProviderState,
10} from "@t3tools/contracts";10} 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";
12import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";14import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
13import { normalizeModelSlug } from "@t3tools/shared/model";15import { normalizeModelSlug } from "@t3tools/shared/model";
14import { isWindowsCommandNotFound } from "../processRunner.ts";16import { isWindowsCommandNotFound } from "../processRunner.ts";
...@@ -25,6 +27,12 @@ export interface CommandResult {...@@ -25,6 +27,12 @@ export interface CommandResult {
25 readonly code: number;27 readonly code: number;
26}28}
2729
30export class ProviderCommandExecutionError extends Data.TaggedError(
31 "ProviderCommandExecutionError",
32)<{
33 readonly message: string;
34}> {}
35
28export interface ProviderProbeResult {36export interface ProviderProbeResult {
29 readonly installed: boolean;37 readonly installed: boolean;
30 readonly version: string | null;38 readonly version: string | null;
...@@ -47,7 +55,7 @@ export function nonEmptyTrimmed(value: string | undefined): string | undefined {...@@ -47,7 +55,7 @@ export function nonEmptyTrimmed(value: string | undefined): string | undefined {
47 return trimmed.length > 0 ? trimmed : undefined;55 return trimmed.length > 0 ? trimmed : undefined;
48}56}
4957
50export function isCommandMissingCause(error: Error): boolean {58export function isCommandMissingCause(error: { readonly message: string }): boolean {
51 const lower = error.message.toLowerCase();59 const lower = error.message.toLowerCase();
52 return lower.includes("enoent") || lower.includes("notfound");60 return lower.includes("enoent") || lower.includes("notfound");
53}61}
...@@ -67,7 +75,7 @@ export const spawnAndCollect = (binaryPath: string, command: ChildProcess.Comman...@@ -67,7 +75,7 @@ export const spawnAndCollect = (binaryPath: string, command: ChildProcess.Comman
6775
68 const result: CommandResult = { stdout, stderr, code: exitCode };76 const result: CommandResult = { stdout, stderr, code: exitCode };
69 if (isWindowsCommandNotFound(exitCode, stderr)) {77 if (isWindowsCommandNotFound(exitCode, stderr)) {
70 return yield* Effect.fail(new Error(`spawn ${binaryPath} ENOENT`));78 return yield* new ProviderCommandExecutionError({ message: `spawn ${binaryPath} ENOENT` });
71 }79 }
72 return result;80 return result;
73 }).pipe(Effect.scoped);81 }).pipe(Effect.scoped);
apps/server/src/provider/providerStatusCache.test.ts+2-1
...@@ -7,7 +7,8 @@ import {...@@ -7,7 +7,8 @@ import {
7} from "@t3tools/contracts";7} from "@t3tools/contracts";
8import { createModelCapabilities } from "@t3tools/shared/model";8import { createModelCapabilities } from "@t3tools/shared/model";
9import { assert, it } from "@effect/vitest";9import { assert, it } from "@effect/vitest";
10import { Effect, FileSystem } from "effect";10import * as Effect from "effect/Effect";
11import * as FileSystem from "effect/FileSystem";
1112
12import {13import {
13 hydrateCachedProvider,14 hydrateCachedProvider,
apps/server/src/provider/providerStatusCache.ts+5-1
...@@ -4,7 +4,11 @@ import {...@@ -4,7 +4,11 @@ import {
4 type ServerProvider,4 type ServerProvider,
5 ServerProvider as ServerProviderSchema,5 ServerProvider as ServerProviderSchema,
6} from "@t3tools/contracts";6} 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
9import { writeFileStringAtomically } from "../atomicWrite.ts";13import { writeFileStringAtomically } from "../atomicWrite.ts";
1014
apps/server/src/provider/testUtils/providerAdapterRegistryMock.ts+5-1
...@@ -18,7 +18,11 @@ import {...@@ -18,7 +18,11 @@ import {
18 ProviderDriverKind,18 ProviderDriverKind,
19 type ProviderInstanceId,19 type ProviderInstanceId,
20} from "@t3tools/contracts";20} 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
23import { ProviderUnsupportedError, type ProviderAdapterError } from "../Errors.ts";27import { ProviderUnsupportedError, type ProviderAdapterError } from "../Errors.ts";
24import type { ProviderAdapterShape } from "../Services/ProviderAdapter.ts";28import type { ProviderAdapterShape } from "../Services/ProviderAdapter.ts";
apps/server/src/provider/unavailableProviderSnapshot.ts+36-29
...@@ -15,6 +15,8 @@ import {...@@ -15,6 +15,8 @@ import {
15 type ProviderInstanceId,15 type ProviderInstanceId,
16 type ServerProvider,16 type ServerProvider,
17} from "@t3tools/contracts";17} from "@t3tools/contracts";
18import * as DateTime from "effect/DateTime";
19import * as Effect from "effect/Effect";
1820
19import { buildServerProvider } from "./providerSnapshot.ts";21import { buildServerProvider } from "./providerSnapshot.ts";
2022
...@@ -25,12 +27,15 @@ export interface UnavailableProviderSnapshotInput {...@@ -25,12 +27,15 @@ export interface UnavailableProviderSnapshotInput {
25 readonly accentColor?: string | undefined;27 readonly accentColor?: string | undefined;
26 readonly reason: string;28 readonly reason: string;
27 /**29 /**
28 * Optional override for `checkedAt`. Defaulted to `new Date()` so callers30 * Optional override for `checkedAt`. Defaulted to the current Effect
31 * `DateTime` so callers
29 * (notably tests) don't have to pass it.32 * (notably tests) don't have to pass it.
30 */33 */
31 readonly checkedAt?: string;34 readonly checkedAt?: string;
32}35}
3336
37const nowIso = Effect.map(DateTime.now, DateTime.formatIso);
38
34/**39/**
35 * Produce a `ServerProvider` snapshot representing a configured instance40 * Produce a `ServerProvider` snapshot representing a configured instance
36 * whose driver the running build does not implement. The result is safe41 * whose driver the running build does not implement. The result is safe
...@@ -39,34 +44,36 @@ export interface UnavailableProviderSnapshotInput {...@@ -39,34 +44,36 @@ export interface UnavailableProviderSnapshotInput {
39 */44 */
40export function buildUnavailableProviderSnapshot(45export function buildUnavailableProviderSnapshot(
41 input: UnavailableProviderSnapshotInput,46 input: UnavailableProviderSnapshotInput,
42): ServerProvider {47): Effect.Effect<ServerProvider> {
43 const checkedAt = input.checkedAt ?? new Date().toISOString();48 return Effect.gen(function* () {
44 const displayName = input.displayName?.trim() || (input.driverKind as string);49 const checkedAt = input.checkedAt ?? (yield* nowIso);
50 const displayName = input.displayName?.trim() || (input.driverKind as string);
4551
46 const base = buildServerProvider({52 const base = buildServerProvider({
47 presentation: { displayName },53 presentation: { displayName },
48 enabled: false,54 enabled: false,
49 checkedAt,55 checkedAt,
50 models: [],56 models: [],
51 skills: [],57 skills: [],
52 probe: {58 probe: {
53 installed: false,59 installed: false,
54 version: null,60 version: null,
55 status: "error",61 status: "error",
56 auth: { status: "unknown" },62 auth: { status: "unknown" },
57 message: input.reason,63 message: input.reason,
58 },64 },
59 });65 });
6066
61 return {67 return {
62 ...base,68 ...base,
63 instanceId: input.instanceId,69 instanceId: input.instanceId,
64 ...(input.accentColor ? { accentColor: input.accentColor } : {}),70 ...(input.accentColor ? { accentColor: input.accentColor } : {}),
65 driver:71 driver:
66 typeof input.driverKind === "string"72 typeof input.driverKind === "string"
67 ? ProviderDriverKind.make(input.driverKind)73 ? ProviderDriverKind.make(input.driverKind)
68 : input.driverKind,74 : input.driverKind,
69 availability: "unavailable",75 availability: "unavailable",
70 unavailableReason: input.reason,76 unavailableReason: input.reason,
71 };77 };
78 });
72}79}
apps/server/src/server.test.ts+59-51
...@@ -27,18 +27,17 @@ import {...@@ -27,18 +27,17 @@ import {
27} from "@t3tools/contracts";27} from "@t3tools/contracts";
28import { assert, it } from "@effect/vitest";28import { assert, it } from "@effect/vitest";
29import { assertFailure, assertInclude, assertTrue } from "@effect/vitest/utils";29import { assertFailure, assertInclude, assertTrue } from "@effect/vitest/utils";
30import {30import * as Clock from "effect/Clock";
31 Deferred,31import * as Deferred from "effect/Deferred";
32 DateTime,32import * as DateTime from "effect/DateTime";
33 Duration,33import * as Duration from "effect/Duration";
34 Effect,34import * as Effect from "effect/Effect";
35 FileSystem,35import * as FileSystem from "effect/FileSystem";
36 Layer,36import * as Layer from "effect/Layer";
37 ManagedRuntime,37import * as ManagedRuntime from "effect/ManagedRuntime";
38 Option,38import * as Option from "effect/Option";
39 Path,39import * as Path from "effect/Path";
40 Stream,40import * as Stream from "effect/Stream";
41} from "effect";
42import { ChildProcessSpawner } from "effect/unstable/process";41import { ChildProcessSpawner } from "effect/unstable/process";
43import {42import {
44 FetchHttpClient,43 FetchHttpClient,
...@@ -92,6 +91,7 @@ import {...@@ -92,6 +91,7 @@ import {
92import { ProjectFaviconResolverLive } from "./project/Layers/ProjectFaviconResolver.ts";91import { ProjectFaviconResolverLive } from "./project/Layers/ProjectFaviconResolver.ts";
93import {92import {
94 ProjectSetupScriptRunner,93 ProjectSetupScriptRunner,
94 ProjectSetupScriptRunnerError,
95 type ProjectSetupScriptRunnerShape,95 type ProjectSetupScriptRunnerShape,
96} from "./project/Services/ProjectSetupScriptRunner.ts";96} from "./project/Services/ProjectSetupScriptRunner.ts";
97import {97import {
...@@ -116,6 +116,7 @@ import { ServerSecretStoreLive } from "./auth/Layers/ServerSecretStore.ts";...@@ -116,6 +116,7 @@ import { ServerSecretStoreLive } from "./auth/Layers/ServerSecretStore.ts";
116import { ServerAuthLive } from "./auth/Layers/ServerAuth.ts";116import { ServerAuthLive } from "./auth/Layers/ServerAuth.ts";
117import * as ProcessDiagnostics from "./diagnostics/ProcessDiagnostics.ts";117import * as ProcessDiagnostics from "./diagnostics/ProcessDiagnostics.ts";
118import * as TraceDiagnostics from "./diagnostics/TraceDiagnostics.ts";118import * as TraceDiagnostics from "./diagnostics/TraceDiagnostics.ts";
119import * as Data from "effect/Data";
119120
120const defaultProjectId = ProjectId.make("project-default");121const defaultProjectId = ProjectId.make("project-default");
121const defaultThreadId = ThreadId.make("thread-default");122const defaultThreadId = ThreadId.make("thread-default");
...@@ -137,7 +138,7 @@ const testEnvironmentDescriptor = {...@@ -137,7 +138,7 @@ const testEnvironmentDescriptor = {
137 },138 },
138};139};
139const makeDefaultOrchestrationReadModel = () => {140const makeDefaultOrchestrationReadModel = () => {
140 const now = new Date().toISOString();141 const now = "2026-01-01T00:00:00.000Z";
141 return {142 return {
142 snapshotSequence: 0,143 snapshotSequence: 0,
143 updatedAt: now,144 updatedAt: now,
...@@ -181,7 +182,7 @@ const makeDefaultOrchestrationReadModel = () => {...@@ -181,7 +182,7 @@ const makeDefaultOrchestrationReadModel = () => {
181const makeDefaultOrchestrationThreadShell = (182const makeDefaultOrchestrationThreadShell = (
182 overrides: Partial<OrchestrationThreadShell> = {},183 overrides: Partial<OrchestrationThreadShell> = {},
183): OrchestrationThreadShell => {184): OrchestrationThreadShell => {
184 const now = new Date().toISOString();185 const now = "2026-01-01T00:00:00.000Z";
185 return {186 return {
186 id: defaultThreadId,187 id: defaultThreadId,
187 projectId: defaultProjectId,188 projectId: defaultProjectId,
...@@ -302,15 +303,13 @@ const makeBrowserOtlpPayload = (spanName: string) =>...@@ -302,15 +303,13 @@ const makeBrowserOtlpPayload = (spanName: string) =>
302 yield* Effect.promise(() => runtime.dispose());303 yield* Effect.promise(() => runtime.dispose());
303 }304 }
304305
305 const request = yield* Effect.promise(() =>306 const request = yield* Effect.raceFirst(
306 Promise.race([307 Effect.promise(() => collector.firstRequest).pipe(Effect.orDie),
307 collector.firstRequest,308 Effect.sleep(Duration.seconds(1)).pipe(
308 new Promise<never>((_, reject) => {309 Effect.andThen(Effect.die(new Error("Timed out waiting for OTLP trace export"))),
309 setTimeout(() => reject(new Error("Timed out waiting for OTLP trace export")), 1_000);310 ),
310 }),
311 ]),
312 );311 );
313312 // @effect-diagnostics-next-line preferSchemaOverJson:off
314 return JSON.parse(request.body) as OtlpTracer.TraceData;313 return JSON.parse(request.body) as OtlpTracer.TraceData;
315 });314 });
316315
...@@ -629,7 +628,7 @@ const buildAppUnderTest = (options?: {...@@ -629,7 +628,7 @@ const buildAppUnderTest = (options?: {
629 snapshotSequence: 0,628 snapshotSequence: 0,
630 projects: [],629 projects: [],
631 threads: [],630 threads: [],
632 updatedAt: new Date(0).toISOString(),631 updatedAt: "1970-01-01T00:00:00.000Z",
633 }),632 }),
634 getSnapshotSequence: () => Effect.succeed({ snapshotSequence: 0 }),633 getSnapshotSequence: () => Effect.succeed({ snapshotSequence: 0 }),
635 getProjectShellById: () => Effect.succeed(Option.none()),634 getProjectShellById: () => Effect.succeed(Option.none()),
...@@ -823,17 +822,23 @@ const bootstrapBearerSession = (credential = defaultDesktopBootstrapToken) =>...@@ -823,17 +822,23 @@ const bootstrapBearerSession = (credential = defaultDesktopBootstrapToken) =>
823 };822 };
824 });823 });
825824
825class AuthenticationGetterError extends Data.TaggedError("AuthenticationGetterError")<{
826 readonly message: string;
827}> {}
828
826const getAuthenticatedSessionCookieHeader = (credential = defaultDesktopBootstrapToken) =>829const getAuthenticatedSessionCookieHeader = (credential = defaultDesktopBootstrapToken) =>
827 Effect.gen(function* () {830 Effect.gen(function* () {
828 const { response, cookie } = yield* bootstrapBrowserSession(credential);831 const { response, cookie } = yield* bootstrapBrowserSession(credential);
829 if (!response.ok) {832 if (!response.ok) {
830 return yield* Effect.fail(833 return yield* new AuthenticationGetterError({
831 new Error(`Expected bootstrap session response to succeed, got ${response.status}`),834 message: `Expected bootstrap session response to succeed, got ${response.status}`,
832 );835 });
833 }836 }
834837
835 if (!cookie) {838 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 });
837 }842 }
838843
839 return cookie.split(";")[0] ?? cookie;844 return cookie.split(";")[0] ?? cookie;
...@@ -843,15 +848,15 @@ const getAuthenticatedBearerSessionToken = (credential = defaultDesktopBootstrap...@@ -843,15 +848,15 @@ const getAuthenticatedBearerSessionToken = (credential = defaultDesktopBootstrap
843 Effect.gen(function* () {848 Effect.gen(function* () {
844 const { response, body } = yield* bootstrapBearerSession(credential);849 const { response, body } = yield* bootstrapBearerSession(credential);
845 if (!response.ok) {850 if (!response.ok) {
846 return yield* Effect.fail(851 return yield* new AuthenticationGetterError({
847 new Error(`Expected bearer bootstrap response to succeed, got ${response.status}`),852 message: `Expected bearer bootstrap response to succeed, got ${response.status}`,
848 );853 });
849 }854 }
850855
851 if (!body.sessionToken) {856 if (!body.sessionToken) {
852 return yield* Effect.fail(857 return yield* new AuthenticationGetterError({
853 new Error("Expected bearer bootstrap response to include a session token."),858 message: "Expected bearer bootstrap response to include a session token.",
854 );859 });
855 }860 }
856861
857 return body.sessionToken;862 return body.sessionToken;
...@@ -1758,6 +1763,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => {...@@ -1758,6 +1763,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
1758 "content-type": "application/json",1763 "content-type": "application/json",
1759 origin: "http://localhost:5733",1764 origin: "http://localhost:5733",
1760 },1765 },
1766 // @effect-diagnostics-next-line preferSchemaOverJson:off
1761 body: HttpBody.text(JSON.stringify(payload), "application/json"),1767 body: HttpBody.text(JSON.stringify(payload), "application/json"),
1762 });1768 });
17631769
...@@ -1803,6 +1809,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => {...@@ -1803,6 +1809,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
1803 ]);1809 ]);
1804 assert.deepEqual(upstreamRequests, [1810 assert.deepEqual(upstreamRequests, [
1805 {1811 {
1812 // @effect-diagnostics-next-line preferSchemaOverJson:off
1806 body: JSON.stringify(payload),1813 body: JSON.stringify(payload),
1807 contentType: "application/json",1814 contentType: "application/json",
1808 },1815 },
...@@ -1875,6 +1882,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => {...@@ -1875,6 +1882,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
1875 cookie: yield* getAuthenticatedSessionCookieHeader(),1882 cookie: yield* getAuthenticatedSessionCookieHeader(),
1876 "content-type": "application/json",1883 "content-type": "application/json",
1877 },1884 },
1885 // @effect-diagnostics-next-line preferSchemaOverJson:off
1878 body: HttpBody.text(JSON.stringify(payload), "application/json"),1886 body: HttpBody.text(JSON.stringify(payload), "application/json"),
1879 });1887 });
18801888
...@@ -2182,7 +2190,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => {...@@ -2182,7 +2190,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
2182 version: 1 as const,2190 version: 1 as const,
2183 sequence: 2,2191 sequence: 2,
2184 type: "ready" as const,2192 type: "ready" as const,
2185 payload: { at: new Date().toISOString(), environment: testEnvironmentDescriptor },2193 payload: { at: "2026-01-01T00:00:00.000Z", environment: testEnvironmentDescriptor },
2186 });2194 });
21872195
2188 yield* buildAppUnderTest({2196 yield* buildAppUnderTest({
...@@ -2370,7 +2378,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => {...@@ -2370,7 +2378,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
2370 instanceId: ProviderInstanceId.make("codex"),2378 instanceId: ProviderInstanceId.make("codex"),
2371 model: "gpt-5-codex",2379 model: "gpt-5-codex",
2372 },2380 },
2373 createdAt: new Date().toISOString(),2381 createdAt: "2026-01-01T00:00:00.000Z",
2374 }),2382 }),
2375 ),2383 ),
2376 );2384 );
...@@ -2913,11 +2921,11 @@ it.layer(NodeServices.layer)("server router seam", (it) => {...@@ -2913,11 +2921,11 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
2913 });2921 });
29142922
2915 const wsUrl = yield* getWsServerUrl("/ws");2923 const wsUrl = yield* getWsServerUrl("/ws");
2916 const startedAt = Date.now();2924 const startedAt = yield* Clock.currentTimeMillis;
2917 const result = yield* Effect.scoped(2925 const result = yield* Effect.scoped(
2918 withWsRpcClient(wsUrl, (client) => client[WS_METHODS.vcsPull]({ cwd: "/tmp/repo" })),2926 withWsRpcClient(wsUrl, (client) => client[WS_METHODS.vcsPull]({ cwd: "/tmp/repo" })),
2919 );2927 );
2920 const elapsedMs = Date.now() - startedAt;2928 const elapsedMs = (yield* Clock.currentTimeMillis) - startedAt;
29212929
2922 assert.equal(result.status, "pulled");2930 assert.equal(result.status, "pulled");
2923 assertTrue(elapsedMs < 1_000);2931 assertTrue(elapsedMs < 1_000);
...@@ -2982,7 +2990,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => {...@@ -2982,7 +2990,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
2982 });2990 });
29832991
2984 const wsUrl = yield* getWsServerUrl("/ws");2992 const wsUrl = yield* getWsServerUrl("/ws");
2985 const startedAt = Date.now();2993 const startedAt = yield* Clock.currentTimeMillis;
2986 yield* Effect.scoped(2994 yield* Effect.scoped(
2987 withWsRpcClient(wsUrl, (client) =>2995 withWsRpcClient(wsUrl, (client) =>
2988 client[WS_METHODS.gitRunStackedAction]({2996 client[WS_METHODS.gitRunStackedAction]({
...@@ -2992,7 +3000,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => {...@@ -2992,7 +3000,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
2992 }).pipe(Stream.runCollect),3000 }).pipe(Stream.runCollect),
2993 ),3001 ),
2994 );3002 );
2995 const elapsedMs = Date.now() - startedAt;3003 const elapsedMs = (yield* Clock.currentTimeMillis) - startedAt;
29963004
2997 assertTrue(elapsedMs < 1_000);3005 assertTrue(elapsedMs < 1_000);
2998 }).pipe(Effect.provide(NodeHttpServer.layerTest)),3006 }).pipe(Effect.provide(NodeHttpServer.layerTest)),
...@@ -3079,7 +3087,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => {...@@ -3079,7 +3087,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
30793087
3080 it.effect("routes websocket rpc orchestration methods", () =>3088 it.effect("routes websocket rpc orchestration methods", () =>
3081 Effect.gen(function* () {3089 Effect.gen(function* () {
3082 const now = new Date().toISOString();3090 const now = "2026-01-01T00:00:00.000Z";
3083 const snapshot = {3091 const snapshot = {
3084 snapshotSequence: 1,3092 snapshotSequence: 1,
3085 updatedAt: now,3093 updatedAt: now,
...@@ -3292,7 +3300,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => {...@@ -3292,7 +3300,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
3292 const threadId = ThreadId.make("thread-archive");3300 const threadId = ThreadId.make("thread-archive");
3293 const effects: string[] = [];3301 const effects: string[] = [];
3294 const dispatchedCommands: Array<OrchestrationCommand> = [];3302 const dispatchedCommands: Array<OrchestrationCommand> = [];
3295 const now = new Date().toISOString();3303 const now = "2026-01-01T00:00:00.000Z";
32963304
3297 yield* buildAppUnderTest({3305 yield* buildAppUnderTest({
3298 layers: {3306 layers: {
...@@ -3363,7 +3371,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => {...@@ -3363,7 +3371,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
3363 const threadId = ThreadId.make("thread-archive-precheck");3371 const threadId = ThreadId.make("thread-archive-precheck");
3364 const effects: string[] = [];3372 const effects: string[] = [];
3365 const dispatchedCommands: Array<OrchestrationCommand> = [];3373 const dispatchedCommands: Array<OrchestrationCommand> = [];
3366 const now = new Date().toISOString();3374 const now = "2026-01-01T00:00:00.000Z";
3367 let archived = false;3375 let archived = false;
33683376
3369 yield* buildAppUnderTest({3377 yield* buildAppUnderTest({
...@@ -3494,7 +3502,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => {...@@ -3494,7 +3502,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
3494 const threadId = ThreadId.make("thread-archive-stopped-session");3502 const threadId = ThreadId.make("thread-archive-stopped-session");
3495 const effects: string[] = [];3503 const effects: string[] = [];
3496 const dispatchedCommands: Array<OrchestrationCommand> = [];3504 const dispatchedCommands: Array<OrchestrationCommand> = [];
3497 const now = new Date().toISOString();3505 const now = "2026-01-01T00:00:00.000Z";
34983506
3499 yield* buildAppUnderTest({3507 yield* buildAppUnderTest({
3500 layers: {3508 layers: {
...@@ -3560,7 +3568,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => {...@@ -3560,7 +3568,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
3560 const threadId = ThreadId.make("thread-archive-stop-failure");3568 const threadId = ThreadId.make("thread-archive-stop-failure");
3561 const effects: string[] = [];3569 const effects: string[] = [];
3562 const dispatchedCommands: Array<OrchestrationCommand> = [];3570 const dispatchedCommands: Array<OrchestrationCommand> = [];
3563 const now = new Date().toISOString();3571 const now = "2026-01-01T00:00:00.000Z";
35643572
3565 yield* buildAppUnderTest({3573 yield* buildAppUnderTest({
3566 layers: {3574 layers: {
...@@ -3637,7 +3645,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => {...@@ -3637,7 +3645,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
3637 const threadId = ThreadId.make("thread-archive-stop-defect");3645 const threadId = ThreadId.make("thread-archive-stop-defect");
3638 const effects: string[] = [];3646 const effects: string[] = [];
3639 const dispatchedCommands: Array<OrchestrationCommand> = [];3647 const dispatchedCommands: Array<OrchestrationCommand> = [];
3640 const now = new Date().toISOString();3648 const now = "2026-01-01T00:00:00.000Z";
36413649
3642 yield* buildAppUnderTest({3650 yield* buildAppUnderTest({
3643 layers: {3651 layers: {
...@@ -3769,7 +3777,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => {...@@ -3769,7 +3777,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
3769 },3777 },
3770 });3778 });
37713779
3772 const createdAt = new Date().toISOString();3780 const createdAt = "2026-01-01T00:00:00.000Z";
3773 const wsUrl = yield* getWsServerUrl("/ws");3781 const wsUrl = yield* getWsServerUrl("/ws");
3774 const response = yield* Effect.scoped(3782 const response = yield* Effect.scoped(
3775 withWsRpcClient(wsUrl, (client) =>3783 withWsRpcClient(wsUrl, (client) =>
...@@ -3864,7 +3872,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => {...@@ -3864,7 +3872,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
3864 );3872 );
3865 const runForThread = vi.fn(3873 const runForThread = vi.fn(
3866 (_: Parameters<ProjectSetupScriptRunnerShape["runForThread"]>[0]) =>3874 (_: Parameters<ProjectSetupScriptRunnerShape["runForThread"]>[0]) =>
3867 Effect.fail(new Error("pty unavailable")),3875 Effect.fail(new ProjectSetupScriptRunnerError({ message: "pty unavailable" })),
3868 );3876 );
38693877
3870 yield* buildAppUnderTest({3878 yield* buildAppUnderTest({
...@@ -3886,7 +3894,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => {...@@ -3886,7 +3894,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
3886 },3894 },
3887 });3895 });
38883896
3889 const createdAt = new Date().toISOString();3897 const createdAt = "2026-01-01T00:00:00.000Z";
3890 const wsUrl = yield* getWsServerUrl("/ws");3898 const wsUrl = yield* getWsServerUrl("/ws");
3891 const response = yield* Effect.scoped(3899 const response = yield* Effect.scoped(
3892 withWsRpcClient(wsUrl, (client) =>3900 withWsRpcClient(wsUrl, (client) =>
...@@ -4003,7 +4011,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => {...@@ -4003,7 +4011,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
4003 },4011 },
4004 });4012 });
40054013
4006 const createdAt = new Date().toISOString();4014 const createdAt = "2026-01-01T00:00:00.000Z";
4007 const wsUrl = yield* getWsServerUrl("/ws");4015 const wsUrl = yield* getWsServerUrl("/ws");
4008 const response = yield* Effect.scoped(4016 const response = yield* Effect.scoped(
4009 withWsRpcClient(wsUrl, (client) =>4017 withWsRpcClient(wsUrl, (client) =>
...@@ -4087,7 +4095,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => {...@@ -4087,7 +4095,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
4087 },4095 },
4088 });4096 });
40894097
4090 const createdAt = new Date().toISOString();4098 const createdAt = "2026-01-01T00:00:00.000Z";
4091 const wsUrl = yield* getWsServerUrl("/ws");4099 const wsUrl = yield* getWsServerUrl("/ws");
4092 const result = yield* Effect.scoped(4100 const result = yield* Effect.scoped(
4093 withWsRpcClient(wsUrl, (client) =>4101 withWsRpcClient(wsUrl, (client) =>
...@@ -4149,7 +4157,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => {...@@ -4149,7 +4157,7 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
4149 history: "",4157 history: "",
4150 exitCode: null,4158 exitCode: null,
4151 exitSignal: null,4159 exitSignal: null,
4152 updatedAt: new Date().toISOString(),4160 updatedAt: "2026-01-01T00:00:00.000Z",
4153 };4161 };
41544162
4155 yield* buildAppUnderTest({4163 yield* buildAppUnderTest({
apps/server/src/server.ts+5-8
...@@ -1,4 +1,5 @@...@@ -1,4 +1,5 @@
1import { Effect, Layer } from "effect";1import * as Effect from "effect/Effect";
2import * as Layer from "effect/Layer";
2import { FetchHttpClient, HttpRouter, HttpServer } from "effect/unstable/http";3import { FetchHttpClient, HttpRouter, HttpServer } from "effect/unstable/http";
34
4import { ServerConfig } from "./config.ts";5import { ServerConfig } from "./config.ts";
...@@ -86,7 +87,7 @@ import {...@@ -86,7 +87,7 @@ import {
86 orchestrationDispatchRouteLayer,87 orchestrationDispatchRouteLayer,
87 orchestrationSnapshotRouteLayer,88 orchestrationSnapshotRouteLayer,
88} from "./orchestration/http.ts";89} from "./orchestration/http.ts";
89import { NetService } from "@t3tools/shared/Net";90import * as NetService from "@t3tools/shared/Net";
90import { disableTailscaleServe, ensureTailscaleServe } from "@t3tools/tailscale";91import { disableTailscaleServe, ensureTailscaleServe } from "@t3tools/tailscale";
9192
92const PtyAdapterLive = Layer.unwrap(93const PtyAdapterLive = Layer.unwrap(
...@@ -333,7 +334,7 @@ export const makeServerLayer = Layer.unwrap(...@@ -333,7 +334,7 @@ export const makeServerLayer = Layer.unwrap(
333 return;334 return;
334 }335 }
335336
336 const state = makePersistedServerRuntimeState({337 const state = yield* makePersistedServerRuntimeState({
337 config,338 config,
338 port: address.port,339 port: address.port,
339 });340 });
...@@ -418,8 +419,4 @@ export const makeServerLayer = Layer.unwrap(...@@ -418,8 +419,4 @@ export const makeServerLayer = Layer.unwrap(
418);419);
419420
420// Important: Only `ServerConfig` should be provided by the CLI layer!!! Don't let other requirements leak into the launch layer.421// 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<422export const runServer = Layer.launch(makeServerLayer);
422 never,
423 any,
424 ServerConfig
425>;
apps/server/src/serverLifecycleEvents.test.ts+3-2
...@@ -1,7 +1,8 @@...@@ -1,7 +1,8 @@
1import { EnvironmentId } from "@t3tools/contracts";1import { EnvironmentId } from "@t3tools/contracts";
2import { assert, it } from "@effect/vitest";2import { assert, it } from "@effect/vitest";
3import { assertTrue } from "@effect/vitest/utils";3import { assertTrue } from "@effect/vitest/utils";
4import { Effect, Option } from "effect";4import * as Effect from "effect/Effect";
5import * as Option from "effect/Option";
56
6import { ServerLifecycleEvents, ServerLifecycleEventsLive } from "./serverLifecycleEvents.ts";7import { ServerLifecycleEvents, ServerLifecycleEventsLive } from "./serverLifecycleEvents.ts";
78
...@@ -37,7 +38,7 @@ it.effect(...@@ -37,7 +38,7 @@ it.effect(
37 version: 1,38 version: 1,
38 type: "ready",39 type: "ready",
39 payload: {40 payload: {
40 at: new Date().toISOString(),41 at: "2026-01-01T00:00:00.000Z",
41 environment,42 environment,
42 },43 },
43 })44 })
apps/server/src/serverLifecycleEvents.ts+6-1
...@@ -1,5 +1,10 @@...@@ -1,5 +1,10 @@
1import type { ServerLifecycleStreamEvent } from "@t3tools/contracts";1import 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
4type LifecycleEventInput =9type LifecycleEventInput =
5 | Omit<Extract<ServerLifecycleStreamEvent, { type: "welcome" }>, "sequence">10 | Omit<Extract<ServerLifecycleStreamEvent, { type: "welcome" }>, "sequence">
apps/server/src/serverLogger.ts+4-1
...@@ -1,4 +1,7 @@...@@ -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
3import { ServerConfig } from "./config.ts";6import { ServerConfig } from "./config.ts";
47
apps/server/src/serverRuntimeStartup.test.ts+6-1
...@@ -1,7 +1,12 @@...@@ -1,7 +1,12 @@
1import * as NodeServices from "@effect/platform-node/NodeServices";1import * as NodeServices from "@effect/platform-node/NodeServices";
2import { DEFAULT_MODEL, ProjectId, ProviderInstanceId, ThreadId } from "@t3tools/contracts";2import { DEFAULT_MODEL, ProjectId, ProviderInstanceId, ThreadId } from "@t3tools/contracts";
3import { assert, it } from "@effect/vitest";3import { 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
6import { ServerConfig } from "./config.ts";11import { ServerConfig } from "./config.ts";
7import {12import {
apps/server/src/serverRuntimeStartup.ts+16-17
...@@ -7,20 +7,19 @@ import {...@@ -7,20 +7,19 @@ import {
7 ProviderInstanceId,7 ProviderInstanceId,
8 ThreadId,8 ThreadId,
9} from "@t3tools/contracts";9} from "@t3tools/contracts";
10import {10import * as Data from "effect/Data";
11 Data,11import * as Deferred from "effect/Deferred";
12 Deferred,12import * as Effect from "effect/Effect";
13 Effect,13import * as Exit from "effect/Exit";
14 Exit,14import * as Layer from "effect/Layer";
15 Layer,15import * as Option from "effect/Option";
16 Option,16import * as Path from "effect/Path";
17 Path,17import * as Queue from "effect/Queue";
18 Queue,18import * as Ref from "effect/Ref";
19 Ref,19import * as Scope from "effect/Scope";
20 Scope,20import * as Context from "effect/Context";
21 Context,21import * as Console from "effect/Console";
22 Console,22import * as DateTime from "effect/DateTime";
23} from "effect";
2423
25import { ServerConfig } from "./config.ts";24import { ServerConfig } from "./config.ts";
26import { Keybindings } from "./keybindings.ts";25import { Keybindings } from "./keybindings.ts";
...@@ -188,7 +187,7 @@ export const resolveAutoBootstrapWelcomeTargets = Effect.gen(function* () {...@@ -188,7 +187,7 @@ export const resolveAutoBootstrapWelcomeTargets = Effect.gen(function* () {
188 let nextProjectDefaultModelSelection: ModelSelection;187 let nextProjectDefaultModelSelection: ModelSelection;
189188
190 if (Option.isNone(existingProject)) {189 if (Option.isNone(existingProject)) {
191 const createdAt = new Date().toISOString();190 const createdAt = DateTime.formatIso(yield* DateTime.now);
192 nextProjectId = ProjectId.make(crypto.randomUUID());191 nextProjectId = ProjectId.make(crypto.randomUUID());
193 const bootstrapProjectTitle = path.basename(serverConfig.cwd) || "project";192 const bootstrapProjectTitle = path.basename(serverConfig.cwd) || "project";
194 nextProjectDefaultModelSelection = getAutoBootstrapDefaultModelSelection();193 nextProjectDefaultModelSelection = getAutoBootstrapDefaultModelSelection();
...@@ -210,7 +209,7 @@ export const resolveAutoBootstrapWelcomeTargets = Effect.gen(function* () {...@@ -210,7 +209,7 @@ export const resolveAutoBootstrapWelcomeTargets = Effect.gen(function* () {
210 const existingThreadId =209 const existingThreadId =
211 yield* projectionReadModelQuery.getFirstActiveThreadIdByProjectId(nextProjectId);210 yield* projectionReadModelQuery.getFirstActiveThreadIdByProjectId(nextProjectId);
212 if (Option.isNone(existingThreadId)) {211 if (Option.isNone(existingThreadId)) {
213 const createdAt = new Date().toISOString();212 const createdAt = DateTime.formatIso(yield* DateTime.now);
214 const createdThreadId = ThreadId.make(crypto.randomUUID());213 const createdThreadId = ThreadId.make(crypto.randomUUID());
215 yield* orchestrationEngine.dispatch({214 yield* orchestrationEngine.dispatch({
216 type: "thread.create",215 type: "thread.create",
...@@ -423,7 +422,7 @@ export const makeServerRuntimeStartup = Effect.gen(function* () {...@@ -423,7 +422,7 @@ export const makeServerRuntimeStartup = Effect.gen(function* () {
423 version: 1,422 version: 1,
424 type: "ready",423 type: "ready",
425 payload: {424 payload: {
426 at: new Date().toISOString(),425 at: DateTime.formatIso(yield* DateTime.now),
427 environment: yield* serverEnvironment.getDescriptor,426 environment: yield* serverEnvironment.getDescriptor,
428 },427 },
429 }),428 }),
apps/server/src/serverRuntimeState.ts+14-9
...@@ -1,4 +1,8 @@...@@ -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
3import { writeFileStringAtomically } from "./atomicWrite.ts";7import { writeFileStringAtomically } from "./atomicWrite.ts";
4import { type ServerConfigShape } from "./config.ts";8import { type ServerConfigShape } from "./config.ts";
...@@ -30,14 +34,15 @@ const runtimeOriginForConfig = (...@@ -30,14 +34,15 @@ const runtimeOriginForConfig = (
30export const makePersistedServerRuntimeState = (input: {34export const makePersistedServerRuntimeState = (input: {
31 readonly config: Pick<ServerConfigShape, "host">;35 readonly config: Pick<ServerConfigShape, "host">;
32 readonly port: number;36 readonly port: number;
33}): PersistedServerRuntimeState => ({37}): Effect.Effect<PersistedServerRuntimeState> =>
34 version: 1,38 Effect.map(DateTime.now, (now) => ({
35 pid: process.pid,39 version: 1,
36 ...(input.config.host ? { host: input.config.host } : {}),40 pid: process.pid,
37 port: input.port,41 ...(input.config.host ? { host: input.config.host } : {}),
38 origin: runtimeOriginForConfig(input.config, input.port),42 port: input.port,
39 startedAt: new Date().toISOString(),43 origin: runtimeOriginForConfig(input.config, input.port),
40});44 startedAt: DateTime.formatIso(now),
45 }));
4146
42export const persistServerRuntimeState = (input: {47export const persistServerRuntimeState = (input: {
43 readonly path: string;48 readonly path: string;
apps/server/src/serverSettings.test.ts+6-1
...@@ -8,7 +8,10 @@ import {...@@ -8,7 +8,10 @@ import {
8} from "@t3tools/contracts";8} from "@t3tools/contracts";
9import { createModelSelection } from "@t3tools/shared/model";9import { createModelSelection } from "@t3tools/shared/model";
10import { assert, it } from "@effect/vitest";10import { 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";
12import { ServerConfig } from "./config.ts";15import { ServerConfig } from "./config.ts";
13import { ServerSettingsLive, ServerSettingsService } from "./serverSettings.ts";16import { ServerSettingsLive, ServerSettingsService } from "./serverSettings.ts";
1417
...@@ -439,6 +442,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {...@@ -439,6 +442,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {
439 assert.equal(next.providers.codex.binaryPath, "/opt/homebrew/bin/codex");442 assert.equal(next.providers.codex.binaryPath, "/opt/homebrew/bin/codex");
440443
441 const raw = yield* fileSystem.readFileString(serverConfig.settingsPath);444 const raw = yield* fileSystem.readFileString(serverConfig.settingsPath);
445 // @effect-diagnostics-next-line preferSchemaOverJson:off
442 assert.deepEqual(JSON.parse(raw), {446 assert.deepEqual(JSON.parse(raw), {
443 addProjectBaseDirectory: "~/Development",447 addProjectBaseDirectory: "~/Development",
444 observability: {448 observability: {
...@@ -490,6 +494,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {...@@ -490,6 +494,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {
490494
491 const raw = yield* fileSystem.readFileString(serverConfig.settingsPath);495 const raw = yield* fileSystem.readFileString(serverConfig.settingsPath);
492 assert.notInclude(raw, "sk-or-secret");496 assert.notInclude(raw, "sk-or-secret");
497 // @effect-diagnostics-next-line preferSchemaOverJson:off
493 assert.deepEqual(JSON.parse(raw).providerInstances.codex_personal.environment, [498 assert.deepEqual(JSON.parse(raw).providerInstances.codex_personal.environment, [
494 {499 {
495 name: "OPENROUTER_API_KEY",500 name: "OPENROUTER_API_KEY",
apps/server/src/serverSettings.ts+17-19
...@@ -24,25 +24,23 @@ import {...@@ -24,25 +24,23 @@ import {
24 ServerSettingsError,24 ServerSettingsError,
25 type ServerSettingsPatch,25 type ServerSettingsPatch,
26} from "@t3tools/contracts";26} from "@t3tools/contracts";
27import {27import * as Cache from "effect/Cache";
28 Cache,28import * as Deferred from "effect/Deferred";
29 Deferred,29import * as Duration from "effect/Duration";
30 Duration,30import * as Effect from "effect/Effect";
31 Effect,31import * as Exit from "effect/Exit";
32 Exit,32import * as FileSystem from "effect/FileSystem";
33 FileSystem,33import * as Layer from "effect/Layer";
34 Layer,34import * as Path from "effect/Path";
35 Path,35import * as Equal from "effect/Equal";
36 Equal,36import * as PubSub from "effect/PubSub";
37 PubSub,37import * as Ref from "effect/Ref";
38 Ref,38import * as Schema from "effect/Schema";
39 Schema,39import * as SchemaIssue from "effect/SchemaIssue";
40 SchemaIssue,40import * as Scope from "effect/Scope";
41 Scope,41import * as Context from "effect/Context";
42 Context,42import * as Stream from "effect/Stream";
43 Stream,43import * as Cause from "effect/Cause";
44 Cause,
45} from "effect";
46import * as Semaphore from "effect/Semaphore";44import * as Semaphore from "effect/Semaphore";
47import { writeFileStringAtomically } from "./atomicWrite.ts";45import { writeFileStringAtomically } from "./atomicWrite.ts";
48import { ServerConfig } from "./config.ts";46import { ServerConfig } from "./config.ts";
apps/server/src/sourceControl/AzureDevOpsCli.test.ts+9-3
...@@ -1,8 +1,10 @@...@@ -1,8 +1,10 @@
1import * as NodeServices from "@effect/platform-node/NodeServices";1import * as NodeServices from "@effect/platform-node/NodeServices";
2import { assert, it, afterEach, describe, expect, vi } from "@effect/vitest";2import { 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";
4import { ChildProcessSpawner } from "effect/unstable/process";7import { ChildProcessSpawner } from "effect/unstable/process";
5import type { VcsError } from "@t3tools/contracts";
68
7import * as VcsProcess from "../vcs/VcsProcess.ts";9import * as VcsProcess from "../vcs/VcsProcess.ts";
8import * as AzureDevOpsCli from "./AzureDevOpsCli.ts";10import * as AzureDevOpsCli from "./AzureDevOpsCli.ts";
...@@ -35,6 +37,7 @@ describe("AzureDevOpsCli.layer", () => {...@@ -35,6 +37,7 @@ describe("AzureDevOpsCli.layer", () => {
35 mockRun.mockReturnValueOnce(37 mockRun.mockReturnValueOnce(
36 Effect.succeed(38 Effect.succeed(
37 processOutput(39 processOutput(
40 // @effect-diagnostics-next-line preferSchemaOverJson:off
38 JSON.stringify({41 JSON.stringify({
39 pullRequestId: 42,42 pullRequestId: 42,
40 title: "Add Azure provider",43 title: "Add Azure provider",
...@@ -91,6 +94,7 @@ describe("AzureDevOpsCli.layer", () => {...@@ -91,6 +94,7 @@ describe("AzureDevOpsCli.layer", () => {
91 mockRun.mockReturnValueOnce(94 mockRun.mockReturnValueOnce(
92 Effect.succeed(95 Effect.succeed(
93 processOutput(96 processOutput(
97 // @effect-diagnostics-next-line preferSchemaOverJson:off
94 JSON.stringify([98 JSON.stringify([
95 {99 {
96 pullRequestId: 7,100 pullRequestId: 7,
...@@ -149,6 +153,7 @@ describe("AzureDevOpsCli.layer", () => {...@@ -149,6 +153,7 @@ describe("AzureDevOpsCli.layer", () => {
149 mockRun.mockReturnValueOnce(153 mockRun.mockReturnValueOnce(
150 Effect.succeed(154 Effect.succeed(
151 processOutput(155 processOutput(
156 // @effect-diagnostics-next-line preferSchemaOverJson:off
152 JSON.stringify({157 JSON.stringify({
153 name: "repo",158 name: "repo",
154 webUrl: "https://dev.azure.com/acme/project/_git/repo",159 webUrl: "https://dev.azure.com/acme/project/_git/repo",
...@@ -181,6 +186,7 @@ describe("AzureDevOpsCli.layer", () => {...@@ -181,6 +186,7 @@ describe("AzureDevOpsCli.layer", () => {
181 mockRun.mockReturnValueOnce(186 mockRun.mockReturnValueOnce(
182 Effect.succeed(187 Effect.succeed(
183 processOutput(188 processOutput(
189 // @effect-diagnostics-next-line preferSchemaOverJson:off
184 JSON.stringify({190 JSON.stringify({
185 name: "repo",191 name: "repo",
186 webUrl: "https://dev.azure.com/acme/project/_git/repo",192 webUrl: "https://dev.azure.com/acme/project/_git/repo",
...@@ -231,7 +237,7 @@ describe("AzureDevOpsCli.layer", () => {...@@ -231,7 +237,7 @@ describe("AzureDevOpsCli.layer", () => {
231 it.effect("creates pull requests using the body file as the Azure description", () =>237 it.effect("creates pull requests using the body file as the Azure description", () =>
232 Effect.gen(function* () {238 Effect.gen(function* () {
233 const fileSystem = yield* FileSystem.FileSystem;239 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`;
235 yield* fileSystem.writeFileString(bodyFile, "Generated body");241 yield* fileSystem.writeFileString(bodyFile, "Generated body");
236 mockRun.mockReturnValueOnce(Effect.succeed(processOutput("{}")));242 mockRun.mockReturnValueOnce(Effect.succeed(processOutput("{}")));
237243
apps/server/src/sourceControl/AzureDevOpsCli.ts+6-1
...@@ -1,4 +1,9 @@...@@ -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";
2import {7import {
3 TrimmedNonEmptyString,8 TrimmedNonEmptyString,
4 type SourceControlRepositoryVisibility,9 type SourceControlRepositoryVisibility,
apps/server/src/sourceControl/AzureDevOpsSourceControlProvider.test.ts+3-1
...@@ -1,5 +1,7 @@...@@ -1,5 +1,7 @@
1import { assert, it } from "@effect/vitest";1import { 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
4import * as AzureDevOpsCli from "./AzureDevOpsCli.ts";6import * as AzureDevOpsCli from "./AzureDevOpsCli.ts";
5import * as AzureDevOpsSourceControlProvider from "./AzureDevOpsSourceControlProvider.ts";7import * as AzureDevOpsSourceControlProvider from "./AzureDevOpsSourceControlProvider.ts";
apps/server/src/sourceControl/AzureDevOpsSourceControlProvider.ts+7-6
...@@ -1,4 +1,5 @@...@@ -1,4 +1,5 @@
1import { Effect, Layer } from "effect";1import * as Effect from "effect/Effect";
2import * as Layer from "effect/Layer";
2import { SourceControlProviderError, type ChangeRequest } from "@t3tools/contracts";3import { SourceControlProviderError, type ChangeRequest } from "@t3tools/contracts";
34
4import * as AzureDevOpsCli from "./AzureDevOpsCli.ts";5import * as AzureDevOpsCli from "./AzureDevOpsCli.ts";
...@@ -30,7 +31,7 @@ function parseAzureAuth(input: SourceControlProviderDiscovery.SourceControlAuthP...@@ -30,7 +31,7 @@ function parseAzureAuth(input: SourceControlProviderDiscovery.SourceControlAuthP
30 });31 });
31 }32 }
3233
33 if (account && account.length > 0) {34 if (account !== undefined && account.length > 0) {
34 return SourceControlProviderDiscovery.providerAuth({35 return SourceControlProviderDiscovery.providerAuth({
35 status: "authenticated",36 status: "authenticated",
36 account,37 account,
...@@ -90,7 +91,7 @@ export const make = Effect.fn("makeAzureDevOpsSourceControlProvider")(function*...@@ -90,7 +91,7 @@ export const make = Effect.fn("makeAzureDevOpsSourceControlProvider")(function*
90 .listPullRequests({91 .listPullRequests({
91 cwd: input.cwd,92 cwd: input.cwd,
92 headSelector: input.headSelector,93 headSelector: input.headSelector,
93 ...(source ? { source } : {}),94 ...(source !== undefined ? { source } : {}),
94 state: input.state,95 state: input.state,
95 ...(input.limit !== undefined ? { limit: input.limit } : {}),96 ...(input.limit !== undefined ? { limit: input.limit } : {}),
96 })97 })
...@@ -111,8 +112,8 @@ export const make = Effect.fn("makeAzureDevOpsSourceControlProvider")(function*...@@ -111,8 +112,8 @@ export const make = Effect.fn("makeAzureDevOpsSourceControlProvider")(function*
111 cwd: input.cwd,112 cwd: input.cwd,
112 baseBranch: input.baseRefName,113 baseBranch: input.baseRefName,
113 headSelector: input.headSelector,114 headSelector: input.headSelector,
114 ...(source ? { source } : {}),115 ...(source !== undefined ? { source } : {}),
115 ...(input.target ? { target: input.target } : {}),116 ...(input.target !== undefined ? { target: input.target } : {}),
116 title: input.title,117 title: input.title,
117 bodyFile: input.bodyFile,118 bodyFile: input.bodyFile,
118 })119 })
...@@ -135,7 +136,7 @@ export const make = Effect.fn("makeAzureDevOpsSourceControlProvider")(function*...@@ -135,7 +136,7 @@ export const make = Effect.fn("makeAzureDevOpsSourceControlProvider")(function*
135 .checkoutPullRequest({136 .checkoutPullRequest({
136 cwd: input.cwd,137 cwd: input.cwd,
137 reference: input.reference,138 reference: input.reference,
138 ...(input.context ? { remoteName: input.context.remoteName } : {}),139 ...(input.context !== undefined ? { remoteName: input.context.remoteName } : {}),
139 })140 })
140 .pipe(Effect.mapError((error) => providerError("checkoutChangeRequest", error))),141 .pipe(Effect.mapError((error) => providerError("checkoutChangeRequest", error))),
141 });142 });
apps/server/src/sourceControl/BitbucketApi.test.ts+8-1
...@@ -1,6 +1,11 @@...@@ -1,6 +1,11 @@
1import { assert, it, vi } from "@effect/vitest";1import { assert, it, vi } from "@effect/vitest";
2import * as NodeServices from "@effect/platform-node/NodeServices";2import * 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";
4import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http";9import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http";
510
6import * as BitbucketApi from "./BitbucketApi.ts";11import * as BitbucketApi from "./BitbucketApi.ts";
...@@ -423,6 +428,7 @@ it.effect("creates repositories through the Bitbucket REST API", () => {...@@ -423,6 +428,7 @@ it.effect("creates repositories through the Bitbucket REST API", () => {
423 assert.ok(request);428 assert.ok(request);
424 const rawBody = (request.body as { readonly body?: Uint8Array }).body;429 const rawBody = (request.body as { readonly body?: Uint8Array }).body;
425 assert.ok(rawBody);430 assert.ok(rawBody);
431 // @effect-diagnostics-next-line preferSchemaOverJson:off
426 assert.deepStrictEqual(JSON.parse(new TextDecoder().decode(rawBody)), {432 assert.deepStrictEqual(JSON.parse(new TextDecoder().decode(rawBody)), {
427 scm: "git",433 scm: "git",
428 is_private: true,434 is_private: true,
...@@ -458,6 +464,7 @@ it.effect("creates pull requests using the official REST payload shape", () => {...@@ -458,6 +464,7 @@ it.effect("creates pull requests using the official REST payload shape", () => {
458 assert.ok(request);464 assert.ok(request);
459 const rawBody = (request.body as { readonly body?: Uint8Array }).body;465 const rawBody = (request.body as { readonly body?: Uint8Array }).body;
460 assert.ok(rawBody);466 assert.ok(rawBody);
467 // @effect-diagnostics-next-line preferSchemaOverJson:off
461 assert.deepStrictEqual(JSON.parse(new TextDecoder().decode(rawBody)), {468 assert.deepStrictEqual(JSON.parse(new TextDecoder().decode(rawBody)), {
462 title: "Provider PR",469 title: "Provider PR",
463 description: "PR body",470 description: "PR body",
apps/server/src/sourceControl/BitbucketApi.ts+7-1
...@@ -1,4 +1,10 @@...@@ -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";
2import {8import {
3 TrimmedNonEmptyString,9 TrimmedNonEmptyString,
4 type SourceControlProviderAuth,10 type SourceControlProviderAuth,
apps/server/src/sourceControl/BitbucketSourceControlProvider.test.ts+3-1
...@@ -1,5 +1,7 @@...@@ -1,5 +1,7 @@
1import { assert, it } from "@effect/vitest";1import { 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
4import * as BitbucketApi from "./BitbucketApi.ts";6import * as BitbucketApi from "./BitbucketApi.ts";
5import * as BitbucketSourceControlProvider from "./BitbucketSourceControlProvider.ts";7import * as BitbucketSourceControlProvider from "./BitbucketSourceControlProvider.ts";
apps/server/src/sourceControl/BitbucketSourceControlProvider.ts+3-1
...@@ -1,4 +1,6 @@...@@ -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";
2import { SourceControlProviderError, type ChangeRequest } from "@t3tools/contracts";4import { SourceControlProviderError, type ChangeRequest } from "@t3tools/contracts";
35
4import * as BitbucketApi from "./BitbucketApi.ts";6import * as BitbucketApi from "./BitbucketApi.ts";
apps/server/src/sourceControl/GitHubCli.test.ts+7-2
...@@ -1,7 +1,8 @@...@@ -1,7 +1,8 @@
1import { assert, it, afterEach, describe, expect, vi } from "@effect/vitest";1import { 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";
3import { ChildProcessSpawner } from "effect/unstable/process";4import { ChildProcessSpawner } from "effect/unstable/process";
4import { VcsProcessExitError, type VcsError } from "@t3tools/contracts";5import { VcsProcessExitError } from "@t3tools/contracts";
56
6import * as VcsProcess from "../vcs/VcsProcess.ts";7import * as VcsProcess from "../vcs/VcsProcess.ts";
7import * as GitHubCli from "./GitHubCli.ts";8import * as GitHubCli from "./GitHubCli.ts";
...@@ -34,6 +35,7 @@ describe("GitHubCli.layer", () => {...@@ -34,6 +35,7 @@ describe("GitHubCli.layer", () => {
34 mockRun.mockReturnValueOnce(35 mockRun.mockReturnValueOnce(
35 Effect.succeed(36 Effect.succeed(
36 processOutput(37 processOutput(
38 // @effect-diagnostics-next-line preferSchemaOverJson:off
37 JSON.stringify({39 JSON.stringify({
38 number: 42,40 number: 42,
39 title: "Add PR thread creation",41 title: "Add PR thread creation",
...@@ -92,6 +94,7 @@ describe("GitHubCli.layer", () => {...@@ -92,6 +94,7 @@ describe("GitHubCli.layer", () => {
92 mockRun.mockReturnValueOnce(94 mockRun.mockReturnValueOnce(
93 Effect.succeed(95 Effect.succeed(
94 processOutput(96 processOutput(
97 // @effect-diagnostics-next-line preferSchemaOverJson:off
95 JSON.stringify({98 JSON.stringify({
96 number: 42,99 number: 42,
97 title: " Add PR thread creation \n",100 title: " Add PR thread creation \n",
...@@ -137,6 +140,7 @@ describe("GitHubCli.layer", () => {...@@ -137,6 +140,7 @@ describe("GitHubCli.layer", () => {
137 mockRun.mockReturnValueOnce(140 mockRun.mockReturnValueOnce(
138 Effect.succeed(141 Effect.succeed(
139 processOutput(142 processOutput(
143 // @effect-diagnostics-next-line preferSchemaOverJson:off
140 JSON.stringify([144 JSON.stringify([
141 {145 {
142 number: 0,146 number: 0,
...@@ -187,6 +191,7 @@ describe("GitHubCli.layer", () => {...@@ -187,6 +191,7 @@ describe("GitHubCli.layer", () => {
187 mockRun.mockReturnValueOnce(191 mockRun.mockReturnValueOnce(
188 Effect.succeed(192 Effect.succeed(
189 processOutput(193 processOutput(
194 // @effect-diagnostics-next-line preferSchemaOverJson:off
190 JSON.stringify({195 JSON.stringify({
191 nameWithOwner: "octocat/codething-mvp",196 nameWithOwner: "octocat/codething-mvp",
192 url: "https://github.com/octocat/codething-mvp",197 url: "https://github.com/octocat/codething-mvp",
apps/server/src/sourceControl/GitHubCli.ts+6-1
...@@ -1,4 +1,9 @@...@@ -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
3import {8import {
4 TrimmedNonEmptyString,9 TrimmedNonEmptyString,
apps/server/src/sourceControl/GitHubSourceControlProvider.test.ts+4-1
...@@ -1,5 +1,8 @@...@@ -1,5 +1,8 @@
1import { assert, it } from "@effect/vitest";1import { 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";
3import { ChildProcessSpawner } from "effect/unstable/process";6import { ChildProcessSpawner } from "effect/unstable/process";
47
5import * as VcsProcess from "../vcs/VcsProcess.ts";8import * as VcsProcess from "../vcs/VcsProcess.ts";
apps/server/src/sourceControl/GitHubSourceControlProvider.ts+5-1
...@@ -1,4 +1,8 @@...@@ -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";
2import {6import {
3 SourceControlProviderError,7 SourceControlProviderError,
4 type ChangeRequest,8 type ChangeRequest,
apps/server/src/sourceControl/GitLabCli.test.ts+15-2
...@@ -1,5 +1,6 @@...@@ -1,5 +1,6 @@
1import { assert, it, afterEach, expect, vi } from "@effect/vitest";1import { 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";
3import { ChildProcessSpawner } from "effect/unstable/process";4import { ChildProcessSpawner } from "effect/unstable/process";
45
5import { VcsProcessExitError } from "@t3tools/contracts";6import { VcsProcessExitError } from "@t3tools/contracts";
...@@ -38,6 +39,7 @@ layer("GitLabCli.layer", (it) => {...@@ -38,6 +39,7 @@ layer("GitLabCli.layer", (it) => {
38 mockedRun.mockReturnValueOnce(39 mockedRun.mockReturnValueOnce(
39 Effect.succeed(40 Effect.succeed(
40 processOutput(41 processOutput(
42 // @effect-diagnostics-next-line preferSchemaOverJson:off
41 JSON.stringify({43 JSON.stringify({
42 iid: 42,44 iid: 42,
43 title: "Add MR thread creation",45 title: "Add MR thread creation",
...@@ -89,6 +91,7 @@ layer("GitLabCli.layer", (it) => {...@@ -89,6 +91,7 @@ layer("GitLabCli.layer", (it) => {
89 mockedRun.mockReturnValueOnce(91 mockedRun.mockReturnValueOnce(
90 Effect.succeed(92 Effect.succeed(
91 processOutput(93 processOutput(
94 // @effect-diagnostics-next-line preferSchemaOverJson:off
92 JSON.stringify([95 JSON.stringify([
93 {96 {
94 iid: 0,97 iid: 0,
...@@ -154,6 +157,7 @@ layer("GitLabCli.layer", (it) => {...@@ -154,6 +157,7 @@ layer("GitLabCli.layer", (it) => {
154 mockedRun.mockReturnValueOnce(157 mockedRun.mockReturnValueOnce(
155 Effect.succeed(158 Effect.succeed(
156 processOutput(159 processOutput(
160 // @effect-diagnostics-next-line preferSchemaOverJson:off
157 JSON.stringify({161 JSON.stringify({
158 path_with_namespace: "octocat/t3code",162 path_with_namespace: "octocat/t3code",
159 web_url: "https://gitlab.com/octocat/t3code",163 web_url: "https://gitlab.com/octocat/t3code",
...@@ -219,10 +223,19 @@ layer("GitLabCli.layer", (it) => {...@@ -219,10 +223,19 @@ layer("GitLabCli.layer", (it) => {
219 it.effect("creates repositories under an explicit namespace", () =>223 it.effect("creates repositories under an explicit namespace", () =>
220 Effect.gen(function* () {224 Effect.gen(function* () {
221 mockedRun225 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 )
223 .mockReturnValueOnce(235 .mockReturnValueOnce(
224 Effect.succeed(236 Effect.succeed(
225 processOutput(237 processOutput(
238 // @effect-diagnostics-next-line preferSchemaOverJson:off
226 JSON.stringify({239 JSON.stringify({
227 path_with_namespace: "octocat/t3code",240 path_with_namespace: "octocat/t3code",
228 web_url: "https://gitlab.com/octocat/t3code",241 web_url: "https://gitlab.com/octocat/t3code",
apps/server/src/sourceControl/GitLabCli.ts+8-1
...@@ -1,4 +1,11 @@...@@ -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
3import { TrimmedNonEmptyString, type SourceControlRepositoryVisibility } from "@t3tools/contracts";10import { TrimmedNonEmptyString, type SourceControlRepositoryVisibility } from "@t3tools/contracts";
411
apps/server/src/sourceControl/GitLabSourceControlProvider.test.ts+3-1
...@@ -1,5 +1,7 @@...@@ -1,5 +1,7 @@
1import { assert, it } from "@effect/vitest";1import { 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
4import * as GitLabCli from "./GitLabCli.ts";6import * as GitLabCli from "./GitLabCli.ts";
5import * as GitLabSourceControlProvider from "./GitLabSourceControlProvider.ts";7import * as GitLabSourceControlProvider from "./GitLabSourceControlProvider.ts";
apps/server/src/sourceControl/GitLabSourceControlProvider.ts+3-1
...@@ -1,4 +1,6 @@...@@ -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";
2import { SourceControlProviderError, type ChangeRequest } from "@t3tools/contracts";4import { SourceControlProviderError, type ChangeRequest } from "@t3tools/contracts";
35
4import * as GitLabCli from "./GitLabCli.ts";6import * as GitLabCli from "./GitLabCli.ts";
apps/server/src/sourceControl/SourceControlDiscovery.test.ts+3-1
...@@ -1,6 +1,8 @@...@@ -1,6 +1,8 @@
1import { assert, it } from "@effect/vitest";1import { assert, it } from "@effect/vitest";
2import * as NodeServices from "@effect/platform-node/NodeServices";2import * 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";
4import { ChildProcessSpawner } from "effect/unstable/process";6import { ChildProcessSpawner } from "effect/unstable/process";
5import { VcsProcessSpawnError } from "@t3tools/contracts";7import { VcsProcessSpawnError } from "@t3tools/contracts";
68
apps/server/src/sourceControl/SourceControlDiscovery.ts+4-1
...@@ -3,7 +3,10 @@ import {...@@ -3,7 +3,10 @@ import {
3 type VcsDiscoveryItem,3 type VcsDiscoveryItem,
4 type VcsDriverKind,4 type VcsDriverKind,
5} from "@t3tools/contracts";5} 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
8import { ServerConfig } from "../config.ts";11import { ServerConfig } from "../config.ts";
9import * as VcsProcess from "../vcs/VcsProcess.ts";12import * as VcsProcess from "../vcs/VcsProcess.ts";
apps/server/src/sourceControl/SourceControlProvider.ts+2-1
...@@ -1,4 +1,5 @@...@@ -1,4 +1,5 @@
1import { Context, Effect } from "effect";1import * as Context from "effect/Context";
2import * as Effect from "effect/Effect";
2import type {3import type {
3 ChangeRequest,4 ChangeRequest,
4 ChangeRequestState,5 ChangeRequestState,
apps/server/src/sourceControl/SourceControlProviderDiscovery.ts+2-1
...@@ -3,7 +3,8 @@ import type {...@@ -3,7 +3,8 @@ import type {
3 SourceControlProviderDiscoveryItem,3 SourceControlProviderDiscoveryItem,
4 SourceControlProviderKind,4 SourceControlProviderKind,
5} from "@t3tools/contracts";5} from "@t3tools/contracts";
6import { Effect, Option } from "effect";6import * as Effect from "effect/Effect";
7import * as Option from "effect/Option";
78
8import type * as VcsProcess from "../vcs/VcsProcess.ts";9import type * as VcsProcess from "../vcs/VcsProcess.ts";
910
apps/server/src/sourceControl/SourceControlProviderRegistry.test.ts+4-1
...@@ -1,6 +1,9 @@...@@ -1,6 +1,9 @@
1import { assert, it } from "@effect/vitest";1import { assert, it } from "@effect/vitest";
2import * as NodeServices from "@effect/platform-node/NodeServices";2import * 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
5import { ServerConfig } from "../config.ts";8import { ServerConfig } from "../config.ts";
6import type * as VcsDriver from "../vcs/VcsDriver.ts";9import type * as VcsDriver from "../vcs/VcsDriver.ts";
apps/server/src/sourceControl/SourceControlProviderRegistry.ts+6-1
...@@ -1,4 +1,9 @@...@@ -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";
2import {7import {
3 SourceControlProviderError,8 SourceControlProviderError,
4 type SourceControlProviderDiscoveryItem,9 type SourceControlProviderDiscoveryItem,
apps/server/src/sourceControl/SourceControlRepositoryService.test.ts+3-1
...@@ -1,6 +1,8 @@...@@ -1,6 +1,8 @@
1import * as NodeServices from "@effect/platform-node/NodeServices";1import * as NodeServices from "@effect/platform-node/NodeServices";
2import { assert, it } from "@effect/vitest";2import { assert, it } from "@effect/vitest";
3import { Effect, FileSystem, Layer } from "effect";3import * as Effect from "effect/Effect";
4import * as FileSystem from "effect/FileSystem";
5import * as Layer from "effect/Layer";
4import { ChildProcessSpawner } from "effect/unstable/process";6import { ChildProcessSpawner } from "effect/unstable/process";
57
6import { GitCommandError, type SourceControlProviderError } from "@t3tools/contracts";8import { GitCommandError, type SourceControlProviderError } from "@t3tools/contracts";
apps/server/src/sourceControl/SourceControlRepositoryService.ts+6-1
...@@ -1,5 +1,10 @@...@@ -1,5 +1,10 @@
1import * as NodeOS from "node:os";1import * 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
4import {9import {
5 SourceControlRepositoryError,10 SourceControlRepositoryError,
apps/server/src/sourceControl/azureDevOpsPullRequests.ts+6-1
...@@ -1,4 +1,9 @@...@@ -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";
2import { PositiveInt, TrimmedNonEmptyString } from "@t3tools/contracts";7import { PositiveInt, TrimmedNonEmptyString } from "@t3tools/contracts";
3import { decodeJsonResult, formatSchemaError } from "@t3tools/shared/schemaJson";8import { decodeJsonResult, formatSchemaError } from "@t3tools/shared/schemaJson";
49
apps/server/src/sourceControl/bitbucketPullRequests.ts+3-1
...@@ -1,4 +1,6 @@...@@ -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";
2import { PositiveInt, TrimmedNonEmptyString } from "@t3tools/contracts";4import { PositiveInt, TrimmedNonEmptyString } from "@t3tools/contracts";
35
4export interface NormalizedBitbucketPullRequestRecord {6export interface NormalizedBitbucketPullRequestRecord {
apps/server/src/sourceControl/gitHubPullRequests.ts+6-1
...@@ -1,4 +1,9 @@...@@ -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";
2import { PositiveInt, TrimmedNonEmptyString } from "@t3tools/contracts";7import { PositiveInt, TrimmedNonEmptyString } from "@t3tools/contracts";
3import { decodeJsonResult, formatSchemaError } from "@t3tools/shared/schemaJson";8import { decodeJsonResult, formatSchemaError } from "@t3tools/shared/schemaJson";
49
apps/server/src/sourceControl/gitLabMergeRequests.ts+6-1
...@@ -1,4 +1,9 @@...@@ -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";
2import { PositiveInt, TrimmedNonEmptyString } from "@t3tools/contracts";7import { PositiveInt, TrimmedNonEmptyString } from "@t3tools/contracts";
3import { decodeJsonResult, formatSchemaError } from "@t3tools/shared/schemaJson";8import { decodeJsonResult, formatSchemaError } from "@t3tools/shared/schemaJson";
49
apps/server/src/startupAccess.ts+1-1
...@@ -1,7 +1,7 @@...@@ -1,7 +1,7 @@
1import { networkInterfaces } from "node:os";1import { networkInterfaces } from "node:os";
22
3import { QrCode } from "@t3tools/shared/qrCode";3import { QrCode } from "@t3tools/shared/qrCode";
4import { Effect } from "effect";4import * as Effect from "effect/Effect";
5import { HttpServer } from "effect/unstable/http";5import { HttpServer } from "effect/unstable/http";
66
7import { ServerConfig } from "./config.ts";7import { ServerConfig } from "./config.ts";
apps/server/src/stream/collectUint8StreamText.test.ts+2-1
...@@ -1,5 +1,6 @@...@@ -1,5 +1,6 @@
1import { assert, describe, it } from "@effect/vitest";1import { assert, describe, it } from "@effect/vitest";
2import { Effect, Stream } from "effect";2import * as Effect from "effect/Effect";
3import * as Stream from "effect/Stream";
34
4import { collectUint8StreamText } from "./collectUint8StreamText.ts";5import { collectUint8StreamText } from "./collectUint8StreamText.ts";
56
apps/server/src/stream/collectUint8StreamText.ts+2-1
...@@ -1,4 +1,5 @@...@@ -1,4 +1,5 @@
1import { Effect, Stream } from "effect";1import * as Effect from "effect/Effect";
2import * as Stream from "effect/Stream";
23
3export interface CollectedUint8StreamText {4export interface CollectedUint8StreamText {
4 readonly text: string;5 readonly text: string;
apps/server/src/telemetry/Identify.ts+5-1
...@@ -1,4 +1,8 @@...@@ -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";
2import * as Crypto from "node:crypto";6import * as Crypto from "node:crypto";
3import { homedir } from "node:os";7import { homedir } from "node:os";
4import { ServerConfig } from "../config.ts";8import { ServerConfig } from "../config.ts";
apps/server/src/telemetry/Layers/AnalyticsService.test.ts+3-1
...@@ -1,7 +1,9 @@...@@ -1,7 +1,9 @@
1import * as NodeHttpServer from "@effect/platform-node/NodeHttpServer";1import * as NodeHttpServer from "@effect/platform-node/NodeHttpServer";
2import * as NodeServices from "@effect/platform-node/NodeServices";2import * as NodeServices from "@effect/platform-node/NodeServices";
3import { assert, it } from "@effect/vitest";3import { 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";
5import * as HttpServer from "effect/unstable/http/HttpServer";7import * as HttpServer from "effect/unstable/http/HttpServer";
6import * as HttpServerRequest from "effect/unstable/http/HttpServerRequest";8import * as HttpServerRequest from "effect/unstable/http/HttpServerRequest";
7import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse";9import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse";
apps/server/src/telemetry/Layers/AnalyticsService.ts+5-1
...@@ -7,7 +7,11 @@...@@ -7,7 +7,11 @@
7 * @module AnalyticsServiceLive7 * @module AnalyticsServiceLive
8 */8 */
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";
11import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http";15import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http";
1216
13import { ServerConfig } from "../../config.ts";17import { ServerConfig } from "../../config.ts";
apps/server/src/telemetry/Services/AnalyticsService.ts+3-1
...@@ -6,7 +6,9 @@...@@ -6,7 +6,9 @@
6 *6 *
7 * @module AnalyticsService7 * @module AnalyticsService
8 */8 */
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
11export interface AnalyticsServiceShape {13export interface AnalyticsServiceShape {
12 /**14 /**
apps/server/src/terminal/Layers/BunPTY.ts+2-1
...@@ -1,4 +1,5 @@...@@ -1,4 +1,5 @@
1import { Effect, Layer } from "effect";1import * as Effect from "effect/Effect";
2import * as Layer from "effect/Layer";
2import { PtyAdapter } from "../Services/PTY.ts";3import { PtyAdapter } from "../Services/PTY.ts";
3import type { PtyAdapterShape, PtyExitEvent, PtyProcess } from "../Services/PTY.ts";4import type { PtyAdapterShape, PtyExitEvent, PtyProcess } from "../Services/PTY.ts";
45
apps/server/src/terminal/Layers/Manager.test.ts+31-38
...@@ -1,5 +1,3 @@...@@ -1,5 +1,3 @@
1import path from "node:path";
2
3import * as NodeServices from "@effect/platform-node/NodeServices";1import * as NodeServices from "@effect/platform-node/NodeServices";
4import { assert, it } from "@effect/vitest";2import { assert, it } from "@effect/vitest";
5import {3import {
...@@ -8,20 +6,19 @@ import {...@@ -8,20 +6,19 @@ import {
8 type TerminalOpenInput,6 type TerminalOpenInput,
9 type TerminalRestartInput,7 type TerminalRestartInput,
10} from "@t3tools/contracts";8} from "@t3tools/contracts";
11import {9import * as Cause from "effect/Cause";
12 Duration,10import * as Duration from "effect/Duration";
13 Effect,11import * as Effect from "effect/Effect";
14 Encoding,12import * as Encoding from "effect/Encoding";
15 Exit,13import * as Exit from "effect/Exit";
16 Fiber,14import * as Fiber from "effect/Fiber";
17 FileSystem,15import * as FileSystem from "effect/FileSystem";
18 Option,16import * as Path from "effect/Path";
19 PlatformError,17import * as PlatformError from "effect/PlatformError";
20 Ref,18import * as Ref from "effect/Ref";
21 Schedule,19import * as Schedule from "effect/Schedule";
22 Scope,20import * as Scope from "effect/Scope";
23} from "effect";21import * as TestClock from "effect/testing/TestClock";
24import { TestClock } from "effect/testing";
25import { expect } from "vitest";22import { expect } from "vitest";
2623
27import type { TerminalManagerShape } from "../Services/Manager.ts";24import type { TerminalManagerShape } from "../Services/Manager.ts";
...@@ -130,20 +127,11 @@ class FakePtyAdapter implements PtyAdapterShape {...@@ -130,20 +127,11 @@ class FakePtyAdapter implements PtyAdapterShape {
130const waitFor = <E, R>(127const waitFor = <E, R>(
131 predicate: Effect.Effect<boolean, E, R>,128 predicate: Effect.Effect<boolean, E, R>,
132 timeout: Duration.Input = 800,129 timeout: Duration.Input = 800,
133): Effect.Effect<void, Error | E, R> =>130): Effect.Effect<void, E | Cause.TimeoutError, R> =>
134 predicate.pipe(131 predicate.pipe(
135 Effect.filterOrFail(132 Effect.filter((done) => done),
136 (done) => done,
137 () => new Error("Condition not met"),
138 ),
139 Effect.retry(Schedule.spaced("15 millis")),133 Effect.retry(Schedule.spaced("15 millis")),
140 Effect.timeoutOption(timeout),134 Effect.timeout(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 ),
147 );135 );
148136
149function openInput(overrides: Partial<TerminalOpenInput> = {}): TerminalOpenInput {137function openInput(overrides: Partial<TerminalOpenInput> = {}): TerminalOpenInput {
...@@ -181,7 +169,7 @@ function multiTerminalHistoryLogName(threadId: string, terminalId: string): stri...@@ -181,7 +169,7 @@ function multiTerminalHistoryLogName(threadId: string, terminalId: string): stri
181}169}
182170
183function historyLogPath(logsDir: string, threadId = "thread-1"): string {171function historyLogPath(logsDir: string, threadId = "thread-1"): string {
184 return path.join(logsDir, historyLogName(threadId));172 return joinPath(logsDir, historyLogName(threadId));
185}173}
186174
187function multiTerminalHistoryLogPath(175function multiTerminalHistoryLogPath(
...@@ -189,7 +177,11 @@ function multiTerminalHistoryLogPath(...@@ -189,7 +177,11 @@ function multiTerminalHistoryLogPath(
189 threadId = "thread-1",177 threadId = "thread-1",
190 terminalId = "default",178 terminalId = "default",
191): string {179): 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("/");
193}185}
194186
195interface CreateManagerOptions {187interface CreateManagerOptions {
...@@ -217,12 +209,13 @@ const createManager = (...@@ -217,12 +209,13 @@ const createManager = (
217): Effect.Effect<209): Effect.Effect<
218 ManagerFixture,210 ManagerFixture,
219 PlatformError.PlatformError,211 PlatformError.PlatformError,
220 FileSystem.FileSystem | Scope.Scope212 FileSystem.FileSystem | Path.Path | Scope.Scope
221> =>213> =>
222 Effect.flatMap(Effect.service(FileSystem.FileSystem), (fs) =>214 Effect.flatMap(Effect.service(FileSystem.FileSystem), (fs) =>
223 Effect.gen(function* () {215 Effect.gen(function* () {
216 const pathService = yield* Path.Path;
224 const baseDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3code-terminal-" });217 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");
226 const ptyAdapter = options.ptyAdapter ?? new FakePtyAdapter();219 const ptyAdapter = options.ptyAdapter ?? new FakePtyAdapter();
227220
228 const manager = yield* makeTerminalManagerWithOptions({221 const manager = yield* makeTerminalManagerWithOptions({
...@@ -302,8 +295,8 @@ it.layer(NodeServices.layer, { excludeTestServices: true })("TerminalManager", (...@@ -302,8 +295,8 @@ it.layer(NodeServices.layer, { excludeTestServices: true })("TerminalManager", (
302 if (process.platform === "win32") return;295 if (process.platform === "win32") return;
303296
304 const { manager, baseDir } = yield* createManager();297 const { manager, baseDir } = yield* createManager();
305 const blockedRoot = path.join(baseDir, "blocked-root");298 const blockedRoot = joinPath(baseDir, "blocked-root");
306 const blockedCwd = path.join(blockedRoot, "cwd");299 const blockedCwd = joinPath(blockedRoot, "cwd");
307 yield* makeDirectory(blockedCwd);300 yield* makeDirectory(blockedCwd);
308 yield* chmod(blockedRoot, 0o000);301 yield* chmod(blockedRoot, 0o000);
309302
...@@ -441,8 +434,8 @@ it.layer(NodeServices.layer, { excludeTestServices: true })("TerminalManager", (...@@ -441,8 +434,8 @@ it.layer(NodeServices.layer, { excludeTestServices: true })("TerminalManager", (
441 it.effect("propagates explicit worktree metadata through snapshots and lifecycle events", () =>434 it.effect("propagates explicit worktree metadata through snapshots and lifecycle events", () =>
442 Effect.gen(function* () {435 Effect.gen(function* () {
443 const { manager, getEvents, baseDir } = yield* createManager();436 const { manager, getEvents, baseDir } = yield* createManager();
444 const firstWorktreePath = path.join(baseDir, "worktrees", "feature-a");437 const firstWorktreePath = joinPath(baseDir, "worktrees", "feature-a");
445 const secondWorktreePath = path.join(baseDir, "worktrees", "feature-b");438 const secondWorktreePath = joinPath(baseDir, "worktrees", "feature-b");
446 yield* makeDirectory(firstWorktreePath);439 yield* makeDirectory(firstWorktreePath);
447 yield* makeDirectory(secondWorktreePath);440 yield* makeDirectory(secondWorktreePath);
448 const startedSnapshot = yield* manager.open(441 const startedSnapshot = yield* manager.open(
...@@ -478,7 +471,7 @@ it.layer(NodeServices.layer, { excludeTestServices: true })("TerminalManager", (...@@ -478,7 +471,7 @@ it.layer(NodeServices.layer, { excludeTestServices: true })("TerminalManager", (
478 it.effect("preserves worktree metadata when reopening an exited session", () =>471 it.effect("preserves worktree metadata when reopening an exited session", () =>
479 Effect.gen(function* () {472 Effect.gen(function* () {
480 const { manager, ptyAdapter, getEvents, baseDir } = yield* createManager();473 const { manager, ptyAdapter, getEvents, baseDir } = yield* createManager();
481 const worktreePath = path.join(baseDir, "worktrees", "feature-a");474 const worktreePath = joinPath(baseDir, "worktrees", "feature-a");
482 yield* makeDirectory(worktreePath);475 yield* makeDirectory(worktreePath);
483476
484 yield* manager.open(477 yield* manager.open(
...@@ -816,7 +809,7 @@ it.layer(NodeServices.layer, { excludeTestServices: true })("TerminalManager", (...@@ -816,7 +809,7 @@ it.layer(NodeServices.layer, { excludeTestServices: true })("TerminalManager", (
816 it.effect("migrates legacy transcript filenames to terminal-scoped history path on open", () =>809 it.effect("migrates legacy transcript filenames to terminal-scoped history path on open", () =>
817 Effect.gen(function* () {810 Effect.gen(function* () {
818 const { manager, logsDir } = yield* createManager();811 const { manager, logsDir } = yield* createManager();
819 const legacyPath = path.join(logsDir, "thread-1.log");812 const legacyPath = joinPath(logsDir, "thread-1.log");
820 const nextPath = historyLogPath(logsDir);813 const nextPath = historyLogPath(logsDir);
821 yield* writeFileString(legacyPath, "legacy-line\n");814 yield* writeFileString(legacyPath, "legacy-line\n");
822815
apps/server/src/terminal/Layers/Manager.ts+61-40
...@@ -1,5 +1,3 @@...@@ -1,5 +1,3 @@
1import path from "node:path";
2
3import {1import {
4 DEFAULT_TERMINAL_ID,2 DEFAULT_TERMINAL_ID,
5 type TerminalEvent,3 type TerminalEvent,
...@@ -7,20 +5,20 @@ import {...@@ -7,20 +5,20 @@ import {
7 type TerminalSessionStatus,5 type TerminalSessionStatus,
8} from "@t3tools/contracts";6} from "@t3tools/contracts";
9import { makeKeyedCoalescingWorker } from "@t3tools/shared/KeyedCoalescingWorker";7import { makeKeyedCoalescingWorker } from "@t3tools/shared/KeyedCoalescingWorker";
10import {8import * as Effect from "effect/Effect";
11 Effect,9import * as Encoding from "effect/Encoding";
12 Encoding,10import * as Equal from "effect/Equal";
13 Equal,11import * as Exit from "effect/Exit";
14 Exit,12import * as Fiber from "effect/Fiber";
15 Fiber,13import * as FileSystem from "effect/FileSystem";
16 FileSystem,14import * as DateTime from "effect/DateTime";
17 Layer,15import * as Layer from "effect/Layer";
18 Option,16import * as Option from "effect/Option";
19 Schema,17import * as Path from "effect/Path";
20 Scope,18import * as Schema from "effect/Schema";
21 Semaphore,19import * as Scope from "effect/Scope";
22 SynchronizedRef,20import * as Semaphore from "effect/Semaphore";
23} from "effect";21import * as SynchronizedRef from "effect/SynchronizedRef";
2422
25import { ServerConfig } from "../../config.ts";23import { ServerConfig } from "../../config.ts";
26import {24import {
...@@ -213,15 +211,25 @@ function normalizeShellCommand(...@@ -213,15 +211,25 @@ function normalizeShellCommand(
213 return firstToken.replace(/^['"]|['"]$/g, "");211 return firstToken.replace(/^['"]|['"]$/g, "");
214}212}
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
216function shellCandidateFromCommand(227function shellCandidateFromCommand(
217 command: string | null,228 command: string | null,
218 platform: NodeJS.Platform = process.platform,229 platform: NodeJS.Platform = process.platform,
219): ShellCandidate | null {230): ShellCandidate | null {
220 if (!command || command.length === 0) return null;231 if (!command || command.length === 0) return null;
221 const shellName =232 const shellName = basename(command, platform).toLowerCase();
222 platform === "win32"
223 ? path.win32.basename(command).toLowerCase()
224 : path.basename(command).toLowerCase();
225 if (platform === "win32" && (shellName === "pwsh.exe" || shellName === "powershell.exe")) {233 if (platform === "win32" && (shellName === "pwsh.exe" || shellName === "powershell.exe")) {
226 return { shell: command, args: ["-NoLogo"] };234 return { shell: command, args: ["-NoLogo"] };
227 }235 }
...@@ -236,7 +244,7 @@ function windowsSystemRoot(env: NodeJS.ProcessEnv): string {...@@ -236,7 +244,7 @@ function windowsSystemRoot(env: NodeJS.ProcessEnv): string {
236}244}
237245
238function windowsPowerShellPath(env: NodeJS.ProcessEnv): string {246function windowsPowerShellPath(env: NodeJS.ProcessEnv): string {
239 return path.win32.join(247 return joinWindowsPath(
240 windowsSystemRoot(env),248 windowsSystemRoot(env),
241 "System32",249 "System32",
242 "WindowsPowerShell",250 "WindowsPowerShell",
...@@ -246,7 +254,7 @@ function windowsPowerShellPath(env: NodeJS.ProcessEnv): string {...@@ -246,7 +254,7 @@ function windowsPowerShellPath(env: NodeJS.ProcessEnv): string {
246}254}
247255
248function windowsCmdPath(env: NodeJS.ProcessEnv): string {256function windowsCmdPath(env: NodeJS.ProcessEnv): string {
249 return path.win32.join(windowsSystemRoot(env), "System32", "cmd.exe");257 return joinWindowsPath(windowsSystemRoot(env), "System32", "cmd.exe");
250}258}
251259
252function formatShellCandidate(candidate: ShellCandidate): string {260function formatShellCandidate(candidate: ShellCandidate): string {
...@@ -714,8 +722,10 @@ const makeTerminalManager = Effect.fn("makeTerminalManager")(function* () {...@@ -714,8 +722,10 @@ const makeTerminalManager = Effect.fn("makeTerminalManager")(function* () {
714export const makeTerminalManagerWithOptions = Effect.fn("makeTerminalManagerWithOptions")(722export const makeTerminalManagerWithOptions = Effect.fn("makeTerminalManagerWithOptions")(
715 function* (options: TerminalManagerOptions) {723 function* (options: TerminalManagerOptions) {
716 const fileSystem = yield* FileSystem.FileSystem;724 const fileSystem = yield* FileSystem.FileSystem;
725 const path = yield* Path.Path;
717 const context = yield* Effect.context<never>();726 const context = yield* Effect.context<never>();
718 const runFork = Effect.runForkWith(context);727 const runFork = Effect.runForkWith(context);
728 const nowIso = Effect.map(DateTime.now, DateTime.formatIso);
719729
720 const logsDir = options.logsDir;730 const logsDir = options.logsDir;
721 const historyLineLimit = options.historyLineLimit ?? DEFAULT_HISTORY_LINE_LIMIT;731 const historyLineLimit = options.historyLineLimit ?? DEFAULT_HISTORY_LINE_LIMIT;
...@@ -1160,6 +1170,7 @@ export const makeTerminalManagerWithOptions = Effect.fn("makeTerminalManagerWith...@@ -1160,6 +1170,7 @@ export const makeTerminalManagerWithOptions = Effect.fn("makeTerminalManagerWith
1160 expectedPid: number,1170 expectedPid: number,
1161 ) {1171 ) {
1162 while (true) {1172 while (true) {
1173 const updatedAt = yield* nowIso;
1163 const action: DrainProcessEventAction = yield* Effect.sync(() => {1174 const action: DrainProcessEventAction = yield* Effect.sync(() => {
1164 if (session.pid !== expectedPid || !session.process || session.status !== "running") {1175 if (session.pid !== expectedPid || !session.process || session.status !== "running") {
1165 session.pendingProcessEvents = [];1176 session.pendingProcessEvents = [];
...@@ -1194,7 +1205,7 @@ export const makeTerminalManagerWithOptions = Effect.fn("makeTerminalManagerWith...@@ -1194,7 +1205,7 @@ export const makeTerminalManagerWithOptions = Effect.fn("makeTerminalManagerWith
1194 historyLineLimit,1205 historyLineLimit,
1195 );1206 );
1196 }1207 }
1197 session.updatedAt = new Date().toISOString();1208 session.updatedAt = updatedAt;
11981209
1199 return {1210 return {
1200 type: "output",1211 type: "output",
...@@ -1221,7 +1232,7 @@ export const makeTerminalManagerWithOptions = Effect.fn("makeTerminalManagerWith...@@ -1221,7 +1232,7 @@ export const makeTerminalManagerWithOptions = Effect.fn("makeTerminalManagerWith
1221 session.exitSignal = Number.isInteger(nextEvent.event.signal)1232 session.exitSignal = Number.isInteger(nextEvent.event.signal)
1222 ? nextEvent.event.signal1233 ? nextEvent.event.signal
1223 : null;1234 : null;
1224 session.updatedAt = new Date().toISOString();1235 session.updatedAt = updatedAt;
12251236
1226 return {1237 return {
1227 type: "exit",1238 type: "exit",
...@@ -1242,22 +1253,24 @@ export const makeTerminalManagerWithOptions = Effect.fn("makeTerminalManagerWith...@@ -1242,22 +1253,24 @@ export const makeTerminalManagerWithOptions = Effect.fn("makeTerminalManagerWith
1242 yield* queuePersist(action.threadId, action.terminalId, action.history);1253 yield* queuePersist(action.threadId, action.terminalId, action.history);
1243 }1254 }
12441255
1256 const createdAt = yield* nowIso;
1245 yield* publishEvent({1257 yield* publishEvent({
1246 type: "output",1258 type: "output",
1247 threadId: action.threadId,1259 threadId: action.threadId,
1248 terminalId: action.terminalId,1260 terminalId: action.terminalId,
1249 createdAt: new Date().toISOString(),1261 createdAt,
1250 data: action.data,1262 data: action.data,
1251 });1263 });
1252 continue;1264 continue;
1253 }1265 }
12541266
1255 yield* clearKillFiber(action.process);1267 yield* clearKillFiber(action.process);
1268 const createdAt = yield* nowIso;
1256 yield* publishEvent({1269 yield* publishEvent({
1257 type: "exited",1270 type: "exited",
1258 threadId: action.threadId,1271 threadId: action.threadId,
1259 terminalId: action.terminalId,1272 terminalId: action.terminalId,
1260 createdAt: new Date().toISOString(),1273 createdAt,
1261 exitCode: action.exitCode,1274 exitCode: action.exitCode,
1262 exitSignal: action.exitSignal,1275 exitSignal: action.exitSignal,
1263 });1276 });
...@@ -1272,6 +1285,7 @@ export const makeTerminalManagerWithOptions = Effect.fn("makeTerminalManagerWith...@@ -1272,6 +1285,7 @@ export const makeTerminalManagerWithOptions = Effect.fn("makeTerminalManagerWith
1272 const process = session.process;1285 const process = session.process;
1273 if (!process) return;1286 if (!process) return;
12741287
1288 const updatedAt = yield* nowIso;
1275 yield* modifyManagerState((state) => {1289 yield* modifyManagerState((state) => {
1276 cleanupProcessHandles(session);1290 cleanupProcessHandles(session);
1277 session.process = null;1291 session.process = null;
...@@ -1282,7 +1296,7 @@ export const makeTerminalManagerWithOptions = Effect.fn("makeTerminalManagerWith...@@ -1282,7 +1296,7 @@ export const makeTerminalManagerWithOptions = Effect.fn("makeTerminalManagerWith
1282 session.pendingProcessEvents = [];1296 session.pendingProcessEvents = [];
1283 session.pendingProcessEventIndex = 0;1297 session.pendingProcessEventIndex = 0;
1284 session.processEventDrainRunning = false;1298 session.processEventDrainRunning = false;
1285 session.updatedAt = new Date().toISOString();1299 session.updatedAt = updatedAt;
1286 return [undefined, state] as const;1300 return [undefined, state] as const;
1287 });1301 });
12881302
...@@ -1361,6 +1375,7 @@ export const makeTerminalManagerWithOptions = Effect.fn("makeTerminalManagerWith...@@ -1361,6 +1375,7 @@ export const makeTerminalManagerWithOptions = Effect.fn("makeTerminalManagerWith
1361 "terminal.cwd": input.cwd,1375 "terminal.cwd": input.cwd,
1362 });1376 });
13631377
1378 const startingAt = yield* nowIso;
1364 yield* modifyManagerState((state) => {1379 yield* modifyManagerState((state) => {
1365 session.status = "starting";1380 session.status = "starting";
1366 session.cwd = input.cwd;1381 session.cwd = input.cwd;
...@@ -1373,7 +1388,7 @@ export const makeTerminalManagerWithOptions = Effect.fn("makeTerminalManagerWith...@@ -1373,7 +1388,7 @@ export const makeTerminalManagerWithOptions = Effect.fn("makeTerminalManagerWith
1373 session.pendingProcessEvents = [];1388 session.pendingProcessEvents = [];
1374 session.pendingProcessEventIndex = 0;1389 session.pendingProcessEventIndex = 0;
1375 session.processEventDrainRunning = false;1390 session.processEventDrainRunning = false;
1376 session.updatedAt = new Date().toISOString();1391 session.updatedAt = startingAt;
1377 return [undefined, state] as const;1392 return [undefined, state] as const;
1378 });1393 });
13791394
...@@ -1404,11 +1419,12 @@ export const makeTerminalManagerWithOptions = Effect.fn("makeTerminalManagerWith...@@ -1404,11 +1419,12 @@ export const makeTerminalManagerWithOptions = Effect.fn("makeTerminalManagerWith
1404 runFork(drainProcessEvents(session, processPid));1419 runFork(drainProcessEvents(session, processPid));
1405 });1420 });
14061421
1422 const startedAt = yield* nowIso;
1407 yield* modifyManagerState((state) => {1423 yield* modifyManagerState((state) => {
1408 session.process = ptyProcess;1424 session.process = ptyProcess;
1409 session.pid = processPid;1425 session.pid = processPid;
1410 session.status = "running";1426 session.status = "running";
1411 session.updatedAt = new Date().toISOString();1427 session.updatedAt = startedAt;
1412 session.unsubscribeData = unsubscribeData;1428 session.unsubscribeData = unsubscribeData;
1413 session.unsubscribeExit = unsubscribeExit;1429 session.unsubscribeExit = unsubscribeExit;
1414 return [undefined, state] as const;1430 return [undefined, state] as const;
...@@ -1418,7 +1434,7 @@ export const makeTerminalManagerWithOptions = Effect.fn("makeTerminalManagerWith...@@ -1418,7 +1434,7 @@ export const makeTerminalManagerWithOptions = Effect.fn("makeTerminalManagerWith
1418 type: eventType,1434 type: eventType,
1419 threadId: session.threadId,1435 threadId: session.threadId,
1420 terminalId: session.terminalId,1436 terminalId: session.terminalId,
1421 createdAt: new Date().toISOString(),1437 createdAt: startedAt,
1422 snapshot: snapshot(session),1438 snapshot: snapshot(session),
1423 });1439 });
1424 }),1440 }),
...@@ -1436,6 +1452,7 @@ export const makeTerminalManagerWithOptions = Effect.fn("makeTerminalManagerWith...@@ -1436,6 +1452,7 @@ export const makeTerminalManagerWithOptions = Effect.fn("makeTerminalManagerWith
1436 yield* startKillEscalation(ptyProcess, session.threadId, session.terminalId);1452 yield* startKillEscalation(ptyProcess, session.threadId, session.terminalId);
1437 }1453 }
14381454
1455 const failedAt = yield* nowIso;
1439 yield* modifyManagerState((state) => {1456 yield* modifyManagerState((state) => {
1440 session.status = "error";1457 session.status = "error";
1441 session.pid = null;1458 session.pid = null;
...@@ -1446,7 +1463,7 @@ export const makeTerminalManagerWithOptions = Effect.fn("makeTerminalManagerWith...@@ -1446,7 +1463,7 @@ export const makeTerminalManagerWithOptions = Effect.fn("makeTerminalManagerWith
1446 session.pendingProcessEvents = [];1463 session.pendingProcessEvents = [];
1447 session.pendingProcessEventIndex = 0;1464 session.pendingProcessEventIndex = 0;
1448 session.processEventDrainRunning = false;1465 session.processEventDrainRunning = false;
1449 session.updatedAt = new Date().toISOString();1466 session.updatedAt = failedAt;
1450 return [undefined, state] as const;1467 return [undefined, state] as const;
1451 });1468 });
14521469
...@@ -1457,7 +1474,7 @@ export const makeTerminalManagerWithOptions = Effect.fn("makeTerminalManagerWith...@@ -1457,7 +1474,7 @@ export const makeTerminalManagerWithOptions = Effect.fn("makeTerminalManagerWith
1457 type: "error",1474 type: "error",
1458 threadId: session.threadId,1475 threadId: session.threadId,
1459 terminalId: session.terminalId,1476 terminalId: session.terminalId,
1460 createdAt: new Date().toISOString(),1477 createdAt: failedAt,
1461 message,1478 message,
1462 });1479 });
1463 yield* Effect.logError("failed to start terminal", {1480 yield* Effect.logError("failed to start terminal", {
...@@ -1529,6 +1546,7 @@ export const makeTerminalManagerWithOptions = Effect.fn("makeTerminalManagerWith...@@ -1529,6 +1546,7 @@ export const makeTerminalManagerWithOptions = Effect.fn("makeTerminalManagerWith
1529 return;1546 return;
1530 }1547 }
15311548
1549 const updatedAt = yield* nowIso;
1532 const event = yield* modifyManagerState((state) => {1550 const event = yield* modifyManagerState((state) => {
1533 const liveSession: Option.Option<TerminalSessionState> = Option.fromNullishOr(1551 const liveSession: Option.Option<TerminalSessionState> = Option.fromNullishOr(
1534 state.sessions.get(toSessionKey(session.threadId, session.terminalId)),1552 state.sessions.get(toSessionKey(session.threadId, session.terminalId)),
...@@ -1543,14 +1561,14 @@ export const makeTerminalManagerWithOptions = Effect.fn("makeTerminalManagerWith...@@ -1543,14 +1561,14 @@ export const makeTerminalManagerWithOptions = Effect.fn("makeTerminalManagerWith
1543 }1561 }
15441562
1545 liveSession.value.hasRunningSubprocess = hasRunningSubprocess.value;1563 liveSession.value.hasRunningSubprocess = hasRunningSubprocess.value;
1546 liveSession.value.updatedAt = new Date().toISOString();1564 liveSession.value.updatedAt = updatedAt;
15471565
1548 return [1566 return [
1549 Option.some({1567 Option.some({
1550 type: "activity" as const,1568 type: "activity" as const,
1551 threadId: liveSession.value.threadId,1569 threadId: liveSession.value.threadId,
1552 terminalId: liveSession.value.terminalId,1570 terminalId: liveSession.value.terminalId,
1553 createdAt: new Date().toISOString(),1571 createdAt: updatedAt,
1554 hasRunningSubprocess: hasRunningSubprocess.value,1572 hasRunningSubprocess: hasRunningSubprocess.value,
1555 }),1573 }),
1556 state,1574 state,
...@@ -1629,6 +1647,7 @@ export const makeTerminalManagerWithOptions = Effect.fn("makeTerminalManagerWith...@@ -1629,6 +1647,7 @@ export const makeTerminalManagerWithOptions = Effect.fn("makeTerminalManagerWith
1629 const history = yield* readHistory(input.threadId, terminalId);1647 const history = yield* readHistory(input.threadId, terminalId);
1630 const cols = input.cols ?? DEFAULT_OPEN_COLS;1648 const cols = input.cols ?? DEFAULT_OPEN_COLS;
1631 const rows = input.rows ?? DEFAULT_OPEN_ROWS;1649 const rows = input.rows ?? DEFAULT_OPEN_ROWS;
1650 const createdAt = yield* nowIso;
1632 const session: TerminalSessionState = {1651 const session: TerminalSessionState = {
1633 threadId: input.threadId,1652 threadId: input.threadId,
1634 terminalId,1653 terminalId,
...@@ -1643,7 +1662,7 @@ export const makeTerminalManagerWithOptions = Effect.fn("makeTerminalManagerWith...@@ -1643,7 +1662,7 @@ export const makeTerminalManagerWithOptions = Effect.fn("makeTerminalManagerWith
1643 processEventDrainRunning: false,1662 processEventDrainRunning: false,
1644 exitCode: null,1663 exitCode: null,
1645 exitSignal: null,1664 exitSignal: null,
1646 updatedAt: new Date().toISOString(),1665 updatedAt: createdAt,
1647 cols,1666 cols,
1648 rows,1667 rows,
1649 process: null,1668 process: null,
...@@ -1734,7 +1753,7 @@ export const makeTerminalManagerWithOptions = Effect.fn("makeTerminalManagerWith...@@ -1734,7 +1753,7 @@ export const makeTerminalManagerWithOptions = Effect.fn("makeTerminalManagerWith
1734 if (liveSession.cols !== targetCols || liveSession.rows !== targetRows) {1753 if (liveSession.cols !== targetCols || liveSession.rows !== targetRows) {
1735 liveSession.cols = targetCols;1754 liveSession.cols = targetCols;
1736 liveSession.rows = targetRows;1755 liveSession.rows = targetRows;
1737 liveSession.updatedAt = new Date().toISOString();1756 liveSession.updatedAt = yield* nowIso;
1738 liveSession.process.resize(targetCols, targetRows);1757 liveSession.process.resize(targetCols, targetRows);
1739 }1758 }
17401759
...@@ -1768,7 +1787,7 @@ export const makeTerminalManagerWithOptions = Effect.fn("makeTerminalManagerWith...@@ -1768,7 +1787,7 @@ export const makeTerminalManagerWithOptions = Effect.fn("makeTerminalManagerWith
1768 }1787 }
1769 session.cols = input.cols;1788 session.cols = input.cols;
1770 session.rows = input.rows;1789 session.rows = input.rows;
1771 session.updatedAt = new Date().toISOString();1790 session.updatedAt = yield* nowIso;
1772 yield* Effect.sync(() => process.resize(input.cols, input.rows));1791 yield* Effect.sync(() => process.resize(input.cols, input.rows));
1773 });1792 });
17741793
...@@ -1783,13 +1802,14 @@ export const makeTerminalManagerWithOptions = Effect.fn("makeTerminalManagerWith...@@ -1783,13 +1802,14 @@ export const makeTerminalManagerWithOptions = Effect.fn("makeTerminalManagerWith
1783 session.pendingProcessEvents = [];1802 session.pendingProcessEvents = [];
1784 session.pendingProcessEventIndex = 0;1803 session.pendingProcessEventIndex = 0;
1785 session.processEventDrainRunning = false;1804 session.processEventDrainRunning = false;
1786 session.updatedAt = new Date().toISOString();1805 const clearedAt = yield* nowIso;
1806 session.updatedAt = clearedAt;
1787 yield* persistHistory(input.threadId, terminalId, session.history);1807 yield* persistHistory(input.threadId, terminalId, session.history);
1788 yield* publishEvent({1808 yield* publishEvent({
1789 type: "cleared",1809 type: "cleared",
1790 threadId: input.threadId,1810 threadId: input.threadId,
1791 terminalId,1811 terminalId,
1792 createdAt: new Date().toISOString(),1812 createdAt: clearedAt,
1793 });1813 });
1794 }),1814 }),
1795 );1815 );
...@@ -1808,6 +1828,7 @@ export const makeTerminalManagerWithOptions = Effect.fn("makeTerminalManagerWith...@@ -1808,6 +1828,7 @@ export const makeTerminalManagerWithOptions = Effect.fn("makeTerminalManagerWith
1808 if (Option.isNone(existingSession)) {1828 if (Option.isNone(existingSession)) {
1809 const cols = input.cols ?? DEFAULT_OPEN_COLS;1829 const cols = input.cols ?? DEFAULT_OPEN_COLS;
1810 const rows = input.rows ?? DEFAULT_OPEN_ROWS;1830 const rows = input.rows ?? DEFAULT_OPEN_ROWS;
1831 const createdAt = yield* nowIso;
1811 session = {1832 session = {
1812 threadId: input.threadId,1833 threadId: input.threadId,
1813 terminalId,1834 terminalId,
...@@ -1822,7 +1843,7 @@ export const makeTerminalManagerWithOptions = Effect.fn("makeTerminalManagerWith...@@ -1822,7 +1843,7 @@ export const makeTerminalManagerWithOptions = Effect.fn("makeTerminalManagerWith
1822 processEventDrainRunning: false,1843 processEventDrainRunning: false,
1823 exitCode: null,1844 exitCode: null,
1824 exitSignal: null,1845 exitSignal: null,
1825 updatedAt: new Date().toISOString(),1846 updatedAt: createdAt,
1826 cols,1847 cols,
1827 rows,1848 rows,
1828 process: null,1849 process: null,
apps/server/src/terminal/Layers/NodePTY.test.ts+3-1
...@@ -1,4 +1,6 @@...@@ -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";
2import { assert, it } from "@effect/vitest";4import { assert, it } from "@effect/vitest";
35
4import { ensureNodePtySpawnHelperExecutable } from "./NodePTY.ts";6import { ensureNodePtySpawnHelperExecutable } from "./NodePTY.ts";
apps/server/src/terminal/Layers/NodePTY.ts+4-1
...@@ -1,6 +1,9 @@...@@ -1,6 +1,9 @@
1import { createRequire } from "node:module";1import { 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";
4import { PtyAdapter } from "../Services/PTY.ts";7import { PtyAdapter } from "../Services/PTY.ts";
5import {8import {
6 PtySpawnError,9 PtySpawnError,
apps/server/src/terminal/Services/Manager.ts+2-1
...@@ -23,7 +23,8 @@ import {...@@ -23,7 +23,8 @@ import {
23 TerminalWriteInput,23 TerminalWriteInput,
24} from "@t3tools/contracts";24} from "@t3tools/contracts";
25import type { PtyProcess } from "./PTY.ts";25import type { PtyProcess } from "./PTY.ts";
26import { Effect, Context } from "effect";26import * as Effect from "effect/Effect";
27import * as Context from "effect/Context";
2728
28export {29export {
29 TerminalCwdError,30 TerminalCwdError,
apps/server/src/terminal/Services/PTY.ts+3-1
...@@ -6,7 +6,9 @@...@@ -6,7 +6,9 @@
6 *6 *
7 * @module PtyAdapter7 * @module PtyAdapter
8 */8 */
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
11/**13/**
12 * PtyError - Error type for PTY adapter operations.14 * PtyError - Error type for PTY adapter operations.
apps/server/src/textGeneration/ClaudeTextGeneration.test.ts+6-1
...@@ -1,7 +1,11 @@...@@ -1,7 +1,11 @@
1import { ClaudeSettings, ProviderInstanceId } from "@t3tools/contracts";1import { ClaudeSettings, ProviderInstanceId } from "@t3tools/contracts";
2import * as NodeServices from "@effect/platform-node/NodeServices";2import * as NodeServices from "@effect/platform-node/NodeServices";
3import { it } from "@effect/vitest";3import { 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";
5import { createModelSelection } from "@t3tools/shared/model";9import { createModelSelection } from "@t3tools/shared/model";
6import { expect } from "vitest";10import { expect } from "vitest";
711
...@@ -287,6 +291,7 @@ it.layer(ClaudeTextGenerationTestLayer)("ClaudeTextGeneration", (it) => {...@@ -287,6 +291,7 @@ it.layer(ClaudeTextGenerationTestLayer)("ClaudeTextGeneration", (it) => {
287 const claudeHome = path.join(process.cwd(), ".claude-work-test");291 const claudeHome = path.join(process.cwd(), ".claude-work-test");
288 return yield* withFakeClaudeEnv(292 return yield* withFakeClaudeEnv(
289 {293 {
294 // @effect-diagnostics-next-line preferSchemaOverJson:off
290 output: JSON.stringify({295 output: JSON.stringify({
291 structured_output: {296 structured_output: {
292 title: "Use Claude home",297 title: "Use Claude home",
apps/server/src/textGeneration/ClaudeTextGeneration.ts+10-3
...@@ -7,7 +7,10 @@...@@ -7,7 +7,10 @@
7 *7 *
8 * @module ClaudeTextGeneration8 * @module ClaudeTextGeneration
9 */9 */
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";
11import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";14import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
1215
13import { type ClaudeSettings, type ModelSelection } from "@t3tools/contracts";16import { type ClaudeSettings, type ModelSelection } from "@t3tools/contracts";
...@@ -93,7 +96,9 @@ export const makeClaudeTextGeneration = Effect.fn("makeClaudeTextGeneration")(fu...@@ -93,7 +96,9 @@ export const makeClaudeTextGeneration = Effect.fn("makeClaudeTextGeneration")(fu
93 outputSchemaJson: S;96 outputSchemaJson: S;
94 modelSelection: ModelSelection;97 modelSelection: ModelSelection;
95 }): Effect.fn.Return<S["Type"], TextGenerationError, S["DecodingServices"]> {98 }): 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 );
97 const caps = getClaudeModelCapabilities(modelSelection.model);102 const caps = getClaudeModelCapabilities(modelSelection.model);
98 const descriptors = getProviderOptionDescriptors({103 const descriptors = getProviderOptionDescriptors({
99 caps,104 caps,
...@@ -126,7 +131,9 @@ export const makeClaudeTextGeneration = Effect.fn("makeClaudeTextGeneration")(fu...@@ -126,7 +131,9 @@ export const makeClaudeTextGeneration = Effect.fn("makeClaudeTextGeneration")(fu
126 "--model",131 "--model",
127 resolveClaudeApiModelId(modelSelection),132 resolveClaudeApiModelId(modelSelection),
128 ...(cliEffort ? ["--effort", cliEffort] : []),133 ...(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 : []),
130 "--dangerously-skip-permissions",137 "--dangerously-skip-permissions",
131 ],138 ],
132 {139 {
apps/server/src/textGeneration/CodexTextGeneration.test.ts+15-5
...@@ -1,6 +1,11 @@...@@ -1,6 +1,11 @@
1import * as NodeServices from "@effect/platform-node/NodeServices";1import * as NodeServices from "@effect/platform-node/NodeServices";
2import { it } from "@effect/vitest";2import { 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";
4import { createModelSelection } from "@t3tools/shared/model";9import { createModelSelection } from "@t3tools/shared/model";
5import { expect } from "vitest";10import { expect } from "vitest";
611
...@@ -113,6 +118,7 @@ function makeFakeCodexBinary(...@@ -113,6 +118,7 @@ function makeFakeCodexBinary(
113 : []),118 : []),
114 ...(input.stdinMustContain !== undefined119 ...(input.stdinMustContain !== undefined
115 ? [120 ? [
121 // @effect-diagnostics-next-line preferSchemaOverJson:off
116 `if ! printf "%s" "$stdin_content" | grep -F -- ${JSON.stringify(input.stdinMustContain)} >/dev/null; then`,122 `if ! printf "%s" "$stdin_content" | grep -F -- ${JSON.stringify(input.stdinMustContain)} >/dev/null; then`,
117 ' printf "%s\\n" "stdin missing expected content" >&2',123 ' printf "%s\\n" "stdin missing expected content" >&2',
118 ` exit 3`,124 ` exit 3`,
...@@ -121,6 +127,7 @@ function makeFakeCodexBinary(...@@ -121,6 +127,7 @@ function makeFakeCodexBinary(
121 : []),127 : []),
122 ...(input.stdinMustNotContain !== undefined128 ...(input.stdinMustNotContain !== undefined
123 ? [129 ? [
130 // @effect-diagnostics-next-line preferSchemaOverJson:off
124 `if printf "%s" "$stdin_content" | grep -F -- ${JSON.stringify(input.stdinMustNotContain)} >/dev/null; then`,131 `if printf "%s" "$stdin_content" | grep -F -- ${JSON.stringify(input.stdinMustNotContain)} >/dev/null; then`,
125 ' printf "%s\\n" "stdin contained forbidden content" >&2',132 ' printf "%s\\n" "stdin contained forbidden content" >&2',
126 ` exit 4`,133 ` exit 4`,
...@@ -128,7 +135,10 @@ function makeFakeCodexBinary(...@@ -128,7 +135,10 @@ function makeFakeCodexBinary(
128 ]135 ]
129 : []),136 : []),
130 ...(input.stderr !== undefined137 ...(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 ]
132 : []),142 : []),
133 'if [ -n "$output_path" ]; then',143 'if [ -n "$output_path" ]; then',
134 " cat > \"$output_path\" <<'__T3CODE_FAKE_CODEX_OUTPUT__'",144 " cat > \"$output_path\" <<'__T3CODE_FAKE_CODEX_OUTPUT__'",
...@@ -415,7 +425,7 @@ it.layer(CodexTextGenerationTestLayer)("CodexTextGeneration", (it) => {...@@ -415,7 +425,7 @@ it.layer(CodexTextGenerationTestLayer)("CodexTextGeneration", (it) => {
415 const fs = yield* FileSystem.FileSystem;425 const fs = yield* FileSystem.FileSystem;
416 const path = yield* Path.Path;426 const path = yield* Path.Path;
417 const { attachmentsDir } = yield* ServerConfig;427 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";
419 const attachmentPath = path.join(attachmentsDir, `${attachmentId}.png`);429 const attachmentPath = path.join(attachmentsDir, `${attachmentId}.png`);
420 yield* fs.makeDirectory(attachmentsDir, { recursive: true });430 yield* fs.makeDirectory(attachmentsDir, { recursive: true });
421 yield* fs.writeFile(attachmentPath, Buffer.from("hello"));431 yield* fs.writeFile(attachmentPath, Buffer.from("hello"));
...@@ -453,7 +463,7 @@ it.layer(CodexTextGenerationTestLayer)("CodexTextGeneration", (it) => {...@@ -453,7 +463,7 @@ it.layer(CodexTextGenerationTestLayer)("CodexTextGeneration", (it) => {
453 const fs = yield* FileSystem.FileSystem;463 const fs = yield* FileSystem.FileSystem;
454 const path = yield* Path.Path;464 const path = yield* Path.Path;
455 const { attachmentsDir } = yield* ServerConfig;465 const { attachmentsDir } = yield* ServerConfig;
456 const attachmentId = `thread-1-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;466 const attachmentId = "thread-1-attachment";
457 const imagePath = path.join(attachmentsDir, `${attachmentId}.png`);467 const imagePath = path.join(attachmentsDir, `${attachmentId}.png`);
458 yield* fs.makeDirectory(attachmentsDir, { recursive: true });468 yield* fs.makeDirectory(attachmentsDir, { recursive: true });
459 yield* fs.writeFile(imagePath, Buffer.from("hello"));469 yield* fs.writeFile(imagePath, Buffer.from("hello"));
...@@ -502,7 +512,7 @@ it.layer(CodexTextGenerationTestLayer)("CodexTextGeneration", (it) => {...@@ -502,7 +512,7 @@ it.layer(CodexTextGenerationTestLayer)("CodexTextGeneration", (it) => {
502 const fs = yield* FileSystem.FileSystem;512 const fs = yield* FileSystem.FileSystem;
503 const path = yield* Path.Path;513 const path = yield* Path.Path;
504 const { attachmentsDir } = yield* ServerConfig;514 const { attachmentsDir } = yield* ServerConfig;
505 const missingAttachmentId = `thread-missing-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;515 const missingAttachmentId = "thread-missing-attachment";
506 const missingPath = path.join(attachmentsDir, `${missingAttachmentId}.png`);516 const missingPath = path.join(attachmentsDir, `${missingAttachmentId}.png`);
507 yield* fs.remove(missingPath).pipe(Effect.catch(() => Effect.void));517 yield* fs.remove(missingPath).pipe(Effect.catch(() => Effect.void));
508518
apps/server/src/textGeneration/CodexTextGeneration.ts+9-2
...@@ -1,4 +1,11 @@...@@ -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";
2import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";9import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
310
4import { type CodexSettings, type ModelSelection } from "@t3tools/contracts";11import { type CodexSettings, type ModelSelection } from "@t3tools/contracts";
...@@ -152,7 +159,7 @@ export const makeCodexTextGeneration = Effect.fn("makeCodexTextGeneration")(func...@@ -152,7 +159,7 @@ export const makeCodexTextGeneration = Effect.fn("makeCodexTextGeneration")(func
152 const schemaPath = yield* writeTempFile(159 const schemaPath = yield* writeTempFile(
153 operation,160 operation,
154 "codex-schema",161 "codex-schema",
155 JSON.stringify(toJsonSchemaObject(outputSchemaJson)),162 Schema.encodeUnknownSync(Schema.UnknownFromJsonString)(toJsonSchemaObject(outputSchemaJson)),
156 );163 );
157 const outputPath = yield* writeTempFile(operation, "codex-output", "");164 const outputPath = yield* writeTempFile(operation, "codex-output", "");
158165
apps/server/src/textGeneration/CursorTextGeneration.test.ts+16-9
...@@ -1,3 +1,4 @@...@@ -1,3 +1,4 @@
1// @effect-diagnostics nodeBuiltinImport:off
1import * as path from "node:path";2import * as path from "node:path";
2import * as os from "node:os";3import * as os from "node:os";
3import { fileURLToPath } from "node:url";4import { fileURLToPath } from "node:url";
...@@ -5,7 +6,11 @@ import { chmodSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync...@@ -5,7 +6,11 @@ import { chmodSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync
56
6import * as NodeServices from "@effect/platform-node/NodeServices";7import * as NodeServices from "@effect/platform-node/NodeServices";
7import { it } from "@effect/vitest";8import { 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";
9import { createModelSelection } from "@t3tools/shared/model";14import { createModelSelection } from "@t3tools/shared/model";
10import { expect } from "vitest";15import { expect } from "vitest";
1116
...@@ -67,17 +72,19 @@ function withFakeAcpAgent<A, E, R>(...@@ -67,17 +72,19 @@ function withFakeAcpAgent<A, E, R>(
67}72}
6873
69function waitForFileContent(path: string): Effect.Effect<string> {74function waitForFileContent(path: string): Effect.Effect<string> {
70 return Effect.promise(async () => {75 return Effect.gen(function* () {
71 const deadline = Date.now() + 5_000;76 const deadline = (yield* Clock.currentTimeMillis) + 5_000;
72 for (;;) {77 for (;;) {
73 try {78 const result = yield* Effect.exit(Effect.sync(() => readFileSync(path, "utf8")));
74 return readFileSync(path, "utf8");79 if (Exit.isSuccess(result)) {
75 } catch (error) {80 return result.value;
76 if (Date.now() >= deadline) {81 }
77 throw error instanceof Error ? error : new Error(String(error));82 {
83 if ((yield* Clock.currentTimeMillis) >= deadline) {
84 return yield* Effect.die(result.cause);
78 }85 }
79 }86 }
80 await new Promise((resolve) => setTimeout(resolve, 25));87 yield* Effect.sleep(25);
81 }88 }
82 });89 });
83}90}
apps/server/src/textGeneration/CursorTextGeneration.ts+4-1
...@@ -1,4 +1,7 @@...@@ -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";
2import { ChildProcessSpawner } from "effect/unstable/process";5import { ChildProcessSpawner } from "effect/unstable/process";
36
4import { type CursorSettings, type ModelSelection } from "@t3tools/contracts";7import { type CursorSettings, type ModelSelection } from "@t3tools/contracts";
apps/server/src/textGeneration/OpenCodeTextGeneration.test.ts+6-3
...@@ -1,9 +1,12 @@...@@ -1,9 +1,12 @@
1import { OpenCodeSettings, ProviderInstanceId } from "@t3tools/contracts";1import { OpenCodeSettings, ProviderInstanceId } from "@t3tools/contracts";
2import * as NodeServices from "@effect/platform-node/NodeServices";2import * as NodeServices from "@effect/platform-node/NodeServices";
3import { it } from "@effect/vitest";3import { it } from "@effect/vitest";
4import { Duration, Effect, Layer, Schema } from "effect";4import * as Duration from "effect/Duration";
5import { TestClock } from "effect/testing";5import * as Effect from "effect/Effect";
6import { NetService } from "@t3tools/shared/Net";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";
7import { beforeEach, expect } from "vitest";10import { beforeEach, expect } from "vitest";
811
9import { ServerConfig } from "../config.ts";12import { ServerConfig } from "../config.ts";
apps/server/src/textGeneration/OpenCodeTextGeneration.ts+5-1
...@@ -1,4 +1,8 @@...@@ -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";
2import * as Semaphore from "effect/Semaphore";6import * as Semaphore from "effect/Semaphore";
37
4import {8import {
apps/server/src/textGeneration/TextGeneration.test.ts+4-1
...@@ -1,5 +1,8 @@...@@ -1,5 +1,8 @@
1import { it } from "@effect/vitest";1import { 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";
3import { describe, expect } from "vitest";6import { describe, expect } from "vitest";
47
5import { ProviderInstanceId } from "@t3tools/contracts";8import { ProviderInstanceId } from "@t3tools/contracts";
apps/server/src/textGeneration/TextGeneration.ts+3-1
...@@ -1,4 +1,6 @@...@@ -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";
2import type { ChatAttachment, ModelSelection, ProviderInstanceId } from "@t3tools/contracts";4import type { ChatAttachment, ModelSelection, ProviderInstanceId } from "@t3tools/contracts";
3import { TextGenerationError } from "@t3tools/contracts";5import { TextGenerationError } from "@t3tools/contracts";
46
apps/server/src/textGeneration/TextGenerationPolicy.ts+1-1
...@@ -1,4 +1,4 @@...@@ -1,4 +1,4 @@
1import { Schema } from "effect";1import * as Schema from "effect/Schema";
22
3export const TextGenerationPolicyKind = Schema.Literals([3export const TextGenerationPolicyKind = Schema.Literals([
4 "default",4 "default",
apps/server/src/textGeneration/TextGenerationPrompts.ts+1-1
...@@ -6,7 +6,7 @@...@@ -6,7 +6,7 @@
6 *6 *
7 * @module textGenerationPrompts7 * @module textGenerationPrompts
8 */8 */
9import { Schema } from "effect";9import * as Schema from "effect/Schema";
10import type { ChatAttachment } from "@t3tools/contracts";10import type { ChatAttachment } from "@t3tools/contracts";
1111
12import { limitSection } from "./TextGenerationUtils.ts";12import { limitSection } from "./TextGenerationUtils.ts";
apps/server/src/textGeneration/TextGenerationUtils.ts+1-1
...@@ -1,4 +1,4 @@...@@ -1,4 +1,4 @@
1import { Schema } from "effect";1import * as Schema from "effect/Schema";
22
3import { TextGenerationError } from "@t3tools/contracts";3import { TextGenerationError } from "@t3tools/contracts";
44
apps/server/src/vcs/GitVcsDriver.test.ts+5-1
...@@ -1,5 +1,9 @@...@@ -1,5 +1,9 @@
1import * as NodeServices from "@effect/platform-node/NodeServices";1import * 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";
3import { ChildProcessSpawner } from "effect/unstable/process";7import { ChildProcessSpawner } from "effect/unstable/process";
4import { assert, it } from "@effect/vitest";8import { assert, it } from "@effect/vitest";
59
apps/server/src/vcs/GitVcsDriver.ts+5-1
...@@ -1,4 +1,8 @@...@@ -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";
2import { ChildProcessSpawner } from "effect/unstable/process";6import { ChildProcessSpawner } from "effect/unstable/process";
37
4import {8import {
apps/server/src/vcs/GitVcsDriverCore.test.ts+6-1
...@@ -1,6 +1,11 @@...@@ -1,6 +1,11 @@
1import * as NodeServices from "@effect/platform-node/NodeServices";1import * as NodeServices from "@effect/platform-node/NodeServices";
2import { assert, it, describe } from "@effect/vitest";2import { 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
5import { GitCommandError } from "@t3tools/contracts";10import { GitCommandError } from "@t3tools/contracts";
6import { ServerConfig } from "../config.ts";11import { ServerConfig } from "../config.ts";
apps/server/src/vcs/GitVcsDriverCore.ts+16-18
...@@ -1,21 +1,19 @@...@@ -1,21 +1,19 @@
1import {1import * as Cache from "effect/Cache";
2 Cache,2import * as Data from "effect/Data";
3 Data,3import * as DateTime from "effect/DateTime";
4 DateTime,4import * as Duration from "effect/Duration";
5 Duration,5import * as Effect from "effect/Effect";
6 Effect,6import * as Exit from "effect/Exit";
7 Exit,7import * as FileSystem from "effect/FileSystem";
8 FileSystem,8import * as Option from "effect/Option";
9 Option,9import * as Path from "effect/Path";
10 Path,10import * as PlatformError from "effect/PlatformError";
11 PlatformError,11import * as Ref from "effect/Ref";
12 Ref,12import * as Result from "effect/Result";
13 Result,13import * as Schema from "effect/Schema";
14 Schema,14import * as Scope from "effect/Scope";
15 Scope,15import * as Semaphore from "effect/Semaphore";
16 Semaphore,16import * as Stream from "effect/Stream";
17 Stream,
18} from "effect";
19import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";17import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
2018
21import { GitCommandError, type VcsRef } from "@t3tools/contracts";19import { GitCommandError, type VcsRef } from "@t3tools/contracts";
apps/server/src/vcs/VcsDriver.ts+2-1
...@@ -1,4 +1,5 @@...@@ -1,4 +1,5 @@
1import { Context, type Effect } from "effect";1import * as Context from "effect/Context";
2import type * as Effect from "effect/Effect";
23
3import type {4import type {
4 VcsDriverCapabilities,5 VcsDriverCapabilities,
apps/server/src/vcs/VcsDriverRegistry.test.ts+2-1
...@@ -1,5 +1,6 @@...@@ -1,5 +1,6 @@
1import { assert, it, describe } from "@effect/vitest";1import { assert, it, describe } from "@effect/vitest";
2import { Effect, Layer } from "effect";2import * as Effect from "effect/Effect";
3import * as Layer from "effect/Layer";
3import { ChildProcessSpawner } from "effect/unstable/process";4import { ChildProcessSpawner } from "effect/unstable/process";
45
5import * as VcsProcess from "./VcsProcess.ts";6import * as VcsProcess from "./VcsProcess.ts";
apps/server/src/vcs/VcsDriverRegistry.ts+6-1
...@@ -1,4 +1,9 @@...@@ -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
3import type { VcsDriverKind, VcsError, VcsRepositoryIdentity } from "@t3tools/contracts";8import type { VcsDriverKind, VcsError, VcsRepositoryIdentity } from "@t3tools/contracts";
4import { VcsUnsupportedOperationError } from "@t3tools/contracts";9import { VcsUnsupportedOperationError } from "@t3tools/contracts";
apps/server/src/vcs/VcsProcess.ts+8-1
...@@ -1,4 +1,11 @@...@@ -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";
2import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";9import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
310
4import {11import {
apps/server/src/vcs/VcsProjectConfig.test.ts+5-1
...@@ -1,6 +1,9 @@...@@ -1,6 +1,9 @@
1import { assert, it, describe } from "@effect/vitest";1import { assert, it, describe } from "@effect/vitest";
2import * as NodeServices from "@effect/platform-node/NodeServices";2import * 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
5import * as VcsProjectConfig from "./VcsProjectConfig.ts";8import * as VcsProjectConfig from "./VcsProjectConfig.ts";
69
...@@ -38,6 +41,7 @@ describe("VcsProjectConfig", () => {...@@ -38,6 +41,7 @@ describe("VcsProjectConfig", () => {
38 yield* fileSystem.makeDirectory(nested, { recursive: true });41 yield* fileSystem.makeDirectory(nested, { recursive: true });
39 yield* fileSystem.writeFileString(42 yield* fileSystem.writeFileString(
40 path.join(configDir, "vcs.json"),43 path.join(configDir, "vcs.json"),
44 // @effect-diagnostics-next-line preferSchemaOverJson:off
41 JSON.stringify({ vcs: { kind: "jj" } }),45 JSON.stringify({ vcs: { kind: "jj" } }),
42 );46 );
4347
apps/server/src/vcs/VcsProjectConfig.ts+6-1
...@@ -1,4 +1,9 @@...@@ -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
3import { VcsDriverKind, type VcsDriverKind as VcsDriverKindType } from "@t3tools/contracts";8import { VcsDriverKind, type VcsDriverKind as VcsDriverKindType } from "@t3tools/contracts";
49
apps/server/src/vcs/VcsProvisioningService.test.ts+4-1
...@@ -1,5 +1,8 @@...@@ -1,5 +1,8 @@
1import { assert, it } from "@effect/vitest";1import { 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";
3import { ChildProcessSpawner } from "effect/unstable/process";6import { ChildProcessSpawner } from "effect/unstable/process";
47
5import * as VcsDriver from "./VcsDriver.ts";8import * as VcsDriver from "./VcsDriver.ts";
apps/server/src/vcs/VcsProvisioningService.ts+3-1
...@@ -1,4 +1,6 @@...@@ -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
3import {5import {
4 type VcsDriverKind,6 type VcsDriverKind,
apps/server/src/vcs/VcsStatusBroadcaster.test.ts+13-2
...@@ -1,6 +1,14 @@...@@ -1,6 +1,14 @@
1import { assert, it, describe } from "@effect/vitest";1import { assert, it, describe } from "@effect/vitest";
2import * as NodeServices from "@effect/platform-node/NodeServices";2import * 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";
4import type {12import type {
5 VcsStatusLocalResult,13 VcsStatusLocalResult,
6 VcsStatusRemoteResult,14 VcsStatusRemoteResult,
...@@ -46,6 +54,7 @@ function makeTestLayer(state: {...@@ -46,6 +54,7 @@ function makeTestLayer(state: {
46 remoteInvalidationCalls: number;54 remoteInvalidationCalls: number;
47}) {55}) {
48 return VcsStatusBroadcaster.layer.pipe(56 return VcsStatusBroadcaster.layer.pipe(
57 Layer.provideMerge(NodeServices.layer),
49 Layer.provide(58 Layer.provide(
50 Layer.mock(GitWorkflowService.GitWorkflowService)({59 Layer.mock(GitWorkflowService.GitWorkflowService)({
51 localStatus: () =>60 localStatus: () =>
...@@ -185,6 +194,7 @@ describe("VcsStatusBroadcaster", () => {...@@ -185,6 +194,7 @@ describe("VcsStatusBroadcaster", () => {
185 remoteInvalidationCalls: 0,194 remoteInvalidationCalls: 0,
186 };195 };
187 const testLayer = VcsStatusBroadcaster.layer.pipe(196 const testLayer = VcsStatusBroadcaster.layer.pipe(
197 Layer.provideMerge(NodeServices.layer),
188 Layer.provide(198 Layer.provide(
189 Layer.mock(GitWorkflowService.GitWorkflowService)({199 Layer.mock(GitWorkflowService.GitWorkflowService)({
190 localStatus: (input) =>200 localStatus: (input) =>
...@@ -231,7 +241,7 @@ describe("VcsStatusBroadcaster", () => {...@@ -231,7 +241,7 @@ describe("VcsStatusBroadcaster", () => {
231 assert.deepStrictEqual(seenCwds, [realPath, realPath]);241 assert.deepStrictEqual(seenCwds, [realPath, realPath]);
232 assert.equal(state.localStatusCalls, 1);242 assert.equal(state.localStatusCalls, 1);
233 assert.equal(state.remoteStatusCalls, 1);243 assert.equal(state.remoteStatusCalls, 1);
234 }).pipe(Effect.provide(Layer.mergeAll(testLayer, NodeServices.layer)));244 }).pipe(Effect.provide(testLayer));
235 });245 });
236246
237 it.effect("streams a local snapshot first and remote updates later", () => {247 it.effect("streams a local snapshot first and remote updates later", () => {
...@@ -286,6 +296,7 @@ describe("VcsStatusBroadcaster", () => {...@@ -286,6 +296,7 @@ describe("VcsStatusBroadcaster", () => {
286 let remoteInterruptedDeferred: Deferred.Deferred<void, never> | null = null;296 let remoteInterruptedDeferred: Deferred.Deferred<void, never> | null = null;
287 let remoteStartedDeferred: Deferred.Deferred<void, never> | null = null;297 let remoteStartedDeferred: Deferred.Deferred<void, never> | null = null;
288 const testLayer = VcsStatusBroadcaster.layer.pipe(298 const testLayer = VcsStatusBroadcaster.layer.pipe(
299 Layer.provideMerge(NodeServices.layer),
289 Layer.provide(300 Layer.provide(
290 Layer.mock(GitWorkflowService.GitWorkflowService)({301 Layer.mock(GitWorkflowService.GitWorkflowService)({
291 localStatus: () =>302 localStatus: () =>
apps/server/src/vcs/VcsStatusBroadcaster.ts+24-26
...@@ -1,18 +1,15 @@...@@ -1,18 +1,15 @@
1import { realpathSync } from "node:fs";1import * as Context from "effect/Context";
22import * as Duration from "effect/Duration";
3import {3import * as Effect from "effect/Effect";
4 Context,4import * as Exit from "effect/Exit";
5 Duration,5import * as Fiber from "effect/Fiber";
6 Effect,6import * as FileSystem from "effect/FileSystem";
7 Exit,7import * as Layer from "effect/Layer";
8 Fiber,8import * as PubSub from "effect/PubSub";
9 Layer,9import * as Ref from "effect/Ref";
10 PubSub,10import * as Scope from "effect/Scope";
11 Ref,11import * as Stream from "effect/Stream";
12 Scope,12import * as SynchronizedRef from "effect/SynchronizedRef";
13 Stream,
14 SynchronizedRef,
15} from "effect";
16import type {13import type {
17 GitManagerServiceError,14 GitManagerServiceError,
18 VcsStatusInput,15 VcsStatusInput,
...@@ -69,18 +66,17 @@ function fingerprintStatusPart(status: unknown): string {...@@ -69,18 +66,17 @@ function fingerprintStatusPart(status: unknown): string {
69 return JSON.stringify(status);66 return JSON.stringify(status);
70}67}
7168
72function normalizeCwd(cwd: string): string {69const normalizeCwd = (cwd: string) =>
73 try {70 Effect.service(FileSystem.FileSystem).pipe(
74 return realpathSync.native(cwd);71 Effect.flatMap((fs) => fs.realPath(cwd)),
75 } catch {72 Effect.orElseSucceed(() => cwd),
76 return cwd;73 );
77 }
78}
7974
80export const layer = Layer.effect(75export const layer = Layer.effect(
81 VcsStatusBroadcaster,76 VcsStatusBroadcaster,
82 Effect.gen(function* () {77 Effect.gen(function* () {
83 const workflow = yield* GitWorkflowService.GitWorkflowService;78 const workflow = yield* GitWorkflowService.GitWorkflowService;
79 const fs = yield* FileSystem.FileSystem;
84 const changesPubSub = yield* Effect.acquireRelease(80 const changesPubSub = yield* Effect.acquireRelease(
85 PubSub.unbounded<VcsStatusChange>(),81 PubSub.unbounded<VcsStatusChange>(),
86 (pubsub) => PubSub.shutdown(pubsub),82 (pubsub) => PubSub.shutdown(pubsub),
...@@ -195,10 +191,12 @@ export const layer = Layer.effect(...@@ -195,10 +191,12 @@ export const layer = Layer.effect(
195 },191 },
196 );192 );
197193
194 const withFileSystem = Effect.provideService(FileSystem.FileSystem, fs);
195
198 const getStatus: VcsStatusBroadcasterShape["getStatus"] = Effect.fn(196 const getStatus: VcsStatusBroadcasterShape["getStatus"] = Effect.fn(
199 "VcsStatusBroadcaster.getStatus",197 "VcsStatusBroadcaster.getStatus",
200 )(function* (input) {198 )(function* (input) {
201 const cwd = normalizeCwd(input.cwd);199 const cwd = yield* withFileSystem(normalizeCwd(input.cwd));
202 const [local, remote] = yield* Effect.all([200 const [local, remote] = yield* Effect.all([
203 getOrLoadLocalStatus(cwd),201 getOrLoadLocalStatus(cwd),
204 getOrLoadRemoteStatus(cwd),202 getOrLoadRemoteStatus(cwd),
...@@ -209,7 +207,7 @@ export const layer = Layer.effect(...@@ -209,7 +207,7 @@ export const layer = Layer.effect(
209 const refreshLocalStatus: VcsStatusBroadcasterShape["refreshLocalStatus"] = Effect.fn(207 const refreshLocalStatus: VcsStatusBroadcasterShape["refreshLocalStatus"] = Effect.fn(
210 "VcsStatusBroadcaster.refreshLocalStatus",208 "VcsStatusBroadcaster.refreshLocalStatus",
211 )(function* (rawCwd) {209 )(function* (rawCwd) {
212 const cwd = normalizeCwd(rawCwd);210 const cwd = yield* withFileSystem(normalizeCwd(rawCwd));
213 yield* workflow.invalidateLocalStatus(cwd);211 yield* workflow.invalidateLocalStatus(cwd);
214 const local = yield* workflow.localStatus({ cwd });212 const local = yield* workflow.localStatus({ cwd });
215 return yield* updateCachedLocalStatus(cwd, local, { publish: true });213 return yield* updateCachedLocalStatus(cwd, local, { publish: true });
...@@ -226,7 +224,7 @@ export const layer = Layer.effect(...@@ -226,7 +224,7 @@ export const layer = Layer.effect(
226 const refreshStatus: VcsStatusBroadcasterShape["refreshStatus"] = Effect.fn(224 const refreshStatus: VcsStatusBroadcasterShape["refreshStatus"] = Effect.fn(
227 "VcsStatusBroadcaster.refreshStatus",225 "VcsStatusBroadcaster.refreshStatus",
228 )(function* (rawCwd) {226 )(function* (rawCwd) {
229 const cwd = normalizeCwd(rawCwd);227 const cwd = yield* withFileSystem(normalizeCwd(rawCwd));
230 const [local, remote] = yield* Effect.all([228 const [local, remote] = yield* Effect.all([
231 refreshLocalStatus(cwd),229 refreshLocalStatus(cwd),
232 refreshRemoteStatus(cwd),230 refreshRemoteStatus(cwd),
...@@ -312,7 +310,7 @@ export const layer = Layer.effect(...@@ -312,7 +310,7 @@ export const layer = Layer.effect(
312 const streamStatus: VcsStatusBroadcasterShape["streamStatus"] = (input) =>310 const streamStatus: VcsStatusBroadcasterShape["streamStatus"] = (input) =>
313 Stream.unwrap(311 Stream.unwrap(
314 Effect.gen(function* () {312 Effect.gen(function* () {
315 const cwd = normalizeCwd(input.cwd);313 const cwd = yield* withFileSystem(normalizeCwd(input.cwd));
316 const subscription = yield* PubSub.subscribe(changesPubSub);314 const subscription = yield* PubSub.subscribe(changesPubSub);
317 const initialLocal = yield* getOrLoadLocalStatus(cwd);315 const initialLocal = yield* getOrLoadLocalStatus(cwd);
318 const initialRemote = (yield* getCachedStatus(cwd))?.remote?.value ?? null;316 const initialRemote = (yield* getCachedStatus(cwd))?.remote?.value ?? null;
apps/server/src/vcs/testing/VcsDriverContractHarness.ts+8-10
...@@ -1,14 +1,12 @@...@@ -1,14 +1,12 @@
1import { assert, it, describe } from "@effect/vitest";1import { assert, it, describe } from "@effect/vitest";
2import {2import * as Effect from "effect/Effect";
3 Effect,3import * as FileSystem from "effect/FileSystem";
4 FileSystem,4import * as Layer from "effect/Layer";
5 Layer,5import * as Path from "effect/Path";
6 Path,6import type * as PlatformError from "effect/PlatformError";
7 type PlatformError,7import type * as Scope from "effect/Scope";
8 type Scope,8import * as DateTime from "effect/DateTime";
9 DateTime,9import * as Option from "effect/Option";
10 Option,
11} from "effect";
1210
13import type { VcsDriverKind } from "@t3tools/contracts";11import type { VcsDriverKind } from "@t3tools/contracts";
14import * as VcsDriver from "../VcsDriver.ts";12import * as VcsDriver from "../VcsDriver.ts";
apps/server/src/workspace/Layers/WorkspaceEntries.test.ts+38-7
...@@ -1,8 +1,13 @@...@@ -1,8 +1,13 @@
1// @effect-diagnostics nodeBuiltinImport:off
1import fsPromises from "node:fs/promises";2import fsPromises from "node:fs/promises";
2
3import * as NodeServices from "@effect/platform-node/NodeServices";3import * as NodeServices from "@effect/platform-node/NodeServices";
4import { it, afterEach, describe, expect, vi } from "@effect/vitest";4import { 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
7import { ServerConfig } from "../../config.ts";12import { ServerConfig } from "../../config.ts";
8import * as VcsDriverRegistry from "../../vcs/VcsDriverRegistry.ts";13import * as VcsDriverRegistry from "../../vcs/VcsDriverRegistry.ts";
...@@ -222,25 +227,37 @@ it.layer(TestLayer)("WorkspaceEntriesLive", (it) => {...@@ -222,25 +227,37 @@ it.layer(TestLayer)("WorkspaceEntriesLive", (it) => {
222 yield* writeTextFile(cwd, "src/components/Composer.tsx");227 yield* writeTextFile(cwd, "src/components/Composer.tsx");
223228
224 let rootReadCount = 0;229 let rootReadCount = 0;
230 let releaseRootRead: (() => void) | undefined;
231 const rootReadGate = new Promise<void>((resolve) => {
232 releaseRootRead = resolve;
233 });
225 const originalReaddir = fsPromises.readdir.bind(fsPromises);234 const originalReaddir = fsPromises.readdir.bind(fsPromises);
226 vi.spyOn(fsPromises, "readdir").mockImplementation((async (235 vi.spyOn(fsPromises, "readdir").mockImplementation((async (
227 ...args: Parameters<typeof fsPromises.readdir>236 ...args: Parameters<typeof fsPromises.readdir>
228 ) => {237 ) => {
229 if (args[0] === cwd) {238 if (args[0] === cwd) {
230 rootReadCount += 1;239 rootReadCount += 1;
231 await new Promise((resolve) => setTimeout(resolve, 20));240 await rootReadGate;
232 }241 }
233 return originalReaddir(...args);242 return originalReaddir(...args);
234 }) as typeof fsPromises.readdir);243 }) as typeof fsPromises.readdir);
235244
236 yield* Effect.all(245 const searches = yield* Effect.all(
237 [246 [
238 searchWorkspaceEntries({ cwd, query: "", limit: 100 }),247 searchWorkspaceEntries({ cwd, query: "", limit: 100 }),
239 searchWorkspaceEntries({ cwd, query: "comp", limit: 100 }),248 searchWorkspaceEntries({ cwd, query: "comp", limit: 100 }),
240 searchWorkspaceEntries({ cwd, query: "src", limit: 100 }),249 searchWorkspaceEntries({ cwd, query: "src", limit: 100 }),
241 ],250 ],
242 { concurrency: "unbounded" },251 { 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
245 expect(rootReadCount).toBe(1);262 expect(rootReadCount).toBe(1);
246 }),263 }),
...@@ -257,6 +274,10 @@ it.layer(TestLayer)("WorkspaceEntriesLive", (it) => {...@@ -257,6 +274,10 @@ it.layer(TestLayer)("WorkspaceEntriesLive", (it) => {
257274
258 let activeReads = 0;275 let activeReads = 0;
259 let peakReads = 0;276 let peakReads = 0;
277 let releaseReads: (() => void) | undefined;
278 const readsGate = new Promise<void>((resolve) => {
279 releaseReads = resolve;
280 });
260 const originalReaddir = fsPromises.readdir.bind(fsPromises);281 const originalReaddir = fsPromises.readdir.bind(fsPromises);
261 vi.spyOn(fsPromises, "readdir").mockImplementation((async (282 vi.spyOn(fsPromises, "readdir").mockImplementation((async (
262 ...args: Parameters<typeof fsPromises.readdir>283 ...args: Parameters<typeof fsPromises.readdir>
...@@ -265,7 +286,7 @@ it.layer(TestLayer)("WorkspaceEntriesLive", (it) => {...@@ -265,7 +286,7 @@ it.layer(TestLayer)("WorkspaceEntriesLive", (it) => {
265 if (typeof target === "string" && target.startsWith(cwd)) {286 if (typeof target === "string" && target.startsWith(cwd)) {
266 activeReads += 1;287 activeReads += 1;
267 peakReads = Math.max(peakReads, activeReads);288 peakReads = Math.max(peakReads, activeReads);
268 await new Promise((resolve) => setTimeout(resolve, 4));289 await readsGate;
269 try {290 try {
270 return await originalReaddir(...args);291 return await originalReaddir(...args);
271 } finally {292 } finally {
...@@ -275,7 +296,17 @@ it.layer(TestLayer)("WorkspaceEntriesLive", (it) => {...@@ -275,7 +296,17 @@ it.layer(TestLayer)("WorkspaceEntriesLive", (it) => {
275 return originalReaddir(...args);296 return originalReaddir(...args);
276 }) as typeof fsPromises.readdir);297 }) 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
280 expect(peakReads).toBeLessThanOrEqual(32);311 expect(peakReads).toBeLessThanOrEqual(32);
281 }),312 }),
apps/server/src/workspace/Layers/WorkspaceEntries.ts+8-1
...@@ -1,8 +1,15 @@...@@ -1,8 +1,15 @@
1// @effect-diagnostics nodeBuiltinImport:off
1import * as OS from "node:os";2import * as OS from "node:os";
2import fsPromises from "node:fs/promises";3import fsPromises from "node:fs/promises";
3import type { Dirent } from "node:fs";4import 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
7import { type FilesystemBrowseInput, type ProjectEntry } from "@t3tools/contracts";14import { type FilesystemBrowseInput, type ProjectEntry } from "@t3tools/contracts";
8import { isExplicitRelativePath, isWindowsAbsolutePath } from "@t3tools/shared/path";15import { isExplicitRelativePath, isWindowsAbsolutePath } from "@t3tools/shared/path";
apps/server/src/workspace/Layers/WorkspaceFileSystem.test.ts+4-1
...@@ -1,6 +1,9 @@...@@ -1,6 +1,9 @@
1import * as NodeServices from "@effect/platform-node/NodeServices";1import * as NodeServices from "@effect/platform-node/NodeServices";
2import { it, describe, expect } from "@effect/vitest";2import { 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
5import { ServerConfig } from "../../config.ts";8import { ServerConfig } from "../../config.ts";
6import * as VcsDriverRegistry from "../../vcs/VcsDriverRegistry.ts";9import * as VcsDriverRegistry from "../../vcs/VcsDriverRegistry.ts";
apps/server/src/workspace/Layers/WorkspaceFileSystem.ts+4-1
...@@ -1,4 +1,7 @@...@@ -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
3import {6import {
4 WorkspaceFileSystem,7 WorkspaceFileSystem,
apps/server/src/workspace/Layers/WorkspacePaths.test.ts+4-1
...@@ -1,6 +1,9 @@...@@ -1,6 +1,9 @@
1import * as NodeServices from "@effect/platform-node/NodeServices";1import * as NodeServices from "@effect/platform-node/NodeServices";
2import { it, describe, expect } from "@effect/vitest";2import { 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
5import { WorkspacePaths } from "../Services/WorkspacePaths.ts";8import { WorkspacePaths } from "../Services/WorkspacePaths.ts";
6import { WorkspacePathsLive } from "./WorkspacePaths.ts";9import { WorkspacePathsLive } from "./WorkspacePaths.ts";
apps/server/src/workspace/Layers/WorkspacePaths.ts+4-1
...@@ -1,5 +1,8 @@...@@ -1,5 +1,8 @@
1import * as OS from "node:os";1import * 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
4import {7import {
5 WorkspacePaths,8 WorkspacePaths,
apps/server/src/workspace/Services/WorkspaceEntries.ts+3-2
...@@ -6,8 +6,9 @@...@@ -6,8 +6,9 @@
6 *6 *
7 * @module WorkspaceEntries7 * @module WorkspaceEntries
8 */8 */
9import { Schema, Context } from "effect";9import * as Schema from "effect/Schema";
10import type { Effect } from "effect";10import * as Context from "effect/Context";
11import type * as Effect from "effect/Effect";
1112
12import type {13import type {
13 FilesystemBrowseInput,14 FilesystemBrowseInput,
apps/server/src/workspace/Services/WorkspaceFileSystem.ts+3-2
...@@ -6,8 +6,9 @@...@@ -6,8 +6,9 @@
6 *6 *
7 * @module WorkspaceFileSystem7 * @module WorkspaceFileSystem
8 */8 */
9import { Schema, Context } from "effect";9import * as Schema from "effect/Schema";
10import type { Effect } from "effect";10import * as Context from "effect/Context";
11import type * as Effect from "effect/Effect";
1112
12import type { ProjectWriteFileInput, ProjectWriteFileResult } from "@t3tools/contracts";13import type { ProjectWriteFileInput, ProjectWriteFileResult } from "@t3tools/contracts";
13import { WorkspacePathOutsideRootError } from "./WorkspacePaths.ts";14import { WorkspacePathOutsideRootError } from "./WorkspacePaths.ts";
apps/server/src/workspace/Services/WorkspacePaths.ts+3-2
...@@ -6,8 +6,9 @@...@@ -6,8 +6,9 @@
6 *6 *
7 * @module WorkspacePaths7 * @module WorkspacePaths
8 */8 */
9import { Schema, Context } from "effect";9import * as Schema from "effect/Schema";
10import type { Effect } from "effect";10import * as Context from "effect/Context";
11import type * as Effect from "effect/Effect";
1112
12export class WorkspaceRootNotExistsError extends Schema.TaggedErrorClass<WorkspaceRootNotExistsError>()(13export class WorkspaceRootNotExistsError extends Schema.TaggedErrorClass<WorkspaceRootNotExistsError>()(
13 "WorkspaceRootNotExistsError",14 "WorkspaceRootNotExistsError",
apps/server/src/ws.ts+89-75
...@@ -1,4 +1,13 @@...@@ -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";
2import {11import {
3 type AuthAccessStreamEvent,12 type AuthAccessStreamEvent,
4 AuthSessionId,13 AuthSessionId,
...@@ -78,6 +87,8 @@ import {...@@ -78,6 +87,8 @@ import {
78} from "./auth/Services/SessionCredentialService.ts";87} from "./auth/Services/SessionCredentialService.ts";
79import { respondToAuthError } from "./auth/http.ts";88import { respondToAuthError } from "./auth/http.ts";
8089
90const nowIso = Effect.map(DateTime.now, DateTime.formatIso);
91
81function isThreadDetailEvent(event: OrchestrationEvent): event is Extract<92function isThreadDetailEvent(event: OrchestrationEvent): event is Extract<
82 OrchestrationEvent,93 OrchestrationEvent,
83 {94 {
...@@ -378,83 +389,86 @@ const makeWsRpcLayer = (currentSessionId: AuthSessionId) =>...@@ -378,83 +389,86 @@ const makeWsRpcLayer = (currentSessionId: AuthSessionId) =>
378 readonly scriptId: string;389 readonly scriptId: string;
379 readonly scriptName: string;390 readonly scriptName: string;
380 readonly terminalId: string;391 readonly terminalId: string;
381 }) => {392 }) =>
382 const payload = {393 Effect.gen(function* () {
383 scriptId: input.scriptId,394 const startedAt = yield* nowIso;
384 scriptName: input.scriptName,395 const payload = {
385 terminalId: input.terminalId,396 scriptId: input.scriptId,
386 worktreePath: input.worktreePath,397 scriptName: input.scriptName,
387 };398 terminalId: input.terminalId,
388 return Effect.all([399 worktreePath: input.worktreePath,
389 appendSetupScriptActivity({400 };
390 threadId: command.threadId,401 yield* Effect.all([
391 kind: "setup-script.requested",402 appendSetupScriptActivity({
392 summary: "Starting setup script",403 threadId: command.threadId,
393 createdAt: input.requestedAt,404 kind: "setup-script.requested",
394 payload,405 summary: "Starting setup script",
395 tone: "info",406 createdAt: input.requestedAt,
396 }),407 payload,
397 appendSetupScriptActivity({408 tone: "info",
398 threadId: command.threadId,409 }),
399 kind: "setup-script.started",410 appendSetupScriptActivity({
400 summary: "Setup script started",411 threadId: command.threadId,
401 createdAt: new Date().toISOString(),412 kind: "setup-script.started",
402 payload,413 summary: "Setup script started",
403 tone: "info",414 createdAt: startedAt,
404 }),415 payload,
405 ]).pipe(416 tone: "info",
406 Effect.asVoid,417 }),
407 Effect.catch((error) =>418 ]).pipe(
408 Effect.logWarning(419 Effect.asVoid,
409 "bootstrap turn start launched setup script but failed to record setup activity",420 Effect.catch((error) =>
410 {421 Effect.logWarning(
411 threadId: command.threadId,422 "bootstrap turn start launched setup script but failed to record setup activity",
412 worktreePath: input.worktreePath,423 {
413 scriptId: input.scriptId,424 threadId: command.threadId,
414 terminalId: input.terminalId,425 worktreePath: input.worktreePath,
415 detail: error.message,426 scriptId: input.scriptId,
416 },427 terminalId: input.terminalId,
428 detail: error.message,
429 },
430 ),
417 ),431 ),
418 ),432 );
419 );433 });
420 };
421434
422 const runSetupProgram = () =>435 const runSetupProgram = () =>
423 bootstrap?.runSetupScript && targetWorktreePath436 Effect.gen(function* () {
424 ? (() => {437 if (!bootstrap?.runSetupScript || !targetWorktreePath) {
425 const worktreePath = targetWorktreePath;438 return;
426 const requestedAt = new Date().toISOString();439 }
427 return projectSetupScriptRunner440 const worktreePath = targetWorktreePath;
428 .runForThread({441 const requestedAt = yield* nowIso;
429 threadId: command.threadId,442 yield* projectSetupScriptRunner
430 ...(targetProjectId ? { projectId: targetProjectId } : {}),443 .runForThread({
431 ...(targetProjectCwd ? { projectCwd: targetProjectCwd } : {}),444 threadId: command.threadId,
432 worktreePath,445 ...(targetProjectId ? { projectId: targetProjectId } : {}),
433 })446 ...(targetProjectCwd ? { projectCwd: targetProjectCwd } : {}),
434 .pipe(447 worktreePath,
435 Effect.matchEffect({448 })
436 onFailure: (error) =>449 .pipe(
437 recordSetupScriptLaunchFailure({450 Effect.matchEffect({
438 error,451 onFailure: (error) =>
439 requestedAt,452 recordSetupScriptLaunchFailure({
440 worktreePath,453 error,
441 }),454 requestedAt,
442 onSuccess: (setupResult) => {455 worktreePath,
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 },
454 }),456 }),
455 );457 onSuccess: (setupResult) => {
456 })()458 if (setupResult.status !== "started") {
457 : Effect.void;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
459 const bootstrapProgram = Effect.gen(function* () {473 const bootstrapProgram = Effect.gen(function* () {
460 if (bootstrap?.createThread) {474 if (bootstrap?.createThread) {
...@@ -597,7 +611,7 @@ const makeWsRpcLayer = (currentSessionId: AuthSessionId) =>...@@ -597,7 +611,7 @@ const makeWsRpcLayer = (currentSessionId: AuthSessionId) =>
597 `session-stop-for-archive:${normalizedCommand.commandId}`,611 `session-stop-for-archive:${normalizedCommand.commandId}`,
598 ),612 ),
599 threadId: normalizedCommand.threadId,613 threadId: normalizedCommand.threadId,
600 createdAt: new Date().toISOString(),614 createdAt: yield* nowIso,
601 });615 });
602616
603 yield* dispatchNormalizedCommand(stopCommand);617 yield* dispatchNormalizedCommand(stopCommand);
apps/server/tsconfig.json+1-15
...@@ -3,21 +3,7 @@...@@ -3,21 +3,7 @@
3 "compilerOptions": {3 "compilerOptions": {
4 "composite": true,4 "composite": true,
5 "types": ["node", "bun"],5 "types": ["node", "bun"],
6 "lib": ["ESNext", "esnext.disposable"],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 ]
21 },7 },
22 "include": ["src", "tsdown.config.ts", "scripts", "integration", "../../scripts/lib"]8 "include": ["src", "tsdown.config.ts", "scripts", "integration", "../../scripts/lib"]
23}9}
apps/web/src/components/BranchToolbar.logic.ts+1-1
...@@ -1,5 +1,5 @@...@@ -1,5 +1,5 @@
1import type { EnvironmentId, VcsRef, ProjectId } from "@t3tools/contracts";1import type { EnvironmentId, VcsRef, ProjectId } from "@t3tools/contracts";
2import { Schema } from "effect";2import * as Schema from "effect/Schema";
3export {3export {
4 dedupeRemoteBranchesWithLocalMatches,4 dedupeRemoteBranchesWithLocalMatches,
5 deriveLocalBranchNameFromRemoteRef,5 deriveLocalBranchNameFromRemoteRef,
apps/web/src/components/ChatView.browser.tsx+1-1
...@@ -22,7 +22,7 @@ import {...@@ -22,7 +22,7 @@ import {
22import { scopedThreadKey, scopeThreadRef } from "@t3tools/client-runtime";22import { scopedThreadKey, scopeThreadRef } from "@t3tools/client-runtime";
23import { createModelCapabilities, createModelSelection } from "@t3tools/shared/model";23import { createModelCapabilities, createModelSelection } from "@t3tools/shared/model";
24import { RouterProvider, createMemoryHistory } from "@tanstack/react-router";24import { RouterProvider, createMemoryHistory } from "@tanstack/react-router";
25import { Option } from "effect";25import * as Option from "effect/Option";
26import { HttpResponse, http, ws } from "msw";26import { HttpResponse, http, ws } from "msw";
27import { setupWorker } from "msw/browser";27import { setupWorker } from "msw/browser";
28import { page } from "vitest/browser";28import { page } from "vitest/browser";
apps/web/src/components/ChatView.logic.ts+1-1
...@@ -10,7 +10,7 @@ import {...@@ -10,7 +10,7 @@ import {
10} from "@t3tools/contracts";10} from "@t3tools/contracts";
11import { type ChatMessage, type SessionPhase, type Thread, type ThreadSession } from "../types";11import { type ChatMessage, type SessionPhase, type Thread, type ThreadSession } from "../types";
12import { type ComposerImageAttachment, type DraftThreadState } from "../composerDraftStore";12import { type ComposerImageAttachment, type DraftThreadState } from "../composerDraftStore";
13import { Schema } from "effect";13import * as Schema from "effect/Schema";
14import { selectThreadByRef, useStore } from "../store";14import { selectThreadByRef, useStore } from "../store";
15import {15import {
16 filterTerminalContextsWithText,16 filterTerminalContextsWithText,
apps/web/src/components/CommandPalette.tsx+1-1
...@@ -13,7 +13,7 @@ import {...@@ -13,7 +13,7 @@ import {
13} from "@t3tools/contracts";13} from "@t3tools/contracts";
14import { useQuery, useQueryClient } from "@tanstack/react-query";14import { useQuery, useQueryClient } from "@tanstack/react-query";
15import { useNavigate, useParams } from "@tanstack/react-router";15import { useNavigate, useParams } from "@tanstack/react-router";
16import { Option } from "effect";16import * as Option from "effect/Option";
17import {17import {
18 ArrowDownIcon,18 ArrowDownIcon,
19 ArrowLeftIcon,19 ArrowLeftIcon,
apps/web/src/components/GitActionsControl.tsx+1-1
...@@ -12,7 +12,7 @@ import type {...@@ -12,7 +12,7 @@ import type {
12} from "@t3tools/contracts";12} from "@t3tools/contracts";
13import { useIsMutating, useMutation, useQueryClient } from "@tanstack/react-query";13import { useIsMutating, useMutation, useQueryClient } from "@tanstack/react-query";
14import { useNavigate } from "@tanstack/react-router";14import { useNavigate } from "@tanstack/react-router";
15import { Option } from "effect";15import * as Option from "effect/Option";
16import { useCallback, useEffect, useEffectEvent, useMemo, useRef, useState } from "react";16import { useCallback, useEffect, useEffectEvent, useMemo, useRef, useState } from "react";
17import { flushSync } from "react-dom";17import { flushSync } from "react-dom";
18import {18import {
apps/web/src/components/settings/ConnectionsSettings.tsx+1-1
...@@ -17,7 +17,7 @@ import {...@@ -17,7 +17,7 @@ import {
17 type DesktopServerExposureState,17 type DesktopServerExposureState,
18 type EnvironmentId,18 type EnvironmentId,
19} from "@t3tools/contracts";19} from "@t3tools/contracts";
20import { DateTime } from "effect";20import * as DateTime from "effect/DateTime";
2121
22import { useCopyToClipboard } from "../../hooks/useCopyToClipboard";22import { useCopyToClipboard } from "../../hooks/useCopyToClipboard";
23import { cn } from "../../lib/utils";23import { cn } from "../../lib/utils";
apps/web/src/components/settings/DiagnosticsSettings.tsx+2-1
...@@ -9,7 +9,8 @@ import {...@@ -9,7 +9,8 @@ import {
9} from "lucide-react";9} from "lucide-react";
10import { useCallback, useMemo, useState, type ReactNode } from "react";10import { useCallback, useMemo, useState, type ReactNode } from "react";
11import type { ServerProcessDiagnosticsEntry, ServerProcessSignal } from "@t3tools/contracts";11import type { ServerProcessDiagnosticsEntry, ServerProcessSignal } from "@t3tools/contracts";
12import { DateTime, Option } from "effect";12import * as DateTime from "effect/DateTime";
13import * as Option from "effect/Option";
1314
14import { ensureLocalApi } from "../../localApi";15import { ensureLocalApi } from "../../localApi";
15import { cn } from "../../lib/utils";16import { cn } from "../../lib/utils";
apps/web/src/components/settings/ProviderSettingsForm.tsx+1-1
...@@ -1,7 +1,7 @@...@@ -1,7 +1,7 @@
1"use client";1"use client";
22
3import { useMemo, type ReactNode } from "react";3import { useMemo, type ReactNode } from "react";
4import { Schema } from "effect";4import * as Schema from "effect/Schema";
5import type {5import type {
6 ProviderSettingsFormAnnotation,6 ProviderSettingsFormAnnotation,
7 ProviderSettingsFormControl,7 ProviderSettingsFormControl,
apps/web/src/components/settings/SettingsPanels.browser.tsx+3-2
...@@ -16,7 +16,8 @@ import {...@@ -16,7 +16,8 @@ import {
16 type ServerProvider,16 type ServerProvider,
17 type SourceControlDiscoveryResult,17 type SourceControlDiscoveryResult,
18} from "@t3tools/contracts";18} from "@t3tools/contracts";
19import { DateTime, Option } from "effect";19import * as DateTime from "effect/DateTime";
20import * as Option from "effect/Option";
20import { page } from "vitest/browser";21import { page } from "vitest/browser";
21import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";22import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
22import { render } from "vitest-browser-react";23import { render } from "vitest-browser-react";
...@@ -261,7 +262,7 @@ function createOutdatedProvider(driver: string): ServerProvider {...@@ -261,7 +262,7 @@ function createOutdatedProvider(driver: string): ServerProvider {
261}262}
262263
263function makeUtc(value: string) {264function makeUtc(value: string) {
264 return DateTime.makeUnsafe(Date.parse(value));265 return DateTime.makeUnsafe(value);
265}266}
266267
267function makePairingLink(input: {268function makePairingLink(input: {
apps/web/src/components/settings/SettingsPanels.tsx+1-1
...@@ -14,7 +14,7 @@ import {...@@ -14,7 +14,7 @@ import {
14import { scopeThreadRef } from "@t3tools/client-runtime";14import { scopeThreadRef } from "@t3tools/client-runtime";
15import { DEFAULT_UNIFIED_SETTINGS } from "@t3tools/contracts/settings";15import { DEFAULT_UNIFIED_SETTINGS } from "@t3tools/contracts/settings";
16import { createModelSelection } from "@t3tools/shared/model";16import { createModelSelection } from "@t3tools/shared/model";
17import { Equal } from "effect";17import * as Equal from "effect/Equal";
18import { APP_VERSION } from "../../branding";18import { APP_VERSION } from "../../branding";
19import {19import {
20 canCheckForUpdate,20 canCheckForUpdate,
apps/web/src/components/settings/SourceControlSettings.tsx+1-1
...@@ -1,5 +1,5 @@...@@ -1,5 +1,5 @@
1import { GitPullRequestIcon, RefreshCwIcon } from "lucide-react";1import { GitPullRequestIcon, RefreshCwIcon } from "lucide-react";
2import { Option } from "effect";2import * as Option from "effect/Option";
3import { type ReactNode } from "react";3import { type ReactNode } from "react";
4import type {4import type {
5 SourceControlProviderKind,5 SourceControlProviderKind,
apps/web/src/components/settings/providerDriverMeta.ts+1-1
...@@ -5,7 +5,7 @@ import {...@@ -5,7 +5,7 @@ import {
5 OpenCodeSettings,5 OpenCodeSettings,
6 ProviderDriverKind,6 ProviderDriverKind,
7} from "@t3tools/contracts";7} from "@t3tools/contracts";
8import type { Schema } from "effect";8import type * as Schema from "effect/Schema";
9import { ClaudeAI, CursorIcon, type Icon, OpenAI, OpenCodeIcon } from "../Icons";9import { ClaudeAI, CursorIcon, type Icon, OpenAI, OpenCodeIcon } from "../Icons";
1010
11type ProviderSettingsSchema = {11type ProviderSettingsSchema = {
apps/web/src/components/ui/sidebar.tsx+1-1
...@@ -19,7 +19,7 @@ import { Skeleton } from "~/components/ui/skeleton";...@@ -19,7 +19,7 @@ import { Skeleton } from "~/components/ui/skeleton";
19import { Tooltip, TooltipPopup, TooltipTrigger } from "~/components/ui/tooltip";19import { Tooltip, TooltipPopup, TooltipTrigger } from "~/components/ui/tooltip";
20import { useIsMobile } from "~/hooks/useMediaQuery";20import { useIsMobile } from "~/hooks/useMediaQuery";
21import { getLocalStorageItem, setLocalStorageItem } from "~/hooks/useLocalStorage";21import { getLocalStorageItem, setLocalStorageItem } from "~/hooks/useLocalStorage";
22import { Schema } from "effect";22import * as Schema from "effect/Schema";
2323
24const SIDEBAR_COOKIE_NAME = "sidebar_state";24const SIDEBAR_COOKIE_NAME = "sidebar_state";
25const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;25const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
apps/web/src/environments/primary/auth.ts+2-1
...@@ -16,7 +16,8 @@ import {...@@ -16,7 +16,8 @@ import {
16} from "../../pairingUrl";16} from "../../pairingUrl";
1717
18import { resolvePrimaryEnvironmentHttpUrl } from "./target";18import { resolvePrimaryEnvironmentHttpUrl } from "./target";
19import { Data, Predicate } from "effect";19import * as Data from "effect/Data";
20import * as Predicate from "effect/Predicate";
2021
21export class BootstrapHttpError extends Data.TaggedError("BootstrapHttpError")<{22export class BootstrapHttpError extends Data.TaggedError("BootstrapHttpError")<{
22 readonly message: string;23 readonly message: string;
apps/web/src/hooks/useSettings.ts+1-1
...@@ -19,7 +19,7 @@ import {...@@ -19,7 +19,7 @@ import {
19 UnifiedSettings,19 UnifiedSettings,
20} from "@t3tools/contracts/settings";20} from "@t3tools/contracts/settings";
21import { ensureLocalApi } from "~/localApi";21import { ensureLocalApi } from "~/localApi";
22import { Struct } from "effect";22import * as Struct from "effect/Struct";
23import { applyServerSettingsPatch } from "@t3tools/shared/serverSettings";23import { applyServerSettingsPatch } from "@t3tools/shared/serverSettings";
24import { applySettingsUpdated, getServerConfig, useServerSettings } from "~/rpc/serverState";24import { applySettingsUpdated, getServerConfig, useServerSettings } from "~/rpc/serverState";
2525
apps/web/src/lib/gitStatusState.ts+1-1
...@@ -4,7 +4,7 @@ import {...@@ -4,7 +4,7 @@ import {
4 type GitManagerServiceError,4 type GitManagerServiceError,
5 type VcsStatusResult,5 type VcsStatusResult,
6} from "@t3tools/contracts";6} from "@t3tools/contracts";
7import { Cause } from "effect";7import * as Cause from "effect/Cause";
8import { Atom } from "effect/unstable/reactivity";8import { Atom } from "effect/unstable/reactivity";
9import { useEffect } from "react";9import { useEffect } from "react";
1010
apps/web/src/lib/processDiagnosticsState.ts+3-1
...@@ -1,6 +1,8 @@...@@ -1,6 +1,8 @@
1import { useAtomValue } from "@effect/atom-react";1import { useAtomValue } from "@effect/atom-react";
2import type { ServerProcessDiagnosticsResult } from "@t3tools/contracts";2import 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";
4import { AsyncResult, Atom } from "effect/unstable/reactivity";6import { AsyncResult, Atom } from "effect/unstable/reactivity";
5import { useCallback } from "react";7import { useCallback } from "react";
68
apps/web/src/lib/projectScriptKeybindings.ts+1-1
...@@ -4,7 +4,7 @@ import {...@@ -4,7 +4,7 @@ import {
4 type KeybindingRule,4 type KeybindingRule,
5 type ResolvedKeybindingsConfig,5 type ResolvedKeybindingsConfig,
6} from "@t3tools/contracts";6} from "@t3tools/contracts";
7import { Schema } from "effect";7import * as Schema from "effect/Schema";
88
9export const PROJECT_SCRIPT_KEYBINDING_INVALID_MESSAGE = "Invalid keybinding.";9export const PROJECT_SCRIPT_KEYBINDING_INVALID_MESSAGE = "Invalid keybinding.";
1010
apps/web/src/lib/providerReactQuery.ts+2-1
...@@ -5,7 +5,8 @@ import {...@@ -5,7 +5,8 @@ import {
5 ThreadId,5 ThreadId,
6} from "@t3tools/contracts";6} from "@t3tools/contracts";
7import { queryOptions } from "@tanstack/react-query";7import { queryOptions } from "@tanstack/react-query";
8import { Option, Schema } from "effect";8import * as Option from "effect/Option";
9import * as Schema from "effect/Schema";
9import { ensureEnvironmentApi } from "../environmentApi";10import { ensureEnvironmentApi } from "../environmentApi";
1011
11interface CheckpointDiffQueryInput {12interface CheckpointDiffQueryInput {
apps/web/src/lib/sourceControlDiscoveryState.ts+1-1
...@@ -7,7 +7,7 @@ import {...@@ -7,7 +7,7 @@ import {
7 sourceControlDiscoveryStateAtom,7 sourceControlDiscoveryStateAtom,
8} from "@t3tools/client-runtime";8} from "@t3tools/client-runtime";
9import { EnvironmentId, type SourceControlDiscoveryResult } from "@t3tools/contracts";9import { EnvironmentId, type SourceControlDiscoveryResult } from "@t3tools/contracts";
10import { Effect } from "effect";10import * as Effect from "effect/Effect";
11import { Atom } from "effect/unstable/reactivity";11import { Atom } from "effect/unstable/reactivity";
1212
13import { readPrimaryEnvironmentDescriptor } from "../environments/primary";13import { readPrimaryEnvironmentDescriptor } from "../environments/primary";
apps/web/src/lib/traceDiagnosticsState.ts+3-1
...@@ -1,6 +1,8 @@...@@ -1,6 +1,8 @@
1import { useAtomValue } from "@effect/atom-react";1import { useAtomValue } from "@effect/atom-react";
2import type { ServerTraceDiagnosticsResult } from "@t3tools/contracts";2import 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";
4import { AsyncResult, Atom } from "effect/unstable/reactivity";6import { AsyncResult, Atom } from "effect/unstable/reactivity";
5import { useCallback } from "react";7import { useCallback } from "react";
68
apps/web/src/observability/clientTracing.ts+5-1
...@@ -1,4 +1,8 @@...@@ -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";
2import { FetchHttpClient, HttpClient } from "effect/unstable/http";6import { FetchHttpClient, HttpClient } from "effect/unstable/http";
3import { OtlpSerialization, OtlpTracer } from "effect/unstable/observability";7import { OtlpSerialization, OtlpTracer } from "effect/unstable/observability";
48
apps/web/src/projectScripts.ts+1-1
...@@ -4,7 +4,7 @@ import {...@@ -4,7 +4,7 @@ import {
4 type KeybindingCommand,4 type KeybindingCommand,
5 type ProjectScript,5 type ProjectScript,
6} from "@t3tools/contracts";6} from "@t3tools/contracts";
7import { Schema } from "effect";7import * as Schema from "effect/Schema";
88
9function normalizeScriptId(value: string): string {9function normalizeScriptId(value: string): string {
10 const cleaned = value10 const cleaned = value
apps/web/src/rpc/protocol.ts+4-1
...@@ -1,5 +1,8 @@...@@ -1,5 +1,8 @@
1import { WsRpcGroup } from "@t3tools/contracts";1import { 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";
3import { RpcClient, RpcSerialization } from "effect/unstable/rpc";6import { RpcClient, RpcSerialization } from "effect/unstable/rpc";
4import * as Socket from "effect/unstable/socket/Socket";7import * as Socket from "effect/unstable/socket/Socket";
58
apps/web/src/rpc/wsRpcClient.ts+2-1
...@@ -10,7 +10,8 @@ import {...@@ -10,7 +10,8 @@ import {
10 WS_METHODS,10 WS_METHODS,
11} from "@t3tools/contracts";11} from "@t3tools/contracts";
12import { applyGitStatusStreamEvent } from "@t3tools/shared/git";12import { applyGitStatusStreamEvent } from "@t3tools/shared/git";
13import { Effect, Stream } from "effect";13import * as Effect from "effect/Effect";
14import * as Stream from "effect/Stream";
1415
15import { type WsRpcProtocolClient } from "./protocol";16import { type WsRpcProtocolClient } from "./protocol";
16import { resetWsReconnectBackoff } from "./wsConnectionState";17import { resetWsReconnectBackoff } from "./wsConnectionState";
apps/web/src/rpc/wsTransport.test.ts+1-1
...@@ -1,5 +1,5 @@...@@ -1,5 +1,5 @@
1import { DEFAULT_SERVER_SETTINGS, WS_METHODS } from "@t3tools/contracts";1import { DEFAULT_SERVER_SETTINGS, WS_METHODS } from "@t3tools/contracts";
2import { Stream } from "effect";2import * as Stream from "effect/Stream";
3import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";3import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
44
5import {5import {
apps/web/src/rpc/wsTransport.ts+9-11
...@@ -1,14 +1,12 @@...@@ -1,14 +1,12 @@
1import {1import * as Cause from "effect/Cause";
2 Cause,2import * as Duration from "effect/Duration";
3 Duration,3import * as Effect from "effect/Effect";
4 Effect,4import * as Exit from "effect/Exit";
5 Exit,5import * as Layer from "effect/Layer";
6 Layer,6import * as ManagedRuntime from "effect/ManagedRuntime";
7 ManagedRuntime,7import * as Option from "effect/Option";
8 Option,8import * as Scope from "effect/Scope";
9 Scope,9import * as Stream from "effect/Stream";
10 Stream,
11} from "effect";
12import { RpcClient } from "effect/unstable/rpc";10import { RpcClient } from "effect/unstable/rpc";
1311
14import { ClientTracingLive } from "../observability/clientTracing";12import { ClientTracingLive } from "../observability/clientTracing";
apps/web/src/store.ts+1-1
...@@ -20,7 +20,7 @@ import type {...@@ -20,7 +20,7 @@ import type {
20} from "@t3tools/contracts";20} from "@t3tools/contracts";
21import { isProviderDriverKind, ProviderDriverKind } from "@t3tools/contracts";21import { isProviderDriverKind, ProviderDriverKind } from "@t3tools/contracts";
22import type { ThreadId, TurnId } from "@t3tools/contracts";22import type { ThreadId, TurnId } from "@t3tools/contracts";
23import { Schema } from "effect";23import * as Schema from "effect/Schema";
24import { resolveModelSlugForProvider } from "@t3tools/shared/model";24import { resolveModelSlugForProvider } from "@t3tools/shared/model";
25import { create } from "zustand";25import { create } from "zustand";
26import {26import {
apps/web/test/wsRpcHarness.ts+5-1
...@@ -1,5 +1,9 @@...@@ -1,5 +1,9 @@
1import { Effect, Exit, PubSub, Scope, Stream } from "effect";
2import { ORCHESTRATION_WS_METHODS, WS_METHODS, WsRpcGroup } from "@t3tools/contracts";1import { 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";
3import { RpcMessage, RpcSerialization, RpcServer } from "effect/unstable/rpc";7import { RpcMessage, RpcSerialization, RpcServer } from "effect/unstable/rpc";
48
5type RpcServerInstance = RpcServer.RpcServer<any>;9type RpcServerInstance = RpcServer.RpcServer<any>;
apps/web/tsconfig.json+13-3
...@@ -15,12 +15,22 @@...@@ -15,12 +15,22 @@
15 "plugins": [15 "plugins": [
16 {16 {
17 "name": "@effect/language-service",17 "name": "@effect/language-service",
18 "namespaceImportPackages": ["@effect/platform-node"],18 "namespaceImportPackages": ["@effect/platform-node", "effect"],
19 "diagnosticSeverity": {19 "diagnosticSeverity": {
20 "importFromBarrel": "error",20 "importFromBarrel": "error",
21 "anyUnknownInErrorContext": "error",21 "anyUnknownInErrorContext": "error",
22 "instanceOfSchema": "warning",22 "unsafeEffectTypeAssertion": "error",
23 "deterministicKeys": "warning"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"
24 }34 }
25 }35 }
26 ]36 ]
bun.lock+1
...@@ -201,6 +201,7 @@...@@ -201,6 +201,7 @@
201 },201 },
202 "devDependencies": {202 "devDependencies": {
203 "@effect/language-service": "catalog:",203 "@effect/language-service": "catalog:",
204 "@effect/platform-node": "catalog:",
204 "@effect/vitest": "catalog:",205 "@effect/vitest": "catalog:",
205 "@types/node": "catalog:",206 "@types/node": "catalog:",
206 "typescript": "catalog:",207 "typescript": "catalog:",
packages/client-runtime/tsconfig.json+1-14
...@@ -1,18 +1,5 @@...@@ -1,18 +1,5 @@
1{1{
2 "extends": "../../tsconfig.base.json",2 "extends": "../../tsconfig.base.json",
3 "compilerOptions": {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 },
17 "include": ["src"]4 "include": ["src"]
18}5}
packages/contracts/src/model.ts+2-2
...@@ -99,10 +99,10 @@ function coerceLegacyOptionsObjectToArray(...@@ -99,10 +99,10 @@ function coerceLegacyOptionsObjectToArray(
99 const entries: Array<ProviderOptionSelection> = [];99 const entries: Array<ProviderOptionSelection> = [];
100 for (const [rawKey, rawValue] of Object.entries(record)) {100 for (const [rawKey, rawValue] of Object.entries(record)) {
101 const id = typeof rawKey === "string" ? rawKey.trim() : "";101 const id = typeof rawKey === "string" ? rawKey.trim() : "";
102 if (!id) continue;102 if (id.length === 0) continue;
103 if (typeof rawValue === "string") {103 if (typeof rawValue === "string") {
104 const trimmed = rawValue.trim();104 const trimmed = rawValue.trim();
105 if (trimmed) entries.push({ id, value: trimmed });105 if (trimmed.length > 0) entries.push({ id, value: trimmed });
106 } else if (typeof rawValue === "boolean") {106 } else if (typeof rawValue === "boolean") {
107 entries.push({ id, value: rawValue });107 entries.push({ id, value: rawValue });
108 }108 }
packages/contracts/src/terminal.test.ts+10-6
...@@ -164,6 +164,8 @@ describe("TerminalCloseInput", () => {...@@ -164,6 +164,8 @@ describe("TerminalCloseInput", () => {
164});164});
165165
166describe("TerminalSessionSnapshot", () => {166describe("TerminalSessionSnapshot", () => {
167 const isoTimestamp = "2026-01-01T00:00:00.000Z";
168
167 it("accepts running snapshots", () => {169 it("accepts running snapshots", () => {
168 expect(170 expect(
169 decodes(TerminalSessionSnapshot, {171 decodes(TerminalSessionSnapshot, {
...@@ -176,20 +178,22 @@ describe("TerminalSessionSnapshot", () => {...@@ -176,20 +178,22 @@ describe("TerminalSessionSnapshot", () => {
176 history: "hello\n",178 history: "hello\n",
177 exitCode: null,179 exitCode: null,
178 exitSignal: null,180 exitSignal: null,
179 updatedAt: new Date().toISOString(),181 updatedAt: isoTimestamp,
180 }),182 }),
181 ).toBe(true);183 ).toBe(true);
182 });184 });
183});185});
184186
185describe("TerminalEvent", () => {187describe("TerminalEvent", () => {
188 const isoTimestamp = "2026-01-01T00:00:00.000Z";
189
186 it("accepts output events", () => {190 it("accepts output events", () => {
187 expect(191 expect(
188 decodes(TerminalEvent, {192 decodes(TerminalEvent, {
189 type: "output",193 type: "output",
190 threadId: "thread-1",194 threadId: "thread-1",
191 terminalId: DEFAULT_TERMINAL_ID,195 terminalId: DEFAULT_TERMINAL_ID,
192 createdAt: new Date().toISOString(),196 createdAt: isoTimestamp,
193 data: "line\n",197 data: "line\n",
194 }),198 }),
195 ).toBe(true);199 ).toBe(true);
...@@ -201,7 +205,7 @@ describe("TerminalEvent", () => {...@@ -201,7 +205,7 @@ describe("TerminalEvent", () => {
201 type: "exited",205 type: "exited",
202 threadId: "thread-1",206 threadId: "thread-1",
203 terminalId: DEFAULT_TERMINAL_ID,207 terminalId: DEFAULT_TERMINAL_ID,
204 createdAt: new Date().toISOString(),208 createdAt: isoTimestamp,
205 exitCode: 0,209 exitCode: 0,
206 exitSignal: null,210 exitSignal: null,
207 }),211 }),
...@@ -214,7 +218,7 @@ describe("TerminalEvent", () => {...@@ -214,7 +218,7 @@ describe("TerminalEvent", () => {
214 type: "activity",218 type: "activity",
215 threadId: "thread-1",219 threadId: "thread-1",
216 terminalId: DEFAULT_TERMINAL_ID,220 terminalId: DEFAULT_TERMINAL_ID,
217 createdAt: new Date().toISOString(),221 createdAt: isoTimestamp,
218 hasRunningSubprocess: true,222 hasRunningSubprocess: true,
219 }),223 }),
220 ).toBe(true);224 ).toBe(true);
...@@ -226,7 +230,7 @@ describe("TerminalEvent", () => {...@@ -226,7 +230,7 @@ describe("TerminalEvent", () => {
226 type: "started",230 type: "started",
227 threadId: "thread-1",231 threadId: "thread-1",
228 terminalId: DEFAULT_TERMINAL_ID,232 terminalId: DEFAULT_TERMINAL_ID,
229 createdAt: new Date().toISOString(),233 createdAt: isoTimestamp,
230 snapshot: {234 snapshot: {
231 threadId: "thread-1",235 threadId: "thread-1",
232 terminalId: DEFAULT_TERMINAL_ID,236 terminalId: DEFAULT_TERMINAL_ID,
...@@ -237,7 +241,7 @@ describe("TerminalEvent", () => {...@@ -237,7 +241,7 @@ describe("TerminalEvent", () => {
237 history: "",241 history: "",
238 exitCode: null,242 exitCode: null,
239 exitSignal: null,243 exitSignal: null,
240 updatedAt: new Date().toISOString(),244 updatedAt: isoTimestamp,
241 },245 },
242 }),246 }),
243 ).toBe(true);247 ).toBe(true);
packages/contracts/src/terminal.ts+5-2
...@@ -170,10 +170,13 @@ export class TerminalCwdError extends Schema.TaggedErrorClass<TerminalCwdError>(...@@ -170,10 +170,13 @@ export class TerminalCwdError extends Schema.TaggedErrorClass<TerminalCwdError>(
170 return `Terminal cwd does not exist: ${this.cwd}`;170 return `Terminal cwd does not exist: ${this.cwd}`;
171 }171 }
172 const causeMessage =172 const causeMessage =
173 this.cause && typeof this.cause === "object" && "message" in this.cause173 this.cause !== undefined &&
174 this.cause !== null &&
175 typeof this.cause === "object" &&
176 "message" in this.cause
174 ? this.cause.message177 ? this.cause.message
175 : undefined;178 : undefined;
176 return causeMessage179 return typeof causeMessage === "string" && causeMessage.length > 0
177 ? `Failed to access terminal cwd: ${this.cwd} (${causeMessage})`180 ? `Failed to access terminal cwd: ${this.cwd} (${causeMessage})`
178 : `Failed to access terminal cwd: ${this.cwd}`;181 : `Failed to access terminal cwd: ${this.cwd}`;
179 }182 }
packages/contracts/tsconfig.json+1-14
...@@ -1,18 +1,5 @@...@@ -1,18 +1,5 @@
1{1{
2 "extends": "../../tsconfig.base.json",2 "extends": "../../tsconfig.base.json",
3 "compilerOptions": {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 },
17 "include": ["src"]4 "include": ["src"]
18}5}
packages/effect-acp/test/examples/cursor-acp-client.example.ts+3-3
...@@ -48,7 +48,7 @@ const program = Effect.gen(function* () {...@@ -48,7 +48,7 @@ const program = Effect.gen(function* () {
48 version: "0.0.0",48 version: "0.0.0",
49 },49 },
50 });50 });
51 yield* Console.log("initialized", JSON.stringify(initialized, null, 4));51 yield* Console.log("initialized", initialized);
5252
53 const session = yield* acp.agent.createSession({53 const session = yield* acp.agent.createSession({
54 cwd: process.cwd(),54 cwd: process.cwd(),
...@@ -61,7 +61,7 @@ const program = Effect.gen(function* () {...@@ -61,7 +61,7 @@ const program = Effect.gen(function* () {
61 value: "claude-opus-4-6",61 value: "claude-opus-4-6",
62 });62 });
6363
64 yield* Console.log("config", JSON.stringify(config, null, 4));64 yield* Console.log("config", config);
6565
66 const result = yield* acp.agent.prompt({66 const result = yield* acp.agent.prompt({
67 sessionId: session.sessionId,67 sessionId: session.sessionId,
...@@ -73,7 +73,7 @@ const program = Effect.gen(function* () {...@@ -73,7 +73,7 @@ const program = Effect.gen(function* () {
73 ],73 ],
74 });74 });
7575
76 yield* Console.log("prompt result", JSON.stringify(result));76 yield* Console.log("prompt result", result);
77 yield* acp.agent.cancel({ sessionId: session.sessionId });77 yield* acp.agent.cancel({ sessionId: session.sessionId });
78 }).pipe(Effect.provide(acpLayer));78 }).pipe(Effect.provide(acpLayer));
79});79});
packages/effect-acp/tsconfig.json+1-14
...@@ -1,18 +1,5 @@...@@ -1,18 +1,5 @@
1{1{
2 "extends": "../../tsconfig.base.json",2 "extends": "../../tsconfig.base.json",
3 "compilerOptions": {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 },
17 "include": ["src", "scripts", "test"]4 "include": ["src", "scripts", "test"]
18}5}
packages/effect-codex-app-server/src/client.ts+20-20
...@@ -17,6 +17,8 @@ import {...@@ -17,6 +17,8 @@ import {
17} from "./_internal/shared.ts";17} from "./_internal/shared.ts";
18import { makeChildStdio, makeTerminationError } from "./_internal/stdio.ts";18import { makeChildStdio, makeTerminationError } from "./_internal/stdio.ts";
1919
20const DEFAULT_APP_SERVER_FORCE_KILL_AFTER = "2 seconds" as const;
21
20export interface CodexAppServerClientOptions {22export interface CodexAppServerClientOptions {
21 readonly logIncoming?: boolean;23 readonly logIncoming?: boolean;
22 readonly logOutgoing?: boolean;24 readonly logOutgoing?: boolean;
...@@ -273,26 +275,24 @@ export const layerCommand = (...@@ -273,26 +275,24 @@ export const layerCommand = (
273> =>275> =>
274 Layer.effect(276 Layer.effect(
275 CodexAppServerClient,277 CodexAppServerClient,
276 Effect.acquireRelease(278 Effect.gen(function* () {
277 Effect.gen(function* () {279 const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
278 const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;280 const command = ChildProcess.make(options.command, [...(options.args ?? [])], {
279 const command = ChildProcess.make(options.command, [...(options.args ?? [])], {281 ...(options.cwd ? { cwd: options.cwd } : {}),
280 ...(options.cwd ? { cwd: options.cwd } : {}),282 ...(options.env ? { env: { ...process.env, ...options.env } } : {}),
281 ...(options.env ? { env: { ...process.env, ...options.env } } : {}),283 forceKillAfter: DEFAULT_APP_SERVER_FORCE_KILL_AFTER,
282 shell: process.platform === "win32",284 shell: process.platform === "win32",
283 });285 });
284 return yield* spawner.spawn(command).pipe(286 return yield* spawner.spawn(command).pipe(
285 Effect.mapError(287 Effect.mapError(
286 (cause) =>288 (cause) =>
287 new CodexError.CodexAppServerSpawnError({289 new CodexError.CodexAppServerSpawnError({
288 command: [options.command, ...(options.args ?? [])].join(" "),290 command: [options.command, ...(options.args ?? [])].join(" "),
289 cause,291 cause,
290 }),292 }),
291 ),293 ),
292 );294 );
293 }),295 }).pipe(
294 (handle) => handle.kill().pipe(Effect.orDie),
295 ).pipe(
296 Effect.flatMap((handle) =>296 Effect.flatMap((handle) =>
297 make(makeChildStdio(handle), options, makeTerminationError(handle)),297 make(makeChildStdio(handle), options, makeTerminationError(handle)),
298 ),298 ),
packages/effect-codex-app-server/test/examples/codex-app-server-probe.ts+3-3
...@@ -45,17 +45,17 @@ const program = Effect.gen(function* () {...@@ -45,17 +45,17 @@ const program = Effect.gen(function* () {
45 optOutNotificationMethods: null,45 optOutNotificationMethods: null,
46 },46 },
47 });47 });
48 yield* Console.log("initialize", JSON.stringify(initialized, null, 2));48 yield* Console.log("initialize", initialized);
4949
50 yield* client.notify("initialized", undefined);50 yield* client.notify("initialized", undefined);
5151
52 const account = yield* client.request("account/read", {});52 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
55 const skills = yield* client.request("skills/list", {55 const skills = yield* client.request("skills/list", {
56 cwds: [process.cwd()],56 cwds: [process.cwd()],
57 });57 });
58 yield* Console.log("skills/list", JSON.stringify(skills, null, 2));58 yield* Console.log("skills/list", skills);
59 }).pipe(Effect.provide(codexLayer));59 }).pipe(Effect.provide(codexLayer));
60});60});
6161
packages/effect-codex-app-server/tsconfig.json+1-14
...@@ -1,18 +1,5 @@...@@ -1,18 +1,5 @@
1{1{
2 "extends": "../../tsconfig.base.json",2 "extends": "../../tsconfig.base.json",
3 "compilerOptions": {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 },
17 "include": ["src", "scripts", "test"]4 "include": ["src", "scripts", "test"]
18}5}
packages/shared/package.json+1
...@@ -95,6 +95,7 @@...@@ -95,6 +95,7 @@
95 },95 },
96 "devDependencies": {96 "devDependencies": {
97 "@effect/language-service": "catalog:",97 "@effect/language-service": "catalog:",
98 "@effect/platform-node": "catalog:",
98 "@effect/vitest": "catalog:",99 "@effect/vitest": "catalog:",
99 "@types/node": "catalog:",100 "@types/node": "catalog:",
100 "typescript": "catalog:",101 "typescript": "catalog:",
packages/shared/src/Net.test.ts+12-10
...@@ -3,7 +3,7 @@ import * as NodeNet from "node:net";...@@ -3,7 +3,7 @@ import * as NodeNet from "node:net";
3import { assert, describe, it } from "@effect/vitest";3import { assert, describe, it } from "@effect/vitest";
4import * as Effect from "effect/Effect";4import * as Effect from "effect/Effect";
55
6import * as Net from "./Net.ts";6import * as NetService from "./Net.ts";
77
8const closeServer = (server: NodeNet.Server) =>8const closeServer = (server: NodeNet.Server) =>
9 Effect.sync(() => {9 Effect.sync(() => {
...@@ -19,19 +19,21 @@ const getPort = (server: NodeNet.Server): number => {...@@ -19,19 +19,21 @@ const getPort = (server: NodeNet.Server): number => {
19 return typeof address === "object" && address !== null ? address.port : 0;19 return typeof address === "object" && address !== null ? address.port : 0;
20};20};
2121
22const openServer = (host?: string): Effect.Effect<NodeNet.Server, Net.NetError> =>22const openServer = (host?: string): Effect.Effect<NodeNet.Server, NetService.NetError> =>
23 Effect.callback<NodeNet.Server, Net.NetError>((resume) => {23 Effect.callback<NodeNet.Server, NetService.NetError>((resume) => {
24 const server = NodeNet.createServer();24 const server = NodeNet.createServer();
25 let settled = false;25 let settled = false;
2626
27 const settle = (effect: Effect.Effect<NodeNet.Server, Net.NetError>) => {27 const settle = (effect: Effect.Effect<NodeNet.Server, NetService.NetError>) => {
28 if (settled) return;28 if (settled) return;
29 settled = true;29 settled = true;
30 resume(effect);30 resume(effect);
31 };31 };
3232
33 server.once("error", (cause) => {33 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 );
35 });37 });
3638
37 if (host) {39 if (host) {
...@@ -43,11 +45,11 @@ const openServer = (host?: string): Effect.Effect<NodeNet.Server, Net.NetError>...@@ -43,11 +45,11 @@ const openServer = (host?: string): Effect.Effect<NodeNet.Server, Net.NetError>
43 return closeServer(server);45 return closeServer(server);
44 });46 });
4547
46it.layer(Net.NetService.layer)("NetService", (it) => {48it.layer(NetService.layer)("NetService", (it) => {
47 describe("Net helpers", () => {49 describe("Net helpers", () => {
48 it.effect("reserveLoopbackPort returns a positive loopback port", () =>50 it.effect("reserveLoopbackPort returns a positive loopback port", () =>
49 Effect.gen(function* () {51 Effect.gen(function* () {
50 const net = yield* Net.NetService;52 const net = yield* NetService.NetService;
51 const port = yield* net.reserveLoopbackPort();53 const port = yield* net.reserveLoopbackPort();
5254
53 assert.ok(port > 0);55 assert.ok(port > 0);
...@@ -59,7 +61,7 @@ it.layer(Net.NetService.layer)("NetService", (it) => {...@@ -59,7 +61,7 @@ it.layer(Net.NetService.layer)("NetService", (it) => {
59 openServer("127.0.0.1"),61 openServer("127.0.0.1"),
60 (server) =>62 (server) =>
61 Effect.gen(function* () {63 Effect.gen(function* () {
62 const net = yield* Net.NetService;64 const net = yield* NetService.NetService;
63 const port = getPort(server);65 const port = getPort(server);
6466
65 const available = yield* net.isPortAvailableOnLoopback(port);67 const available = yield* net.isPortAvailableOnLoopback(port);
...@@ -71,7 +73,7 @@ it.layer(Net.NetService.layer)("NetService", (it) => {...@@ -71,7 +73,7 @@ it.layer(Net.NetService.layer)("NetService", (it) => {
7173
72 it.effect("findAvailablePort returns preferred when it is free", () =>74 it.effect("findAvailablePort returns preferred when it is free", () =>
73 Effect.gen(function* () {75 Effect.gen(function* () {
74 const net = yield* Net.NetService;76 const net = yield* NetService.NetService;
75 const preferred = yield* net.reserveLoopbackPort();77 const preferred = yield* net.reserveLoopbackPort();
7678
77 const resolved = yield* net.findAvailablePort(preferred);79 const resolved = yield* net.findAvailablePort(preferred);
...@@ -84,7 +86,7 @@ it.layer(Net.NetService.layer)("NetService", (it) => {...@@ -84,7 +86,7 @@ it.layer(Net.NetService.layer)("NetService", (it) => {
84 openServer(),86 openServer(),
85 (server) =>87 (server) =>
86 Effect.gen(function* () {88 Effect.gen(function* () {
87 const net = yield* Net.NetService;89 const net = yield* NetService.NetService;
88 const preferred = getPort(server);90 const preferred = getPort(server);
8991
90 const resolved = yield* net.findAvailablePort(preferred);92 const resolved = yield* net.findAvailablePort(preferred);
packages/shared/src/Net.ts+16-19
...@@ -4,22 +4,21 @@ import * as Data from "effect/Data";...@@ -4,22 +4,21 @@ import * as Data from "effect/Data";
4import * as Effect from "effect/Effect";4import * as Effect from "effect/Effect";
5import * as Layer from "effect/Layer";5import * as Layer from "effect/Layer";
6import * as Context from "effect/Context";6import * as Context from "effect/Context";
7import * as Predicate from "effect/Predicate";
78
8export class NetError extends Data.TaggedError("NetError")<{9export class NetError extends Data.TaggedError("NetError")<{
9 readonly message: string;10 readonly message: string;
10 readonly cause?: unknown;11 readonly cause?: unknown;
11}> {}12}> {}
1213
13function isErrnoExceptionWithCode(cause: unknown): cause is {14const isErrnoExceptionWithCode = (
15 cause: unknown,
16): cause is {
14 readonly code: string;17 readonly code: string;
15} {18} =>
16 return (19 Predicate.isObject(cause) &&
17 typeof cause === "object" &&20 Predicate.hasProperty(cause, "code") &&
18 cause !== null &&21 Predicate.isString(cause.code);
19 "code" in cause &&
20 typeof (cause as { readonly code: unknown }).code === "string"
21 );
22}
2322
24const closeServer = (server: NodeNet.Server) => {23const closeServer = (server: NodeNet.Server) => {
25 try {24 try {
...@@ -85,6 +84,13 @@ export interface NetServiceShape {...@@ -85,6 +84,13 @@ export interface NetServiceShape {
85 readonly findAvailablePort: (preferred: number) => Effect.Effect<number, NetError>;84 readonly findAvailablePort: (preferred: number) => Effect.Effect<number, NetError>;
86}85}
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
88export const make = () => {94export const make = () => {
89 /**95 /**
90 * Returns true when a TCP server can bind to {host, port}.96 * Returns true when a TCP server can bind to {host, port}.
...@@ -175,13 +181,4 @@ export const make = () => {...@@ -175,13 +181,4 @@ export const make = () => {
175 } satisfies NetServiceShape;181 } satisfies NetServiceShape;
176};182};
177183
178/**184export const layer = Layer.sync(NetService, make);
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;
packages/shared/src/logging.ts+1
...@@ -1,3 +1,4 @@...@@ -1,3 +1,4 @@
1// @effect-diagnostics nodeBuiltinImport:off
1import fs from "node:fs";2import fs from "node:fs";
2import path from "node:path";3import path from "node:path";
34
packages/shared/src/observability.test.ts+108-114
...@@ -1,11 +1,10 @@...@@ -1,11 +1,10 @@
1import fs from "node:fs";
2import os from "node:os";
3import path from "node:path";
4
5import { assert, describe, it } from "@effect/vitest";1import { assert, describe, it } from "@effect/vitest";
2import * as NodeServices from "@effect/platform-node/NodeServices";
6import * as Effect from "effect/Effect";3import * as Effect from "effect/Effect";
4import * as FileSystem from "effect/FileSystem";
7import * as Layer from "effect/Layer";5import * as Layer from "effect/Layer";
8import * as Logger from "effect/Logger";6import * as Logger from "effect/Logger";
7import * as Path from "effect/Path";
9import * as References from "effect/References";8import * as References from "effect/References";
10import * as Schema from "effect/Schema";9import * as Schema from "effect/Schema";
11import * as Tracer from "effect/Tracer";10import * as Tracer from "effect/Tracer";
...@@ -57,13 +56,14 @@ const makeRecord = (name: string, suffix = ""): TraceRecord => ({...@@ -57,13 +56,14 @@ const makeRecord = (name: string, suffix = ""): TraceRecord => ({
57 },56 },
58});57});
5958
60const readTraceRecords = (tracePath: string) =>59const readTraceRecords = Effect.fn("readTraceRecords")(function* (tracePath: string) {
61 fs60 const fileSystem = yield* FileSystem.FileSystem;
62 .readFileSync(tracePath, "utf8")61 return (yield* fileSystem.readFileString(tracePath))
63 .trim()62 .trim()
64 .split("\n")63 .split("\n")
65 .filter((line) => line.length > 0)64 .filter((line) => line.length > 0)
66 .map((line) => decodeTraceRecordLine(line));65 .map((line) => decodeTraceRecordLine(line));
66});
6767
68const makeTestLayer = (tracePath: string) =>68const makeTestLayer = (tracePath: string) =>
69 Layer.mergeAll(69 Layer.mergeAll(
...@@ -106,9 +106,11 @@ describe("observability", () => {...@@ -106,9 +106,11 @@ describe("observability", () => {
106 });106 });
107107
108 it("normalizes invalid dates without throwing", () => {108 it("normalizes invalid dates without throwing", () => {
109 // @effect-diagnostics-next-line globalDate:off
110 const invalidDate = new Date("not-a-real-date");
109 assert.deepStrictEqual(111 assert.deepStrictEqual(
110 compactTraceAttributes({112 compactTraceAttributes({
111 invalidDate: new Date("not-a-real-date"),113 invalidDate,
112 }),114 }),
113 {115 {
114 invalidDate: "Invalid Date",116 invalidDate: "Invalid Date",
...@@ -119,122 +121,116 @@ describe("observability", () => {...@@ -119,122 +121,116 @@ describe("observability", () => {
119 it.effect("flushes buffered trace records on close", () =>121 it.effect("flushes buffered trace records on close", () =>
120 Effect.scoped(122 Effect.scoped(
121 Effect.gen(function* () {123 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-" });
123 const tracePath = path.join(tempDir, "shared.trace.ndjson");127 const tracePath = path.join(tempDir, "shared.trace.ndjson");
124128
125 try {129 const sink = yield* makeTraceSink({
126 const sink = yield* makeTraceSink({130 filePath: tracePath,
127 filePath: tracePath,131 maxBytes: 1024,
128 maxBytes: 1024,132 maxFiles: 2,
129 maxFiles: 2,133 batchWindowMs: 10_000,
130 batchWindowMs: 10_000,134 });
131 });135
132136 sink.push(makeRecord("alpha"));
133 sink.push(makeRecord("alpha"));137 sink.push(makeRecord("beta"));
134 sink.push(makeRecord("beta"));138 yield* sink.close();
135 yield* sink.close();139
136140 const lines = yield* readTraceRecords(tracePath);
137 const lines = readTraceRecords(tracePath);141
138142 assert.equal(lines.length, 2);
139 assert.equal(lines.length, 2);143 assert.equal(lines[0]?.name, "alpha");
140 assert.equal(lines[0]?.name, "alpha");144 assert.equal(lines[1]?.name, "beta");
141 assert.equal(lines[1]?.name, "beta");145 }).pipe(Effect.provide(NodeServices.layer)),
142 } finally {
143 fs.rmSync(tempDir, { recursive: true, force: true });
144 }
145 }),
146 ),146 ),
147 );147 );
148148
149 it.effect("rotates the trace file when the configured max size is exceeded", () =>149 it.effect("rotates the trace file when the configured max size is exceeded", () =>
150 Effect.scoped(150 Effect.scoped(
151 Effect.gen(function* () {151 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-" });
153 const tracePath = path.join(tempDir, "shared.trace.ndjson");155 const tracePath = path.join(tempDir, "shared.trace.ndjson");
154156
155 try {157 const sink = yield* makeTraceSink({
156 const sink = yield* makeTraceSink({158 filePath: tracePath,
157 filePath: tracePath,159 maxBytes: 180,
158 maxBytes: 180,160 maxFiles: 2,
159 maxFiles: 2,161 batchWindowMs: 10_000,
160 batchWindowMs: 10_000,162 });
161 });163
162164 for (let index = 0; index < 8; index += 1) {
163 for (let index = 0; index < 8; index += 1) {165 sink.push(makeRecord("rotate", `${index}-${"x".repeat(48)}`));
164 sink.push(makeRecord("rotate", `${index}-${"x".repeat(48)}`));166 yield* sink.flush;
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 });
187 }167 }
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)),
189 ),185 ),
190 );186 );
191187
192 it.effect("drops only the invalid trace record when serialization fails", () =>188 it.effect("drops only the invalid trace record when serialization fails", () =>
193 Effect.scoped(189 Effect.scoped(
194 Effect.gen(function* () {190 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-" });
196 const tracePath = path.join(tempDir, "shared.trace.ndjson");194 const tracePath = path.join(tempDir, "shared.trace.ndjson");
197195
198 try {196 const sink = yield* makeTraceSink({
199 const sink = yield* makeTraceSink({197 filePath: tracePath,
200 filePath: tracePath,198 maxBytes: 1024,
201 maxBytes: 1024,199 maxFiles: 2,
202 maxFiles: 2,200 batchWindowMs: 10_000,
203 batchWindowMs: 10_000,201 });
204 });202
205203 const circular: Array<unknown> = [];
206 const circular: Array<unknown> = [];204 circular.push(circular);
207 circular.push(circular);205
208206 sink.push(makeRecord("alpha"));
209 sink.push(makeRecord("alpha"));207 sink.push({
210 sink.push({208 ...makeRecord("invalid"),
211 ...makeRecord("invalid"),209 attributes: {
212 attributes: {210 circular,
213 circular,211 },
214 },212 } as TraceRecord);
215 } as TraceRecord);213 sink.push(makeRecord("beta"));
216 sink.push(makeRecord("beta"));214 yield* sink.close();
217 yield* sink.close();215
218216 const lines = yield* readTraceRecords(tracePath);
219 const lines = readTraceRecords(tracePath);217
220218 assert.deepStrictEqual(
221 assert.deepStrictEqual(219 lines.map((line) => line.name),
222 lines.map((line) => line.name),220 ["alpha", "beta"],
223 ["alpha", "beta"],221 );
224 );222 }).pipe(Effect.provide(NodeServices.layer)),
225 } finally {
226 fs.rmSync(tempDir, { recursive: true, force: true });
227 }
228 }),
229 ),223 ),
230 );224 );
231225
232 it.effect("writes nested spans to disk and captures log messages as span events", () =>226 it.effect("writes nested spans to disk and captures log messages as span events", () =>
233 Effect.gen(function* () {227 Effect.scoped(
234 const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "t3-local-tracer-"));228 Effect.gen(function* () {
235 const tracePath = path.join(tempDir, "shared.trace.ndjson");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 {
238 yield* Effect.scoped(234 yield* Effect.scoped(
239 Effect.gen(function* () {235 Effect.gen(function* () {
240 const program = Effect.gen(function* () {236 const program = Effect.gen(function* () {
...@@ -254,7 +250,7 @@ describe("observability", () => {...@@ -254,7 +250,7 @@ describe("observability", () => {
254 }),250 }),
255 );251 );
256252
257 const records = readTraceRecords(tracePath);253 const records = yield* readTraceRecords(tracePath);
258 assert.equal(records.length, 2);254 assert.equal(records.length, 2);
259255
260 const parent = records.find((record) => record.name === "parent-span");256 const parent = records.find((record) => record.name === "parent-span");
...@@ -281,18 +277,18 @@ describe("observability", () => {...@@ -281,18 +277,18 @@ describe("observability", () => {
281 child.events.some((event) => event.attributes["effect.logLevel"] === "INFO"),277 child.events.some((event) => event.attributes["effect.logLevel"] === "INFO"),
282 true,278 true,
283 );279 );
284 } finally {280 }).pipe(Effect.provide(NodeServices.layer)),
285 fs.rmSync(tempDir, { recursive: true, force: true });281 ),
286 }
287 }),
288 );282 );
289283
290 it.effect("serializes interrupted spans with an interrupted exit status", () =>284 it.effect("serializes interrupted spans with an interrupted exit status", () =>
291 Effect.gen(function* () {285 Effect.scoped(
292 const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "t3-local-tracer-"));286 Effect.gen(function* () {
293 const tracePath = path.join(tempDir, "shared.trace.ndjson");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 {
296 yield* Effect.scoped(292 yield* Effect.scoped(
297 Effect.exit(293 Effect.exit(
298 Effect.interrupt.pipe(294 Effect.interrupt.pipe(
...@@ -302,13 +298,11 @@ describe("observability", () => {...@@ -302,13 +298,11 @@ describe("observability", () => {
302 ),298 ),
303 );299 );
304300
305 const records = readTraceRecords(tracePath);301 const records = yield* readTraceRecords(tracePath);
306 assert.equal(records.length, 1);302 assert.equal(records.length, 1);
307 assert.equal(records[0]?.name, "interrupt-span");303 assert.equal(records[0]?.name, "interrupt-span");
308 assert.equal(records[0]?.exit?._tag, "Interrupted");304 assert.equal(records[0]?.exit?._tag, "Interrupted");
309 } finally {305 }).pipe(Effect.provide(NodeServices.layer)),
310 fs.rmSync(tempDir, { recursive: true, force: true });306 ),
311 }
312 }),
313 );307 );
314});308});
packages/shared/src/schemaJson.ts+18
...@@ -84,6 +84,10 @@ export const fromLenientJsonString = new SchemaTransformation.Transformation(...@@ -84,6 +84,10 @@ export const fromLenientJsonString = new SchemaTransformation.Transformation(
84 SchemaGetter.stringifyJson(),84 SchemaGetter.stringifyJson(),
85);85);
8686
87export const prettyJsonString = SchemaGetter.parseJson<string>().compose(
88 SchemaGetter.stringifyJson({ space: 2 }),
89);
90
87/**91/**
88 * Build a schema that decodes a lenient JSON string into `A`.92 * Build a schema that decodes a lenient JSON string into `A`.
89 *93 *
...@@ -92,3 +96,17 @@ export const fromLenientJsonString = new SchemaTransformation.Transformation(...@@ -92,3 +96,17 @@ export const fromLenientJsonString = new SchemaTransformation.Transformation(
92 */96 */
93export const fromLenientJson = <S extends Schema.Top>(schema: S) =>97export const fromLenientJson = <S extends Schema.Top>(schema: S) =>
94 Schema.String.pipe(Schema.decodeTo(schema, fromLenientJsonString));98 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,3 +1,4 @@
1// @effect-diagnostics nodeBuiltinImport:off
1import * as NodeOS from "node:os";2import * as NodeOS from "node:os";
2import { execFileSync } from "node:child_process";3import { execFileSync } from "node:child_process";
3import { accessSync, constants, statSync } from "node:fs";4import { accessSync, constants, statSync } from "node:fs";
packages/shared/tsconfig.json+1-14
...@@ -1,18 +1,5 @@...@@ -1,18 +1,5 @@
1{1{
2 "extends": "../../tsconfig.base.json",2 "extends": "../../tsconfig.base.json",
3 "compilerOptions": {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 },
17 "include": ["src"]4 "include": ["src"]
18}5}
packages/ssh/src/command.test.ts+1-1
...@@ -7,7 +7,7 @@ import * as Layer from "effect/Layer";...@@ -7,7 +7,7 @@ import * as Layer from "effect/Layer";
7import * as Result from "effect/Result";7import * as Result from "effect/Result";
8import * as Sink from "effect/Sink";8import * as Sink from "effect/Sink";
9import * as Stream from "effect/Stream";9import * as Stream from "effect/Stream";
10import { TestClock } from "effect/testing";10import * as TestClock from "effect/testing/TestClock";
11import { ChildProcessSpawner } from "effect/unstable/process";11import { ChildProcessSpawner } from "effect/unstable/process";
1212
13import {13import {
packages/ssh/src/tunnel.test.ts+4-4
...@@ -1,6 +1,6 @@...@@ -1,6 +1,6 @@
1import { assert, describe, it } from "@effect/vitest";1import { assert, describe, it } from "@effect/vitest";
2import * as NodeServices from "@effect/platform-node/NodeServices";2import * as NodeServices from "@effect/platform-node/NodeServices";
3import { NetService } from "@t3tools/shared/Net";3import * as NetService from "@t3tools/shared/Net";
4import * as Duration from "effect/Duration";4import * as Duration from "effect/Duration";
5import * as Effect from "effect/Effect";5import * as Effect from "effect/Effect";
6import * as Fiber from "effect/Fiber";6import * as Fiber from "effect/Fiber";
...@@ -8,7 +8,7 @@ import * as Layer from "effect/Layer";...@@ -8,7 +8,7 @@ import * as Layer from "effect/Layer";
8import * as Result from "effect/Result";8import * as Result from "effect/Result";
9import * as Sink from "effect/Sink";9import * as Sink from "effect/Sink";
10import * as Stream from "effect/Stream";10import * as Stream from "effect/Stream";
11import { TestClock } from "effect/testing";11import * as TestClock from "effect/testing/TestClock";
12import { HttpClient, HttpClientResponse } from "effect/unstable/http";12import { HttpClient, HttpClientResponse } from "effect/unstable/http";
13import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";13import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
1414
...@@ -74,7 +74,7 @@ const testHttpClient = HttpClient.make((request) =>...@@ -74,7 +74,7 @@ const testHttpClient = HttpClient.make((request) =>
7474
75const hangingHttpClient = HttpClient.make(() => Effect.never);75const hangingHttpClient = HttpClient.make(() => Effect.never);
7676
77const testNetService = NetService.of({77const testNetService = NetService.NetService.of({
78 canListenOnHost: () => Effect.succeed(true),78 canListenOnHost: () => Effect.succeed(true),
79 isPortAvailableOnLoopback: () => Effect.succeed(true),79 isPortAvailableOnLoopback: () => Effect.succeed(true),
80 reserveLoopbackPort: () => Effect.succeed(41_773),80 reserveLoopbackPort: () => Effect.succeed(41_773),
...@@ -270,7 +270,7 @@ describe("ssh tunnel scripts", () => {...@@ -270,7 +270,7 @@ describe("ssh tunnel scripts", () => {
270 NodeServices.layer,270 NodeServices.layer,
271 Layer.succeed(ChildProcessSpawner.ChildProcessSpawner, spawner),271 Layer.succeed(ChildProcessSpawner.ChildProcessSpawner, spawner),
272 Layer.succeed(HttpClient.HttpClient, testHttpClient),272 Layer.succeed(HttpClient.HttpClient, testHttpClient),
273 Layer.succeed(NetService, testNetService),273 Layer.succeed(NetService.NetService, testNetService),
274 SshPasswordPrompt.disabledLayer,274 SshPasswordPrompt.disabledLayer,
275 SshEnvironmentManager.layer(),275 SshEnvironmentManager.layer(),
276 );276 );
packages/ssh/src/tunnel.ts+6-6
...@@ -2,7 +2,7 @@ import type {...@@ -2,7 +2,7 @@ import type {
2 DesktopSshEnvironmentBootstrap,2 DesktopSshEnvironmentBootstrap,
3 DesktopSshEnvironmentTarget,3 DesktopSshEnvironmentTarget,
4} from "@t3tools/contracts";4} from "@t3tools/contracts";
5import { type NetError, NetService } from "@t3tools/shared/Net";5import * as NetService from "@t3tools/shared/Net";
6import { fromLenientJson } from "@t3tools/shared/schemaJson";6import { fromLenientJson } from "@t3tools/shared/schemaJson";
7import * as Context from "effect/Context";7import * as Context from "effect/Context";
8import * as Deferred from "effect/Deferred";8import * as Deferred from "effect/Deferred";
...@@ -82,7 +82,7 @@ type SshEnvironmentEffectContext =...@@ -82,7 +82,7 @@ type SshEnvironmentEffectContext =
82 | FileSystem.FileSystem82 | FileSystem.FileSystem
83 | Path.Path83 | Path.Path
84 | HttpClient.HttpClient84 | HttpClient.HttpClient
85 | NetService85 | NetService.NetService
86 | SshPasswordPrompt;86 | SshPasswordPrompt;
8787
88type SshEnvironmentEffectError =88type SshEnvironmentEffectError =
...@@ -92,7 +92,7 @@ type SshEnvironmentEffectError =...@@ -92,7 +92,7 @@ type SshEnvironmentEffectError =
92 | SshPairingError92 | SshPairingError
93 | SshReadinessError93 | SshReadinessError
94 | SshPasswordPromptError94 | SshPasswordPromptError
95 | NetError;95 | NetService.NetError;
9696
97function makeSshTunnelCancelledError(target: DesktopSshEnvironmentTarget): SshCommandError {97function makeSshTunnelCancelledError(target: DesktopSshEnvironmentTarget): SshCommandError {
98 return new SshCommandError({98 return new SshCommandError({
...@@ -903,7 +903,7 @@ export const fetchLoopbackSshJson = Effect.fn("ssh/tunnel.fetchLoopbackSshJson")...@@ -903,7 +903,7 @@ export const fetchLoopbackSshJson = Effect.fn("ssh/tunnel.fetchLoopbackSshJson")
903});903});
904904
905const reserveLocalTunnelPort = Effect.fn("ssh/tunnel.reserveLocalTunnelPort")(function* () {905const reserveLocalTunnelPort = Effect.fn("ssh/tunnel.reserveLocalTunnelPort")(function* () {
906 const net = yield* NetService;906 const net = yield* NetService.NetService;
907 return yield* net.reserveLoopbackPort();907 return yield* net.reserveLoopbackPort();
908});908});
909909
...@@ -923,7 +923,7 @@ const startSshTunnel = Effect.fn("ssh/tunnel.startSshTunnel")(function* (input:...@@ -923,7 +923,7 @@ const startSshTunnel = Effect.fn("ssh/tunnel.startSshTunnel")(function* (input:
923 | FileSystem.FileSystem923 | FileSystem.FileSystem
924 | Path.Path924 | Path.Path
925 | HttpClient.HttpClient925 | HttpClient.HttpClient
926 | NetService926 | NetService.NetService
927 | Scope.Scope927 | Scope.Scope
928> {928> {
929 const hostSpec = yield* buildSshHostSpecEffect(input.resolvedTarget);929 const hostSpec = yield* buildSshHostSpecEffect(input.resolvedTarget);
...@@ -1076,7 +1076,7 @@ const startSshTunnel = Effect.fn("ssh/tunnel.startSshTunnel")(function* (input:...@@ -1076,7 +1076,7 @@ const startSshTunnel = Effect.fn("ssh/tunnel.startSshTunnel")(function* (input:
1076 ),1076 ),
1077 Effect.tapError((cause) =>1077 Effect.tapError((cause) =>
1078 Effect.gen(function* () {1078 Effect.gen(function* () {
1079 const net = yield* NetService;1079 const net = yield* NetService.NetService;
1080 const processRunningExit = yield* Effect.exit(child.isRunning);1080 const processRunningExit = yield* Effect.exit(child.isRunning);
1081 const localPortAvailableExit = yield* Effect.exit(1081 const localPortAvailableExit = yield* Effect.exit(
1082 net.canListenOnHost(input.localPort, "127.0.0.1"),1082 net.canListenOnHost(input.localPort, "127.0.0.1"),
packages/ssh/tsconfig.json+1-14
...@@ -1,18 +1,5 @@...@@ -1,18 +1,5 @@
1{1{
2 "extends": "../../tsconfig.base.json",2 "extends": "../../tsconfig.base.json",
3 "compilerOptions": {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 },
17 "include": ["src"]4 "include": ["src"]
18}5}
packages/tailscale/tsconfig.json+1-13
...@@ -1,17 +1,5 @@...@@ -1,17 +1,5 @@
1{1{
2 "extends": "../../tsconfig.base.json",2 "extends": "../../tsconfig.base.json",
3 "compilerOptions": {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 },
16 "include": ["src"]4 "include": ["src"]
17}5}
scripts/apply-web-brand-assets.ts+4-1
...@@ -2,7 +2,10 @@...@@ -2,7 +2,10 @@
22
3import * as NodeRuntime from "@effect/platform-node/NodeRuntime";3import * as NodeRuntime from "@effect/platform-node/NodeRuntime";
4import * as NodeServices from "@effect/platform-node/NodeServices";4import * 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";
6import { Argument, Command } from "effect/unstable/cli";9import { Argument, Command } from "effect/unstable/cli";
7import { resolveWebIconOverrides, type WebAssetBrand } from "./lib/brand-assets.ts";10import { resolveWebIconOverrides, type WebAssetBrand } from "./lib/brand-assets.ts";
811
scripts/build-desktop-artifact.test.ts+3-1
...@@ -1,6 +1,8 @@...@@ -1,6 +1,8 @@
1import * as NodeServices from "@effect/platform-node/NodeServices";1import * as NodeServices from "@effect/platform-node/NodeServices";
2import { assert, it } from "@effect/vitest";2import { assert, it } from "@effect/vitest";
3import { ConfigProvider, Effect, Option } from "effect";3import * as ConfigProvider from "effect/ConfigProvider";
4import * as Effect from "effect/Effect";
5import * as Option from "effect/Option";
46
5import {7import {
6 resolveBuildOptions,8 resolveBuildOptions,
scripts/build-desktop-artifact.ts+10-12
...@@ -10,18 +10,16 @@ import { resolveCatalogDependencies } from "./lib/resolve-catalog.ts";...@@ -10,18 +10,16 @@ import { resolveCatalogDependencies } from "./lib/resolve-catalog.ts";
1010
11import * as NodeRuntime from "@effect/platform-node/NodeRuntime";11import * as NodeRuntime from "@effect/platform-node/NodeRuntime";
12import * as NodeServices from "@effect/platform-node/NodeServices";12import * as NodeServices from "@effect/platform-node/NodeServices";
13import {13import * as Config from "effect/Config";
14 Config,14import * as Data from "effect/Data";
15 Data,15import * as Effect from "effect/Effect";
16 Effect,16import * as FileSystem from "effect/FileSystem";
17 FileSystem,17import * as Layer from "effect/Layer";
18 Layer,18import * as Logger from "effect/Logger";
19 Logger,19import * as Option from "effect/Option";
20 Option,20import * as Path from "effect/Path";
21 Path,21import * as Schema from "effect/Schema";
22 Schema,22import * as Stream from "effect/Stream";
23 Stream,
24} from "effect";
25import { Command, Flag } from "effect/unstable/cli";23import { Command, Flag } from "effect/unstable/cli";
26import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";24import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
2725
scripts/dev-runner.test.ts+2-1
...@@ -1,7 +1,8 @@...@@ -1,7 +1,8 @@
1import * as NodeServices from "@effect/platform-node/NodeServices";1import * as NodeServices from "@effect/platform-node/NodeServices";
2import * as NodeOS from "node:os";2import * as NodeOS from "node:os";
3import { assert, describe, it } from "@effect/vitest";3import { assert, describe, it } from "@effect/vitest";
4import { Effect, Path } from "effect";4import * as Effect from "effect/Effect";
5import * as Path from "effect/Path";
56
6import {7import {
7 checkPortAvailabilityOnHosts,8 checkPortAvailabilityOnHosts,
scripts/dev-runner.ts+16-8
...@@ -4,8 +4,16 @@ import * as NodeOS from "node:os";...@@ -4,8 +4,16 @@ import * as NodeOS from "node:os";
44
5import * as NodeRuntime from "@effect/platform-node/NodeRuntime";5import * as NodeRuntime from "@effect/platform-node/NodeRuntime";
6import * as NodeServices from "@effect/platform-node/NodeServices";6import * as NodeServices from "@effect/platform-node/NodeServices";
7import { NetService } from "@t3tools/shared/Net";7import * as NetService from "@t3tools/shared/Net";
8import { Config, Data, Effect, Hash, Layer, Logger, Option, Path, Schema } from "effect";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";
9import { Argument, Command, Flag } from "effect/unstable/cli";17import { Argument, Command, Flag } from "effect/unstable/cli";
10import { ChildProcess } from "effect/unstable/process";18import { ChildProcess } from "effect/unstable/process";
1119
...@@ -238,22 +246,22 @@ export function checkPortAvailabilityOnHosts<R>(...@@ -238,22 +246,22 @@ export function checkPortAvailabilityOnHosts<R>(
238 });246 });
239}247}
240248
241const defaultCheckPortAvailability: PortAvailabilityCheck<NetService> = (port) =>249const defaultCheckPortAvailability: PortAvailabilityCheck<NetService.NetService> = (port) =>
242 Effect.gen(function* () {250 Effect.gen(function* () {
243 const net = yield* NetService;251 const net = yield* NetService.NetService;
244 return yield* checkPortAvailabilityOnHosts(port, DEV_PORT_PROBE_HOSTS, (candidatePort, host) =>252 return yield* checkPortAvailabilityOnHosts(port, DEV_PORT_PROBE_HOSTS, (candidatePort, host) =>
245 net.canListenOnHost(candidatePort, host),253 net.canListenOnHost(candidatePort, host),
246 );254 );
247 });255 });
248256
249interface FindFirstAvailableOffsetInput<R = NetService> {257interface FindFirstAvailableOffsetInput<R = NetService.NetService> {
250 readonly startOffset: number;258 readonly startOffset: number;
251 readonly requireServerPort: boolean;259 readonly requireServerPort: boolean;
252 readonly requireWebPort: boolean;260 readonly requireWebPort: boolean;
253 readonly checkPortAvailability?: PortAvailabilityCheck<R>;261 readonly checkPortAvailability?: PortAvailabilityCheck<R>;
254}262}
255263
256export function findFirstAvailableOffset<R = NetService>({264export function findFirstAvailableOffset<R = NetService.NetService>({
257 startOffset,265 startOffset,
258 requireServerPort,266 requireServerPort,
259 requireWebPort,267 requireWebPort,
...@@ -300,7 +308,7 @@ export function findFirstAvailableOffset<R = NetService>({...@@ -300,7 +308,7 @@ export function findFirstAvailableOffset<R = NetService>({
300 });308 });
301}309}
302310
303interface ResolveModePortOffsetsInput<R = NetService> {311interface ResolveModePortOffsetsInput<R = NetService.NetService> {
304 readonly mode: DevMode;312 readonly mode: DevMode;
305 readonly startOffset: number;313 readonly startOffset: number;
306 readonly hasExplicitServerPort: boolean;314 readonly hasExplicitServerPort: boolean;
...@@ -308,7 +316,7 @@ interface ResolveModePortOffsetsInput<R = NetService> {...@@ -308,7 +316,7 @@ interface ResolveModePortOffsetsInput<R = NetService> {
308 readonly checkPortAvailability?: PortAvailabilityCheck<R>;316 readonly checkPortAvailability?: PortAvailabilityCheck<R>;
309}317}
310318
311export function resolveModePortOffsets<R = NetService>({319export function resolveModePortOffsets<R = NetService.NetService>({
312 mode,320 mode,
313 startOffset,321 startOffset,
314 hasExplicitServerPort,322 hasExplicitServerPort,
scripts/merge-update-manifests.test.ts+3-1
...@@ -1,6 +1,8 @@...@@ -1,6 +1,8 @@
1import * as NodeServices from "@effect/platform-node/NodeServices";1import * as NodeServices from "@effect/platform-node/NodeServices";
2import { assert, describe, it } from "@effect/vitest";2import { 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";
4import { Command, CliError } from "effect/unstable/cli";6import { Command, CliError } from "effect/unstable/cli";
57
6import {8import {
scripts/merge-update-manifests.ts+5-1
...@@ -2,7 +2,11 @@...@@ -2,7 +2,11 @@
22
3import * as NodeRuntime from "@effect/platform-node/NodeRuntime";3import * as NodeRuntime from "@effect/platform-node/NodeRuntime";
4import * as NodeServices from "@effect/platform-node/NodeServices";4import * 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";
6import { Argument, Command, Flag } from "effect/unstable/cli";10import { Argument, Command, Flag } from "effect/unstable/cli";
711
8import {12import {
scripts/mock-update-server.test.ts+5-2
...@@ -1,7 +1,10 @@...@@ -1,7 +1,10 @@
1import * as NodeServices from "@effect/platform-node/NodeServices";1import * as NodeServices from "@effect/platform-node/NodeServices";
2import { NodeHttpServer } from "@effect/platform-node";2import * as NodeHttpServer from "@effect/platform-node/NodeHttpServer";
3import { assert, it } from "@effect/vitest";3import { 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";
5import { HttpClient, HttpRouter } from "effect/unstable/http";8import { HttpClient, HttpRouter } from "effect/unstable/http";
69
7import { makeMockUpdateRouteLayer } from "./mock-update-server.ts";10import { makeMockUpdateRouteLayer } from "./mock-update-server.ts";
scripts/mock-update-server.ts+8-2
...@@ -1,8 +1,14 @@...@@ -1,8 +1,14 @@
1// @effect-diagnostics-next-line nodeBuiltinImport:off - NodeHttpServer.layer takes `NodeHttp.createServer` as arg
1import * as NodeHttp from "node:http";2import * 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";
4import * as NodeServices from "@effect/platform-node/NodeServices";6import * 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";
6import { HttpRouter, HttpServerRequest, HttpServerResponse } from "effect/unstable/http";12import { HttpRouter, HttpServerRequest, HttpServerResponse } from "effect/unstable/http";
713
8interface MockUpdateServerConfig {14interface MockUpdateServerConfig {
scripts/notify-discord-release.ts+7-1
...@@ -2,7 +2,13 @@...@@ -2,7 +2,13 @@
22
3import * as NodeRuntime from "@effect/platform-node/NodeRuntime";3import * as NodeRuntime from "@effect/platform-node/NodeRuntime";
4import * as NodeServices from "@effect/platform-node/NodeServices";4import * 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";
6import { Argument, Command, Flag } from "effect/unstable/cli";12import { Argument, Command, Flag } from "effect/unstable/cli";
7import {13import {
8 FetchHttpClient,14 FetchHttpClient,
scripts/release-smoke.ts+4-1
...@@ -1,3 +1,4 @@...@@ -1,3 +1,4 @@
1// @effect-diagnostics nodeBuiltinImport:off
1import { execFileSync } from "node:child_process";2import { execFileSync } from "node:child_process";
2import {3import {
3 cpSync,4 cpSync,
...@@ -11,6 +12,8 @@ import {...@@ -11,6 +12,8 @@ import {
11import { tmpdir } from "node:os";12import { tmpdir } from "node:os";
12import { dirname, join, resolve } from "node:path";13import { dirname, join, resolve } from "node:path";
13import { fileURLToPath } from "node:url";14import { fileURLToPath } from "node:url";
15import * as Console from "effect/Console";
16import * as Effect from "effect/Effect";
1417
15const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");18const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
1619
...@@ -389,7 +392,7 @@ try {...@@ -389,7 +392,7 @@ try {
389 "Windows release smoke unexpectedly removed the x64 builder debug fixture.",392 "Windows release smoke unexpectedly removed the x64 builder debug fixture.",
390 );393 );
391394
392 console.log("Release smoke checks passed.");395 Effect.runSync(Console.log("Release smoke checks passed."));
393} finally {396} finally {
394 rmSync(tempRoot, { recursive: true, force: true });397 rmSync(tempRoot, { recursive: true, force: true });
395}398}
scripts/resolve-nightly-release.ts+8-2
...@@ -2,7 +2,13 @@...@@ -2,7 +2,13 @@
22
3import * as NodeRuntime from "@effect/platform-node/NodeRuntime";3import * as NodeRuntime from "@effect/platform-node/NodeRuntime";
4import * as NodeServices from "@effect/platform-node/NodeServices";4import * 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";
6import { Command, Flag } from "effect/unstable/cli";12import { Command, Flag } from "effect/unstable/cli";
713
8interface NightlyReleaseMetadata {14interface NightlyReleaseMetadata {
...@@ -93,7 +99,7 @@ const writeOutput = Effect.fn("writeOutput")(function* (...@@ -93,7 +99,7 @@ const writeOutput = Effect.fn("writeOutput")(function* (
93 yield* fs.writeFileString(githubOutputPath, serialized, { flag: "a" });99 yield* fs.writeFileString(githubOutputPath, serialized, { flag: "a" });
94 } else {100 } else {
95 for (const [key, value] of entries) {101 for (const [key, value] of entries) {
96 console.log(`${key}=${value}`);102 yield* Console.log(`${key}=${value}`);
97 }103 }
98 }104 }
99});105});
scripts/resolve-previous-release-tag.ts+7-1
...@@ -2,7 +2,13 @@...@@ -2,7 +2,13 @@
22
3import * as NodeRuntime from "@effect/platform-node/NodeRuntime";3import * as NodeRuntime from "@effect/platform-node/NodeRuntime";
4import * as NodeServices from "@effect/platform-node/NodeServices";4import * 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";
6import { Command, Flag } from "effect/unstable/cli";12import { Command, Flag } from "effect/unstable/cli";
7import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";13import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
814
scripts/tsconfig.json+1-6
...@@ -3,12 +3,7 @@...@@ -3,12 +3,7 @@
3 "compilerOptions": {3 "compilerOptions": {
4 "composite": true,4 "composite": true,
5 "types": ["node"],5 "types": ["node"],
6 "lib": ["ESNext", "esnext.disposable"],6 "lib": ["ESNext", "esnext.disposable"]
7 "plugins": [
8 {
9 "name": "@effect/language-service"
10 }
11 ]
12 },7 },
13 "include": ["**/*.ts"]8 "include": ["**/*.ts"]
14}9}
scripts/update-release-package-versions.test.ts+11-13
...@@ -1,8 +1,14 @@...@@ -1,8 +1,14 @@
1import * as NodeServices from "@effect/platform-node/NodeServices";1import * as NodeServices from "@effect/platform-node/NodeServices";
2import { assert, it } from "@effect/vitest";2import { assert, it } from "@effect/vitest";
3import { 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";
4import { Command, CliError } from "effect/unstable/cli";9import { Command, CliError } from "effect/unstable/cli";
5import * as TestConsole from "effect/testing/TestConsole";10import * as TestConsole from "effect/testing/TestConsole";
11import { fromJsonStringPretty } from "@t3tools/shared/schemaJson";
612
7import {13import {
8 releasePackageFiles,14 releasePackageFiles,
...@@ -13,17 +19,9 @@ import {...@@ -13,17 +19,9 @@ import {
13const ScriptTestLayer = Layer.mergeAll(NodeServices.layer, TestConsole.layer);19const ScriptTestLayer = Layer.mergeAll(NodeServices.layer, TestConsole.layer);
14const runCli = Command.runWith(updateReleasePackageVersionsCommand, { version: "0.0.0" });20const runCli = Command.runWith(updateReleasePackageVersionsCommand, { version: "0.0.0" });
15const PackageJsonSchema = Schema.Record(Schema.String, Schema.Unknown);21const PackageJsonSchema = Schema.Record(Schema.String, Schema.Unknown);
16const PrettyJsonString = SchemaGetter.parseJson<string>().compose(22const PackageJsonPrettyJson = fromJsonStringPretty(PackageJsonSchema);
17 SchemaGetter.stringifyJson({ space: 2 }),23const decodePackageJson = Schema.decodeEffect(PackageJsonPrettyJson);
18);24const encodePackageJson = Schema.encodeEffect(PackageJsonPrettyJson);
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);
2725
28const writePackageJsonFixtures = Effect.fn("writePackageJsonFixtures")(function* (26const writePackageJsonFixtures = Effect.fn("writePackageJsonFixtures")(function* (
29 rootDir: string,27 rootDir: string,
...@@ -37,7 +35,7 @@ const writePackageJsonFixtures = Effect.fn("writePackageJsonFixtures")(function*...@@ -37,7 +35,7 @@ const writePackageJsonFixtures = Effect.fn("writePackageJsonFixtures")(function*
37 yield* fs.makeDirectory(path.dirname(filePath), { recursive: true });35 yield* fs.makeDirectory(path.dirname(filePath), { recursive: true });
38 yield* fs.writeFileString(36 yield* fs.writeFileString(
39 filePath,37 filePath,
40 `${encodePackageJson({38 `${yield* encodePackageJson({
41 name: relativePath,39 name: relativePath,
42 version,40 version,
43 private: true,41 private: true,
scripts/update-release-package-versions.ts+9-10
...@@ -2,8 +2,15 @@...@@ -2,8 +2,15 @@
22
3import * as NodeRuntime from "@effect/platform-node/NodeRuntime";3import * as NodeRuntime from "@effect/platform-node/NodeRuntime";
4import * as NodeServices from "@effect/platform-node/NodeServices";4import * 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";
6import { Argument, Command, Flag } from "effect/unstable/cli";12import { Argument, Command, Flag } from "effect/unstable/cli";
13import { fromJsonStringPretty } from "@t3tools/shared/schemaJson";
714
8export const releasePackageFiles = [15export const releasePackageFiles = [
9 "apps/server/package.json",16 "apps/server/package.json",
...@@ -17,15 +24,7 @@ interface UpdateReleasePackageVersionsOptions {...@@ -17,15 +24,7 @@ interface UpdateReleasePackageVersionsOptions {
17}24}
1825
19const PackageJsonSchema = Schema.Record(Schema.String, Schema.Unknown);26const PackageJsonSchema = Schema.Record(Schema.String, Schema.Unknown);
20const PrettyJsonString = SchemaGetter.parseJson<string>().compose(27const PackageJsonPrettyJson = fromJsonStringPretty(PackageJsonSchema);
21 SchemaGetter.stringifyJson({ space: 2 }),
22);
23const PackageJsonPrettyJson = Schema.fromJsonString(PackageJsonSchema).pipe(
24 Schema.encode({
25 decode: PrettyJsonString,
26 encode: PrettyJsonString,
27 }),
28);
29const decodePackageJson = Schema.decodeUnknownEffect(PackageJsonPrettyJson);28const decodePackageJson = Schema.decodeUnknownEffect(PackageJsonPrettyJson);
30const encodePackageJson = Schema.encodeSync(PackageJsonPrettyJson);29const encodePackageJson = Schema.encodeSync(PackageJsonPrettyJson);
3130
tsconfig.base.json+34-1
...@@ -17,6 +17,39 @@...@@ -17,6 +17,39 @@
17 "skipLibCheck": true,17 "skipLibCheck": true,
18 "allowSyntheticDefaultImports": true,18 "allowSyntheticDefaultImports": true,
19 "esModuleInterop": true,19 "esModuleInterop": true,
20 "resolveJsonModule": true20 "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 ]
21 }54 }
22}55}